- Call blur_input when agent settings panel opens (shortcut + click)
on both native and web hosts, so chat.focused becomes false.
- Guard agent_settings focus check with agent_settings_open in
input_active() to prevent stale focus from claiming keyboard.
- Reorder handle_cmd_paste to check non-chat inputs first, with an
additional !chat.focused guard.
Windows CI segfaulted (STATUS_ACCESS_VIOLATION 0xc0000005) because the
design worker thread runs SkiaMeasure layout concurrently with the UI
thread paint/measure, and skia Windows FontMgr (DirectWrite) is not
thread-safe. Wrap NativeBackend::{new,draw_text,enumerate_system_font_families}
in jian_skia::with_font_lock and bump vendor/jian to the commit adding
that global reentrant lock.
Also un-gate chat_intent_host_tests on Windows (the lock makes its
intra-test worker/UI concurrency safe) and harden the flaky
explicit_family_typeface_lookup_is_cached test with a
retry-until-clean-window against concurrent generation bumps.
A fresh starter canvas and a just-opened .op no longer ship the root
frame pre-selected, which both reads cleaner and stops a stray selection
from mis-routing a whole-screen design request into modify.
The FontStore + font-import tests register fonts via jian_skia (skia
FontMgr::new_from_data, DirectWrite on Windows) from cargo's parallel
test-worker threads, which segfaults (STATUS_ACCESS_VIOLATION) in Windows
CI — newly surfaced now that the submodule checkout resolves. Production
resolves fonts on the main render thread; macOS + Linux keep the coverage.
Gated with cfg(not(target_os = windows)) as the least-invasive fix.
Three related hover fixes:
- Font picker "穿透": the open popup is a floating overlay but the
cursor-move handler fell through to the canvas/layer hovers when the
picker hover was unchanged, so a node behind the popup highlighted.
update_font_picker_hover now CONSUMES the move + clears lower-overlay
hover while the cursor is over the popup.
- "Import font…" row had no hover wash: add font_picker_import_hover
state + PropertyPanel::font_picker_import_action_at hit + a
paint_button_feedback_wash on the ImportAction row (threaded through
the paint_font_picker signature).
- Web parity: op-host-web overlay_cursor now updates the Effects add-menu
effect_add_menu_hover (mirrors the fill-type picker branch), so the
Drop Shadow / Layer Blur menu highlights on web too.
The Effects "+" add-menu (Drop Shadow / Layer Blur) painted flat rows
with no hover feedback, unlike the sibling property-panel dropdowns.
Add effect_add_menu_hover to editor_ui (cleared on toggle/close), a
PropertyPanel::effect_add_menu_row_at hit helper, and a native
update_effect_add_menu_hover cursor-move pass (mirroring the export
picker) that highlights the hovered row with the standard muted wash.
Wire user-imported fonts into the Typography font picker and the native
import/remove flow.
- op-editor-ui (wasm32-clean): FontPickerEntry gains `imported`;
font_picker_entries builds Imported -> Bundled -> System groups.
Imported entries carry an inline remove-x; a bottom "Import font…" row
drives import. A new `allow_import` capability (threaded through layout/
hit/paint) omits that row where the host can't import, so web shows no
dead control. Split property_panel_typography.rs into +_paint/+_tests to
stay under the 800-line cap. New actions ImportFont / RemoveImportedFont.
- op-editor-core: editor_ui gains imported_font_families snapshot,
pending_font_import / pending_font_remove request flags, and
font_import_supported capability (default false).
- op-host-native: refresh_imported_fonts rebuilds the snapshot from
jian_skia::list_families; ImportFont/RemoveImportedFont raise pending
requests (family resolved against the same entries list).
- op-host-desktop: font_import_host drains the requests — import opens an
rfd .ttf/.otf dialog (size pre-checked via metadata before read) ->
FontStore::import; remove -> FontStore::remove; both refresh the
snapshot. DesktopApp seeds the snapshot + sets font_import_supported.
- op-host-web: passes the imported list; import/remove are no-ops until
the Phase 4 web file-input (row hidden via the capability flag).
Codex-reviewed (2 rounds) to APPROVED.
Thread the jian_skia font-registry generation through the native render
+ measure caches so a runtime font import reflows an already-open
document instead of keeping stale fallback-font layout.
- op-pen-loader: CachingMeasureBackend drops its memo map, and
SceneBuildCache folds font_generation into its rebuild-decision
inputs, whenever the generation advances (current_font_generation is
cfg-gated on skia-measure; const 0 for the estimate build).
- op-host-native: NativeBackend + the export EXPORT_BACKEND inherit the
refresh for free through their shared FontResolver. widget_host's
refresh_layout_scene now also rebuilds when the generation changed
(tracked in layout_scene_font_generation) since a font import does not
dirty editor_state; the generation is read before the initial scene is
built to avoid a constructor race.
- Tests: end-to-end measure-changes-after-register (native resolver) and
a host-level regression test for the scene-rebuild gate.
Bumps vendor/jian to the mutable imported-font registry commit.
Replace the two effect add-buttons with a single "+" that opens a
Drop Shadow / Layer Blur choice menu. The "+" toggles the menu; a row
click adds that effect and closes; Escape or an outside click
dismisses. Wired on both native and web hosts.
The effects section's "+" only ever appended a Drop Shadow, so there
was no way to add a Gaussian layer blur from the panel. Add a second
add-button (blur-circle) beside the "+" that appends a default
PenEffect::Blur to the selected node, backed by
add_layer_blur_to_selected / push_layer_blur. The two add-buttons emit
non-overlapping hit rects so a click resolves to one effect kind.
Figma "Layer blur" effects were imported into the canonical document
but dropped before rendering (the scene carried only drop shadows), so
blurred background shapes rendered sharp. Carry the layer-blur radius
through the adapter (NodePayload.layer_blur) into an Effect::Blur, and
wrap the node's paint in a Skia blur layer (save_layer + blur image
filter, sigma = radius/2 × zoom) at every paint return path. Bumps the
vendored jian for the scene Effect::Blur variant + painter blur hook.
The palette slot removal shifted the right cluster 28px right, and
the input-height width basis fix (measuring wrap against the real
inner width) made the old probe text wrap and lift the footer band;
probe with a single-line text at the new offsets.
An active canvas selection now biases intent routing to modify (the
selection IS the target — only an explicit new-whole-screen request
or a plain chat question escapes it), restoring select-then-ask
editing. The panel paints a selected-count chip above the input with
a clear affordance, so the armed state is visible. Chat-question CJK
keywords keep questions about a selection conversational, and the
footer hover math reserves the chip row like paint does.
Bump jian for the shared font resolver, path/line flex sizing, and
input-chrome measurement; route the native backend's draw_text and
measure_text_weighted through the resolver so fit_content boxes match
what actually paints (synthetic bold and multi-line heights included).
Preview (Play) mode becomes genuinely interactive: PreviewSession
gains phase-level pointer dispatch (Down/Move/Up/Hover) + wheel, and
the native host routes cursor moves (drag while pressed, hover
otherwise; floating overlays keep their cursor via over_topmost_panel
and the modal/color-picker guard chain), releases, and wheels (canvas
pan/zoom fallback when no onScroll consumes) into the runtime — so
sliders drag and onHoverEnter/onScroll actions fire. Non-bind:value
bindings compile at enter (binding_sites.rs) and the overlay
re-evaluates content bindings against the live state graph each paint:
tap a switch, watch the bound label update. Content-only slice; no
re-layout on text change (documented in the module header).
Note: preview tests require --features gl-host.
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.
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.
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.
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.
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.
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).
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).
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.
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).
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).
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 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).