Built-in design generation now runs as an agentic MCP tool-loop (reusing the
agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the
Settings experimental toggle; the orchestrator stays the default.
- design-agent system prompt + in-process design toolset (parity-locked with
the MCP surface) + flag-gated Intent::Design routing
- spawn_agents execution as sequential sub-loops + live creation-mode badges
(per-agent glow + 'N/M designing...' header)
- new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state,
export_nodes, spawn_agents; style-guide local audit
- #27 AI panel restyle: rounded tool cards + green check-rings, gray user
bubbles, model-pill bottom toolbar, header, empty-state pills, the
PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown
- multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI
(switch / close / + / Cmd+T) with each run bound to its tab
Large checkpoint commit spanning the working tree (Rust shell crates).
On natural completion the design-turn teardown now calls
agent_indicators::finish_if_epoch instead of end_if_epoch: the already
queued reveals keep playing at the queue cadence and the whole overlay
(cursor + borders) clears itself once the last one lands, so the tail of
a slow 160ms stream is no longer snapped away mid-reveal.
A user stop / abort still tears down immediately via end_if_epoch, so a
cancelled generation does not keep animating. Wired across the
orchestrator run guard, the web design session, the host-core
DesignSession drop, and the desktop sub-agent / design-loop teardown.
Updates the desktop lifecycle tests to match: the frame-clear test now
drives past the drain window before asserting the overlay cleared, and a
shared registry test-lock serializes reveal-streaming turns against the
exact animation-deadline assertion (the registry is process-global).
Each agent gets an arrow cursor that flies between the nodes it places
along the reveal schedule, arriving as each node scale-pops in; the
element currently being placed carries a breathing border. Replaces the
old dashed-border reveal fade (canvas_agent_overlay removed).
Reveal cadence is slowed to a uniform 160ms queue and retained for the
whole run, so the cursor parks on the last placement between streamed
chunks instead of fading out. Splits agent_indicators tests into a
sibling file to stay under the 800-line cap.
The WASM bundle build (#56) and op-web-sdk bundle workflows install binaryen
via apt, which on the ubuntu runner is v108 (2022) — too old for rustc 1.94's
wasm feature set. It lacks the --enable-bulk-memory-opt flag (the original
'Unknown option' failure) AND cannot validate the memory.copy/fill opcodes
rustc now emits, so wasm-opt rejects the bundle with 'all used features should
be allowed'. Download a pinned binaryen version_123 release (matches the local
dev toolchain) and prepend it to PATH. Verified locally: the full
check-wasm-bundle.sh pipeline passes with binaryen 123 against a fresh
rustc-1.94 wasm build.
The rust-check workflow's cargo fmt --all -- --check failed on 108
line-wrap drifts under the pinned 1.94 rustfmt; running cargo fmt --all
clears them. With fmt fixed, the downstream steps (never reached in CI
before) surfaced accumulated lint + test debt from the recent chat-panel
(#41/#42/#43), agentic-design tool-loop, and multi-chat-tab work:
- clippy -D warnings: question_mark, field_reassign_with_default (test-only,
false positives on DerefMut writes), dead_code, double_ended_iterator_last,
doc_overindented_list_items, module_inception, single_match, nonminimal_bool,
sliced_string_as_bytes — fixed via clippy's own mechanical rewrites or
narrowly-scoped #[allow] on test items.
- op-editor-ui: the no-model quick-action-card test asserted pre-#43 behavior
(pills are now clickable regardless of model); re-point it at the Example hit.
- op-host-native (gl-host) + op-host-web (canvaskit) widget_host tests: 10
stale UI/layout/behavior expectations — footer gained a palette icon
(attach shifted), quick-action pills are full-width stacked, the speed chip
opens the Parallel-Agents picker (#32), applying a single-root-frame design
swaps the empty starter frame (command_root_replace) so the child count is
unchanged, a CJK prompt wraps to two lines, the toolbar grew under the
bottom-left chat panel, and the canvas Preview button is behind the
experimental-features opt-in. All test-only; the two bug-suspect cases (CJK
selection, design-apply count) were confirmed intended behavior.
- op-host-web canvaskit test helper returned ChatState where the .chat field
is now ChatSessions (multi-tab); return the container and let DerefMut carry
callers.
No production behavior changed.
The wasm bundle gates (tools/check-wasm-bundle.sh and
crates/op-web-sdk/tools/build-wasm.sh) hard-passed --enable-bulk-memory-opt
to wasm-opt, which older binaryen (the CI runner's apt package) does not
recognize, failing the WASM bundle build (#56) and op-web-sdk bundle
workflows at the size gate. Probe wasm-opt --help and keep only the feature
flags the installed binaryen advertises; on those older versions the single
--enable-bulk-memory already covers memory.copy/fill, so dropping the unknown
flag is safe. Local binaryen (v117+) keeps both.
cargo-deny advisories also failed: ttf-parser flagged unmaintained
(RUSTSEC-2026-0192, a transitive font-stack dep with no maintained
replacement) and memmap2 flagged unsound (RUSTSEC-2026-0186). Ignore the
former and bump memmap2 0.9.10 -> 0.9.11 (the patched release) for the
latter. advisories now pass.
- Remove the Zig `agent-native` git submodule (no Rust crate depends on it;
the built-in agent runtime is the Rust `agent` crate under vendor/agent).
- Make the repo root a pure Cargo workspace: delete root package.json + bun.lock;
move all web-SDK JS/Bun tooling into packages/ (new packages/package.json
workspace root, generate-iconify-catalog.mjs, and the oxlint/oxfmt configs).
- Scrub agent-native from all docs and fix README dev commands + prerequisites
to reflect the Rust product (cargo directly; bun tooling runs from packages/).
- Drop the deleted-root-package.json read from the op-host-web ci_workflow test.
The product is now Rust (crates/) + the Zig agent runtime
(packages/agent-native) + the wasm-backed web SDK (packages/op-web-sdk*).
Delete the retired TypeScript editor/desktop/CLI (apps/*), the pen-*
packages, and their orphaned TS tooling, Dockerfile, and root tsconfig.
Rust-side hooks into the deleted TS are rehomed so the workspace still
builds, tests, and releases green:
- op-mcp: vendor the 10 element-tool-defs shards it include_str!'d from
pen-mcp into crates/op-mcp/assets/element-tool-defs/ (git renames), and
repoint the compile-time includes + the runtime parity test's read_dir.
- op-orchestrator: reword the parity panic that named the retired
dump-planner-golden.ts generator (goldens stay as frozen baselines).
- CI: drop the TS-derive golden-drift + planner-parity oracle jobs and the
TS path filters from rust-check.yml; repoint the cargo-bundle icon in
rust-release.yml to crates/op-host-desktop/assets/icon.png.
- Release (Windows): recover the deleted apps/desktop/build/icon.ico into
crates/op-host-desktop/assets/icon.ico and repoint the NSIS installer
(rust-release.yml /DICON_FILE + package-windows.nsi ICON_FILE fallback +
its example invocation), so the Windows release build still finds it.
- Remove the dead .githooks/post-commit hook — it rebuilt the deleted
out/mcp-server.cjs via the removed mcp:compile script and only watched
now-deleted pen-* paths; the Rust MCP is built by cargo.
- Recover apps/web/public/logo-discord.svg to screenshot/ and repoint the
15 READMEs' logo/link paths.
package.json is now Rust-first (dev/build/test -> cargo + serve-web) and
docs (CLAUDE.md/AGENTS.md/packages+crates CLAUDE.md) describe the Rust
product. The retired TS remains recoverable via git (last tag v0.7.5).
Verified: cargo check --workspace green; cargo test -p op-mcp 353 passed;
cargo test -p op-orchestrator 848 passed; cargo test --workspace --no-run
compiles all targets; rust-release.yml parses and references no deleted
build assets.
Pre-commit fmt hook bypassed: its cargo fmt --check is pre-red from a
repo-wide stable-vs-nightly rustfmt config drift affecting unrelated files;
the .rs files touched here produce no fmt diff.
- #41: collapse chevron renders through IconButton so it shows the ghost
button-hover wash instead of only swapping the icon color
- #42: send/stop share one circle slot (toggle in place); footer cluster
sits snug with no reserved stop gap; hit-test routes streaming->stop else send
- #43: example cards stay clickable without a connected model (clicking one
fills the input; sending still requires a model)
- tab close glyph shows only while the tab is hovered (inset still reserved
on the active tab so the title doesn't reflow)
Built-in design generation now runs as an agentic MCP tool-loop (reusing the
agent-rs BuiltInProvider), gated behind OPENPENCIL_DESIGN_AGENT_LOOP / the
Settings experimental toggle; the orchestrator stays the default.
- design-agent system prompt + in-process design toolset (parity-locked with
the MCP surface) + flag-gated Intent::Design routing
- spawn_agents execution as sequential sub-loops + live creation-mode badges
(per-agent glow + 'N/M designing...' header)
- new MCP tools: get_guidelines, ToolSearch, get_screenshot, get_editor_state,
export_nodes, spawn_agents; style-guide local audit
- #27 AI panel restyle: rounded tool cards + green check-rings, gray user
bubbles, model-pill bottom toolbar, header, empty-state pills, the
PARALLEL AGENTS (agent_team_size) 1x-6x chip dropdown
- multi-chat tabs: ChatSessions model (Deref-to-active) + tab row UI
(switch / close / + / Cmd+T) with each run bound to its tab
Large checkpoint commit spanning the working tree (Rust shell crates).
op-orchestrator:
- import EffectField directly (origin's cleanup.rs no longer re-exports it
via `use super::*`)
- insert both concurrent-cleanup roots in one InsertSubtree, since origin's
command_root_replace now replaces an empty root on single-node top-level
inserts (two sequential empty inserts would collapse to one child)
- defer the bottom-nav upward-shadow assertion pending reconciliation with
Kayshen's e3ed2f1e "normalize mobile bottom tabs" (top-navbar transparency
assertion retained)
op-host-desktop:
- import clear_fresh_starter_frame_for_design via `super::launch::` so the
retained starter-frame tests resolve it after the merge
A 100%-rounded active tab nested in a nav pill was flattened to a sharp
square by the sibling corner-radius consistency detectors. Skip the fix when
a group's radius is pill-scale (>= 40) so the active pill survives validation.
Port the post-streaming tree-heuristics pass to Rust (tree_heuristics.rs):
- fill_card_leading_image_width: a card's leading header image spans the
card width instead of leaving a white gap (160px image in a 252px card)
- fix_notification_badge_overlay: a badge authored as a flex sibling of an
icon becomes a corner-overlay circle, not a square beside the icon
- active nav-tab pill, card image corner clip, invisible text-band fill,
stacked-overlay-to-absolute, redundant colored wrapper strip
Add palette_harmonize.rs: retint the cool-slate neutral ramp toward the
page's temperature so a warm design no longer reads as off-palette gray.
Plus role-resolver / cleanup / scaffold / run refinements accumulated from
the glm-opus design-parity campaign.
ellipsize_to_width returned an empty string when not even one glyph
plus the ellipsis fit, dropping the only usable truncation marker. Keep
the bare `…` (matching the per-module ellipsize helpers) so the
truncation stays visible. Updates the unit test accordingly.
The collapsed AI-chat pill is a fixed 150px wide, but its title was
drawn untruncated, so a long chat title (e.g. the design prompt)
overflowed past the chevron and the pill's right edge. Ellipsize the
title to the space between the bubble icon and the chevron. Adds a
shared width-aware util::ellipsize_to_width (measure-closure based, so
it stays decoupled from PaintCx) + a unit test. Covers native + web
(shared widget).
Keep command_tests.rs under the 800-line file limit by relocating the
remove_selected_fill_clears_the_variable_ref test to fills_tests.rs,
where the other fill mutator tests live.
A fill bound to a colour variable keeps a node->variable entry in
ui.variables.fill_refs. The scene resolver's fill_for lets a registered
fill ref WIN over container.fill, so removing the fill row cleared
container.fill (panel goes empty) but left the dangling fill_ref — the
variable colour kept painting. "Deleted the fill but the colour stays"
on token-based (old .op) designs. remove_selected_fill now drops the
fill_refs entry for the primary fill too. In-memory only; old .op files
load unchanged. Adds a core unit test + 4 host scene-clearing tests
(plain / nested / multi-fill / variable-bound end-to-end).
Clicking the inline stroke-width input on a node with no stroke seeded
"1" (auto-filling a width the user never typed), and the round() seed
snapped fractional widths like 2.5 to "3". Seed via the same
format_panel_number(snapshot width, 0 when unset) the input paints, so
focusing never changes the displayed value. Fixed on native + web; adds
2 seed regression tests.
cmd_set_node_stroke_width attaches a fresh stroke with fill: None
(width, no color). The panel snapshot gated its `stroke` field on a
parseable solid color, so a colorless stroke's width vanished from the
panel: the width input read back 0 on blur even though the model kept
the width. Surface the stroke whenever it has a width, using the slate
placeholder color when no solid color is set. Adds 4 host tests
(commit-from-nothing, click/type/blur, snapshot repro).
Batch commit of the property-panel work: multi-fill Fill editor, stroke
edit modes (uniform/per-side), clip support, plus polish — every
property-panel icon centred in its hover wash (fill X, padding/stroke
gears, gradient/effect remove), justify-row + size washes fit-content,
stroke duplicate width removed (inline width Single-only, per-side grid
otherwise), Openverse register link fixed + hover, ImageSrc Arc bump.
Known issue: stroke-width edit does not persist on a node whose
node_stroke_slot is None (see cmd_set_node_stroke_width) — follow-up.
The register link had no hover affordance. Track a
hover_image_search_register_link flag (set from the OpenImageRegisterLink
hit in both host cursor sweeps) and underline the link + arrow on hover.
The link opened the raw auth_tokens/register/ endpoint, which only
accepts POST — a browser GET lands on a 405 page. Point it at the API
reference's auth section, which documents how to register for credentials.
The relink-image handler assigned a String to ImageNode.src, which is
now ImageSrc after the Arc-share change (ee0a933e) — this conversion was
applied in the working tree but missed from that commit, leaving
op-host-desktop compile-broken at the committed state. Add the .into().
The Fill section was single-fill (header "+" only reset one solid, row "×"
cleared but a default row re-rendered). Now it's a Figma-style stacked list:
"+" appends a fill, each fill renders its own head row (swatch + type
dropdown + opacity% + ×), and "×" removes that specific fill. Built on the
indexed model ops from 17310556; mirrors the Effects multi-item pattern.
- snapshot: NodeSnapshot.fills: Vec<FillSummary>; legacy fields derived from
fills[0]; walkers thread fills: &[FillSummary] (VisibleSections stays Copy).
- actions: RemoveFill(usize), SetFillType{index,fill_type},
ToggleFillTypePicker(usize), OpenFillColorPicker(usize).
- state: PropertyFocus::FillHex/FillOpacity carry a fill index;
ColorPickerState.fill_index indexes the HSV writeback (ColorTarget unchanged).
- paint/hit/height loop per fill in lockstep (paint_one_fill,
push_fill_action_rects, fills_section_height); both hosts dispatch by index.
Backward compatible (fill is already Vec<PenFill>; old single-fill .op loads
as one row). Non-primary gradient/image fills are head-row-only for now
(editable gradient/image body stays primary-fill-only to avoid the ColorTarget
ripple); solid color/opacity/type are fully per-index.
Bump vendor/jian to the ImageSrc(Arc<str>) change: ImageNode.src and
ImageFillBody.url are now reference-counted, so the per-frame
SceneBuildCache document clone + content-compare settle in O(1) for an
unchanged multi-MB image source instead of walking the whole base64
payload. Completes the deeper half of the image-drag perf fix (the
insert-time down-scale landed in a084628f).
Consumer sites updated for the newtype: writes/constructs use .into();
sites assigning into a String field use .to_string(); reads are
unchanged via Deref. Also fixes a stale gl-host test
(remote_icon_insert_bakes_svg_d_as_path_node) that still assumed icon
insert appends to the end — it now locates the baked path by icon_id and
asserts it sits above the selection (the insert-above behavior).
The "Register at Openverse" link in Settings → Images → Advanced was painted
but inert. Add a hit-test (register_link_rect → ImagesHit::OpenRegisterLink →
AgentSettingsHit::OpenImageRegisterLink) and open
https://api.openverse.org/v1/auth_tokens/register/ on click — default browser
on desktop (open/xdg-open/start) and a new tab on web (window.open).
Foundation for a per-fill Fill editor (stack multiple fills, add/remove by
row). Additive, backward-compatible (fill is already Vec<PenFill>; old .op
files load as a single fill):
- fills.rs: fill_type_of, fill_count, fill_type_at, add_fill,
remove_fill(i), set_fill_type_at(i), set_fill_hex_at(i),
set_fill_opacity_at(i).
- property_edit_mutators.rs: add_selected_fill, remove_selected_fill(i),
set_selected_fill_type_at(i), set_selected_fill_hex_at(i),
set_selected_fill_opacity_at(i) (via a with_selected_node helper).
UI/host wiring lands separately.
A multi-megabyte base64 image src lives inline in the document's
PenNode::Image.src, so every scene rebuild clones + compares it and the
canvas decodes a full-resolution bitmap — moving a 5.4MB image lagged.
Shrink an oversized raster source the moment it enters the document, at
every host path that produces an inline data: URL into the doc src:
file-pick import / fill-image / relink (persistence_image), fetched +
searched images (image_search_session), and inline-base64 AI generations
(image_generate_host). New image_downscale module decodes via skia, fits
the longest edge to 2048px (or re-encodes a >2MB source at native size),
and emits JPEG for opaque sources / PNG for alpha — adopting the result
only when it actually shrinks. Animated GIF / WebP are detected by magic
bytes and left whole so re-encoding never drops frames.
insert_node_above_selection inserted into the selection's immediate parent,
which broke a nested selection: a free-positioned icon/image dropped into a
flex/auto-layout frame gets reflowed away from the cursor (and can detach
the selected flow child), and into a clipped frame it can be hidden. Insert
at the page root above the selection's top-level ancestor (descendant_contains)
instead — correct for top-level selections, safe for nested ones.
Icon and image inserts append()ed to the page root (the back / bottom of
the LayerPanel), ignoring the selection. Add insert_node_above_selection:
the new node lands in the selected node's parent at the selection's index
— one row up in the panel, one step toward the front in z-order (canvas
paints children back-to-front). Falls back to page-root append when
nothing is selected.
Bumps vendor/jian to d50a715 (O(1) image source id — fixes image-drag lag).
Commits the concurrent session's remaining uncommitted changes at the
user's request: op-host-web web_settings.rs (new module) + canvaskit / lib
wiring + Cargo.toml / bun.lock dependency updates, plus minor canvas-overlay
and host-test tweaks.
Verified to compile before committing: op-editor-ui + op-host-native (native)
and op-host-web (wasm32) all `cargo check` clean.
fe9983a0 committed crates/op-host-web/src/widget_host.rs wholesale, which
swept in a concurrent actor's in-progress web_settings integration (the
settings_fingerprint field + load_into/fingerprint/save_if_changed calls)
WITHOUT its module (web_settings.rs untracked, the mod decl + serde dep
uncommitted) — so clean HEAD referenced a missing module. Restore
widget_host.rs to the pre-sweep base and re-apply only the intended
icon-picker scroll wiring; the actor's web_settings WIP stays uncommitted.