Commit graph

1947 commits

Author SHA1 Message Date
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
Fini cd2131ce28 feat(smoke): minimal scaffold seed for loop (OPENPENCIL_SMOKE_LOOP_SEED) reusing build_fallback_plan; data-gate harness for orchestrator-vs-loop migration 2026-07-02 21:21:28 +08:00
Fini 958c544293 feat(ai-skills): design-agent loop teaching prefers emit_elements (high-level element kinds) over raw batch_design primitives 2026-07-02 21:21:27 +08:00
Fini 03fbc8fbd8 feat(host): emit_elements loop tool — element-manifest builders (el:kind→role-tagged subtree) as a 15th design tool, reusing op_orchestrator::manifest::parse_manifest 2026-07-02 21:21:26 +08:00
Fini 02f31477b9 feat(ai-skills): budget pin for forced skills (trim_by_budget_pinned) + dual-protocol (type:ref/el:ref) component-composition teaching 2026-07-02 21:21:25 +08:00
Fini cc2846d618 feat(orchestrator): force-include component-composition teaching when library present + el:ref/type:ref protocol-branched manifest 2026-07-02 21:21:24 +08:00
Fini 03d7c9b437 feat(mcp): el:ref component-instance kind in Element-Manifest IR (manifest-arm composition → PenNode::Ref) 2026-07-02 21:21:23 +08:00
Fini 0c2fdf8650 feat(loader): merge library masters onto separate Components page (avoids active-page pollution / dashboard scaffold collision) 2026-07-02 21:21:22 +08:00
Fini a67723fbc4 feat(editor-core): append_components_page_masters — design-kit masters on a hidden Components page, active design page stays clean 2026-07-02 21:21:21 +08:00
Fini 77f60a4efb fix(editor-core): ref slot-children fallback — instance inline children fill empty-master shells (Pencil slot-component pattern)
Pencil's slot-component pattern uses an empty styled master (slot:[...] + zero children)
with the instance supplying content via its own inline children. expand_ref only
populated children from the master, dropping instance content → empty shell → blank
white circle in converted templates. Now falls back to instance children (remapped)
when the master has none. Fixes converted-template stat-card rendering + composed-ref
content. +2 ref_resolve tests +1 variables_resolve nested-token regression guard.
2026-07-02 21:21:20 +08:00
Fini 72dbf535d5 chore(editor-ui): chat panel + checklist working-tree progress (WIP) 2026-07-02 21:21:19 +08:00
Fini 5b612ed6c6 feat(smoke): headless agentic-loop mode (OPENPENCIL_SMOKE_LOOP) + design-library loading for benchmarking 2026-07-02 21:21:18 +08:00
Fini 7b1307d72e feat(ai-skills): integrate harvested Pencil guide depth (web-app/mobile/landing/slides/dashboard/design-system) + component-composition + shader-fill teaching 2026-07-02 21:21:17 +08:00
Fini c80f43dbf5 feat(desktop): chat-session loop bridge (finalize/spawn) + smoke library load + image downscale 2026-07-02 21:21:16 +08:00
Fini ed8f763af4 feat(host): agentic-loop screenshot/finalize/spawn bridge + real vision providers + smoke library env 2026-07-02 21:21:15 +08:00
Fini 31ca7c1168 feat(orchestrator): loop-finalize backstop + concurrent spawn-agents + vision validation providers + component-manifest prompt 2026-07-02 21:21:14 +08:00
Fini 89fbee2095 feat(editor-core): mesh/shader fill state + ref id-remap + chat-session state 2026-07-02 21:21:13 +08:00
Fini 41f46ff0d2 feat(mcp): batch_design ref-node emission + full PenFill passthrough + set_node_stroke_side_width tool + get_editor_state 2026-07-02 21:21:12 +08:00
Fini 8adb09220c fix(lint): typography detector adjustment 2026-07-02 21:21:11 +08:00
Fini e6c14f9550 feat(render): skia mesh-gradient via draw_vertices + SkSL shader paint (shader_cache) 2026-07-02 21:21:10 +08:00
Fini bf4a741d8e feat(loader): mesh/shader fills + .pen→.op legacy normalize hardening + pen2op bin + design-library merge
Bumps jian submodule for mesh-gradient/shader PenFill variants. normalize_legacy_doc
gains 8 .pen-compat repairs (stroke-fill string, gradient fill, icon/prompt nodes,
$ref cornerRadius, single-object fill). New pen2op bin converts Pencil .pen→.op.
library.rs merges a .lib.op's reusable masters+variables into a working doc.
2026-07-02 21:21:09 +08:00