Advance past c8fb619b (head/tail grep cap that could hide a match in a
long line) to origin/main, which centers the per-line cap on the match
plus repairs feature gates/advisories.
Advance the submodule past the pre-rebase 876c890 (no longer on any
remote branch) to origin/main, which carries the ripgrep/compression
series rebased onto main plus the grep per-line output cap.
- rquickjs 0.9->0.12 (op-mcp): 0.12 ships aarch64-pc-windows-msvc bindings
(0.9 doesn't), so the native-only script feature builds on windows-arm64
with full parity (as Zode's zode-core already uses 0.12). Clean drop-in;
25 script_runner tests pass. Reverts the earlier per-target gating.
- Workspace MSRV 1.85->1.87 (rquickjs 0.12's floor; pinned toolchain is 1.94).
- agent_settings switch test: assert theme.input (the token the off-track
paints) now the light theme gives input its own value distinct from border.
- Call blur_input when agent settings panel opens (shortcut + click)
on both native and web hosts, so chat.focused becomes false.
- Guard agent_settings focus check with agent_settings_open in
input_active() to prevent stale focus from claiming keyboard.
- Reorder handle_cmd_paste to check non-chat inputs first, with an
additional !chat.focused guard.
Read `~/.zode/config.json` on startup (native/daemon) and merge every
Zode `(provider, model)` pair into agent_settings.builtin_agents as a
BuiltinAgentConfig, so providers already configured in the Zode CLI show
up as OpenPencil custom models without re-entering keys. Anthropic vs
OpenAI-compat is chosen by the provider `type`; one custom model per
model id (provider name qualified by model when a provider has several).
Best-effort + idempotent: missing/malformed file is a silent no-op, and
add_builtin_agent_config backend-dedupes so re-running each launch adds
nothing new. Imported agents are tracked in imported_agent_ids and
excluded from settings.json persistence — Zode's config stays the single
source of truth and its API keys are never silently duplicated onto a
second on-disk location (they're re-imported every launch).
Three independent CI failures, none caused by the font-lock push:
1. CanvasKit web-host test variables_panel_open_does_not_paint_legacy_modal
false-tripped: its '> half viewport' heuristic was a proxy for 'no
full-viewport modal', but only passed because the property panel used
to narrow the canvas and clamp the floating VariablesPanel below
half-width. The nothing-selected-by-default change collapsed the
property panel, widening the canvas so the legitimate 820x480 panel
paints unclamped. There is no legacy modal. Retarget the assertion at
a near full-viewport round fill and positively assert the panel paints.
2. windows-aarch64 cross-check failed: op-host-services + op-orchestrator
pulled op-mcp's native-only 'script' feature unconditionally, dragging
in rquickjs-sys which ships no bindings for aarch64-pc-windows-msvc.
Enable 'script' per-target (off on that unshipped check-only arch).
3. Windows workspace tests segfaulted single-threaded (before the first
test prints, under --test-threads=1) — an intra-test race a thread
flag can't serialize. Switch the Windows runner to cargo-nextest (one
process per test) so the crash is isolated + named; doctests run
separately.
Windows CI segfaulted (STATUS_ACCESS_VIOLATION 0xc0000005) because the
design worker thread runs SkiaMeasure layout concurrently with the UI
thread paint/measure, and skia Windows FontMgr (DirectWrite) is not
thread-safe. Wrap NativeBackend::{new,draw_text,enumerate_system_font_families}
in jian_skia::with_font_lock and bump vendor/jian to the commit adding
that global reentrant lock.
Also un-gate chat_intent_host_tests on Windows (the lock makes its
intra-test worker/UI concurrency safe) and harden the flaky
explicit_family_typeface_lookup_is_cached test with a
retry-until-clean-window against concurrent generation bumps.
Whole-page PNG/raster export added a 16px transparent margin around the
content union, so the exported image did not fill to its content (a
transparent border on all sides). Drop MARGIN to 0 for a tight export;
update the test expectations to the no-margin coordinates.
A recursive cleanup pass detects a horizontal row of >=3 vertical
icon+label tabs (a bottom nav bar, incl. a nested tab container) and makes
each tab fill_container + the row space_between, so tabs spread the full
width instead of bunching to one side and overflowing off-screen.
Replace the empty dashed-placeholder plot-area guidance (which left charts
blank) with a simple real bar chart via flexbox (layout=horizontal,
alignItems=end, value-proportional bar heights) instead of the layout=none
manual positioning the model mis-computes. Also add donut/ring concentric-
stacking guidance.
A fresh starter canvas and a just-opened .op no longer ship the root
frame pre-selected, which both reads cleaner and stops a stray selection
from mis-routing a whole-screen design request into modify.
Turn the product-row-overflow self-check from reject-and-retry into
accept-and-auto-fix (fixed-width cards -> fill_container, gap 12), and
skip clipContent scroll rows entirely. Extend the contrast fixer to any
container with a resolvable solid background (not just buttons) so light
text on a light chip flips to a legible foreground. Add a JS bracket
auto-repair in the script runner so a model's missing closing brace no
longer fails the whole subtask.
Geometry-driven post-passes on generated mobile screens: equalize
side-by-side card heights, collapse a bloating fill_container content
section on a fixed-height artboard, pad a clipping horizontal scroll row
so child strokes aren't shaved on any side, expand a collapsed absolute
(layout:none) image wrapper to its image height, and collapse nested
double horizontal padding. Wired into run_cleanup_passes + loop_finalize.
get_guidelines(category=style) returns a resolved style from an own-built
catalog (7 styles x 7 palettes) with OKLCH-based contrast validation, baked
into the design pipeline (loop prompt + orchestrator subagent instruction).
Also raise the per-subtask output budget 8000 -> 12000 so image-rich
data-list sections stop truncating mid-script.
The STATUS_ACCESS_VIOLATION is not confined to op-host-native/-desktop: any
crate whose tests reach a skia FontMgr (DirectWrite) via NativeBackend or
SkiaMeasure crashes when that test binary runs across parallel worker threads
(op-host-services surfaced next). Excluding crates one-by-one doesn't
converge. Instead run the entire workspace with --test-threads=1 on Windows
only; each binary is serialized (cross-process concurrency is safe), covering
every skia-touching crate at once. macOS / Linux stay parallel.
The Windows STATUS_ACCESS_VIOLATION surfaced in the op_host_native test
binary once op-host-desktop was serialized: op-host-native's backend::skia
tests create a skia FontMgr (DirectWrite) and segfault when run across
parallel test-worker threads. Extend the single-threaded run to cover
op-host-native as well as op-host-desktop in both CI workflows.
Windows runs multi-line run: blocks under PowerShell, which reports only the
last command's exit code. Keeping the workspace test and the serialized
op-host-desktop test in one run: block would let a workspace-test failure be
masked by the second command passing. Split into two steps so each fails the
job independently.
Mirror the rust-check.yml fix into the cross-platform host-test step: the
op-host-desktop test binary segfaults on the Windows runner when its native
(skia/DirectWrite) tests run across parallel test-worker threads. Run that
crate single-threaded here too.
op-host-desktop tests build native NativeBackends (skia/DirectWrite fonts);
across cargo's parallel test-worker threads this segfaults on the Windows
runner. Split them out of the parallel workspace run and serialize with
--test-threads=1.
The FontStore + font-import tests register fonts via jian_skia (skia
FontMgr::new_from_data, DirectWrite on Windows) from cargo's parallel
test-worker threads, which segfaults (STATUS_ACCESS_VIOLATION) in Windows
CI — newly surfaced now that the submodule checkout resolves. Production
resolves fonts on the main render thread; macOS + Linux keep the coverage.
Gated with cfg(not(target_os = windows)) as the least-invasive fix.
Update the vendor/agent pointer from a6f1897 to the current agent-rs
origin/main tip (ea27441). Verified: cargo check --workspace builds clean
against it; ea27441 is on origin so submodule checkout resolves in CI.
The test module's assert_eq! calls exceeded max_width on one line; rustfmt
wraps them multi-line. Committing the formatted form so cargo fmt --all
--check (the CI gate) passes on the committed tree.
The family-aware CanvasKit change added a `family` param to drawText and
moved the browser/system-font fallback fast path into the shared
`drawScriptRun` helper. Re-anchor the 'fallback before Paint allocation'
assertion on drawScriptRun where that invariant now lives.
The native File ▸ Open Recent submenu was only refreshed after native-menu
actions + Finder opens; an in-canvas File-menu open/save (which never
reaches handle_menu_action) left it stale. Refresh every loop iteration
instead, rebuilding the muda submenu only when the labels actually changed
(cached in recent_menu_labels) — cheap, and correct regardless of which
path touched the recent list.
Add an Open Recent submenu to the native File menu, populated from
editor_ui.recent_files (file names, newest first; each item id
`recent:<index>`). MenuAction gains OpenRecent(usize), dispatched through
the existing FileAction::OpenRecent path. The muda submenu is rebuilt
(set_recent_files) whenever the recent list can change — at startup and
after any menu action / Finder open — with a disabled "No Recent
Documents" placeholder when empty. No-op off macOS.
Three related hover fixes:
- Font picker "穿透": the open popup is a floating overlay but the
cursor-move handler fell through to the canvas/layer hovers when the
picker hover was unchanged, so a node behind the popup highlighted.
update_font_picker_hover now CONSUMES the move + clears lower-overlay
hover while the cursor is over the popup.
- "Import font…" row had no hover wash: add font_picker_import_hover
state + PropertyPanel::font_picker_import_action_at hit + a
paint_button_feedback_wash on the ImportAction row (threaded through
the paint_font_picker signature).
- Web parity: op-host-web overlay_cursor now updates the Effects add-menu
effect_add_menu_hover (mirrors the fill-type picker branch), so the
Drop Shadow / Layer Blur menu highlights on web too.
The Effects "+" add-menu (Drop Shadow / Layer Blur) painted flat rows
with no hover feedback, unlike the sibling property-panel dropdowns.
Add effect_add_menu_hover to editor_ui (cleared on toggle/close), a
PropertyPanel::effect_add_menu_row_at hit helper, and a native
update_effect_add_menu_hover cursor-move pass (mirroring the export
picker) that highlights the hovered row with the standard muted wash.
The Imported group header + "Import font…" row painted hardcoded English
literals. Add text.font.imported / text.font.importAction to all 15 locale
tables and translate them via op_i18n like the neighbouring bundled/system/
noResults labels, dropping the placeholder consts.
A fresh from-scratch canvas holds only the blank starter frame, and that
frame ships selected. A whole-new-design request phrased as a bare noun
phrase (no page/screen noun, no create verb — e.g. "Luxury webapp for
managing barbershop clients") slips past both new-screen exemption gates,
and the selected starter makes selected_target_instruction true, so the
request was launched as a modify turn on a near-empty canvas — the model
then emits an unparseable fragment and the user sees "Could not parse
design nodes". Guard should_launch_direct_modify: a canvas that is only
the blank starter frame has nothing real to modify, so any request on it
is NEW (desktop parity of web_chat_standard's page_children_empty => New).
A single provider rate-limit (HTTP 429) or overload (503/529) on one
design sub-agent request had no recovery: no client-side pacing to stay
under the RPM limit, and no backoff-retry once tripped — so the section
burned its attempts and the run reported a failed subtask while the rest
of the design was fine. Add a process-wide min-gap throttle (default
350ms, env-overridable) and transparent backoff-retry that honors
Retry-After (else exponential 1/2/4s, capped) around both the openai-
compatible and anthropic send paths. Benefits every builtin path
(orchestrator, design loop, chat). retry.rs stays unchanged: 429 remains
non-retryable at the ladder level since the http layer already backed off.
A section frame with equal L/R padding wrapping a single transparent
fill-width frame that ALSO carries equal L/R padding double-insets its
content (measured: header content at 40px vs sibling sections' 20px).
Add a cleanup pass that zeroes the inner wrapper's horizontal padding
(preserving its vertical padding and the outer section padding), gated
so real cards (with fill/stroke/effects) and multi-child sections are
never touched. Wired into run_cleanup_passes + loop_finalize.
The prior fix invalidated the layout scene for imported fonts but the
async system-font load path (load_used_system_fonts -> register_system_font)
had the same gap: it marked dirty + repainted but did not invalidate the
scene cache, so text using a just-loaded system family kept a layout scene
shaped/measured against the fallback glyphs (web has no jian_skia
font-generation signal). Call invalidate_layout_scene() there too. Every
web runtime font-registration path (system + imported import/remove/
mount-restore) now forces the rebuild.
The web SceneBuildCache never invalidates on a font change: op-host-web
has no jian_skia font registry, so current_font_generation() is a constant
0 there, and a font import/removal/restore doesn't touch the doc/page/
theme the cache compares. So refresh_layout_scene's maybe_rebuild returned
None and the layout scene stayed stale (shaped/measured against the old
fonts) after a restore. Add WidgetHost::invalidate_layout_scene() and call
it from refresh_imported_font_snapshot (covers mount-restore + import +
remove) to force the rebuild. Native already handles this via the
layout_scene_font_generation watch (its registry IS jian_skia).
Bring user-imported fonts to the browser host, matching the native flow.
Phase 3 — family-aware CanvasKit text (the web BLOCKER): drawText now
carries font_family and a new measureTextFamilyStyled FFI mirrors it, so
the editor measures caret/layout with the same family it draws (closing
the family-blind trap). op_ck_bridge.js keys imported typefaces by family
and resolves them PER CHARACTER: chars the imported face covers draw with
it, the rest fall to the existing script-segmented system/CJK/emoji path
(no tofu, no dropped family) — draw + measure split on identical
importedCoverage segments so advances agree. register/removeImportedFont
with replace + wasm-heap free.
Phase 4 — import UI + persistence: web ImportFont opens a hidden
.ttf/.otf file input -> FileReader -> 16 MiB cap -> family parsed in Rust
via ttf-parser (font_meta.rs; the vendored CanvasKit exposes no
getFamilyName) -> register + persist bytes in IndexedDB (font_store_idb.rs;
DB openpencil / store imported_fonts, keyed by family, async errors
logged) -> refresh snapshot + repaint. Remove drops registry + IndexedDB.
Mount re-registers persisted fonts (skipping any the user already changed
this session) before the first family-aware paint. font_import_supported
is true on web, so the picker's imported group + Import row are live.
font_meta family extraction is unit-tested (real .ttf bytes -> family)
so the core is verified headlessly; runtime IndexedDB/FileReader/rendering
need a browser smoke test. Codex-reviewed (2 rounds) — getFamilyName
BLOCKER, mixed-script fallback, IndexedDB async errors, and the mount
race all addressed.
FontStore::import deleted the old file of a replaced face DURING the
index-mutation retain — before save_index. If save_index then failed, the
previously persisted font was already gone while the on-disk index still
referenced it, so the prior font was lost on a failed replacement import.
Collect the superseded files and delete them only after save_index
succeeds. New test replacement_import_prunes_the_old_file_only_after_saving_the_index.
FontStore::import registered the font in the process-global registry
(bumping the generation) BEFORE writing it to disk. A failed
create_dir_all/write/save_index then left the font live + rendered this
session but unpersisted — while the caller reported the import as failed
and popped an error dialog. Reorder to parse (no registry mutation via
the new jian_skia::parse_imported_font_meta) -> persist to disk ->
register last, so the live registry is only mutated once persistence is
durable. New test failed_persist_does_not_leak_into_the_live_registry
(file-rooted store forces a disk failure) pins it.
Bumps vendor/jian to the parse_imported_font_meta commit.
Wire user-imported fonts into the Typography font picker and the native
import/remove flow.
- op-editor-ui (wasm32-clean): FontPickerEntry gains `imported`;
font_picker_entries builds Imported -> Bundled -> System groups.
Imported entries carry an inline remove-x; a bottom "Import font…" row
drives import. A new `allow_import` capability (threaded through layout/
hit/paint) omits that row where the host can't import, so web shows no
dead control. Split property_panel_typography.rs into +_paint/+_tests to
stay under the 800-line cap. New actions ImportFont / RemoveImportedFont.
- op-editor-core: editor_ui gains imported_font_families snapshot,
pending_font_import / pending_font_remove request flags, and
font_import_supported capability (default false).
- op-host-native: refresh_imported_fonts rebuilds the snapshot from
jian_skia::list_families; ImportFont/RemoveImportedFont raise pending
requests (family resolved against the same entries list).
- op-host-desktop: font_import_host drains the requests — import opens an
rfd .ttf/.otf dialog (size pre-checked via metadata before read) ->
FontStore::import; remove -> FontStore::remove; both refresh the
snapshot. DesktopApp seeds the snapshot + sets font_import_supported.
- op-host-web: passes the imported list; import/remove are no-ops until
the Phase 4 web file-input (row hidden via the capability flag).
Codex-reviewed (2 rounds) to APPROVED.