Commit graph

2109 commits

Author SHA1 Message Date
Kayshen-X 23449e2c4e fix(ci): avoid windows text layout in hover fixture 2026-07-07 23:44:28 +08:00
Kayshen-X 4716aaac4c fix(ci): stabilize native font tests 2026-07-07 23:28:27 +08:00
Kayshen-X 311c11548c fix(ci): skip windows preview skia runtime tests 2026-07-07 23:12:30 +08:00
Kayshen-X 0bca02faa9 fix(ci): skip windows skia text height parity 2026-07-07 22:52:58 +08:00
Kayshen-X a116b15969 fix(ci): repair rust checks on v0.8.0-new 2026-07-07 22:37:30 +08:00
Kayshen-X da7fa17a78 chore(agent): bump vendor/agent to 5c0f950 (match-centered grep cap)
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.
2026-07-07 21:44:12 +08:00
Kayshen-X 8e9f1c5f1e chore(agent): bump vendor/agent to c8fb619b (rebased onto main + grep cap)
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.
2026-07-07 21:33:18 +08:00
Kayshen-X ab6c6835d0 fix(mcp): bump rquickjs 0.9->0.12 for windows-aarch64; MSRV 1.87; switch test input token
- 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.
2026-07-07 21:26:38 +08:00
leinaldo bcef937644 fix(ai): blur chat input when agent settings open to prevent paste misrouting (#172)
- 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.
2026-07-07 11:17:44 +08:00
Fini 6b78592010 test(canvas): cover node drag interactions 2026-07-07 01:19:03 +08:00
Fini 5d62c74274 feat(canvas): refine node drag previews 2026-07-07 01:18:25 +08:00
Fini 8d1e2e5dd7 feat(canvas): add cross-container drag controls 2026-07-06 22:24:05 +08:00
Fini fcf5ddf7a4 fix(canvas): polish selection overlays and viewport fit 2026-07-06 22:23:26 +08:00
Fini 4d9fabd6d1 fix(ai): support script local edit apply 2026-07-06 22:22:58 +08:00
Kayshen-X 08b5be80b4 chore(agent): bump vendor/agent to 876c890
Advance the vendored agent runtime submodule to origin/main
(876c890 'feat(shell): apply output compression before the Bash cap').
2026-07-06 22:13:02 +08:00
Kayshen-X 14bbb0c409 feat(agent): auto-import Zode providers as custom models
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).
2026-07-06 22:07:34 +08:00
Kayshen-X 8cd03adacd fix(ci): repair three v0.8.0-new failures (canvaskit test, windows-aarch64 deps, windows nextest)
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.
2026-07-06 22:04:56 +08:00
Kayshen-X 41a023ca9a fix(renderer): serialize native skia font access via jian font lock
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.
2026-07-06 21:07:49 +08:00
Fini 67a8c48197 fix(export): tight whole-page raster export
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.
2026-07-06 08:07:23 +08:00
Fini b39c1636c0 feat(orchestrator): distribute bottom-nav tabs evenly
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.
2026-07-06 01:24:49 +08:00
Fini 741c3a8b65 feat(ai): teach a real flex bar chart in the dashboard skill
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.
2026-07-06 01:24:48 +08:00
Fini 2090b5a8eb fix(editor): new/opened documents open with nothing selected
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.
2026-07-06 00:47:11 +08:00
Fini 92977e08dd feat(orchestrator): self-check auto-fix, content contrast, script bracket repair
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.
2026-07-06 00:47:10 +08:00
Fini 8a9919579e feat(orchestrator): mobile-artboard layout cleanup passes
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.
2026-07-06 00:47:09 +08:00
Fini ea27689d0e feat(ai): design style-resolution color system + subtask token budget
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.
2026-07-06 00:47:08 +08:00
Kayshen-X 40c3094077 ci: run whole workspace single-threaded on Windows (skia/DirectWrite)
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.
2026-07-05 23:22:29 +08:00
Kayshen-X 247583a054 ci: serialize op-host-native tests too (Windows DirectWrite segfault)
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.
2026-07-05 22:56:22 +08:00
Kayshen-X 6efae16f14 ci: split multiplatform host-test into two steps (avoid pwsh exit masking)
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.
2026-07-05 22:52:40 +08:00
Kayshen-X ce3ba6d527 ci: serialize op-host-desktop tests in multiplatform Windows job
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.
2026-07-05 22:48:02 +08:00
Kayshen-X 7439e25f70 ci: run op-host-desktop tests single-threaded (Windows skia segfault)
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.
2026-07-05 22:41:18 +08:00
Kayshen-X 0611cc9714 chore(vendor): bump jian for the empty-source FontMgr fix (Windows CI segfault) 2026-07-05 22:24:31 +08:00
Kayshen-X 60a0043d46 test(fonts): gate skia font-registration tests off Windows CI
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.
2026-07-05 22:05:51 +08:00
Kayshen-X 54b626759e chore(vendor): bump agent submodule to ea27441 (origin/main)
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.
2026-07-05 19:41:04 +08:00
Kayshen-X 1b8e64b1ce style(desktop): rustfmt window_resize.rs test asserts
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.
2026-07-05 19:31:33 +08:00
Kayshen-X d48ef56624 chore(vendor): bump jian for the widgets MSRV clippy fix 2026-07-05 19:26:11 +08:00
Kayshen-X 4210fccabb test(web): update paint-order guard for the drawScriptRun refactor
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.
2026-07-05 19:18:47 +08:00
Kayshen-X 3b19a16055 fix(desktop): keep Open Recent in sync after in-canvas file actions
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.
2026-07-05 18:44:28 +08:00
Kayshen-X b7a55b6831 feat(desktop): File ▸ Open Recent submenu in the native macOS menu bar
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.
2026-07-05 18:37:39 +08:00
Kayshen-X 9c99e20e97 fix(panels): font-picker hover bleed-through + Import-row hover + web effect-menu hover
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.
2026-07-05 17:55:13 +08:00
Kayshen-X bf955e4dc2 fix(panels): hover highlight for the Effects add-menu rows
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.
2026-07-05 17:55:12 +08:00
Kayshen-X 5755719696 i18n(panels): localize the imported-font picker labels across 15 locales
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.
2026-07-05 17:55:11 +08:00
Fini ed50b1b5b8 fix(ai): never route a pristine-starter canvas into direct-modify
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).
2026-07-05 16:52:02 +08:00
Fini 768ea37d7b fix(ai): add request throttle + 429/503 backoff-retry to builtin http provider
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.
2026-07-05 16:52:01 +08:00
Fini 20606cd632 fix(orchestrator): collapse nested horizontal padding in structural wrappers
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.
2026-07-05 16:52:00 +08:00
Kayshen-X b4036617da fix(web): rebuild layout scene on system-font load too
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.
2026-07-05 14:21:53 +08:00
Kayshen-X 3964009221 fix(web): rebuild layout scene on font import/restore
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).
2026-07-05 14:21:52 +08:00
Kayshen-X a218115010 feat(web): user font import — family-aware CanvasKit + import UI + IndexedDB (phases 3-4)
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.
2026-07-05 14:21:51 +08:00
Kayshen-X 00c6938c26 fix(desktop): prune superseded font file only after the index is saved
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.
2026-07-05 14:21:50 +08:00
Kayshen-X 53da54ab8b fix(desktop): persist imported font before mutating the live registry
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.
2026-07-05 14:21:49 +08:00
Kayshen-X 892705cf79 feat(panels): font-picker import UI + native import/remove dispatch (phase 2b)
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.
2026-07-05 14:21:48 +08:00