Commit graph

1967 commits

Author SHA1 Message Date
Kayshen-X 16ef8db360 feat(editor): host doc-root app-state hoisting in op-editor-core
Minimal slice of the in-flight app-state work that the script-gen
series depends on (subagent post-processing calls
op_editor_core::hoist_app_state). The orchestrator-local copy is
retired when the protocol collapse lands; the rest of the app-state
feature stays in the working tree, uncommitted.
2026-07-03 22:08:18 +08:00
Fini dd034b030b feat(orchestrator): teach the geometry detectors design intent
Corpus-run refinements, each anchored to a measured case:
- a JAM participant must be a text-bearing CONTAINER cell — two bare
  text siblings set tight on purpose (price + unit) are typography;
- the row-gap repair also covers a TWO-column jam (date column against
  a details stack at 0px) when the row top-packs;
- text overflow now triggers on the right EDGE, catching the combined
  overflow a width-only check misses (avatar + name pair);
- one sibling centered inside another is an intentional overlay (a
  number set on a ring), not an overlap accident;
- drop an unreachable duplicate match arm the merge left in the lint
  contrast detector.

Corpus re-audit: 51/52 clean as generated, 52/52 after finalize.
2026-07-03 03:02:43 +08:00
Fini 3bce0ea5d8 fix(orchestrator): stop planning truncation from shipping skeleton designs
The desktop LLM adapter capped design turns at 8192 output tokens; a
rich plan truncated mid-JSON, the parse failed, and the heuristic
fallback shipped a skeleton (hero + three cards) with no visible error.
Raise the budget to the headless-harness value that ran a 52-prompt
corpus with zero truncations (16384; 24576 for M3 which spends budget
on reasoning), and give planning a second attempt before the fallback —
a truncated stream or transient blip usually parses fine on retry.
2026-07-03 03:02:42 +08:00
Fini 0d326ec9a5 style(orchestrator): appease clippy doc-indent and lifetime lints 2026-07-03 02:17:05 +08:00
Fini caf80d41ec feat(orchestrator): geometry-proven row gaps + rigid fit-child shrink
Self-loop run v1 findings, fixed at the root:
- geometry row-gap repair: a horizontal row whose >=3 text-bearing frame
  cells ALL resolve jammed gets a gap injected — name-blind, so rows
  buried under any depth of unnamed wrappers are covered (the name-gated
  table pass also now recurses through wrapper chains as a fast path).
- rigid fit_content children overflowing a narrow flex parent are
  retargeted to fill_container (fit never shrinks; an icon+text pair
  painted over its siblings inside an 80px card); the text inside then
  wraps via the text-overflow fixer on the next loop round.
- prompt: one brand name reused verbatim across logo/footer/sample data.

Offline replay of both v1 artifacts: 12 audit issues -> 0.
2026-07-03 02:11:53 +08:00
Fini 2dca4d44c0 fix(ci): drop the retired packages/agent-native gitlink
The TS-side packages tree was retired on the remote but the submodule
gitlink survived without its .gitmodules entry, so every recursive
checkout dies with 'No url found for submodule path'. The native agent
runtime lives in vendor/agent.
2026-07-03 01:31:48 +08:00
Fini f04c724c09 merge: sync mesh/shader rendering, web live-sync and platform fixes from remote
Conflicts were the two mesh/shader implementations meeting: kept the
remote's newer complete version (typed shader uniforms, shader color
uniform binding, mesh vertex editing defaults, status-bar shell stroke
handling); deduped two identically-replayed RenderBackend methods.
2026-07-03 01:25:08 +08:00
Kayshen-X 10cb199129 feat(canvas): thread mesh/shader fills through the editor scene path
The backend + dispatch wiring landed, but the real editor scene
builder (editor_state_to_layout_scene → pen_document_to_payload)
still collapsed PenFill::MeshGradient / PenFill::Shader at the
extraction layer — fill_type mapped to "solid", the gradient payload
only knew linear/radial, and SceneNode.shader was hardwired None —
so the new painters were unreachable from an actual document. Extend
the payload chain end to end: GradientPayload grows a Mesh variant
(row-major lattice, sparse vertices stay transparent) and NodePayload
grows a resolved ShaderPayload (uniforms pre-expanded, colour hex →
premultiplied vec4, fallback = first colour uniform else mid-gray),
both mirroring jian-core's canonical try_mesh_gradient / try_shader
rules; fill_type maps "mesh"/"shader" into the scene enum; node
opacity folds into the shader alpha like the gradient path. The flat
`fill` now matches the shader fallback so painters without SkSL (web)
show the same colour. Reachability is pinned by scene-threading tests
driving the real `.op` → EditorState → scene chain.
2026-07-03 00:08:27 +08:00
Kayshen-X 2d0bc74cbe feat(canvas): render mesh-gradient + sksl shader fills natively
The mesh/shader migration only papered over the compile errors: the
canvas dispatched Mesh bodies to a flat first-vertex fill and never
dispatched SceneNode.shader at all, even though the jian Painter trait
already carries fill_round_rect_mesh_gradient / fill_round_rect_shader
with real Skia implementations available. Port jian-skia's Gouraud
vertices + cached-RuntimeEffect paint onto NativeBackend (sibling
mesh_shader.rs, ShaderCache shared with jian-skia so sources compile
once), delegate through NativeFrameBackend, and route the canvas fill
dispatch through the trait methods — shader wins over gradient over
solid. CanvasKit/web inherits the trait's first-vertex / fallback-solid
defaults (documented parity gap, same as jian's own non-native
painters). Pixel-level raster tests prove interpolation actually
happens and a compiled shader beats its fallback.
2026-07-03 00:08:26 +08:00
Kayshen-X c0e4f654d2 chore(ci): ignore quick-xml dos advisories pinned by wayland/zbus
RUSTSEC-2026-0194 / -0195 flag quick-xml < 0.41 for XML-parsing DoS.
quick-xml only reaches this tree through wayland-scanner (build-time
parse of the bundled wayland protocol XML) and zbus_xml/ashpd (local
D-Bus introspection replies) — never attacker-controlled input — and
both pin quick-xml requirements below 0.41, so there is no upgrade
path yet. Documented ignore, same pattern as the backoff/ttf-parser
entries; revisit when the wayland/zbus stacks bump quick-xml.
2026-07-03 00:08:25 +08:00
Kayshen-X 7607f061ac ci: trigger rust workflows on vendored-dep changes
PRs touching vendor/casement (the winit fork) or vendor/agent rode
green with zero CI: rust-check's pull_request paths had no vendor
entry and rust-multiplatform only listed vendor/jian. Add them, and
refresh the Linux test-step comment now that the GPU smoke tests run
un-ignored via the provider GL loader.
2026-07-03 00:08:24 +08:00
Kayshen-X 70dac19008 feat(web): undoable live-sync applies + agent-indicator relay
Two web-parity gaps from the audit:

- AI/MCP writes were not undoable in the browser: the live-sync pull
  replaced the document through replace_document, which resets History.
  EditorState grows replace_document_with_undo (same stale-draft
  clearing, pre-replace snapshot pushed as one undo step); the glue
  uses it for every apply after the mount-time first pull, gated on
  WebSyncClient::initialized().

- Agent-team indicators never showed on web: design runs execute in
  the serve-web daemon, whose process-global registry the browser wasm
  can't see. The registry gains a JSON relay (relay_json /
  parse_relay_json / apply_remote — reveal timestamps insert-only so
  locally rebased clocks survive re-polls), the daemon serves it at
  GET /api/mcp/indicators, and the web host polls on the live-sync
  cadence, mirrors into its local registry, and drives a
  self-terminating rAF pump so reveal/breathing animations play
  between DOM events.
2026-07-03 00:08:23 +08:00
Kayshen-X 8f1be67ab4 feat(web): hide desktop-only entry points on the web host
The image Search/Generate buttons, the six MCP CLI-integration
toggles, and the auto-update card all painted on web with no executor
behind them — clicking did nothing (silent dead UI, worse than a
missing feature). Gate each behind a cfg const following the existing
GIT_BUTTON_AVAILABLE pattern, with paint, hit-test, and layout heights
kept in sync; the experimental-features card moves up into the hidden
auto-update slot.
2026-07-03 00:08:22 +08:00
Kayshen-X 47ff290cf6 fix(desktop): windows console, spawn, and url-opening hygiene
Four Windows runtime defects from the platform audit:

- The binary stayed in the console subsystem, parking a console window
  behind the GUI when launched from Explorer. Release builds now set
  windows_subsystem = "windows"; debug keeps stderr tracing visible.
- Background CLI probes (model discovery, provider version checks) and
  the vendored Claude SDK's per-turn spawns lacked CREATE_NO_WINDOW,
  flashing console windows once the GUI detaches from the console.
- MCP stdio servers naming .cmd/.bat shims (npx and most npm-installed
  servers) could not spawn: CreateProcess cannot execute shims and Rust
  1.77+ refuses them as program names. vendor/agent now resolves the
  command PATHEXT-style against the PATH the server will actually see
  (per-server env override wins) and routes only genuine shims through
  cmd /c — real executables keep direct spawn semantics.
- cmd /C start truncated URLs at `&` (every OAuth authorize URL). The
  URL now travels double-quoted via raw_arg so cmd keeps it literal.
2026-07-03 00:08:21 +08:00
Kayshen-X bde00122d5 fix(native): unblock linux startup — provider GL loader + wayland-dlopen
Two Linux startup blockers:

1. Skia's Interface::new_native() dlopens libGL/GLX, which fails on
   the EGL contexts this project always creates on Linux — GL init
   errored and the app exited (the tracked LINUX_GPU_SKIA_LOADER_TBD
   gap). GlContextProvider now exposes gl_proc_address; the glutin
   provider resolves through its display (eglGetProcAddress on EGL)
   and SharedSkiaContext builds the interface via new_load_with,
   falling back to new_native for providers without a loader. The
   Linux GPU smoke tests are un-ignored (they soft-skip without a
   working EGL stack unless STEP1A_REQUIRE_GPU=1).

2. Dropping casement's default features also dropped wayland-dlopen,
   making libwayland-client a DT_NEEDED hard dependency — the binary
   could not even load on X11-only hosts. Restore the feature so
   libwayland loads at runtime when present.
2026-07-03 00:08:20 +08:00
Kayshen-X 061296c779 feat(render): handle mesh-gradient + shader fills across editor consumers
vendor/jian ff39a41 added PenFill::MeshGradient / PenFill::Shader plus
SceneGradient::Mesh and SceneNode.shader; every exhaustive match over
those types stopped compiling. Cover them with faithful conservative
semantics: opacity getters/setters treat the new bodies like their
gradient siblings, variable binding walks mesh vertex stops (shader
uniforms are untyped — left alone), canvas paint degrades mesh to its
first vertex colour until the RenderBackend grows a mesh method, the
property panel reports the neutral Solid type (no lossy conversion
offered), and lint contrast checks skip both like image fills.
2026-07-03 00:08:19 +08:00
Kayshen-X 0ba60361d4 fix(canvas): don't arm an erase frame when a design run finishes empty
finish_if_epoch previously routed the empty-queue case through
drain_finished_run, which set the process-global needs_final_frame flag
even though a run that never queued a reveal never put a cursor on
screen. That stray flag made next_reveal_deadline_ms return a redraw
deadline out of an idle registry, and — because the registry is shared
across the whole test binary — perturbed an unrelated exact
animation-deadline assertion whenever a design-session test dropped an
empty session in parallel.

Clear an empty finish inline without arming the flag; the paint-path
drain still arms it after real reveals prune, where a cursor genuinely
was on screen. Adds a regression test.

Verification note: the op-editor-core suite could not be run for this
commit because the shared workspace is transiently non-compiling under a
concurrent mesh-gradient/SkSL-shader jian bump (new PenFill variants not
yet handled in fills.rs — unrelated files). Change is trace-verified and
touches only agent_indicators; re-run pending the tree compiling again.
2026-07-03 00:08:18 +08:00
Fini 6ed9548c0d merge: land the align-branch work onto the refreshed base
47 commits from the align branch merged onto the force-updated remote
base (which had replayed an earlier snapshot of the same work plus new
overlay/pointer features and CI fixes). Conflict resolution: kept the
newer align side for the generation pipeline (orchestrator, mcp, skills,
design tools), kept the base side for the chat-panel test semantics and
graceful overlay teardown, fused both in sub_agent_session (design-turn
thinking policy + graceful epoch finish), and dropped the files each
side had deleted (legacy concurrent/dashboard paths, retired TS skills).
Deduped two identical replayed hunks (export.rs, chat_session_tests.rs).

Known issue carried over: provider_probe_host::landed_connected_outcome_
without_models_is_failure fails on a host with a live provider config
(env-sensitive test, both sides byte-identical there; green on CI).
2026-07-03 00:08:17 +08:00
Fini 6842d0fcca chore(smoke): audit mode, reasoning gate for glm, self-loop harness
- OPENPENCIL_SMOKE_AUDIT=<file.op>: load the doc, run the real-layout
  geometry diagnostics, print a JSON report, exit non-zero on issues —
  the machine-checkable leg of the generate→render→audit loop.
- extend the harness thinking-disable gate to glm models (reasoning
  burned the whole token budget and returned empty content; an
  orchestrator sidebar subtask failed 3x and shipped missing).
- scripts/self-loop.sh: prompts file → generate → render (the real
  canvas pipeline) → audit → scorecard.json, fully unattended.
2026-07-02 21:46:35 +08:00
Fini 2db7d0bb7c feat(ai): act-on-layout-feedback rule, avatar headshot queries, table density bar
- design-agent: layoutIssues in a tool result are measured facts — fix
  them with a follow-up batch before building the next section.
- avatar image searches must include face/headshot (bare 'portrait'
  returns torsos and cropped bodies).
- dashboard domain: hard completeness bar — data tables carry >=6 varied
  rows and a dashboard ships its full section set.
2026-07-02 21:46:34 +08:00
Fini 4dcbb57c19 fix(editor-ui): make text_input honor its authored box style
paint_text_field used to paint an unconditional white fill, a default
grey border and a 6px radius floor, with near-black value text — a model
embedding an input in its own styled wrapper zeroes all of that out
(fill:[], stroke thickness 0, cornerRadius 0) and got a glaring white
pill with a duplicate icon on a dark themed search bar. No authored fill
now paints no box, no authored stroke draws no border, radius is taken
as-is, and the value text color adapts to the authored fill's luminance.
2026-07-02 21:46:33 +08:00
Fini 4065364f3b fix(orchestrator): reach table column gaps through unnamed wrappers
A model groups [toolbar, rows-wrapper] inside the table frame; the
gap-less rows all lived behind that unnamed vertical wrapper, so the
name-gated column-gap pass never saw them and columns rendered touching
('Oct 24, 202442Marcus Thorne'). The table name gate stays on the outer
node; an unnamed structural wrapper now inherits it, for both the gap
injection and the container row-count gate.
2026-07-02 21:46:17 +08:00
Fini 1e4b4085ea fix(orchestrator): hug non-positive root heights + salvage failed subtasks
A plan's rootFrame.height is often 0 ('compute from content'); the
desktop path let that literal 0 through, so every fill_container
descendant resolved to 0px mid-pipeline and the geometry pass demoted a
healthy fill-height sidebar before adjust_root_height ever assigned the
real number (footer floated mid-page on three consecutive runs). Map
non-positive scaffold root heights to fit_content; the final adjust pass
still writes the definitive numeric height. Golden end-to-end test uses
a height-0 plan.

Also add an end-of-run salvage pass: transient provider failures (network
blips, empty responses) used to burn all 3 back-to-back attempts and the
section shipped missing with no visible signal; every failed subtask now
gets one late retry after the others complete.
2026-07-02 21:46:16 +08:00
Fini d597775b46 feat(agent): attach per-batch layout feedback to design tool results
Every mutating batch_design / emit_elements result now carries
layoutIssues (real-layout geometry diagnostics) plus an actionable hint,
so the model sees each batch's geometric consequences immediately and
repairs them in-process instead of piling defects up for the loop-end
finalize. Verified end-to-end: the model received a table-overflow
report, recomputed its column widths and fixed them in the next batch.
2026-07-02 21:46:15 +08:00
Fini ada50813d6 feat(orchestrator): geometry diagnostics + post-pass overhaul
- geometry_diagnostics: report-mode counterpart of the fix loop (collapse,
  table overflow, text/frame overflow, sibling jam with row-cell gating),
  powering per-batch layout feedback and the audit gate.
- new fixer: a numeric-width child resolved wider than its flex parent is
  retargeted to fill_container (an 800px avatar bar inside a ~550px row
  spilled across the design).
- retire the tree-shape circular-height demoter: the layout engine now
  resolves a fill-height child of a hugging parent to its content size on
  both axes, so the collapse it guessed at no longer exists while its
  demotions broke healthy shells; real collapses stay covered by the
  geometry loop. Engine-contract sentinel test added.
- theme variable polarity repair: a dark design leaving stock light
  values in surface-family slots (chips resolving #F1F5F9 on #0A0A0A)
  adopts the correct-polarity value from the variable's other theme slot,
  written to the exact slot the resolver reads.
- backdrop sibling merge: an 'active background' authored as a flex
  sibling (empty fill×fill rectangle) moves its fill onto the container
  instead of eating half the row.
- text-fill injection is now tri-state: gradient/image/unresolvable
  backgrounds get NO guessed color (was: mirror-image dark-on-dark).
- env-gated per-pass debug probes + pre-geometry tree dump + repair/
  finalize/rect-probe harnesses for offline diagnosis.
2026-07-02 21:45:56 +08:00
Fini c1fb08f158 fix(mcp): fall back to root insert for phantom parent refs
A generated program that parents its first line onto a binding that was
never created used to ride into InsertAuthoredSubtree unvalidated; the
host's existence check then rejected the WHOLE otherwise-valid program
and the section was lost to retries. Insert at the document root instead
and surface a warning in the envelope.

Also: drop the ambiguous bare-word 'auto' sizing (schema default wins
instead of inverting intent), and recover word-based textGrowth
spellings (fixed_width_and_height and friends) instead of silently
removing them.
2026-07-02 21:45:55 +08:00
Fini 1e3fdc91e1 chore(deps): bump jian for main-axis fill_container grow
Points the jian submodule at the layout fix that grows a main-axis
fill_container child (a sidebar footer spacer) into the column remainder.
2026-07-02 21:21:52 +08:00
Fini ef4a2f7623 feat(ai): require populated, complete content in the design loop
Generation density was inconsistent — some runs shipped a full dashboard,
others stopped at a header + four stat cards + a two-row table. A vision-less
model cannot use the get_screenshot completeness check (it never sees the
render), so it declared done on a coherent-but-sparse skeleton. Add a text-based
completeness bar it CAN follow: tables need >=6 realistic rows, a dashboard needs
its full section set (stats + primary table + a secondary section), and every
list/card carries varied data.
2026-07-02 21:21:51 +08:00
Fini 6ee624713f fix(orchestrator): fill-height the app-shell sidebar so its footer sinks
An already-row shell whose sidebar hugs its content (height=fit_content) is
only as tall as its nav, so a space_between / fill_container footer inside it has
no room to sink and floats mid-page (measured: a 260x532 sidebar in a 260x1234
row stranded the profile card 700px above the bottom). ensure_split_shell_is_row
now also promotes the sidebar of an ALREADY-horizontal shell to fill_container
height, not just the freshly-flipped case.
2026-07-02 21:21:50 +08:00
Fini 6306b9e4cf feat(orchestrator): wrap text that overflows its resolved block
A fill_container block keeps min-size 0 so a fixed sibling gets its space, but
that lets it shrink below its fit_content text — the text then overflows into
the next column (measured: a 260px sidebar schedule row painted the client name
over the appointment time). Add a geometry-driven pass: when a text resolves
wider than its parent block, constrain it (width fill_container + textGrowth
fixed-width) so it wraps inside instead of overflowing.
2026-07-02 21:21:49 +08:00
Fini dc10e9bcc5 feat(ai): require image fills on avatar/photo slots
The design-loop prompt teaches the G(...) image-fill op but never required
using it, so a model built avatar/photo frames and left them as empty colored
squares. Add a hard rule: every avatar, photo, thumbnail, hero, or logo slot
must get an image fill via G(id, "search", "<subject>") in the same batch
that creates the frame.
2026-07-02 21:21:48 +08:00
Fini d65d70d21d feat(ai): dashboard domain skill + image-prompt guidance
Add a dashboard domain skill and teach models to emit an imagePrompt alongside
imageSearchQuery so a configured generation model produces a rich image while
stock search stays the fallback.
2026-07-02 21:21:47 +08:00
Fini 7459f07d53 feat(agent): image generate-vs-search enrichment
When a generation profile is configured, generate images from the node-bound
prompt; otherwise search stock placeholders. Both paths keep the prompt/query on
the node so it can be re-generated or re-searched later.
2026-07-02 21:21:46 +08:00
Fini d03c2d7dd3 fix(agent): disable reasoning + raise budget in the design tool-loop
The OpenAI-compat tool-loop body never sent the reasoning-off field, so a GLM/
MiniMax reasoning model spent its whole per-turn budget on hidden reasoning and
truncated the design mid-op. Send thinking:{type:disabled} for those models in
the loop body (matching the single-shot path) and raise the per-turn output
budget for headroom.
2026-07-02 21:21:45 +08:00
Fini 32192b9348 feat(orchestrator): geometry validation + layout repair passes
Resolve real layout geometry to correct table-column overflow and collapsed
fill-container heights, iterating until stable. Add whole-root passes: sidebar
app-shell reshape + content eviction + split-shell row layout, table column
gap/regroup, footer sink, surface discipline, scaffold padding. The design-loop
finalize backstop runs these once at loop end and injects background-contrasting
fills for fill-less text. run.rs: continue past a failed subtask and compute
zero-content before cleanup.
2026-07-02 21:21:44 +08:00
Fini 396f75eb42 feat(orchestrator): structured generation protocols
Add a parent-by-reference program DSL and an executable-script protocol with
best-effort per-line parsing, so a truncated or malformed line drops only that
op instead of the whole design. Route protocol selection in prompt/subagent and
normalize flex keywords in parse. Registers the new modules in lib.
2026-07-02 21:21:43 +08:00
Fini 6eb3527ebb fix(mcp): tolerate weak-model batch_design field dialects
Map Figma/flex auto-layout field names (layoutMode/direction/itemSpacing/
strokeWeight and nested {type,gap,padding} / {Horizontal:{…}} layout objects)
onto the flat schema, recover image src from url/source aliases, normalize
textGrowth/sizing keywords, and quote bareword DSL values — so a single
unfamiliar spelling no longer drops the whole node or op.
2026-07-02 21:21:42 +08:00
Fini 9e9cdec7cf feat(smoke): OPENPENCIL_SMOKE_PROGRAM mode runs a batch_design program headless
Reads a program file, runs it through op_mcp::batch_design_snapshot, applies, saves
the .op (postProcess off = raw structure) — the headless harness for benchmarking
the program-DSL path vs flat JSONL across weak models.
2026-07-02 21:21:41 +08:00
Fini b43cee87e5 feat(orchestrator): program-DSL generation protocol (parent-by-reference)
OPENPENCIL_PROGRAM_GEN makes the sub-agent emit a batch_design program
(binding=I(parent,{...})) instead of flat _parent JSONL; nesting by captured
binding makes weak-model row decomposition / header-only tables near-inexpressible.
Runs the existing op_mcp executor to a section forest. Gated to the full first
attempt; drops the conflicting jsonl-format skill so PROGRAM_FORMAT governs alone.
2026-07-02 21:21:40 +08:00
Fini 618f60df37 fix(mcp): harden batch_design DSL parser against weak-model JSON typos
split_operations now groups by operation-start grammar + bracket depth (not a
quote/bracket state machine), so a stray unbalanced quote can no longer swallow
following operations. parse_json_arg gains lenient repairs for fused close-quote+
comma, missing opening quote on a string value, and unclosed trailing brackets;
normalize_node_shape drops an empty stroke (0-length PenStroke) instead of failing
the whole node. +5 regression tests (369 pass).
2026-07-02 21:21:39 +08:00
Fini b29e50b632 fix(ai): add connect + overall timeouts to builtin LLM HTTP client 2026-07-02 21:21:38 +08:00
Fini b7deb369dc feat(orchestrator): app-shell content gutter + sidebar footer sink 2026-07-02 21:21:37 +08:00
Fini 7956595853 feat(orchestrator): table-repair post-pass + thread root id through cleanup
Weak models emit a table as a header row followed by FLAT row-indexed sibling
cells (R1 Client Cell, R1 Visit, …, R2 Client Cell, …) that render stacked
vertically with full-width status bars. table_repair::regroup_flat_table_rows
groups them into Table→Row→Cell with header-aligned column widths. Detection is
narrow and never guesses (adversarial review caught heuristic-header /
chunk-of-N over-firing on toolbars/feeds): it requires an explicit table header
AND every cell to carry an R{n} row index, aborting on any ragged/ambiguous run.

ReplaceSubtree allocates a fresh root id, so the structural restructures
(app_shell + table_repair) ran via a new apply_root_transform helper that
returns the current root id; run_cleanup_passes threads it into the subsequent
per-root passes instead of the stale id (which they'd otherwise no-op on).
2026-07-02 21:21:36 +08:00
Fini 46fe45a5b0 feat(orchestrator): app-shell restructure for flat sidebar dashboards
Weak models emit a desktop dashboard's sidebar either as a full-width band on a
vertical root or crammed into a horizontal row with every section — both render
broken (the removed bespoke scaffold used to pre-build the two-column root).
app_shell::reshape_sidebar_to_app_shell detects a sidebar dashboard and rewrites
it to a horizontal [sidebar(260) | content-column(fill)] shell, run in the shared
cleanup::run_cleanup_passes finalize point (covers orchestrator + agentic loop).
A layout==vertical|none guard on fix_horizontal_overflow stops it re-widening the
narrowed sidebar. Detection is hardened (strong sidebar token + structural
dashboard-content gate) against restaurant/landing/top-nav/mobile/multi-screen
false-positives; 14 unit + 1 integration test, verified end-to-end via op-smoke.
2026-07-02 21:21:35 +08:00
Fini e830759cab fix(ai): classify product-noun prompts as Design intent
classify_intent matched only creation verbs, so a noun-phrase request like
"Luxury webapp for managing barbershop clients" fell through to the weak chat
loop instead of the orchestrator. Add product/app nouns (webapp/web app/website/
app for/mobile app/admin panel/saas + 网站/网页/小程序/后台) to DESIGN_KEYWORDS.
2026-07-02 21:21:34 +08:00
Fini a4c71ea2c3 fix(ai): disable thinking for reasoning models on design turns
Reasoning models (glm-5.x / minimax) burn their whole token budget on hidden
<think> and emit an empty design when thinking is left on (glm-5.2: thinking≈30k,
text=0). design_turn_thinking_mode forces ThinkingMode::Disabled for any model
whose profile is thinking_disabled, applied across all design-capable paths —
the design-agent loop, the builtin tool-executing chat loop, and sub-agent
spawns. Claude (thinking productive) keeps the chat default.
2026-07-02 21:21:33 +08:00
Fini 4be2355174 chore(render-parity): SSIM/bbox/ΔE per-node diff harness
diff_nodes.py computes windowed-SSIM + bbox-hit + ΔE76 + color_de per node and
classifies each as PASS / PAINT / LAYOUT; prep_parity.sh drives pen2op→.op→
render-shots vs Pencil export_nodes baselines.
2026-07-02 21:21:32 +08:00
Fini fb2ebb62ac fix(loader): .op render-parity — fixed-height clip, status-bar box, path geometry
A clipped frame with an explicit numeric height now honours that height and
clips overflow (layout_repair no longer lets the root grow to content), matching
Pencil's fixed-height artboards. Status-bar shell strokes are suppressed in the
scene path (width-independent) so they don't paint a stray black frame, and
legacy .op path nodes remap geometry→d so authored paths aren't dropped.
2026-07-02 21:21:31 +08:00
Fini 0730e1ccce feat(host): headless render-parity tooling — bundled fonts, margin env, layout dump
Bundle 11 OFL design-font TTFs and register them with jian-skia at startup so
--render-shots reproduces Pencil's glyph metrics without system fonts.
OPENPENCIL_RENDER_MARGIN tightens the export crop to match Pencil export_nodes
(no frame), and OPENPENCIL_DUMP_LAYOUT prints every node's computed rect as JSONL
to diff our layout against Pencil snapshot_layout element-for-element. Bumps
vendor/jian to the bundled-font + Auto=no-wrap render-parity commits.
2026-07-02 21:21:30 +08:00
Fini c179fd2837 refactor(orchestrator): simplify — remove planning mode-rotation ladder, concurrent multi-screen path, and dashboard bespoke scaffold (−6.9k lines)
Data verdict (copy-pencil-verdict.md): weak-model M3 quality comes entirely from the
SEQUENTIAL deterministic core (manifest element-builders + role-resolver + post-passes
+ finalize), exercised at concurrency=1. The 3 scaffold strategies / mode-rotation /
per-subtask retry ladder are removable complexity not paid for by M3. Collapsed to a
single sequential path; concurrent (perf-only) folds to sequential; dashboard folds into
the generic path (cleanup_desktop_dashboard already handles sparse rows). Kept the
deterministic core 100% intact + BufferDocSink/clamp_concurrency (spawn_agents) + the
dashboard normalizer trio (feeds plan_normalize). M3 gate held within noise (50%→ the one
flip re-ran 2/3 PASS; 7/8 prompts identical; no new failures from removed code).
2026-07-02 21:21:29 +08:00