Commit graph

81 commits

Author SHA1 Message Date
Fini 9af99ba62e feat(ai): infer semantic roles from node names (role-resolver I1)
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.
2026-06-06 21:42:57 +08:00
Fini cd8837128b test(ai): add direct-client headless benchmark harness to op-smoke
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.
2026-06-06 15:45:37 +08:00
Kayshen-X 561687de1b fix(mcp): resolve fit-content text bounds 2026-06-06 11:32:10 +08:00
Kayshen-X 54fb746881 feat(mcp): expand rust editor mcp surface 2026-06-05 22:19:08 +08:00
Fini b349b06f2a feat(ai): headless model-design benchmark harness
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.
2026-06-04 02:33:08 +08:00
Kayshen-X 0282283cb6 feat(editor): command-layer ops for MCP layered design
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.
2026-06-02 09:42:04 +08:00
Kayshen-X 68394826fe fix(cli): add figma import parity 2026-06-02 09:41:47 +08:00
Kayshen-X cb1cf55ae9 fix(mcp): add batch get parity 2026-06-02 09:41:39 +08:00
Kayshen-X 2469f424f0 fix(mcp): add design prompt parity 2026-06-02 09:41:36 +08:00
Kayshen-X e3ae058b29 fix(cli): align rust op with live mcp 2026-05-31 22:00:41 +08:00
Kayshen-X 43e6bedcfb fix(ci): vendor openssl for macos cross checks 2026-05-31 17:53:17 +08:00
Kayshen-X 22305a29d4 fix(ci): address native action failures 2026-05-31 17:30:30 +08:00
Kayshen-X 5974b2f55e fix(git): migrate repo operations to libgit2 2026-05-31 17:04:56 +08:00
Kayshen-X 19652f0724 feat(panels): improve native property controls and icons 2026-05-24 23:30:00 +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 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 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
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 059c261028 fix(editor): wire rust ui interactions 2026-05-24 12:57:32 +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
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
Fini 8cff5abaf5 feat(ai): implement op-design-lint Rust crate (S1)
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.
2026-05-23 18:39:08 +08:00
Fini 3ff8d92601 Merge branch 'v0.8.0-new' of github.com:ZSeven-W/openpencil into v0.8.0-new 2026-05-23 14:29:26 +08:00
Kayshen-X 2efca5aa3b feat(topbar): sun/moon theme toggle icon + traffic-light reposition fixes
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.
2026-05-23 12:59:02 +08:00
Fini 7f5152ac07 feat(orchestrator): concurrent executor — semaphore + serialized replay
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.
2026-05-23 04:06:51 +08:00
Fini 599e9b4ffa feat(orchestrator): run — planning mode-rotation retry loop
Replace the single hardcoded-Rich planning call with the mode-rotation
retry loop (S3b-1b Task C2). Resolves tier via model_profile, loops
attempt_modes(tier) ([Rich]/[Rich,Minimal]/[Rich,Minimal,Compact]),
handles stream-error rotation, abort short-circuit, parse failure
continue/fall-through, compact forced_style_guide_name backfill, and
exhaustion → build_fallback_plan. Adds tracing dep for warn! diagnostics.
2026-05-23 02:48:00 +08:00
Fini 1fa2d9415a Merge branch 's3a-orchestrator' into v0.8.0-new 2026-05-23 01:27:30 +08:00
Fini b1c3136deb feat(editor): EditorCommand::InsertSubtree — nested PenNode subtree insert
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.
2026-05-22 22:56:10 +08:00
Kayshen-X a550af4e7f feat(figma): wire .fig binary import into the desktop host
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.
2026-05-22 22:55:47 +08:00
Kayshen-X b979b97b54 feat(desktop): set the Dock name + icon for the dev binary
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.
2026-05-22 16:13:00 +08:00
Kayshen-X f1c45d6b9c feat(chat): OS clipboard copy/paste for the chat input
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.
2026-05-22 11:40:30 +08:00
Kayshen-X 6e012049e4 feat(editor): add Design-MD panel and complete i18n coverage
Design-MD panel — closes the last TS-vs-Rust parity gap:
- New floating op-editor-ui design_md_panel + design_md_markdown
  renderer: collapsible theme/colour/typography/component/layout/
  notes sections, colour swatches, inline markdown highlighting.
- op-editor-core: parse_design_md parser + panel UI state.
- Host: View-menu toggle, top-most paint + hit-test, drag,
  .md import/export; bumps the vendor/jian DesignMdSpec pointer.

i18n coverage:
- Git panel, merge/conflict/error dialogs and GitError messages
  routed through op-i18n; git locale tables split into
  <base>_git.rs under the 800-line cap.
- Closed 10 app-wide hardcoded-string gaps (AI chat, Figma import,
  file menu, layer context menu, property-panel fill, update /
  file-picker / load-error dialogs, accesskit labels).
2026-05-19 22:29:22 +08:00
Kayshen-X 64a5934dcf feat(desktop): add op-git crate for in-app version control
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.
2026-05-18 21:45:00 +08:00
Kayshen-X e775fd49c4 feat(desktop): winit shell platform integration
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>
2026-05-18 08:29:36 +08:00
Kayshen-X 4adfa38bae feat(ai): chat image attachments + per-provider thinking/effort
ChatRequest gains an attachments field; ChatState carries a per-turn
thinking mode, effort level and staged attachments (capped at 4 files /
5 MiB, attachment-only sends allowed). The chat panel grows a controls
row (thinking / effort / attach) and a dedicated attachment-chip row,
with hit-test and paint sharing one input-block origin.

All five providers consume the knobs — Claude maps thinking onto the
SDK token budget, Copilot onto reasoning_effort, the CLI / built-in
transports prepend an in-band directive, the HTTP transport adds body
fields; attachments spill to a private per-turn temp dir (cleaned on
drop) or inline base64. Also wires op-ai-skills into the built-in
provider and op-acp in as the AcpProvider chat backend.
2026-05-17 22:44:28 +08:00
Kayshen-X 155f9daee7 feat(figma): wire parse_fig binary pipeline + image resolver
Stage G — the binary `.fig` parser is now end-to-end functional;
`parse_fig` no longer returns NotYetImplemented for binary input.

- image_resolver.rs: resolve_image_blobs walks the converted document
  and replaces `__blob:N` / `__hash:HEX` image-fill placeholders with
  base64 `data:` URLs (MIME sniffed from magic bytes).
- lib.rs: new `parse_fig_binary(bytes, name, layout_mode) -> FigImport`
  runs the whole pipeline — container split → Kiwi decode → tree
  build → node conversion → image resolution. `parse_fig`'s Binary
  arm delegates to it; `FigParseError::NotYetImplemented` retired in
  favour of `Binary(String)`. Entry points re-exported.
- binary_e2e_tests.rs: assembles a real fig-kiwi container from
  scratch (hand-built Kiwi schema + data chunks, deflate-compressed)
  and asserts the full pipeline yields a PenDocument with the
  rectangle at the right position/size + canonical-JSON round-trip.

op-figma 83 tests green (+6); clean build, no warnings.

Closes the §2.1 P0 gap — Figma binary `.fig` parsing.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:23:19 +08:00
Kayshen-X 3fd1f20540 feat(figma): binary .fig container + decompression layer
First stage of the Figma binary `.fig` parser port (op-figma was
clipboard-JSON only; binary returned NotYetImplemented).

- zip_reader.rs: hand-rolled minimal ZIP reader — EOCD scan, central
  directory walk, store (method 0) + raw-deflate (method 8) entries.
  Avoids the full `zip` crate dependency tree. 512 MiB per-entry cap.
- container.rs: ports `fig-parser.ts::figToBinaryParts` — unwraps the
  ZIP archive form (`canvas.fig` + `images/*`), verifies the
  `fig-kiwi` magic, splits length-prefixed chunks, decompresses each
  via the deflate→zstd→raw fallback chain (PNG payloads passed
  through). Output: decompressed parts + embedded image map.
- deps: flate2 (miniz_oxide pure-Rust backend), ruzstd, base64 — all
  license-clean + wasm32-buildable.

Modules are `allow(dead_code)` until `parse_fig` is wired in the
final stage. op-figma 23 tests green (+6).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:50:15 +08:00
Kayshen-X c18ad5a776 feat: close six TS-parity gaps in the Rust shell
Closes six verified gaps from the 2026-05-17 TS-vs-Rust gap analysis,
each build- and test-green:

- editor: SetNodeFlip + SetEllipseArc commands (+ set_node_flip /
  set_ellipse_arc MCP tools) — schema already had the fields, only
  the command path was missing.
- export: export_node_raster crops a raster to one node's bbox;
  File -> Export is now selection-aware (single selection -> layer).
- editor: SVG import — hand-rolled parser (shapes + path M/L/H/V/
  C/S/Q/T/Z) in svg_import.rs; cubic curves flatten to dense straight
  anchors at import time so the renderer/pen-tool stay on their 1:1
  straight-segment model. + EditorCommand::ImportSvg + import_svg tool.
- mcp: HTTP transport — mcp_serve::run_http serves MCP over a
  TcpListener (--mcp-http <port> <path>); process_message is shared
  with the stdio path.
- cli: new op-cli crate (binary `op`) — a dependency-free HTTP MCP
  client driving every tool via `op <tool> key=value...`.
- ai: ChatRequest gains thinking/effort fields (ThinkingMode /
  EffortLevel, defaults Adaptive/Low to match the TS runtime config).

MCP tool catalog 77 -> 80. Oversized files split to honour the
800-line cap (svg_import, export, mcp_serve, adapter).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:03:11 +08:00
Kayshen-X 4b8e0ce956 refactor(rust): add op-app composition-root crate
Phase 7.3 strangler reorg — add op-app, the thin crate that names the
editor application's composition root.

Investigation found no shared host bootstrap left to extract: the
editor-UI composition (widgets, theme, layout scene) already lives in
op-editor-ui, and each host (op-host-native / op-host-web) owns only
platform-specific backend wiring. So per YAGNI op-app stays thin — it
re-exports op-editor-ui plus the per-platform host entry point behind
its target cfg, and documents the composition. If real cross-host
wiring later emerges, it lands here.

Builds green on both native and wasm32-unknown-unknown.
2026-05-16 23:57:03 +08:00
Kayshen-X 8733f45e2d refactor(rust): dissolve openpencil-shell-core re-export shim
Phase 7.3 strangler reorg — the final consumer (op-pen-loader) is
repointed off the openpencil-shell-core shim onto op-editor-ui (the
real source crate for the layout scene / scene-var / render-backend
facade), then the shim crate is deleted.

- op-pen-loader: openpencil-shell-core dep -> op-editor-ui;
  every openpencil_shell_core:: path -> op_editor_ui::
- git rm crates/openpencil-shell-core/ (lib + jian.rs module + the
  two re-export anchor tests, all superseded by op-editor-ui's own
  surface; the jian.rs module had no consumers outside the shim)
- stale shell-core / shell-native comment refs in op-editor-core +
  op-host-desktop manifests updated
2026-05-16 23:54:33 +08:00
Kayshen-X 1e0b3cab7a refactor(rust): rename native + desktop hosts to op-host-* crates
Phase 7.3 strangler reorg — rename the native widget host and the
desktop runner crates to the op- prefix. The desktop+native merge was
declined: keeping the library / binary split preserves the mobile-
checkable op-host-native lib (cargo check -p op-host-native on iOS /
Android, relied on by check-jian-boundaries.sh + the CI mobile job),
which a folded-in winit binary would break. A clean separate rename is
purely mechanical and the brief permits it.

- openpencil-shell-native -> op-host-native (lib op_host_native)
- openpencil-desktop -> op-host-desktop crate; the shipped executable
  keeps the stable openpencil-desktop [[bin]] name so release
  artifacts + external CLI integrations are unaffected
- every openpencil_shell_core:: path -> op_editor_ui::
- every openpencil_shell_native:: path -> op_host_native::
- doc-comment / manual-smoke note refs updated
2026-05-16 23:49:58 +08:00
Kayshen-X 65c2aa055a refactor(rust): rename openpencil-shell-web host to op-host-web
Phase 7.3 strangler reorg — rename the web widget host crate to the
op- prefix. The crate's openpencil-shell-core dependency is repointed
to op-editor-ui (the real source crate for the widget facade / theme
/ layout scene / scene vars / render-backend / gesture types).

- crate name: openpencil-shell-web -> op-host-web
- lib name: openpencil_shell_web -> op_host_web
- every openpencil_shell_core:: path -> op_editor_ui::
- native skia.rs include_bytes! path follows the moved assets dir
- smoke harness + lib doc-comment refs updated
2026-05-16 23:42:49 +08:00
Kayshen-X 43cb0fdf7e chore: sync Cargo.lock for the op-* crate extractions
Record the op-codegen / op-figma / op-mcp / op-ai / op-editor-ui
package entries in the workspace lockfile.
2026-05-16 23:35:10 +08:00
Kayshen-X e9960e6944 refactor(editor): make EditorState the web host's single source of truth
Mirror the native host migration for `openpencil-shell-web`. The web
`WidgetHost` now holds an `op_editor_core::EditorState` authoritatively
instead of a shell-core `Document`. The ~30 shared widgets stay
`&Document`-bound and read-only — they are fed a derived `paint_doc`
snapshot rebuilt lazily by `refresh_paint_doc()` whenever
`editor_state_dirty` is set.

- Every mutation routes through an `op-editor-core` mutator + flags the
  dirty bit; the snapshot re-derives once before paint / before any
  hit-testing input event.
- Hit-test-then-mutate handlers refresh, hit-test on `&paint_doc`,
  extract owned results, then mutate `editor_state`; shell-core hit
  enums translate via `op_pen_loader::rev::*`.
- `paint` takes `&mut self` to drain the cache at the top of the pass.
- `op-editor-core` / `op-pen-loader` are optional deps behind the
  `skia` feature (matching `skia-safe`) so the skia-free wasm32 CI stub
  baseline stays clean — `op-pen-loader` pulls `jian-skia` transitively.
- Fix two pre-existing `--features skia` build blockers in `lib.rs`
  (missing `Performance` web-sys feature, `canvas` moved before reuse).

No web-editor behaviour change — feature parity preserved.
2026-05-16 20:13:42 +08:00
Kayshen-X f0bb56b6cd refactor(editor): make EditorState the native host's single source of truth
Flip WidgetHostNative off shell-core's Document onto
op_editor_core::EditorState. The host now owns one authoritative
state; every paint pass + hit-test reads a lazily-derived,
read-only Document snapshot (`paint_doc`), rebuilt via
op-pen-loader's pen_document_to_document + apply_editor_state_ui
whenever an `editor_state_dirty` flag is set.

Why: removes the dual-state strangler scaffold so the canonical
PenDocument model is the only editable state — input handlers
mutate EditorState, widgets stay read-only over the derived
Document. .op save now serializes editor_state.doc; load seeds
EditorState::from_document. shell-core's Document becomes a
paint-only target.

- op-editor-core: add host_support (EditorState::sample,
  create_node_for_tool, replace_paths_with_polyline) + re-export
  VariableScalar/VariableKind.
- shell-native: rewrite all 8 widget_host submodules onto
  editor_state; split press.rs -> press_helpers.rs and keyboard
  click routing -> click.rs to stay under the 800-line cap;
  boolean_ops becomes a pure compute_boolean_op committed back
  through EditorState.
- desktop: persistence / settings_io / chat_session /
  model_discovery / main / frame move onto editor_state accessors.
2026-05-16 19:29:34 +08:00
Kayshen-X a79dbaf0e3 feat(op-editor-core): close variable/theme gaps for the host migration
Three additive variable-handling fixes so the editor host can migrate
onto EditorState as a mechanical port:

- Gap 1: add `variable: Option<String>` to ColorPickerState, port
  `open_color_picker_for_variable`, and route `color_picker_set_hsv` /
  `close_color_picker` through `set_variable_color` when set.
- Gap 2: add `op_pen_loader::editor_state_var_table(&EditorState)`,
  folding persisted variables/themes plus the transient active-theme
  selection + ref caches into a shell-core VariableTable. Lives in
  op-pen-loader (not op-editor-core) to keep op-editor-core wasm-clean
  and free of any shell-core dependency.
- Gap 3: confirmed undo/redo of variable create/delete/rename round-
  trips for free (EditorSnapshot clones the whole PenDocument); added
  tests to lock it in.
2026-05-16 18:19:35 +08:00
Kayshen-X 77f89b6ef8 refactor: extract op-pen-loader (canonical .op → Document) into a shared crate 2026-05-16 18:04:46 +08:00
Kayshen-X ec5ac70118 feat(op-editor-core): extend EditorState into a full state superset of Document 2026-05-16 17:47:53 +08:00