The bracket scan could return a nested children array or a stray field array instead of the real node tree. Earlier heuristics each had a hole: combined-depth missed valid JSON after an unmatched prose bracket, and colon-skip dropped legitimately-labelled outputs (a wrapper object whose node array sits under a key).
balanced_spans now just collects candidate spans robustly: each bracket tried independently (an unmatched prose bracket cannot hide a later array), skip-past so nested sub-spans are not separate candidates, string-aware. parse_nodes tries both the array-form candidates and the bare-object/JSONL collection, then keeps the result with the MOST total nodes including descendants — a full tree (root + children) always beats a stray inner array, with no colon/label heuristic.
Adds regression tests for nested-children, JSONL, unmatched-prose, and labelled-wrapper inputs. 40 parse tests pass. Hardened across Codex stop-time reviews.
The headless benchmark paths reported success even when their output/render step failed: op-smoke still returned SUCCESS when the OPENPENCIL_SMOKE_OUT write failed, and --render-shots returned the GUI-skip 'handled' signal (exit 0) on an unreadable/unparsable .op, an empty page, or a node that failed to render.
op-smoke now exits 4 when a requested save fails; --render-shots exits 1/2 on any read/parse/empty/partial-render failure and only exits 0 when every top-level node rendered. Verified: good .op -> 0, missing file -> 1. Found by Codex stop-time review.
main.rs reached 927 lines (over the project 800-line cap, mostly pre-existing) after wiring --render-shots. Move the 177-line handle_menu_action into a sibling menu_action.rs (same pattern as the widget_host split), dropping main.rs to 748. Behaviour unchanged; the method becomes pub(crate). Also drops the now-unused ActiveEventLoop/Fullscreen imports from main.rs. Addresses Codex stop-time review (touched file over 800-cap).
DeepSeek emits node JSON as JSONL: bare top-level objects, one per line inside a json code fence, with no enclosing array brackets. The bracket scan only saw the inner fill sub-arrays (not node arrays), so the parser returned 'no JSON array found' and DeepSeek produced nothing.
parse_nodes now falls back to collecting top-level brace objects and deserializing each as a PenNode when the array path yields no nodes. balanced_arrays/balanced_end were generalized to a shared balanced_spans(open, close) used by both the bracket and brace scans. Adds a JSONL test; 37 parse tests pass.
Multi-candidate selection picked the candidate with the most nodes, but balanced_arrays also collected nested children arrays. For nested-format output (a single root frame whose children array holds 3 nodes), the inner array could outrank the outer (1 node), returning the children and dropping the real root. Flat _parent-style output (what M2.7 emitted in testing) hid it. balanced_arrays now collects only top-level arrays, skipping past each captured span. Adds a regression test. Found by Codex stop-time review.
Adds a fully-headless pipeline to benchmark built-in-Agent design generation across models, with no GUI or live canvas.
op-smoke: OPENPENCIL_SMOKE_OUT saves the generated PenDocument to a .op; OPENPENCIL_SMOKE_DUMP dumps the raw LLM response for diagnosing weak-model JSON malformations. openpencil-desktop --render-shots FILE OUTDIR SCALE loads a .op, runs the jian layout pass, and exports one cropped PNG per top-level node via export_node_raster (skia CPU raster) for node-only benchmark screenshots.
M2.7 and other weak models emit malformed PenNode JSON the strict parser rejected, losing whole subtasks (Food Delivery restaurants; Dashboard charts-row dropped to 0 nodes and aborted downstream subtasks).
Multi-candidate extraction scans every balanced bracket span and keeps the result with the most nodes instead of grabbing the first open bracket, so reasoning-prose brackets (e.g. step markers) no longer trigger 'expected ident'. Per-element tolerant deserialize keeps valid PenNodes and skips+logs bad elements instead of failing the whole array on one stray node (e.g. a fill of type 'solid' written where a node was expected). Both are strict generalizations; fully-valid JSON is unchanged. Adds tests for both classes; 35 parse tests pass.
The Maximize icon (rightmost of the TopBar right cluster) was painted
but had no hit-test mapping, so clicking it did nothing.
- Add TopBarHit::ToggleFullscreen + a hit-test rect matching its paint
position; add a regression test.
- Native: the host raises a pending_fullscreen_toggle intent (it can't
reach the winit window); app_handler consumes it after the press and
routes through the existing handle_menu_action(ToggleFullscreen) ->
window.set_fullscreen(), the same path the macOS menu item used.
- Web: the WASM host toggles the browser Fullscreen API directly
(document request_fullscreen / exit_fullscreen) — no runner round-trip
and no unconsumed intent.
Right-press menu-open + Delete/Duplicate dispatch kept the multi-selection instead of collapsing to the right-clicked row (web + native). Adds a multi-delete core test.
Extend the op-orchestrator phased design runner (single-screen scaffold, sequential sub-agent execution, dashboard columns, validation-fix apply) and adapt the headless smoke runner to the new InsertSubtree page_id field.
Expand the Rust MCP server toward pen-mcp parity: layered batch_design helpers with TS-shaped structured args, semantic element-alias builders for high-frequency elements, insert/update node-data parsers, and desktop mcp_serve/runtime wiring (incl. a file-path tool) with tests.
Add EditorCommand support powering the MCP layered-design workflow: authored-id subtree insertion for design skeletons, deterministic RefineDesign cleanup, plus batch-page and replace-node apply paths and an InsertSubtree page_id target. Includes command_apply/command_node wiring and tests.
Bring the Rust git panel to TS parity across four surfaces:
- Commit rows expand inline into a semantic diff card (ported
diffDocuments / indexNodesById / engineDiff over serde_json::Value)
instead of navigating to a separate diff page; the card shows the
"~modify N nodes" summary plus a per-node patch list. blob_at_commit
reads the {rev} and {rev}^ blobs to compute base-vs-next.
- Overflow menu (...) with the 5 TS actions — switch tracked file, clear
commit author, remote settings, SSH keys, close repository — backed by
GitOverflowView subviews (tracked-file picker + SSH-keys list) and
remote-settings rows (origin URL + ahead/behind + fetch; no HTTPS token
field, matching TS).
- Commit signature form: empty commits are refused, and committing with no
configured author opens an inline name/email form (Enter saves, Escape
cancels) that writes the repo-local identity before retrying the commit.
- Polish: taller commit-detail card, removed panel shadow, lengthened the
ready panel, and unified the commit-textarea caret blink to the app's
500 ms cadence.
New overflow/SSH/author/remote labels resolve via Document::t with an
English fallback for keys not yet in the locale tables.
The TS top-bar shows the active provider glyph inside a rounded bg-muted
box; the Rust chip painted the bare icon, so a spinning/active provider
read as a floating glyph with no chip affordance. Paint a theme.muted
rounded backdrop (pad 4, radius 5) behind the brand icon(s) to match.