Commit graph

1074 commits

Author SHA1 Message Date
Kayshen-X 25e9739e57 fix(canvas): codex review round 4 — arc clamp, path hit-test, drag cleanup
Addresses the remaining round-4 codex findings (the `short_src`
byte-slice panic + the arc-handle reverse-iteration fixes already
landed via an earlier sweep).

- `cmd_set_ellipse_arc` clamps `sweep_angle` to ±360° — an API /
  MCP sweep beyond a full turn just over-draws; it now persists a
  sane single-revolution value.
- Path hit-test (`point_in_node`) follows the flattened, bezier-
  aware outline instead of the bounding box: a curved or thin path
  no longer selects empty bbox space, a zero-height stroked path
  stays clickable, and a filled closed path is hittable across its
  interior (new `point_in_polygon` even-odd test).
- The viewport-less `apply_release` now commits / clears
  `path_anchor_drag` + `arc_handle_drag` (parity with
  `apply_release_with_viewport`) so a drag can't leak across that
  release path.

op-editor-core 242 / op-editor-ui 157 / op-host-native 21 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 23:00:56 +08:00
Kayshen-X 5b6874b7d8 style: apply rustfmt across the AI-subsystem crates
CI's Rust Check runs a workspace-wide `cargo fmt --check`. Reformat the
new op-acp / op-ai-skills crates and the Part A chat changes to rustfmt
canon. Also sweeps two files an earlier commit left non-compliant
(canvas_viewport_paint.rs, op-pen-loader/adapter.rs) so the workspace
check is clean. Formatting only — no behaviour change.
2026-05-17 22:50:27 +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 4496beaf03 feat(ai): add op-ai-skills crate — phase-driven prompt-skill engine
Port of the TS packages/pen-ai-skills engine: a hand-rolled frontmatter
parser (inline / multi-line / next-line / block-list YAML forms), phase
filter + keyword/flag intent matching, token budgeting, the resolve
pipeline, design-context + generation-history memory, and style-guide
selection/parsing. The 95-file skill markdown corpus is embedded
verbatim via include_dir; the crate stays wasm-clean.
2026-05-17 22:44:19 +08:00
Kayshen-X ddaf3b7a17 feat(ai): add op-acp crate — Agent Client Protocol client
Port of the TS packages/pen-acp: ndJSON transport, a hand-rolled
JSON-RPC engine (request-id correlation, session/update notification
routing, session/request_permission auto-approval, and a pending-request
drain on EOF so a dead agent fails fast instead of timing out), and an
AcpConnection driving initialize / session/new / session/prompt over
local stdio or a remote WebSocket. The event adapter maps ACP session
updates onto the chat panel's ChatDelta vocabulary.
2026-05-17 22:44:10 +08:00
Kayshen-X a459a29825 fix(canvas): a filled closed path no longer draws an implicit stroke
The round-3 closed-path fill left the stroke loop unconditional, so
a closed path with a fill but no explicit stroke was filled AND
outlined in the fill colour. Now the stroke paints only for an
explicit `node.stroke`, or — with no stroke — only when the path is
unfilled (so a bare path stays visible). op-editor-ui 176 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 19:37:50 +08:00
Kayshen-X 0428c6e17b fix(canvas): codex review round 3 — sweep sign, path bounds, closed fill
Addresses the 3 CONCERNs from the third codex review.

- `signed_sweep`: a negative-sweep drag that collapses onto the fixed
  endpoint now snaps to a full -360° circle instead of 0° (mirrors
  `norm_sweep`'s positive 0 → 360 rule) — a negative arc no longer
  silently loses its sign in that degenerate case.
- Path fallback sizing (`path_to_payload`): an unsized path now
  derives its width/height from the handle-aware
  `path_bounds_from_anchors` (cubic extrema included) instead of the
  endpoint-only point bbox, so handles bowing past the anchors no
  longer under-size the scene node.
- Closed-path fill: a closed `Path` with a fill now paints its
  enclosed area via `fill_polygon` over the flattened outline — was
  stroked-only, so authored fills rendered as bare outlines.

Verified against op-editor-core 239 / op-pen-loader 21 / op-editor-ui
155 (op-host-native not test-built — an unrelated in-progress
AIChat thinking/effort change in the working tree leaves its
`AIChatHit` match non-exhaustive; `signed_sweep` is a pure-fn change).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 19:31:41 +08:00
Kayshen-X 4794e323ac fix(canvas): codex review round 2 — drag history + closed paths
Addresses the 4 CONCERNs from the second codex review.

- No-op undo: arc + path/handle drags now mutate nothing until the
  cursor first travels — a press-release leaves the document and
  undo stack untouched. Once the drag has moved, every event keeps
  writing (so a drag back to the start point still lands), gated on
  `is_move || already_moved`. This closes the hole where a ghost-
  handle press-release created a handle with no undo entry.
- Negative arc sweep: `signed_sweep` keeps the sign of the arc being
  dragged, so an MCP-authored counter-clockwise (negative) sweep no
  longer flips to the major arc under a canvas drag.
- Closed paths: `path_closed` threaded through NodePayload +
  SceneNode; `flatten_path` appends the last-anchor → first-anchor
  closing segment (cubic or straight) so a closed canonical path
  draws its closing edge.

op-editor-core 235 / op-pen-loader 21 / op-editor-ui 148 /
op-host-native 64 / op-host-desktop 21 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 19:16:47 +08:00
Kayshen-X 82d33e27df fix(canvas): arc-sector hit-test handles a negative sweep
A negative `sweep_angle` covers the angular range `[start + sweep,
start]`; the hit-test compared against `sweep.abs()` from `start`,
rejecting points that are actually inside such an arc. Normalise to
a forward sweep before the angle-window test.

Self-review follow-up to the arc/pen-handle work (the codex second
review stalled mid-run). op-editor-ui 148 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 18:56:04 +08:00
Kayshen-X 442dbabb78 fix(canvas): codex review — rotation, arc hit-test, path bounds
Addresses the first codex review of the arc + pen-handle features.

BLOCKs:
- Rotation: the arc + pen-handle overlays and the host hit-tests now
  account for node rotation. The overlay paint wraps in save/rotate
  like the selection overlay; the hit-tests + drag math un-rotate the
  cursor into the node's local frame via `rotate_point`.
- Arc ellipse hit-test: a pie / arc / donut now hit-tests against the
  actual sector — the missing wedge + the donut hole are no longer
  selectable (was always the full oval).
- Arc no-op undo: `ArcHandleDragState` gains `start_doc`; the move
  handler gates `moved` on real cursor travel so a press-release
  pushes no undo entry.

CONCERNs:
- Path bounds: the bezier handle-aware bounds algorithm moved into a
  shared `op_editor_core::path_bounds`; `refit_path_bounds` uses it
  (and is now called after handle / point-type edits) so the loader's
  absolutize scale stays 1.0 — a handle no longer rescales the path.
- `cmd_set_ellipse_arc` now honours the locked/hidden editability
  guard like the other geometry mutators.
- A full-ring donut strokes its two concentric ovals instead of the
  polygon, so the radial seam is not drawn.

op-editor-core 235 / op-editor-ui 148 / op-pen-loader 21 /
op-host-native 64 / op-host-desktop 21 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 18:07:11 +08:00
Kayshen-X 112d1f08bb feat(canvas): pen bezier handle editing — render, handles, drag
Completes pen bezier-handle editing on top of the Stage-1 data model.

- canvas_viewport_paint: `flatten_path` — Path nodes with anchor
  control handles render as tessellated cubic Beziers (16 steps /
  segment); handle-free paths keep the straight `points` polyline.
- canvas_viewport: the Pen-tool anchor overlay now draws each
  anchor's two control handles (line + dot), with a faint "ghost"
  dot offset from the anchor when a handle is unset — grab it to
  create the handle. `path_handle_positions` resolves real / ghost
  handle positions, shared with the host hit-test.
- op-host-native: `path_anchor_hit` now distinguishes anchor body
  vs handle_in / handle_out (`AnchorDragTarget`); `PathAnchorDragState`
  carries the target, the anchor's fixed position, and the Shift
  state. The move handler drags the anchor or a handle — a handle
  drag sets the anchor's point type on first motion (Shift =
  independent/broken, else mirrored/smooth) so `set_path_anchor_handle`
  mirrors the opposite handle. Release commits history only on
  actual motion.

op-editor-ui 148 / op-host-native 64 / op-host-desktop 21 /
op-pen-loader 21 tests green (+3 flatten-path units).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 17:47:08 +08:00
Kayshen-X 79b51573dd feat(editor): pen anchor bezier-handle data model + setters
Stage 1 of pen bezier-handle editing — the data + command layer.

- op-editor-core: `EditorState::set_path_anchor_handle` (set / clear
  an anchor's in/out handle; a `Mirrored` anchor keeps both handles
  collinear) + `set_path_anchor_point_type` (switching to `Mirrored`
  snaps the handles collinear). New `PathHandleSide` enum.
- op-pen-loader: `AnchorPayload` (absolute-coord anchor + resolved
  handles + point-type code) on `NodePayload.path_anchors`;
  `absolutize_path_anchors` now resolves the schema's anchor-relative
  handle deltas into the same absolute frame as `points`.
- op-editor-ui: `SceneAnchor` + `ScenePointType` on
  `SceneNode.path_anchors` so the painter + host can read the
  editable handles.

op-editor-core 234 / op-pen-loader 21 / op-editor-ui 145 tests green
(+3 handle-setter units).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 17:33:44 +08:00
Kayshen-X b7bcd9db12 feat(canvas): ellipse arc drag handles
Adds the canvas drag handles that author an ellipse arc — start
angle, sweep (end) angle, and the donut inner-radius.

- op-editor-ui: `ArcHandle` enum + `arc_handle_positions` (doc-space
  positions of the 3 handles for an Ellipse scene node), re-exported
  from `widgets`. The canvas overlay paints them as filled
  primary-tinted dots for a single-selected Ellipse with the Select
  tool.
- op-host-native: `ArcHandleDragState` + `arc_handle_hit` (checked
  before the resize handles since the sweep grip can overlap the
  right-mid resize handle). Press starts the drag with a history
  snapshot; each move recomputes start/sweep/inner from the cursor
  via `arc_drag_command` and re-applies `SetEllipseArc` (no-history
  apply); release commits the snapshot only when the arc changed.
  Dragging the start handle keeps the end fixed; a sweep collapsing
  to 0 snaps to a full 360° circle.

op-editor-ui 145 / op-host-native 64 / op-host-desktop 21 tests
green (+2 arc-handle-position units).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 17:26:37 +08:00
Kayshen-X bfbe3b161d feat(canvas): render ellipse arcs / pie / donut sectors
Ellipse nodes with authored arc geometry (`start_angle` /
`sweep_angle` / `inner_radius`) were always painted as a full oval —
the arc fields never reached the painter.

- NodePayload + SceneNode gain `arc_start_angle` / `arc_sweep_angle`
  / `arc_inner_radius`; threaded through `ellipse_to_payload` and
  `node_payload_to_scene`.
- canvas_viewport_paint: new `arc_polygon` tessellator (pie wedge =
  centre + outer arc; donut sector = outer arc + reversed inner arc)
  + `paint_ellipse` — full oval when no arc is authored, otherwise a
  filled/stroked polygon. A 360° sweep with no donut hole still
  short-circuits to the plain oval path.

Prep for the arc canvas drag-handle UI. op-editor-ui 143 /
op-pen-loader 21 / op-host-desktop 64 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 17:10:50 +08:00
Kayshen-X 418c4acf3b fix(figma): codex review round 2 — pre-validation allocation guards
Addresses the 4 BLOCKs from the second codex review.

- kiwi: schema field-count pre-allocation is capped at the remaining
  buffer size (a hostile count no longer forces a huge Vec alloc).
- kiwi: array-length validation + pre-alloc now use the *remaining*
  byte count rather than the total buffer length (tighter bound).
- zip_reader: the aggregate-size budget is checked against the
  central directory's declared uncompressed size *before* the entry
  is decompressed, not after.
- zip_reader: stored (uncompressed) entries are now subject to the
  per-entry MAX_ENTRY_SIZE cap, matching the deflate path.

op-figma 84 tests green; clean build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:43:44 +08:00
Kayshen-X ac63c701e6 fix(figma): codex review round 1 — hardening + ZIP entrypoint
Addresses the first codex review of the binary `.fig` parser.

BLOCKs:
- kiwi: 64-bit varint now uses Kiwi's terminal-byte rule (eight
  7-bit groups then a final full-8-bit byte) — the old `& 127` mask
  on every byte corrupted u64 values above 2^56.
- kiwi: an invalid schema definition kind (> 2) is now rejected
  instead of silently treated as a message.
- kiwi: array decode rejects a length exceeding the buffer size —
  guards against a hostile zero-byte-element array spinning the
  decode loop billions of times.
- zip_reader: aggregate 2 GiB decompression budget + 10k entry cap
  on top of the existing per-entry limit (zip-bomb defence).

CONCERNs:
- detect_kind now recognises the `PK\x03\x04` ZIP magic as Binary —
  the common Figma export form (`canvas.fig` + `images/` in a ZIP)
  was being rejected before container.rs could unwrap it.
- resolve_style_references now also resolves style refs inside
  instance `symbolData.symbolOverrides` entries.
- kiwi: enum field type codes are no longer resolved (unused; kiwi
  writes 0) so a stray code can't reject a valid schema.

Plus a zip-wrapped end-to-end test + the misleading zstd test rename.
op-figma 84 tests green (+1); clean build.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:37:31 +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 7f41eeeeda feat(figma): node converters + figma-to-document mapper
Stages D+E — ports converters/* + figma-node-mapper.ts.

- common.rs: ConversionContext, JS-rounding helpers, transform →
  position/rotation/flip extraction, corner-radius mapping,
  common_props, resolve_width/height, scale_tree_children,
  collect_image_blobs, SKIPPED_TYPES.
- node_build.rs: PenNode constructors (frame/group/rectangle/ellipse/
  line/path/text/ref) — fill the behaviour-trait slots with None.
- converters.rs: convert_node dispatch + per-type converters
  (frame/group/component/instance/rectangle/ellipse/line/text/
  vector); auto-layout child ordering, arc-data flip absorption,
  zero-size vector bounds derivation, stroke-only-outline handling.
- instance.rs: apply_instance_overrides (size-scale fast path +
  direct-GUID override/derived resolution + nested forwarding) +
  merge_symbol_props.
- node_mapper.rs: resolve_style_references (inline style refs) +
  figma_to_pen_document / figma_all_pages_to_pen_document /
  get_figma_pages / figma_node_changes_to_pen_nodes entry points.

Whole pipeline compiles clean; op-figma 77 tests green (+5).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:17:26 +08:00
Kayshen-X 203e424950 feat(figma): document tree builder
Stage D part 1 — ports figma-tree-builder.ts.

- TreeNode: owned figma node + ordered children tree.
- build_tree: indexes node changes by guid, builds parent→children
  adjacency, materializes from the DOCUMENT root; children sorted
  descending by parentIndex.position (z-order). REMOVED / guid-less
  changes skipped; cyclic chains capped at MAX_TREE_DEPTH.
- build_tree_for_clipboard: orphan-rooted trees for clipboard data
  with no DOCUMENT wrapper.
- is_user_page (CANVAS, non-"Internal Only"), guid_to_string,
  collect_components (SYMBOL guid → fig_N id), collect_symbol_tree
  (SYMBOL guid → subtree).

op-figma 72 tests green (+4).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:09:23 +08:00
Kayshen-X e18916c68c feat(figma): text mapper + vector geometry decoder
Stage F part 2 — ports figma-text-mapper.ts + figma-vector-decoder.ts.

- text_mapper.rs: map_figma_text_props → TextProps. Builds TextContent
  (plain | per-run StyledTextSegment[] from characterStyleIDs +
  styleOverrideTable, UTF-16-indexed), parses font weight from the
  style name (ordered substring match), line-height → multiplier,
  letter-spacing → px, align / vertical-align / growth enums, and
  applies textCase (UPPER / LOWER / TITLE).
- vector_decoder.rs:
  - decode_figma_path_blob — the opcode command stream (Z/M/L/Q/C,
    f32-LE operands, graceful truncation).
  - compute_svg_path_bounds — coordinate-pair bbox.
  - decode_figma_vector_path — geometry-blob path (stroke centerline
    preferred for stroke-only shapes).
  - decode_vector_network_blob — vertex/segment table fallback, chain-
    walked into M/L/C subpaths, scaled by nodeSize/normalizedSize.

op-figma 68 tests green (+16).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:07:16 +08:00
Kayshen-X 1e32c6f6cf feat(figma): fill/stroke/effect/layout/sizing style mappers
Stage F part 1 — ports figma-color-utils.ts + figma-fill-mapper.ts +
figma-stroke-mapper.ts + figma-effect-mapper.ts + figma-layout-mapper.ts.

- color.rs: figma_color_to_hex (linear 0-1 → #RRGGBB[AA], JS
  round-half-up) + figma_color_opacity.
- mappers.rs:
  - map_figma_fills → PenFill[] — solid / linear (angle from the
    paint transform's column-0 direction) / radial-angular-diamond
    (cx/cy/r = 0.5) / image (`__hash:`/`__blob:` placeholder URLs);
    invisible + unmappable paints dropped.
  - map_figma_stroke → PenStroke — uniform vs per-side thickness,
    align/join/cap, dash pattern; stroke paints reuse the fill mapper.
  - map_figma_effects → PenEffect[] — drop/inner shadow, fore/
    background blur.
  - map_figma_layout → LayoutProps — stack direction, gap (skipped
    under space-between), per-side padding collapse, justify/align,
    clip-content default.
  - map_width_sizing / map_height_sizing → SizingBehavior — auto-
    layout fit-content / fill-container resolution per axis.

op-figma 52 tests green (+17).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 16:02:11 +08:00
Kayshen-X ed8e96a01d feat(figma): decoded-file model + parseFigFile pipeline
Stage 3 — ports figma-types.ts + fig-parser.ts::parseFigFile.

- FigGuid / FigMatrix / FigColor / FigVec2: the geometric primitives
  worth typing, each extracted from a decoded FigValue object;
  FigGuid::to_key is the canonical `sessionID:localID` map key.
- BlobOrString: the `blobs` pool element (raw geometry bytes | string).
- FigmaDecodedFile { node_changes, blobs, image_files }.
- parse_fig_file: the full pipeline — fig_to_binary_parts → decode the
  Kiwi schema chunk → decode the data chunk against it → extract
  nodeChanges (with the fallback scan for a guid-bearing array) +
  blobs. Mirrors parseFigFile / extractBlobs.
- FigValue gains generic get_f64/get_str/get_bool/get_array accessors.

op-figma 35 tests green (+5).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:56:56 +08:00
Kayshen-X aa72296be0 feat(figma): Kiwi schema decoder
Stage 2 of the binary `.fig` parser — ports the `kiwi-schema` subset
the format needs.

- ByteBuffer: LEB128 var-uint/int (32 + 64-bit, zigzag), Kiwi's
  exponent-rotated var-float (single-0-byte zero optimisation),
  NUL-terminated UTF-8 strings, length-prefixed byte blocks.
- decode_binary_schema: the self-describing schema chunk — definitions
  (enum/struct/message) + fields; native type codes resolve via
  `!code` into NATIVE_TYPES, definition codes by index (forward refs
  handled with a two-pass bind).
- FigValue: dynamically-typed decoded tree (the Rust stand-in for the
  TS untyped `any`) with get/as_f64/as_str/as_bytes/as_array accessors.
- decode_message: dynamic decoder (the `compileSchema` + `decode*`
  equivalent) — message id-prefixed fields, ordered structs, enum
  ordinal→name, `byte[]` as a raw block, MAX_DEPTH recursion guard.
  Roots at the `Message` definition like Figma's findDecoder.

Tested via an in-test Kiwi writer that round-trips schema + data
fixtures (enum/struct/message/arrays/byte-arrays/floats). op-figma
30 tests green (+7).

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 15:54:32 +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 448b39ec6c refactor(panels): split effects paint into property_panel_effects.rs
`property_panel_sections.rs` reached 862 lines after the effect
parameter-stepper work — over the 800-line repo file gate. Move
`paint_effects_section` + `paint_effect_row` + `paint_effect_param_row`
into a new `property_panel_effects.rs` (128 lines) and re-export
`paint_effects_section` from `property_panel_sections` so callers
keep using `sections::*`.

`property_panel_sections.rs` is now 755 lines. No behaviour change;
op-editor-ui 140 / op-host-desktop 64 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:45:43 +08:00
Kayshen-X 722445b017 feat(panels): shadow/blur parameter steppers in the Effects section
Completes the Effects-controls gap — each effect row now exposes its
editable scalar parameters:

- op-editor-core: `EffectField` enum + `EditorCommand::SetEffectParam`
  + `cmd_set_effect_param` — writes one shadow param (offset X/Y,
  blur, spread) or a blur/background-blur radius; blur values clamp
  to >= 0, field/effect mismatches reject.
- panels: each effect block paints a parameter row per field —
  `<label> <value> [-] [+]`; the "-"/"+" steppers emit
  `AdjustEffectParam` (the walker computes the post-step value from
  the current one). `effects_section_height` / the action-rect
  walker now take the effects slice so the variable per-kind block
  height stays aligned with paint (`VisibleSections.effect_count`
  retired in favour of the slice).
- both hosts dispatch `AdjustEffectParam` via `SetEffectParam`,
  history-committed.

op-editor-core 233 / op-editor-ui 140 / op-host-desktop 64 tests
green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:38:26 +08:00
Kayshen-X e30f3b12b4 feat(panels): effect rows + remove in the Effects section
The property panel's Effects section painted only a header + the
"+" add affordance. It now lists the selected node's effects:

- op-editor-core: `node_effects` read accessor exposes a node's
  `PenEffect` slice (container kinds via `container`, leaf shapes
  directly).
- panels: `NodeSnapshot.effects` carries an `EffectSummary` per
  effect; `paint_effects_section` paints one row per effect (type
  label + a "✕"); `VisibleSections.effect_count` + the shared
  `effects_section_height` keep paint and the action-rect walker's
  y-math aligned through the now-variable-height section.
- `PropertyPanelAction::RemoveEffect(index)` — both hosts dispatch
  it through `EditorCommand::RemoveNodeEffect` (history-committed).

Stage 1 of the Effects-controls gap (rows + add + remove); the
per-effect shadow/blur parameter inputs follow. op-editor-core 227 /
op-editor-ui 161 / op-host-desktop 64 tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:26:19 +08:00
Kayshen-X 99a2140049 test(editor): split attribute-command tests under the 800-line cap
`command_tests.rs` had grown to 906 lines past the repo's 800-line
cap as the flip / ellipse-arc / node-effect command tests landed.
Move those per-node-attribute apply tests into a sibling
`command_attr_tests.rs` (both files now well under the cap); the
core CRUD / selection / variable apply tests stay in place.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 14:00:34 +08:00
Kayshen-X aca1af5447 feat(editor): add node-effect add/remove commands + MCP tools
Effects were inert — the schema carries `PenEffect` (Shadow / Blur /
BackgroundBlur) but nothing could add or drop one.

- editor: `EditorCommand::AddNodeEffect` / `RemoveNodeEffect` +
  appliers. `node_effects_slot` reaches the `effects` field on every
  variant that has one (Frame/Group/Rectangle via `container`, the
  leaf shapes directly); `add` appends a default-parameter effect,
  `remove` drops by index and clears the list to `None` when empty.
- mcp: `add_node_effect` / `remove_node_effect` tools, registered on
  the host server (catalog 80 -> 82).

This is the command + MCP layer of the Effects gap; the property-
panel editing UI is the remaining piece. op-editor-core 227 /
op-mcp 138 / op-host-desktop mcp tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:48:31 +08:00
Kayshen-X 87e9655a59 feat(canvas): wire smart guides into node dragging
Completes the smart-guides gap on top of the align_guides geometry:

- editor: EditorUiState.active_guides — transient guide lines for the
  current drag (view-only, never serialized / undone).
- host: apply_smart_guides() runs after each node-drag translate —
  gathers the moving + sibling AABBs off the layout scene, calls
  compute_alignment_guides, applies the snap offset, stores the guide
  lines; drag release clears them.
- canvas: paints the active guide lines (magenta) over the nodes.

The "grid" half of the matrix row was already implemented
(canvas_viewport::paint_grid). op-editor-ui 140 + op-host-desktop 64
tests green.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:17:16 +08:00
Kayshen-X f65346c72e feat(editor): add smart-guide alignment geometry
`align_guides.rs` — pure alignment-guide computation for node
dragging: given the moving node's AABB + sibling AABBs it returns the
guide lines to paint and the snap offset that locks the drag onto the
nearest edge/centre alignment within a threshold. Two axes resolved
independently; closest candidate per axis wins, edge-to-edge
preferred on a tie.

This is the computation core of the smart-guides gap — fully unit
tested (6 tests). Host drag-handler wiring + canvas paint of the
returned guides follow as separate steps.

🤖 Generated with [Claude Code](https://claude.com/claude-code)
2026-05-17 13:07:50 +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 74c16077c1 ci: drop dead cross machinery from release + multiplatform workflows 2026-05-17 10:28:46 +08:00
Kayshen-X 7bead20e14 ci: build linux-aarch64 on native ARM runner instead of cross 2026-05-17 10:24:29 +08:00
Kayshen-X a3025d550e ci: fix aarch64 dev package name (libfreetype6-dev) 2026-05-17 10:16:38 +08:00
Kayshen-X efcac6d408 ci: install aarch64 freetype/fontconfig in cross container 2026-05-17 10:12:41 +08:00
Kayshen-X 031cedabf4 ci: run rust multi-platform build on all branches 2026-05-17 09:53:42 +08:00
Kayshen-X 6a29fb17c7 docs: translate remaining Chinese source comments to English 2026-05-17 09:33:41 +08:00
Kayshen-X 9b8f4baf19 fix(ci): drop redundant windows CommandExt import + tokio feature ban
- chat_subprocess.rs: `std::os::windows::process::CommandExt` is unused
  on Rust 1.94 (`creation_flags` resolves without it, like the unix
  `process_group` twin) — rejected by clippy `-D warnings` on Windows.
- deny.toml: remove the tokio process/rt-multi-thread feature ban; the
  native agent runtime legitimately needs them and the native cargo-deny
  job tripped on it. wasm exclusion stays structural.
2026-05-17 02:01:05 +08:00
Kayshen-X 3a084dd94c fix(ci): drop STEP1A_REQUIRE_GPU so headless runners use the raster fallback 2026-05-17 01:49:56 +08:00
Kayshen-X 7adfbd2e4d fix(ci): trigger rust-check on vendor/deny/workflow changes 2026-05-17 01:39:11 +08:00
Kayshen-X 22806a8d27 fix(ci): disable autocrlf on the Windows runner so cargo fmt --check passes 2026-05-17 01:37:09 +08:00
Kayshen-X 7d1e54b6e8 fix: track skia-safe/src/docs/pdf_document.rs (was caught by a global docs/ ignore) 2026-05-17 01:36:11 +08:00
Kayshen-X 6f4ab59313 fix: clear clippy -D warnings across the workspace
The op-* crate reorg never ran `cargo clippy -- -D warnings`, so the CI
lint gate failed. Fix every violation surgically: real fixes for
mechanical lints (needless_range_loop, derivable_impls, ptr_arg,
needless_borrow, doc_lazy_continuation, unused_imports, manual_find,
collapsible_match, never_loop, dead_code, complex types via type
aliases) and scoped `#[allow]` for intrusive ones (too_many_arguments
on paint helpers, result_large_err where ToolOutcome / PenNode payloads
are deliberately the Err type).
2026-05-17 01:26:29 +08:00
Kayshen-X 90c1cc4360 style: apply cargo fmt across the workspace 2026-05-17 00:26:15 +08:00
Kayshen-X 81e8e79b99 fix(ci): point op-host-desktop's agent dep at the vendor/agent submodule
The dependency referenced a sibling working copy (../../../agent-rs)
that only exists on a local dev machine, so cargo metadata failed in
CI. vendor/agent is the same agent-rs repo as a git submodule (CI
checks out submodules recursively) and is pinned to the identical
commit, so the build is unchanged.
2026-05-17 00:12:09 +08:00
Kayshen-X 4d3a201a51 ci: run rust-check + wasm-bundle-check on any branch push 2026-05-17 00:05:11 +08:00
Kayshen-X 08c07604d0 chore(ci): re-point tooling + CI workflows at the op-host-* crates
Phase 7.3 strangler reorg — update every reference to the renamed /
dissolved crates across the boundary scripts and CI workflows.

tools/:
- check-wasm-bundle.sh: openpencil-shell-web -> op-host-web; the
  wasm-bindgen output filenames follow the op_host_web lib name
- check-jian-boundaries.sh: shell-native -> op-host-native,
  shell-web -> op-host-web
- check-widget-boundary.sh: WEB_SRC + path-exclusion regexes ->
  crates/op-host-web; openpencil_shell_core::widgets ->
  op_editor_ui::widgets (the dissolved shim's real source crate)

.github/workflows/:
- rust-multiplatform.yml: wasm + mobile-check jobs -> op-host-web /
  op-host-native; the mobile shell-core clean check -> op-editor-ui
- rust-release.yml: cargo build -p openpencil-desktop ->
  -p op-host-desktop (the shipped executable name is unchanged)
- wasm-bundle-check.yml: -p openpencil-shell-web -> -p op-host-web

Cargo.toml skia-patch comment updated. Boundary checks pass; the
wasm-bundle gate cleanly skips while EMSDK is unset.
2026-05-17 00:01:35 +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