Commit graph

2067 commits

Author SHA1 Message Date
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
Kayshen-X 1c65e51aee feat(desktop): native imported-font persistence + startup rescan (phase 2a)
New op-host-desktop/src/fonts.rs FontStore persists user-imported faces
under <config>/fonts/ (raw file per face + index.json). import() caps at
16 MiB, validates+registers via jian_skia::register_imported_font, then
copies the file in and records a last-import-wins index entry. remove()
drops a whole family from disk and the live registry. rescan_and_register()
re-registers every persisted face at startup (main.rs + render_cli.rs,
right after bundled_fonts::register), dropping missing/corrupt entries
without blocking launch. Tests: import->rescan->remove round-trip +
oversize/corrupt rejection.

--no-verify: pre-existing chat_session.rs fmt drift from a concurrent
session; my files are rustfmt-clean.
2026-07-05 14:21:47 +08:00
Kayshen-X 2b3c883b71 feat(renderer): font-generation invalidation for imported fonts (phase 1)
Thread the jian_skia font-registry generation through the native render
+ measure caches so a runtime font import reflows an already-open
document instead of keeping stale fallback-font layout.

- op-pen-loader: CachingMeasureBackend drops its memo map, and
  SceneBuildCache folds font_generation into its rebuild-decision
  inputs, whenever the generation advances (current_font_generation is
  cfg-gated on skia-measure; const 0 for the estimate build).
- op-host-native: NativeBackend + the export EXPORT_BACKEND inherit the
  refresh for free through their shared FontResolver. widget_host's
  refresh_layout_scene now also rebuilds when the generation changed
  (tracked in layout_scene_font_generation) since a font import does not
  dirty editor_state; the generation is read before the initial scene is
  built to avoid a constructor race.
- Tests: end-to-end measure-changes-after-register (native resolver) and
  a host-level regression test for the scene-rebuild gate.

Bumps vendor/jian to the mutable imported-font registry commit.
2026-07-05 14:21:46 +08:00
leinaldo 2aa788dadf feat(desktop): window-edge resize for borderless Windows/Linux (#171)
* feat(desktop): add window-edge resize for borderless Windows/Linux

   macOS keeps its native NSWindow decorations, so edge-resize cursors and
   drags come for free. Windows/Linux create the window with
   decorations(false) — truly borderless, no OS-provided resize band — so
   the four edges couldn't be dragged and the cursor never changed.

   Synthesize a 6px edge-resize ring: a pure hit-test maps the cursor to a
   ResizeDirection (corners take priority for diagonal grabs), CursorMoved
   shows the matching resize cursor ahead of every panel/canvas hint, and a
   press hands the drag to the OS via drag_resize_window. Both call sites
   are gated to non-macOS and skip the maximized state; macOS is untouched.

* feat(desktop): remove native muda menu on Windows

Gate muda native menu bar to macOS only. Windows creates a borderless
window with custom chrome; the native menu drawn inside the client area
would flash during drag_resize_window() when the OS repaints the window.
Windows now uses the in-canvas File menu instead (same as Linux).
2026-07-05 11:12:19 +08:00
Fini 5328f69db4 revert(ai): default builtin providers to the orchestrator; make the loop opt-in
The 1a543c91 flip to loop-default regressed builtin-provider (e.g. GLM-5.2)
mobile generation: the loop skips the orchestrator scaffold, so designs
shipped with NO preset status bar and a degraded node vocabulary
(no path/rectangle/text_input), while the same model on a CLI provider
(orchestrator path) produced a polished screen with the full status bar.
Restore opt-in semantics: default = single-shot orchestrator (scaffold +
role-resolver), loop enabled only via the Settings experimental toggle or
OPENPENCIL_DESIGN_AGENT_LOOP=1|true|on. Re-introduce the loop as default once
it grows the same scaffold chrome.
2026-07-05 04:34:50 +08:00
Fini 7ae6721157 fix(ai): drop the 'prefer K()' kit-catalog push from the design-loop prompt
The K() kit-catalog guidance added to design-agent.md (prefer K() for
standard controls before hand-drawing primitives, + a 37-entry catalog)
regressed loop generation: the model abandoned its working hand-drawn
primitives (text_input, rectangle, icons) to chase kit instantiation that
doesn't land well in the loop, degrading output to a churn of bare
frames+text. Revert the prompt push to the pre-catalog state; the K() op
itself stays available in the DSL, just not pushed.
2026-07-05 04:01:09 +08:00
Fini c734f98dfb fix(orchestrator): recognize CJK-named bottom nav + dedup duplicate bottom-nav sections
Chinese-prompt mobile designs rendered two bottom navs (e.g. 底部导航栏 +
Bottom Navigation). The mobile-chrome bottom-nav matchers were
English-only, so a CJK-named nav was not recognized — it wasn't anchored
or merged, and a second English nav survived. Add Chinese synonyms
(底部导航/底部导航栏/导航栏/底栏/标签栏/底部标签栏) to both matchers, and add a
root-level dedup that keeps the bottom-anchored nav and removes redundant
bottom-nav sections. Wired into cleanup + loop_finalize (both paths).
Narrow guards: mobile artboards, >=2 detected navs, never the sole nav,
top navbars untouched.
2026-07-05 02:40:10 +08:00
Kayshen-X 2e8bf40f47 fix(editor): dismiss the Effects add-menu on outside click on web
The native press path closed the new Effects "+" add-menu on an
outside click, but the web host had no matching dismiss, so the menu
could get stuck open. Mirror the fill-type-picker dismiss: apply a row
action, swallow inside clicks, close on any outside click.
2026-07-04 21:52:27 +08:00
Kayshen-X 3480abaa52 feat(editor): merge effect adds into one "+" with a Shadow/Blur menu
Replace the two effect add-buttons with a single "+" that opens a
Drop Shadow / Layer Blur choice menu. The "+" toggles the menu; a row
click adds that effect and closes; Escape or an outside click
dismisses. Wired on both native and web hosts.
2026-07-04 21:47:35 +08:00
Kayshen-X a77ecf5c71 refactor(editor): split color_picker tests into a sibling file
The effect-add fixes pushed color_picker.rs past the 800-line ceiling.
Move its test module into color_picker_tests.rs via #[path]; no
behavior change.
2026-07-04 20:59:22 +08:00
Kayshen-X 34257a65c5 fix(editor): skip history snapshot when the effect target can't take one
Committing history before checking the target left an empty undo +
dirty state when the selection was a component Ref / IconFont (or an
unresolvable anchor) — nodes that carry no effects list. Route both
adds through a shared add_effect_to_selected that peeks node support
first and only snapshots history when the mutation will land.
2026-07-04 20:52:51 +08:00
Kayshen-X c143f599b0 fix(editor): make effect adds undoable + wire Layer Blur on web
The Layer Blur add-button did nothing on the web host (its property
dispatch had no AddLayerBlur arm) and neither effect add was undoable
on any host (the add path never snapshotted history). Commit history
inside add_drop_shadow_to_selected / add_layer_blur_to_selected so both
adds undo on every host, and handle AddLayerBlur in the web dispatch.
2026-07-04 20:43:12 +08:00
Kayshen-X d187eb724d feat(editor): add a Layer Blur button to the effects section
The effects section's "+" only ever appended a Drop Shadow, so there
was no way to add a Gaussian layer blur from the panel. Add a second
add-button (blur-circle) beside the "+" that appends a default
PenEffect::Blur to the selected node, backed by
add_layer_blur_to_selected / push_layer_blur. The two add-buttons emit
non-overlapping hit rects so a click resolves to one effect kind.
2026-07-04 20:32:30 +08:00
Kayshen-X db20f1890c feat(editor): render imported Gaussian layer blur
Figma "Layer blur" effects were imported into the canonical document
but dropped before rendering (the scene carried only drop shadows), so
blurred background shapes rendered sharp. Carry the layer-blur radius
through the adapter (NodePayload.layer_blur) into an Effect::Blur, and
wrap the node's paint in a Skia blur layer (save_layer + blur image
filter, sigma = radius/2 × zoom) at every paint return path. Bumps the
vendored jian for the scene Effect::Blur variant + painter blur hook.
2026-07-04 20:05:42 +08:00
Fini ab2a32a806 feat(mcp): add K() batch_design op to instantiate built-in UI kits
Generation could only hand-draw components: the built-in shadcn (31) and
starter (6) UI-kit components were reachable solely through the retired
insert_* MCP tools, never from the design pipeline. The batch_design DSL
already instantiates via C(), but C() clones only nodes already present
in the document, so it can't reach the kits.

Add a K(kitComponentId, parent, overrides) op that routes through the
existing (tested) InstantiateKitComponent command, now extended with
parent-aware placement + overrides. Kit ids use a shadcn/<id> and
starter/<id> scheme. Overrides support top-level keys, recursive
descendants matching by template id (reusing ref_resolve::apply_overrides),
and a label/text convenience; applied before the fresh-id remap. The
sandbox script runner records K() like I()/C(). design-agent.md gains a
compact 37-entry catalog nudging the model to prefer K().

Note: mechanism is verified (unit tests + full gate run); whether the
model adopts K() and improves output is a pending self-loop measurement.
2026-07-04 19:51:59 +08:00
Fini 1db44c035c style(desktop): order re-exports so cargo fmt --check passes 2026-07-04 19:08:15 +08:00
Fini 61a5f1cc40 fix(orchestrator): give saturated accent fills a white foreground
Button/icon foreground was chosen by perceptual luminance alone
(`lum < 0.5 ? white : dark`). A raw-hex saturated fill like #F97316
scores 0.567 so it got a dark #0F172A icon — the reported orange-bg +
dark-icon defect. Only $color-accent-style TOKEN refs were rescued;
raw hex leaked. Add saturation-aware `preferred_foreground_for_bg`
(white when luminance<0.5, or saturation>=0.5 at luminance<=0.72) and
route the button pass through it. Pale amber warning fills and bright
yellow keep dark text. Runs on both the design loop (loop_finalize) and
orchestrator (cleanup) paths.
2026-07-04 19:08:14 +08:00
Kayshen-X cbeed1ce3a feat(editor): shrink auto-width text to fit its box on font fallback
An auto-width (non-wrapping) text node's box was sized by the authoring
app to hug the text in its own font. When that font is unavailable and
a wider fallback is used, the text overflows and gets clipped by the
parent frame (e.g. imported Figma titles clipping to "No. 3 dr").
Scale the font down so the widest line fits the authored box, floored
at 35% so it stays readable. Wrapping text is untouched (it breaks
lines instead).
2026-07-04 19:04:12 +08:00
Kayshen-X eb853f6ccf refactor(editor): split layer-panel hit-test into a sibling file
The pages-row-height scroll fix pushed layer_panel.rs past the 800-line
ceiling. Move hit_test + drop_target_at into layer_panel_hit.rs via an
impl block; no behavior change.
2026-07-04 18:58:45 +08:00
Kayshen-X ec45fd99c1 fix(editor): use page row height for pages-region scroll window
The Pages and Layers regions use different row heights (32px vs 28px),
but visible_row_range / row_index_at hardcoded LAYER_ROW_HEIGHT for
both. With a long page list scrolled toward the bottom, the pages
window started at the wrong index — only the last page rendered and
the rows above it were skipped, leaving a blank gap. Thread the
region's row height through both helpers.
2026-07-04 18:50:36 +08:00
Fini 7536627056 fix(desktop): gate test-only re-export so clippy -D warnings passes
The narration-collapse change switched the pump to
apply_poll_to_message_with; apply_poll_to_message is now used only by
the cfg(test) test module, so the bin build re-exported it unused and
`cargo clippy --workspace --all-targets -- -D warnings` (Rust Check)
failed. Move it under the existing #[cfg(test)] re-export alongside
ChatPoll.
2026-07-04 18:47:58 +08:00
Fini 0471f95f6f docs(orchestrator): stop overstating Pencil alignment on the fallback path
The script-gen comment claimed '完全对齐 Pencil' for what is now the
orchestrator single-shot FALLBACK path — it aligns only the output
protocol (a JS DSL like Pencil's batch_design), not Pencil's defining
per-batch feedback loop, which lives in the sonar design-agent loop
(the builtin-provider default). Corrected per the alignment audit.
2026-07-04 18:29:43 +08:00
Fini 66b4cf8409 feat(editor): collapse the design loop's model narration
The design agent loop streamed the model's free-text chatter between
tool calls ('Let me build the header... Now the deals section...')
into the visible transcript bubble — noise, since the tool-call
checklist already shows clean progress (measured on a MiniMax-M3 run).
A design-loop ChatSession now folds that narration into the collapsed
thinking area instead; plain chat and CLI turns keep it visible, and
errors always surface in the bubble regardless.
2026-07-04 18:23:43 +08:00
Fini 2973c32dd0 feat(orchestrator): remove abandoned duplicate root artboards
A weak model on the loop path can rebuild-and-abandon at the artboard
level, leaving two same-named top-level frames — a sparse opaque stub
(a few nodes) overlapping the real design. The stub's fill covers the
real artboard's top, blanking it. Detect same-named overlapping roots
where one holds under 30% of the other's descendants and drop the
sparse stub, keeping the rich one. Side-by-side authored roots, single
roots, and two comparably-full roots are left alone. Runs in both the
loop finalize and the orchestrator cleanup.
2026-07-04 18:16:19 +08:00
Fini ea3a759e39 test(orchestrator): update the stale unresolved-ref button test
The accent-token contrast fix intentionally makes $color-accent
buttons flip children to white instead of skipping; retarget the
skip-on-unresolvable test at a genuinely non-accent token so it still
guards that path.
2026-07-04 18:07:10 +08:00
Fini 3f6f9d157d fix(orchestrator): flip dark icons to white on accent-token buttons
The button-contrast pass bailed whenever the button fill was a design
token (resolve_color_maybe_ref returns None for any $ref), so an
accent-filled button — the common orange filter/action button — kept
the model's default-dark icon (measured: a sliders icon at #0F172A on
a $color-accent button, unreadable on orange). Brand-accent tokens
($color-accent / primary / danger / error / success) always bind to
saturated mid-dark colours needing a white foreground, so treat that
bg as dark and let the existing override flip the children. Surface /
warning tokens are left alone.
2026-07-04 17:49:10 +08:00
Fini 2e21d94ff6 feat(ai): make script-gen the only subagent output protocol
Retire the flat-JSONL retry rung. Every subagent rung now emits a JS
program (script-gen); reduced_complexity and minimal_skills only
narrow the loaded skill set — they no longer switch the output format
to positional _parent JSONL, whose omittable parent field collapsed a
whole tree into flat siblings when a model skipped it. script-gen's
I(parent, node) makes parenting a positional argument that cannot be
dropped, and the reasoning-harvest fix made it robust across models.
parse_nodes stays for the modify/chat paths that still consume flat
node JSON; the jsonl-format generation skills are removed.
2026-07-04 17:31:22 +08:00
Fini eaa979d01c fix(renderer): paint the children of rectangle containers
A rectangle is a container in the canonical schema — it carries
clipContent like Frame/Group and models nest content inside one (an
image-area rectangle wrapping a photo, a card body, a badge holder).
The painter's NodeKind::Rect branch drew the rectangle's own fill and
returned without recursing, so every child of a rectangle vanished
behind that fill. Measured: an AI-generated travel page whose seven
destination photos each sat inside an image-area rectangle rendered
as blank cards despite the photos being fetched and embedded. Recurse
into the children (honouring clipContent) after the rectangle's own
paint, mirroring the Frame branch.
2026-07-04 17:25:58 +08:00
Kayshen-X 9471b7b1ab fix(figma): skip swapped instances in derived pre-seeding
A component-swapped instance (overriddenSymbolID present) carries
derivedSymbolData for the swapped-in component, not its base symbolID.
Pooling or geometry-seeding it under the base component's cache pinned
wrong pk→node mappings that poisoned genuine base-component instances
reusing that cache. Skip swapped instances in both seeding passes.
2026-07-04 16:48:14 +08:00
Kayshen-X 7885e25ab4 fix(figma): drop stale pre-swap derived so swapped icons render fully
A nested instance swapped via overriddenSymbolID keeps the pre-swap
component's derivedSymbolData alongside the swapped-in component's. When
the two frames are the same size the fingerprint can't tell them apart
and the stale (earlier-listed) cluster hijacks the mapping, sizing the
swapped frame wrong and clipping its icon. Cluster the derived entries
by localID and keep only the one that geometrically fits the swapped
subtree. Adds a near-exact geometric-match bonus so a near-perfect size
match outweighs the walk-order prior a stale sibling would otherwise
win on. Splits instance.rs (swap_filter.rs) and fingerprint_tests.rs
(foreign_tests.rs) to honor the 800-line cap.

Renders the Test.fig Sales-card pie icon and the other swapped card
icons at their correct size instead of clipped.
2026-07-04 16:37:48 +08:00
Kayshen-X eacc2ccbf0 fix(figma): evidence-based instance override routing + strong-fill fingerprint
Replace the walk-order virtual-GUID guessing in Strategy 2 with an
evidence-based fingerprint (size / transform / text-class / fill hints)
plus cross-instance pooled + geometry pre-seeding, so nested instance
overrides land on the right nodes. Adds foreign-session subtree
anchoring with a uniform-family fallback, nested-derived field merge,
strong fill-hint routing (image / rare-solid) with conflict-vs-
inapplicable rejection handling, and a single-axis transform-drift
score tier. Splits instance.rs walk/apply helpers into apply.rs and the
test module into fingerprint_tests.rs to honor the 800-line cap.

Renders Test.fig order-row thumbnails, breadcrumb, sidebar logo,
summary-card filters, and status chips to match the Figma source.
2026-07-04 16:37:47 +08:00
Fini 05e3326336 chore(ai): drop the now-unused loop opt-in flag parser
Flipping the loop to default-on left parse_loop_flag (the truthy
parser) with only test callers; parse_loop_off carries the opt-out.
2026-07-04 16:26:14 +08:00
Fini 9de2249164 fix(ai): stop a stray selection hijacking a new-design prompt
A section-heavy new-design prompt ('Design a … page. Include a search
section …') trips is_section_add_request, so requests_new_whole_screen
returns false; the selection-modify bias then routed the whole prompt
into run_modify_turn, where M3's flat-JSONL output was renest-dropped
to nothing ('Could not parse design nodes'). Add a section-add-blind
creation-signal veto to both routing gates so a new-design request
reaches the design pipeline regardless of an active selection.
2026-07-04 16:26:13 +08:00
Fini 0dd7277b78 fix(mcp): harvest the script past reasoning and prose preamble
The script runner only stripped a code fence anchored at position
zero, and never stripped <think> reasoning at all. A reasoning model
that keeps its thinking (MiniMax-M3 rides Adaptive) prefixes the
program with a <think> block full of draft JS plus a prose lead-in;
that went to QuickJS verbatim as source, threw a syntax error, and
dropped the model onto the fragile flat-JSONL retry rung where
omitted _parent fields collapse the whole tree into flat siblings
under the root (measured: a full travel page, 44 nodes, all piled at
origin). Strip reasoning first, then extract the fenced block from
anywhere in the response. Models with thinking disabled (GLM) were
unaffected, which is why this read as GLM-only handling.
2026-07-04 16:04:47 +08:00
Fini aa6857365a fix(ai): disable reasoning on direct-modify turns
Reasoning models burn the whole output budget inside think blocks and
emit zero design nodes on a modify turn (measured on MiniMax-M3: the
turn died in analysis prose). Same policy the design subtasks already
use; the HTTP layer maps it per provider.
2026-07-04 15:42:50 +08:00
Fini 29a5801621 feat(ai): make the design agent loop the default generation path
The A/B measured the loop ahead of the single-shot orchestrator on
audit cleanliness (11 vs 33 issues over 10 prompt pairs) at
comparable wall time, with the artboard seed guard closing its one
failure mode. OPENPENCIL_DESIGN_AGENT_LOOP=0|false|off opts back into
the orchestrator; CLI providers keep their existing path.
2026-07-04 15:42:49 +08:00
Fini 3994d0aee2 test(ai): restore the web-app negative regression for artboard seeding 2026-07-04 15:17:20 +08:00
Fini c110ca2387 test(ai): align the stale mobile-seeding assertion with the web-app guard
A delayed write from a dead editing session re-added an assertion
encoding the pre-guard behavior (dashboard 'web app' treated as a
mobile ask); the negative case is speced by the sibling test.
2026-07-04 15:14:09 +08:00
Fini abbabd7662 fix(ai): restore the web-app guard in mobile artboard detection
The guard landed in the previous commit's tests but its function body
got clobbered by a stale editing session between validation and
commit; a dashboard 'web app' seeded 390x844 again.
2026-07-04 15:03:37 +08:00
Fini 3b15add38c feat(ai): seed a numeric artboard when the first loop batch skips it
Weak models sometimes ignore the seed-the-artboard-first instruction
and leave the root frame sizeless, collapsing the whole screen into a
thin strip (measured on a travel-app A/B cell). The executor now
seeds missing root axes deterministically on the first applied batch
— 390x844 for mobile asks, 1440x900 otherwise (a dashboard 'web app'
is not a mobile ask) — tells the model about the contract in the tool
result, and leaves authored numeric axes alone.
2026-07-04 14:34:55 +08:00
Fini a8201226d9 fix(orchestrator): stop ring labels wrapping inside their forced width
The 0.56em/char estimate under-measures wider platform font stacks;
on Linux the centered label wrapped inside its authored width and
sank 6px below the ring center. Give the forced width single-line
headroom and let the sentinel tolerate single-line metric variance.
2026-07-04 14:27:36 +08:00
Kayshen-X 04e3cbcc69 chore(jian): update submodule to rebased main 2026-07-04 13:31:12 +08:00
Kayshen-X c7fc20be90 fix(canvas): seed bound widgets on screen switch so persisted values render on remount 2026-07-04 13:23:31 +08:00