Commit graph

1232 commits

Author SHA1 Message Date
Kayshen-X d4e035d03f fix(panels): keep file-menu dropdown anchor in sync with the divider
The new sidebar↔file-menu divider shifted the folder button right, but
`TopBar::file_menu_rect` (the dropdown anchor) still used the pre-divider
x, so the file menu opened left of its button (Codex stop-time review).
Route the anchor through the divider span and have hit_test reuse
`file_menu_rect` so paint / hit-test / anchor can't drift again.
2026-05-29 21:23:46 +08:00
Kayshen-X 3eb70ba4ce style(panels): align top-bar glyphs + smaller icons + left dividers
Match the TS top bar and the user's refinement feedback:
- icons 16→14px, chevron 12→10px (top-bar-local consts; other widgets
  keep their own sizes) — a touch smaller / more refined;
- route every glyph through `glyph_top(center_y, size)` and center the
  agent chip's icon + 11px text on the shared center line (fixes the
  off-axis drift);
- add a divider between the sidebar toggle and the file-menu (user
  request) and unify all three dividers via `paint_divider`
  (1×14, border@60%, 4px gaps) — TS `w-px h-3.5 bg-border/60 mx-1`;
- keep paint + hit-test in sync for the new divider spans.
2026-05-29 21:16:48 +08:00
Kayshen-X 8e4515f0e0 feat(host): web parity for zoom-to-fit + rename caret
Mirror the native host on the web backend:
- ArrowLeft/Right move the inline-rename caret (`apply_rename_caret`)
  before falling through to node-nudge;
- StatusBar search icon frames the active page content
  (`status_bar_search_hit` + `zoom_to_fit`, reusing `canvas_region` +
  `LayoutScene::content_bounds` + `Viewport::fit_to`).

Type-checks on the host target; the wasm bundle build still needs EMSDK.
2026-05-29 21:00:14 +08:00
Kayshen-X 6af2ef853b style(panels): refine top-bar agent chip to match TS
- add a 1px×14px divider between the agent/MCP chip and the locale
  button (TS `w-px h-3.5 bg-border/60`) so the status chip reads as a
  separate group;
- chip text 12→11px in muted-foreground, status dot 8→6px in
  emerald-500 (#10b981) — matches the TS chip density/colour.

Note: the chip showing "Agents & MCP" vs "N agent · M MCP" is
connection state (0 connected agents/MCP → the set-up label), not a
layout bug.
2026-05-29 20:52:54 +08:00
Kayshen-X 88428ae72c fix(canvas): apply node opacity to raster images
Node-level opacity was folded into fill/stroke/gradient/shadow alpha at
scene-build, but rasters carry no colour to bake into, so image nodes
rendered fully opaque (codex stop-time review). Carry cumulative opacity
on `SceneNode` and modulate the image paint's alpha in the native draw
path (`draw_image_with_options` gains an `opacity` arg; trait default +
web fall back to opaque).
2026-05-29 20:46:23 +08:00
Kayshen-X 0b9bae6709 chore: bump casement to fix(macos) traffic-light reposition idempotent across resize + fullscreen exit 2026-05-29 20:25:25 +08:00
Kayshen-X 9cac369d15 feat(editor): node opacity, path gradient/inner-shadow fills, zoom-to-fit, rename caret
- canvas: fold node-level `opacity` into resolved fill/stroke/gradient/
  shadow alpha at scene-build (was dropped entirely); render linear/radial
  gradients on SVG path nodes (native skia shader; solid first-stop fallback
  on backends without one); render inset (inner) shadows on path nodes via
  clip + DstOut blur. Fixes the risk-monitor pedestal vs the TS reference.
- editor: StatusBar search icon frames the active page's content in the
  viewport (`Viewport::fit_to` + `LayoutScene::content_bounds`); inline-rename
  input supports ArrowLeft/Right caret movement with char-based mid-string
  insert/delete (CJK-safe).
- also carries in-progress flip/text canvas transforms, web backend, and
  figma-probe tweaks already present in the working tree, plus a clippy
  needless-borrow/closure cleanup in op-orchestrator (1.94 gate).
2026-05-29 20:24:36 +08:00
Fini 95e551fee7 chore: bump casement to fix(macos) launch on demand 2026-05-28 00:10:24 +08:00
Fini 52a10cee32 fix(orchestrator): clamp mobile status-bar levels to root width
The mobile status-bar chrome injected by `mobile_status_bar_json`
hardcoded levels.x=286 — the right-aligned cellular/wifi/battery
group, sized for a 390-wide iPhone reference. When the C2 plan
work started honouring explicit prompt sizes via
`explicit_mobile_size` (accepts 240..=520 wide), a 320 x 568
iPhone-SE prompt rendered the chrome at x=286..364 — 44 px past
the right edge.

Take root `width` as an argument and derive levels.x as
`width - 78 - 26` (78 = chrome width, 26 = iOS safe-area gutter,
matches the existing 390 reference: 390 - 104 = 286). New test
exercises 320-wide root to lock in the no-overflow contract.
2026-05-27 23:19:49 +08:00
Fini 6c7b352e41 refactor(host): split tests + trim comments for 800-line cap
chat_subprocess.rs (850) and design_session.rs (807) blew past
the ceiling after the Codex provider + design-viewport work
landed in 63f787c3. Move their inline `mod tests` bodies out to
sibling `*_tests.rs` files via `#[cfg(test)] #[path = …]
mod tests;`.

app_handler.rs (805) is one bare `impl ApplicationHandler` with
no natural split point; trim the file-level doc and two inline
comments in `exiting` to land at exactly 800.

After: chat_subprocess.rs 650, design_session.rs 508,
app_handler.rs 800. 114 host-desktop tests pass.
2026-05-27 23:19:48 +08:00
Fini f873c61a65 refactor(orchestrator): split tests into sibling files for 800-line cap
cleanup.rs / prompt.rs / scaffold.rs all blew past the repo's
800-line ceiling after the typography + mobile-screen work landed
in d26b5685. Move each file's inline `mod tests` body out to a
sibling `*_tests.rs` and re-attach via `#[cfg(test)] #[path = …]
mod tests;` — same pattern the existing `cleanup_tests_c1.rs`
uses.

After split: cleanup.rs 772, prompt.rs 313, scaffold.rs 422.
Test counts unchanged (612 orchestrator tests pass).
2026-05-27 23:19:47 +08:00
Fini b118e45d62 feat(host): wire Codex CLI provider and polish design-turn launch
- chat_subprocess: wire Codex CLI through SubprocessProvider via
  `codex exec --json` + positional-arg prompt, and parse the new
  `item.completed` agent_message events into TextDelta
- chat_session: route chat_selected_agent=1 through the Codex
  provider; on design-turn launch, clear the fresh starter frame
  so generated nodes don't stack on top of the demo content
- design_session: fit_design_viewport_to_content keeps generated
  designs centred while the orchestrator streams nodes
- app_handler: thread viewport size through pump_commands so the
  fit step has canvas dimensions to work with
2026-05-27 23:19:46 +08:00
Fini 6fb5ff58cd feat(orchestrator): mobile screen plans, typography cleanup, parse fixes
- plan: when the prompt looks like a mobile screen (or carries an
  explicit WxH), emit a top-summary + main-content fallback plan
  instead of the generic three-section split
- cleanup_typography: new module + repair_overbold_text_hierarchy
  to soften LLM over-bolding when >=65% of text nodes come back as
  weight >=700
- parse: default missing/null image src to "" so generated image
  nodes no longer fail strict deserialization
- plan_normalize / scaffold / prompt / subagent: associated reworks
  feeding the above paths
2026-05-27 23:19:45 +08:00
Fini f9bdec5fe9 refactor(editor): collapse panels and drop unused host modules
Bundle three cleanups that share glue files (widget_host, lib.rs,
widgets/mod.rs) and so must land together:

- collapse variables_panel/{header,paint,tests}.rs into
  variables_panel.rs and fold the four host variables_panel_*.rs
  modules into the main widget_host
- remove property_panel_font_picker along with the system_fonts
  loader thread and the system_font_rx plumbing on WidgetHostNative
- delete the widget_host keyboard_motion module and roll its logic
  into keyboard.rs

Drops the now-orphan themed-variable scalar setters,
variables.type{Color,Number,String} i18n keys (×15 locales), and
the Cmd+V / Cmd+D shortcuts that targeted the removed panels.
2026-05-27 23:19:44 +08:00
Kayshen-X 46e7e8d792 fix(editor): improve rust parity and figma import performance 2026-05-27 21:51:57 +08:00
Kayshen-X 14c3aaa09b feat(editor): improve native panel parity 2026-05-26 21:36:03 +08:00
Fini 37d0998b03 fix(canvas): align rust z-order with renderer 2026-05-26 07:02:32 +08:00
Kayshen-X 19652f0724 feat(panels): improve native property controls and icons 2026-05-24 23:30:00 +08:00
Fini def3a50d32 fix(host): inline orchestrator system prompt into user message
Codex stop-time review caught a silent prompt drop in the
`ChatProviderLlmClient` adapter from the previous commit. I put the
orchestrator's `CallRequest.system_prompt` into
`ChatRequest.system_prompt`, but all three CLI-backed `ChatProvider`
impls (`ClaudeCodeProvider`, `CopilotProvider`, `SubprocessProvider`)
ignore that field — they drive their respective CLIs through
subprocess / SDK channels with no per-turn system-prompt slot. Net
effect: the planner / sub-agent role prompt was discarded and the
model only saw the bare user prompt.

Fix matches the precedent in `chat_runtime::BuiltInProvider::send`
(line 138): prepend the system prompt to the user message with a
`\n\n---\n\n` separator, then leave `ChatRequest.system_prompt`
empty. If a CLI later grows a real system-prompt channel, unwrap the
prepend back into the field.

cargo test -p op-host-desktop 106 passed; fmt + clippy clean.
2026-05-24 20:45:45 +08:00
Fini ec8df2c05d refactor(host): orchestrator uses chat-panel agent, drop Anthropic-key dependency
User pointed out: the desktop chat panel already has 5 CLI agents
(Claude Code / Codex / OpenCode / Copilot / Gemini) that manage their
own auth — Claude Code is logged in by the user, Copilot rides GitHub
auth, Gemini rides gcloud. The #27 intent-gate landing required a
separate `OPENPENCIL_ANTHROPIC_API_KEY` env var to launch the
orchestrator, which broke UX consistency and was a real design defect:
the existing CLI agents already provide LLM access, the orchestrator
should reuse them instead of requiring users to wire up a parallel
direct-API key.

Root cause was a trait mismatch: `Orchestrator` needs an
`LlmClient` impl, and `DesktopLlmClient` (now deleted) was written to
take `Arc<dyn agent::Provider>` — but `agent::Provider` is only
implemented by `AnthropicProvider` / `OpenAiCompatProvider`, NOT the
CLI-backed `ChatProvider`s (`ClaudeCodeProvider` etc).

Fix is a thin `ChatProvider → LlmClient` adapter:

- New `chat_provider_llm::ChatProviderLlmClient` (~80 lines): owns
  an `Arc<dyn ChatProvider>`, each `LlmClient::call` spawns a thread
  that drains the provider's blocking iterator into a futures mpsc
  channel and returns the receive half as a `BoxStream`. Same
  async↔sync bridge `BlockingRecvIter` uses in the opposite
  direction.
- `DesignSession::start` now generic over `L: LlmClient + Send +
  'static` instead of taking `Arc<dyn Provider>` + default_model;
  caller picks the LlmClient impl. Test e2e path
  (`from_channels`) unaffected.
- `chat_session::launch_if_pending`: Design branch reads
  `chat_selected_agent`, wraps the existing `provider_for_agent`
  output in `ChatProviderLlmClient`, hands it to `DesignSession`.
  Unwired agents (Codex / OpenCode) fall through to the chat-path
  unwired-agent error bubble — same UX as a chat send to an
  unwired agent.

Deletions:

- `chat_orchestrator.rs` — `DesktopLlmClient` was its last
  surviving content; with the new adapter no caller needs it.
  Removed the file + the `mod chat_orchestrator;` line.
- `chat_session::provider_for_design` and the
  `OPENPENCIL_ANTHROPIC_API_KEY` / `ANTHROPIC_API_KEY` /
  `OPENPENCIL_ORCHESTRATOR_MODEL` env reads.
- `op-host-desktop/Cargo.toml` `agent` crate's `["anthropic"]`
  feature — no path inside the host needs an `agent::Provider`
  impl anymore (the trait stays imported for the
  `BuiltInProvider` shim in `chat_runtime.rs`, future-facing).

`op-smoke` keeps `AnthropicProvider` + `OpenAiCompatProvider`
directly because the smoke deliberately bypasses any CLI auth path
to validate the orchestrator against a raw API endpoint
independently of the host UI.

cargo test -p op-host-desktop 106 passed (unchanged). cargo fmt
--all -- --check + cargo clippy --workspace --all-targets -- -D
warnings clean.
2026-05-24 20:41:36 +08:00
Fini 1b103a017c Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 19:45:55 +08:00
Kayshen-X 2766952b3e fix(editor): improve shape controls and drag snapping 2026-05-24 19:03:11 +08:00
Fini 935d923927 style(smoke): apply rustfmt to op-smoke main.rs
Codex stop-time review caught: op-smoke was not rustfmt-clean. Three
hunks — outer `let model = ...unwrap_or_else` had its match body
inlined; one `eprintln!` line exceeded 100 chars and needed
expansion; one other formatting nit.

The previous commit's `cargo fmt --all -- --check` invocation reported
FMT_OK because the output scrolled past the diff hunks before I saw
them — the exit code path Codex relies on (not the truncated grep
output I read) caught the residual diffs. Lesson for the runbook:
trust the exit code, not eyeballing tail output.

`cargo fmt --all -- --check` clean now; `cargo clippy --workspace
--all-targets -- -D warnings` clean.
2026-05-24 19:03:10 +08:00
Fini b1d585e7b3 feat(smoke): support OpenAI-compatible providers (DeepSeek/方舟/百炼/Moonshot)
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`.
2026-05-24 19:00:03 +08:00
Fini 007d15a982 chore: lockfile update for op-smoke crate added in e2c6e74b 2026-05-24 18:43:20 +08:00
Fini d8d3b1a517 feat(smoke): headless op-orchestrator smoke runner (task #28)
New `op-smoke` binary crate — drives one `Orchestrator::run` against
`AnthropicProvider` without the desktop UI / `DesignSession` actor model,
single-threaded `block_on` on an inline `DocSink`, every event dumped to
stderr. Decouples task #28's "does the pipeline reach the LLM and apply
EditorCommands correctly" verification from GUI smoke (canvas paint /
chat panel rendering), which still needs the desktop binary.

Usage:

    export OPENPENCIL_ANTHROPIC_API_KEY=sk-ant-...
    cargo run -p op-smoke -- "design a login screen"

Optional `OPENPENCIL_ORCHESTRATOR_MODEL` (default `claude-sonnet-4-6`).

What's traced to stderr:
- `[SMOKE]` model + prompt
- `[LLM]` per-call system/user lengths + engine errors
- `[PROGRESS]` every `op_orchestrator::Progress` variant (Planning,
  ScaffoldDone, SubtaskStarted/Done/Failed, CleanupDone, validation +
  visual-ref variants)
- `[CMD]` per `EditorCommand::apply` one-line label + applied result
  (InsertSubtree shows parent_id + nodes.len(); SetNodeStrokeHex shows
  hex; etc.)
- `[UNDO]` begin / end batch boundaries
- `[FINAL]` Ok/Err + elapsed; on Ok prints root_frame_id, total_nodes,
  per-subtask outcomes

What this verifies vs the desktop GUI smoke
(`superpowers/notes/2026-05-24-orchestrator-smoke-steps.md`):
- LLM client construction (`AnthropicProvider::new` + auth)
- Network reachability (200 / 401 / 429 surfaces as `LlmError`)
- Planner → scaffold → subtask → cleanup transitions
- `InsertSubtree` ID-remapping (via post-apply state)
- Terminal `RunSummary` / `OrchestratorError`

What it deliberately skips:
- Canvas paint correctness — run the desktop binary
- chat panel progress rendering — run the desktop binary
- Pre-validation fixes — smoke uses `SkippedPreValidator` so the
  trace stays focused on orchestrator behaviour; the desktop binary
  uses `LintPreValidator`

`op-host-desktop::chat_orchestrator::DesktopLlmClient` would have been
ideal to reuse, but the host crate is binary-only (no lib target); the
op-smoke `SmokeLlmClient` is a ~50-line copy that swaps
`shared_runtime().spawn` for `tokio::spawn` (smoke owns its own tokio
runtime via `#[tokio::main]`).

cargo fmt + clippy --workspace --all-targets -D warnings clean.
Sanity-checked the no-prompt and no-API-key error paths.
2026-05-24 18:42:56 +08:00
Kayshen-X 6b1b4f2e1c fix(panels): render image fill controls 2026-05-24 17:45:53 +08:00
Kayshen-X 85dd6d8900 chore: enforce rust pre-commit checks 2026-05-24 17:44:42 +08:00
Fini 05d111d2ba refactor(ai): drop dead node_base_role helper in validation_fixes_apply
memory-tagged follow-up — `node_base_role(node) -> Option<&str>` (line
568 of `validation_fixes_apply.rs`) was a thin shim over
`node.base().role.as_deref()` with zero call sites anywhere in
crates/* or vendor/*. Inlining at the (single, hypothetical) caller is
shorter than the helper itself.

`cargo test -p op-orchestrator` still 585 passed; workspace 1880
passed; fmt + clippy --workspace -D warnings clean.
2026-05-24 16:41:36 +08:00
Fini dc98f0a1d4 refactor(host): drop dead chat_orchestrator code now that DesignSession owns the route
Task #27 swapped the orchestrator route from `chat_orchestrator::run_design_request`
(direct `block_on` on the UI thread, never called) to
`design_session::DesignSession::start` (worker thread + RemoteDocSink +
ack channel). Confirming the leftover types are truly dead:

- `DesktopDocSink` — grep finds zero callers; replaced by `RemoteDocSink`
  in `design_session.rs` which owns its own mirror + sends `EditorCommand`
  back to the UI thread over mpsc.
- `run_design_request` — grep finds zero callers; replaced by the
  direct `Orchestrator::new().run(...)` invocation inside the worker
  closure in `DesignSession::start`.
- `DesktopLlmClient` — actively used (`design_session::start` builds one
  per turn). Kept.

So this commit:

- Deletes `DesktopDocSink` struct + impl block (~30 lines).
- Deletes `run_design_request` async fn (~40 lines).
- Removes `#![allow(dead_code)]` — the surviving code (`DesktopLlmClient`)
  is reachable so the module no longer needs the blanket allow.
- Trims module doc — drops the 4-item TODO list that described the
  pre-#27 wiring gap (intent gate / threading model / undo batch /
  validation providers) since #27 + #35 closed all four.
- Drops unused imports (`EditorCommand`, `EditorState`, `DesignRequest`,
  `Orchestrator`, `OrchestratorError`, `Progress`, `RunSummary`,
  `DocSink`, `AbortFlag`, `SkippedScreenshotProvider`,
  `SkippedVisionLlmClient`, `ValidationProviders`).
- Touches the `design_session.rs` comment that referenced `DesktopDocSink`
  by name — rephrased to describe the no-op behaviour without the
  dangling pointer.

`cargo test -p op-host-desktop` 106 passed (unchanged). cargo fmt +
clippy --workspace -D warnings clean.
2026-05-24 16:16:40 +08:00
Fini aef03e5541 test(host): end-to-end actor-channel round-trip for DesignSession
Task #28 prep. The orchestrator side has its own end-to-end tests
under `crates/op-orchestrator/`, but the host's actor seam
(worker → mpsc → UI thread → ack → mirror update + Progress → chat
bubble) was only exercised piecewise by the three `RemoteDocSink`
tests. This adds a single end-to-end smoke that drives both
`pump_commands` + `pump_progress` from a fake worker thread against a
real `WidgetHostNative`, then asserts:

- Progress events rendered into the trailing assistant bubble
  (`• Planning…` line surfaces verbatim).
- `RunSummary::Ok` rendered as the `Done — K subtask(s) succeeded`
  trailer with `streaming = false`.
- `*current` cleared after `Done` so the event loop stops ticking
  at 33ms for this turn.

Internal hook: `DesignSession::from_channels` (cfg(test)) wraps
externally-supplied channels so a fake worker can drive the UI
pumps without spinning up `Orchestrator::run` / an `agent::Provider`
/ a live API key. Production path still goes through
`DesignSession::start`.

The full LLM smoke is documented separately in
openpencil-docs/superpowers/notes/2026-05-24-orchestrator-smoke-steps.md
— it needs `OPENPENCIL_ANTHROPIC_API_KEY` and can't live in cargo test.

cargo test -p op-host-desktop 106 passed (was 105). cargo fmt + clippy
--workspace -D warnings clean.
2026-05-24 15:35:25 +08:00
Fini 37f1bb202c fix(host): drop in-flight design session when falling to chat path
Codex stop-time review caught a real cross-session leak in the intent
gate introduced by the previous commit. `launch_if_pending` clears
`current_chat` when the design branch is taken, but the chat branch
only cleared `current_chat` (for the unwired-agent error path) — it
never cleared `current_design`.

Symptom: if a design turn is in flight (worker pumping `apply` requests
+ `Progress` deltas into the trailing assistant bubble) and the user
sends a chat message, the new chat bubble begins streaming chat deltas
while the still-running design worker keeps acking apply requests on
the canonical doc and appending `Progress` strings to the same bubble.
The fix mirrors the design branch — when the chat path is taken (chat
intent, design intent without a Provider, or any unwired agent),
`*current_design = None` drops the worker's command receiver so its
next `apply` returns false and the turn ends.

`cargo test -p op-host-desktop` 105 passed. workspace fmt + clippy
clean.
2026-05-24 15:16:07 +08:00
Fini f00f1d213d feat(host,orchestrator): wire intent gate + DesignSession into chat runtime
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).
2026-05-24 15:11:34 +08:00
Kayshen-X 26501fb84a fix(panels): keep image fill popover interactive 2026-05-24 14:01:39 +08:00
Kayshen-X bfd1876538 feat(editor): add hover wash to vertical toolbar buttons
Mirror the existing file_menu / locale_picker / shape_picker hover
pattern: state-layer ToolbarHover enum on EditorUiState, widget reads
it during paint and renders theme.button_hover, host updates it on
apply_cursor_move AFTER drag detection so a path-anchor / node / pan
drag whose cursor crosses the toolbar isn't intercepted.

Extracted into widget_host/toolbar_hover.rs to keep geometry.rs under
the 800-line cap.
2026-05-24 14:01:38 +08:00
Fini 90e8b7ff0a Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 13:03:26 +08:00
Kayshen-X 83067b42d9 feat(editor): add hover wash to vertical toolbar buttons
Mirror the existing file_menu / locale_picker / shape_picker hover
pattern: state-layer ToolbarHover enum on EditorUiState, widget reads
it during paint and renders theme.button_hover, host updates it on
apply_cursor_move AFTER drag detection so a path-anchor / node / pan
drag whose cursor crosses the toolbar isn't intercepted.

Extracted into widget_host/toolbar_hover.rs to keep geometry.rs under
the 800-line cap.
2026-05-24 12:57:33 +08:00
Kayshen-X 059c261028 fix(editor): wire rust ui interactions 2026-05-24 12:57:32 +08:00
Fini 02e81c7974 fix(editor-core,host): SetNode{Stroke,Fill}Hex accept $variable refs
Codex stop-time review caught a real regression in LintPreValidator:
when the doc declares a `color-border` variable, op-design-lint's
`border_stroke()` emits the suggested stroke fill as `$color-border`
(a design-token reference, not raw hex). The host adapter decomposes
`SetStroke` into `SetNodeStrokeHex` + `SetNodeStrokeWidth`. But
`cmd_set_node_stroke_hex` strict-parsed the hex via `parse_hex_rgb`
and rejected the `$`-prefixed string with `return false`, while
`SetNodeStrokeWidth` still applied successfully — so
`any_applied = true` and `applied_count += 1` overreported success
while the stroke color was silently dropped.

The TS counterpart (`fixes.rs::set_stroke_from_json` via
`serde_json::from_value::<PenStroke>`) treats `color` as a free
`String` and accepts `$ref` verbatim. The underlying
`set_primary_{stroke,fill}_hex` already writes `slot.color =
hex.to_string()` unconditionally — only the cmd-layer gate was wrong.

Fix:
- `cmd_set_node_stroke_hex` + `cmd_set_node_fill_hex` (op-editor-core):
  let `$`-prefixed strings bypass `parse_hex_rgb`. Literal hex paths
  unchanged; doc-comment notes the design-token contract.
- New fixture `invisible-container-with-var.json` (op-design-lint):
  doc declares `color-border` var so the detector emits
  `$color-border` ref. Regen'd golden confirms the ref reaches the
  Issue's `suggestedValue`.
- New equivalence test `equivalence_invisible_container_with_var` in
  `plan.rs`: `detect_and_plan + apply` produces same doc as
  `detect_and_fix`, AND the plan carries the `$color-border` ref
  (not a resolved hex).
- New parity test `parity_invisible_container_with_var` in
  `pre_validator.rs`: cross-crate adapter round-trip preserves the
  ref through EditorCommand application.

All 5 `Skipped*Provider` and 4 `PreValidator`-path stubs still in
place. drift guard re-runs clean (14 goldens, same as committed).

`cargo test` op-design-lint 143+13 / op-host-desktop 102 /
op-editor-core 280 / op-orchestrator 585+1 all green.
`cargo clippy --workspace -- -D warnings` clean.
`cargo fmt --all -- --check` clean.
2026-05-24 12:57:31 +08:00
Fini fe7f173906 feat(host): LintPreValidator adapter — wire op-design-lint into PreValidator
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.
2026-05-24 12:35:02 +08:00
Fini 38a8503ae6 feat(design-lint): editor-agnostic PlannedFix + detect_and_plan API
Add `PlannedFix` / `PlannedAction` types and `detect_and_plan()` in a
new `plan.rs` module. `detect_and_plan` runs `detect_all` then applies
the same filter as `apply_fixes` (skip Info severity, skip Fill no-op,
skip Remove on status-bar nodes) and returns a `Vec<PlannedFix>` — an
editor-agnostic plan that host adapters can translate to EditorCommands.

`PlannedAction` covers all 8 mutation paths in `apply_fixes` today:
`Remove`, `SetHeightFitContent`, `SetRotation`, `SetCornerRadius`,
`SetFontSize`, `ClearEffects`, `SetPadding`, `SetStroke`.

Adds 6 equivalence tests asserting that applying each `PlannedFix` via
`node_mut` primitives produces a document structurally identical to
`detect_and_fix` on the same input (invisible-container, text-explicit-
height, stacked-horizontal-padding, empty-path, status-bar guard,
unexpected-rotation fixtures). All 142 + 13 parity tests pass.
2026-05-24 12:24:52 +08:00
Fini 0710f940e4 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 01:52:13 +08:00
Kayshen-X b7ff669d7c fix(ci): restore diagnostics golden formatting 2026-05-24 00:53:05 +08:00
Fini 0af4f22069 fix(ci): protect op-design-lint goldens from oxfmt + restore TS-oracle format
CI 'Fail on golden drift' (rust-check.yml) was red because oxfmt
inlined the 4-element padding arrays in the committed goldens, while
the TS oracle's `JSON.stringify(value, null, 2)` emits them multi-line.
Every `bun run format` would re-introduce the drift.

Fix:
- .prettierignore: ignore crates/op-design-lint/tests/fixtures/golden/
  so oxfmt leaves them at the TS-oracle byte-exact format.
- regenerate the two affected goldens via the oracle
  (edge-section-padding.json, stacked-horizontal-padding.json) so they
  match what the CI drift guard expects.

`bun run tools/dump-diagnostics-golden.ts` + `git diff` now clean.
`cargo test -p op-design-lint` 149/149 green. fmt-check clean.
This was the only red workflow on the v0.8.0-new push; CI should be
fully green after this lands.
2026-05-24 00:50:31 +08:00
Fini 8a39bc7b03 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-24 00:39:09 +08:00
Kayshen-X cb9a0ae692 fix(ci): clear rust check failures 2026-05-24 00:29:43 +08:00
Kayshen-X 44c2b990fa feat(editor): enable figma import drop zone 2026-05-24 00:29:05 +08:00
Kayshen-X caa53e55ff chore(test): format golden fixtures 2026-05-23 23:55:00 +08:00
Kayshen-X 66b6330914 fix(canvas): render image fills on canvas 2026-05-23 23:48:11 +08:00
Kayshen-X 0948e97de9 feat(panels,canvas): editable gradients/effects + SVG/image import + locale-aware dialogs
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.
2026-05-23 23:11:38 +08:00