Weak models stop tool-calling 188 element schemas and instead emit a
JSONL element manifest — one {"el":"stat_card",...} declaration per
line. Nesting uses manifest-local line numbers ("in"), node ids are
system-assigned (the id-hallucination class disappears at the protocol
level), and a repairing argument layer replaces strict rejection:
enum synonyms, schema-driven placeholders for missing required params,
plain-string-to-JSON shape coercion, and a bounded second-chance retry
when a builder names its missing arg. Raw PenNode lines stay available
as an escape hatch and raw frames can serve as "in" containers.
- op-mcp: element_manifest.rs — build_element facade over
semantic_alias_node, kind normalization (prefers theme-aware _v1,
light default until palette seeding lands), schema-generated prompt
catalog; every known kind builds from empty args (the never-reject
guarantee, covered by a 94-kind fixture test)
- op-orchestrator: manifest.rs parser (line-tolerant, id injection for
raw subtrees, depth-clamped sections) wired into run_subtask behind
OPENPENCIL_MANIFEST=1; retry ladder falls back to the raw-JSONL path
- op-ai-skills: element-manifest skill (priority 0, replaces the
jsonl-format skills when active) with anti-hand-roll teaching;
build.rs adds rerun-if-changed so include_dir! sees new skill files
- subagent.rs: childless rectangles count as content — skeleton-screen
designs no longer reject as blank
ab-v9 (52-prompt corpus, full orchestration): ark-code-latest and
glm-5.1 hit 85% expected-shape success vs the 58% tool-calling
baseline, at 5-8k prompt tokens per call vs ~19.9k.
First increment of the role-resolver port (spec
openpencil-docs/.../2026-06-06-role-resolver-rust-port.md). Port of
inferRoleFromName + the name-inference path of resolveNodeRole + the
resolveTreeRoles walk from role-resolver.ts.
New `role_infer` module: NAME_EXACT_MAP + NAME_PATTERN_MAP (regex) + the
container-suffix and role-part-word guards, plus the page-chrome-in-card guard
(a card's inner "Header" must not become a navbar) and the tiny-card guard
("Status Dot" must not become stat-card). Runs on each sub-agent subtree in
run_subtask BEFORE the fallback sizing normalize (semantic-before-fallback).
No role DEFAULTS injection yet (that is increment I2) — but setting `role`
already has effect: the existing cleanup passes (nav-surface repair, section
logic) key off node.role, so an inferred navbar/footer is now repaired the
same way an explicitly-tagged one is.
Adds the `regex` dep (already locked at 1.12.3). 12 tests cover the exact /
pattern maps, all guards, explicit-role-wins, and the tree walk.
op-smoke's QueryEngine path can't send MiniMax's `thinking:{type:
"disabled"}` field, so reasoning models (M3 etc.) thought themselves out
of the token budget and couldn't be benchmarked end-to-end. Add a
non-streaming DirectClient (OPENPENCIL_SMOKE_DIRECT=1) that posts
openai-compat directly and disables thinking at the wire for MiniMax —
or any endpoint via OPENPENCIL_SMOKE_DISABLE_THINKING=1 (Volcengine
honors the same param) — so the production thinking-disable path can be
exercised headless without a GUI.
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.
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.
Smoke binary now picks a Provider via `OPENPENCIL_LLM_PROVIDER`:
- `anthropic` (default) — `AnthropicProvider` + `OPENPENCIL_ANTHROPIC_API_KEY`
(or legacy `ANTHROPIC_API_KEY`).
- `openai-compat` (or `openai`) — `OpenAiCompatProvider` +
`OPENPENCIL_LLM_API_KEY` + `OPENPENCIL_LLM_BASE_URL`. Standard dialect;
works against DeepSeek, 火山方舟, 百炼, Moonshot, OpenRouter, OpenAI,
any OpenAI-compat vendor.
`agent` feature flags bumped from `["anthropic"]` to
`["anthropic", "openai"]` to pull in the `async-openai`-backed provider.
Default model selection branches on provider:
- anthropic → `claude-sonnet-4-6`
- everything else → `gpt-4o-mini` (override via
`OPENPENCIL_ORCHESTRATOR_MODEL`)
Verified end-to-end against 方舟 CP / glm-5.1 — pipeline topology works
through to a partial-success `Ok` summary. Full run details + the
non-Claude quality finding (`stroke.fill` accepting bare string vs
expected sequence) archived in openpencil-docs
`superpowers/notes/2026-05-24-orchestrator-headless-smoke-result.md`.
Task #27. Routes user messages through `op_orchestrator::classify_intent`
in `chat_session::launch_if_pending` — `Intent::Design` with a configured
`agent::Provider` launches into the orchestrator pipeline; everything
else (chat intent, or design intent with no Provider) falls through to
the existing `ChatProvider` CLI path.
The orchestrator is `async + &mut EditorState`; the chat path is built
around worker threads + blocking iterators (`ChatProvider::send`). Codex
architectural review picked Option E (UI-owned actor + RemoteDocSink +
ack channel):
- Worker thread owns a `RemoteDocSink` that forwards each `apply(cmd)`
over an mpsc channel to the UI thread, which `apply()`s on the real
`EditorState` and replies with an ack carrying a fresh state snapshot.
- `RemoteDocSink::state()` reads from a locally cached mirror updated
by each ack — covers `EditorCommand::InsertSubtree`'s ID-remapping +
history bookkeeping which must run on the UI thread.
- Two mpsc channels per turn: progress deltas (Planning / SubtaskStarted
/ etc.) into the chat transcript, and `DesignCmdReq` for apply +
undo-batch boundaries.
- `BeginUndoBatch` / `EndUndoBatch` are forwarded as own `DesignCmdOp`
variants so the UI can route them through real history batching once
`op-editor-core` exposes that API (currently no-op, matching
`DesktopDocSink`).
Provider source MVP: `OPENPENCIL_ANTHROPIC_API_KEY` (preferred) or
`ANTHROPIC_API_KEY` from env constructs an `AnthropicProvider`. Model
override via `OPENPENCIL_ORCHESTRATOR_MODEL` (defaults to
claude-sonnet-4-6). When neither key is set, design intent falls back
to the chat-CLI path so the user still gets an answer.
Wiring:
- `main.rs` `current_design: Option<DesignSession>` field next to
`current_chat`; mutually exclusive routing in `launch_if_pending`.
- `app_handler.rs` `RedrawRequested` pumps both `pump_commands`
(apply + ack) and `pump_progress` (deltas + summary); WaitUntil tick
schedules a 33 ms wake when either session is in flight.
- `keyboard_input.rs` Enter / send sites pass both Option<&mut> args.
Provider features: `agent` crate gains the `anthropic` cargo feature in
`op-host-desktop/Cargo.toml` so `AnthropicProvider` is reachable
(previously `default-features = false` left it gated out — chat path
only needed the trait + engine wiring).
3 new `RemoteDocSink` tests cover ack round-trip + closed-channel safety
+ undo-batch signal distinguishability. Workspace 1882 passed / 0
failed. cargo fmt + clippy --workspace -D warnings clean.
The `chat_orchestrator.rs::run_design_request` legacy entry stays for
now (used by future single-shot programmatic callers); the live path is
`DesignSession::start`. Validation providers stay `Skipped*` until
jian-skia `captureRegion` + vision LLM crate land (task #28).
Implements the D′ architecture host-side half:
- New `src/pre_validator.rs` with `LintPreValidator` struct and full
`impl PreValidator` that calls `op_design_lint::detect_and_plan()`
and translates each `PlannedFix → Vec<EditorCommand>` via
`planned_fix_to_commands`.
- `ClearEffects` handled by reading current effect count from
`sink.state()` and emitting N × `RemoveNodeEffect` highest→lowest.
- `SetStroke` decomposes into `SetNodeStrokeHex` + `SetNodeStrokeWidth`
to work around the absence of a full-PenStroke EditorCommand.
- Parity tests in `#[cfg(test)]` (binary crate, can't use tests/) prove
`LintPreValidator + EditorState` produces an equivalent document to
`detect_and_fix` for 5 fixtures (invisible container, text explicit
height, stacked horizontal padding, unexpected rotation, text effect).
JSON comparison normalises away the `children: None→Some([])` side
effect in the EditorState walker.
- `chat_orchestrator.rs` swaps `SkippedPreValidator` → `LintPreValidator`.
- `op-design-lint` added to `op-host-desktop/Cargo.toml` dependencies.
Continues the gradient + property-panel polish from the previous
commit and rounds out two new flows the TS app already has:
Gradient stops + effects:
- ColorTarget gains GradientStop(i) + EffectColor(i); HSV picker
preserves alpha across hue/SV drags so a transparent stop stays
transparent. Hex pill stays 6-char; alpha is reattached at commit
and the swatch sits on a 2x2 alpha checker so #00000000 reads as
transparent rather than empty.
- Effects section reflowed into card-style blocks (image #9 spec):
title + minus, X/Y and Blur/Spread 2-col grids, color row with
swatch + rgba(...) text; clicking the swatch opens an HSV picker
bound to that effect index via SetEffectColor.
- Press dispatch on both hosts anchors picker overlays at the
clicked y so they pop adjacent to the swatch instead of the top.
Image + SVG import (toolbar + Fill section "图片" row):
- New FileAction::ImportImageOrSvg / PickFillImage; persistence_image
pops rfd, decodes raster as data: URL, inserts an Image node or
rewrites the selected node's primary fill.
- ImageNode actually renders on the canvas: NodePayload + SceneNode
carry image_src, canvas_viewport_paint.rs decodes the data URL
once and hands raw bytes to RenderBackend::draw_image with a
src-hash cache id. Grey placeholder paints only when decode fails
so transparent PNGs don't get a grey matte underneath.
- SVG import ported to TS-parity (packages/pen-engine svg-parser):
recursive <g> tree walk with inherited fill/stroke/style="...",
viewBox-aware scaling with maxDim cap, multi-subpath split, raw
d preserved on PathNode. Imports land wrapped in a Group named
after the source file.
Locale-aware first run:
- settings_io detects the OS locale (LC_ALL/LANG/LC_MESSAGES with
zh-Hans/zh-Hant heuristics) and seeds editor_ui.locale before
settings.json is read; persisted user choice still wins.
- macOS bundle declares CFBundleLocalizations + AllowMixedLocalizations
so NSOpenPanel / NSSavePanel render in the same language as the
rest of the chrome.
Web host kept exhaustive across the new variants (PickFillImage,
OpenEffectColorPicker, ColorTarget::EffectColor, GradientStop). Two
new files: persistence_image.rs (file-pick handlers, ≤120 lines) and
svg_path_data.rs (path-d tokenizer + bbox + normaliser, split from
svg_import.rs to stay under the 800-line cap). 277 op-editor-core
tests pass.
Port the pen-ai-skills diagnostics layer to a new pure Rust crate
`op-design-lint`: 14 design-lint detectors, the detect_all aggregator,
apply_fixes / detect_and_fix, and golden parity tests against the TS
oracle. Wire it into op-mcp as the read-only debug_validation_report
tool, gated by OPENPENCIL_DEBUG_TOOLS=1.
Detectors: empty_paths, unexpected_rotation, excessive_frame_effects,
invisible_containers, text_explicit_heights, text_effect,
text_corner_radius, text_stroke, text_bg_contrast, edge_section_padding,
stacked_horizontal_padding, sibling_inconsistencies (+ check_consistency),
detect_all.
Also includes: node_util shared helpers + pen-core color/visibility
ports, node_mut field accessors, set_property issue->node mutation
dispatch, golden fixture corpus + TS dump script, structural-parity
test, a CI golden-drift guard, and the gitignore fix so the fixture
docs/ dir is tracked.
This branch's per-commit history was squashed: the original 28 commits
carried fabricated timestamps and could not be honestly reconstructed,
so the work is recorded as a single commit at its real completion time.
Theme-toggle button now paints a Sun glyph in dark mode (click → light) and a Moon glyph in light mode (click → dark); the Sun icon was hardcoded before. Adds Icon::Moon (lucide crescent) and threads theme_mode into TopBar.
Bumps the casement submodule with a fix for the native macOS traffic-light reposition: idempotent absolute placement against resize, baseline invalidation on fullscreen exit, and a poison guard so a transitional re-capture can't drop the lights below their default position.
Task B2 of S3b-2: implements `run_concurrent` — the concurrent executor
that drives screen-group workers via a tokio Semaphore (RAII permits,
FIFO-fair, cap = effective_concurrency), collects per-worker buffered
EditorCommands, replays them into the real DocSink in subtask-plan-index
order, and fan-ins Progress events via mpsc channel.
Also splits concurrent.rs test block into concurrent_tests.rs (STEP 0)
to keep both files under the 800-line ceiling, wired via
`#[path = "concurrent_tests.rs"] mod tests` (same pattern as plan_repair).
Updated run_screen_group_worker signature: now takes Arc<Semaphore> +
mpsc::UnboundedSender<Progress> instead of &mut dyn FnMut(Progress).
Added CountingLlm to test_support for semaphore-cap verification.
Added tokio (sync + rt + macros) to op-orchestrator Cargo.toml.
208 tests pass; clippy -D warnings clean; fmt clean.
concurrent.rs: 432 lines; concurrent_tests.rs: 727 lines.
Existing EditorCommand variants are leaf-only (BatchInsertItem carries
only kind/name/x/y/w/h/fill_hex). The design orchestrator (S3a) must
apply rich nested designs — frames with children, layout, text — so
add InsertSubtree { nodes: Vec<PenNode>, parent_id }.
cmd_insert_subtree validates the parent is a container (or NONE = page
root), remaps every incoming node id to a fresh editor id via
remap_subtree_ids (so an externally-authored subtree can't collide
with live ids), and appends under the parent. The apply arm wraps it
in a history snapshot so the insert is one undo step.
NOT verified locally: op-editor-core does not currently build —
vendor/jian is pinned to unpushed commit 80121906 whose DesignMd*
types op-editor-core depends on are absent from every available jian.
The 8 InsertSubtree tests in command_subtree_tests.rs run once the
jian build is restored.
S3a Plan A.
Add op-figma as a dependency and implement FileAction::ImportFigma — an rfd .fig picker parses the binary file via parse_fig_binary and re-seeds EditorState.
run_action now returns a 3-state ActionOutcome instead of a bool: an import returns PathChangedUnsaved so it is treated as unsaved work (close still prompts) while the Git session is rebound to the now-pathless document. mark_document_saved is split so the rebind can run without refreshing the dirty baseline.
Run bare, the non-bundled binary shows in the Dock as the raw
`openpencil-desktop` executable name with a blank icon — no
`Info.plist` to read `CFBundleName` / the icon from.
New `macos_app::apply()` sets both at startup via objc2:
`NSProcessInfo::setProcessName` for the Dock / menu-bar name and
`NSApplication::setApplicationIconImage` (from an embedded
`assets/icon.png`) for the Dock tile. `[package.metadata.bundle]`
also gains `icon`, so a packaged `.app` carries it natively.
objc2-app-kit / -foundation are pinned to the 0.2 line winit /
casement already lock.
Cmd+C / Cmd+V / Cmd+X did document node-clipboard ops regardless
of focus, so there was no way to paste a prompt into the AI chat
input. They now branch on chat focus: with the chat input focused
they read/write the OS text clipboard (`arboard`), otherwise the
node clipboard as before.
- `clipboard.rs` — thin best-effort arboard wrapper.
- `WidgetHostNative::chat_input_paste` / `chat_input_cut` — append
/ cut on the focused chat buffer.
Drives the user's installed `git` via std::process::Command — the
same approach as the TS app's git-sys backend — so no libgit2/git2 C
dependency enters the workspace.
Covers repo discovery / init, working-tree status, staging, commit,
branch list / create / switch, history + diff, remotes, SSH keys and
credential storage. Adds a worktree-isolated merge orchestrator: a
branch merge runs in a throwaway detached worktree so a conflicting
.op merge never writes conflict markers into the live document — a
clean merge is fast-forwarded back, a conflicting one is reported as
a file-granular ConflictBag with the live tree left pristine.
Add five native-platform features to the winit desktop host
(op-host-desktop), closing the gap with the Electron app:
- Native menu bar (muda) — File / Edit / View / Help plus the macOS
app menu; selections route to the same host actions the keyboard
shortcuts use. Gated to macOS / Windows — muda needs GTK, which
this winit build does not link, so Linux keeps the in-canvas File
menu.
- Auto-update — a background probe of the GitHub releases API
reports status into the settings System tab; a found update
offers to open the download page, and a "Check for Updates" menu
item re-runs the probe.
- File association — argv parsing opens a .op / .pen document on
launch; [package.metadata.bundle] declares the OS-level handler.
- Window-state persistence — position / size / maximized restore
across restarts, with an off-screen guard for monitor changes.
- Drag-and-drop — dropping a .op / .pen file opens it.
Codex review round 1 findings (1 MAJOR + 3 MINOR) all addressed:
monitor-aware restore, failed-startup geometry guard, single-flight
update probe, case-insensitive extension match.
Also sink the agent-settings modal's hand-maintained EN/ZH string
table into the canonical 15-locale op-i18n tables, so the settings
chrome (including the new auto-update strings) is fully translated;
agent_settings_i18n.rs is now a thin op-i18n adapter.
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>