Commit graph

25 commits

Author SHA1 Message Date
Kayshen-X 12747384c6 fix(agent): tolerate unknown Claude stream message types
Newer Claude Code CLIs emit stream message types the bundled SDK
was not compiled against — `rate_limit_event` being the one that
surfaced — and serde aborted the whole chat stream on the first
one ("unknown variant rate_limit_event").

The `Message` enum gains a `#[serde(other)] Unknown` catch-all so
any unmodelled `type` deserializes cleanly instead of failing the
parse; `hooks.rs` and `chat_claude.rs` match it as a silent no-op
(an unknown event carries no hook payload and no chat turn).
2026-05-22 11:39:57 +08:00
Kayshen-X cecf9aa9af fix(desktop): silence dead_code on MenuAction for Linux
`muda` is gated to macOS / Windows so the Linux backend stub
returns `None` from `poll()` and never constructs a `MenuAction`
variant. clippy's `-D dead_code` then fires on every variant on
Linux. Adding a target-gated `#[cfg_attr(…, allow(dead_code))]`
silences it there while keeping the lint live on macOS / Windows
where the variants actually need to stay reachable.
2026-05-21 22:45:34 +08:00
Kayshen-X 25e644dbc7 build(deps): vendor casement (winit fork) as a submodule
The casement crate was depended on through a sibling-repo path
(`../../../winit`) that only existed on the maintainer's machine,
so CI couldn't load the workspace manifest and every Rust Check
job died with "failed to read winit/Cargo.toml".

Vendoring it as a real submodule under `vendor/casement` (matching
the `vendor/jian` pattern, picked up by CI's `submodules:
recursive` checkout) closes that gap. The renamed GitHub repo
`ZSeven-W/casement` (was `ZSeven-W/winit`) tracks the `op-file-open`
branch — `feat(macos): drain_opened_file_urls` + the package rename
landed there as commit 5877fa83.

- `.gitmodules`: add vendor/casement.
- Root Cargo.toml: exclude vendor/casement from the workspace glob
  (it's its own workspace).
- op-host-native + op-host-desktop: path = "../../vendor/casement".
2026-05-21 22:23:34 +08:00
Kayshen-X 2aacde0ce2 style: apply rustfmt to the workspace
cargo fmt --all -- --check was failing on CI after the recent
feature work landed unformatted (insert_<comp> MCP tools,
Component-Browser panel, Design-MD panel, op-i18n locale tables,
op-opmerge). Running cargo fmt --all touches 48 files; no behaviour
change.
2026-05-21 22:03:43 +08:00
Kayshen-X e8cf002a7f feat(mcp): expose UIKit components as insert_<comp> MCP tools
Closes the architectural piece of the "MCP element toolset" P1 gap
(TS pen-mcp ships ~100 add_card_*/add_toast_* element tools).

- op-editor-core: new `EditorCommand::InstantiateKitComponent`
  variant + applier branch that calls
  `EditorState::instantiate_kit_component` with the requested
  drop point (defaults to (0, 0)).
- op-mcp: `element_tools.rs` — `InsertKitComponent` per-component
  tool returning `OkWithCommand(_, InstantiateKitComponent)`;
  `insert_kit_component_tools(state)` walks every loaded kit;
  `element_tool_schemas(state)` emits the matching tools/list
  JSON. Tool names sanitize dashes: `insert_btn_primary`,
  `insert_card_basic`, etc.
- op-host-desktop: `rebuild_registry` chains the dynamic tools
  in; `tools_list_response` takes EditorState and appends the
  dynamic schemas next to TOOL_SCHEMAS.
- op-editor-core: tidies the empty-pages `ensure_pages` guard to
  `is_none_or`.

Result: 6 starter-kit components → 6 working MCP tools. The 100-tool
catalog parity is now a data fill-in (more components in op-editor-
core/uikit.rs auto-register as more MCP tools).
2026-05-21 21:55:44 +08:00
Kayshen-X 1902d57807 feat(editor): add UIKit component-browser panel
Closes the last TS-vs-Rust parity gap — the Rust shell lacked the
TS `component-browser-panel.tsx` (UIKit library browser +
click-to-instantiate).

- op-editor-core: `uikit.rs` with `UIKit` / `KitComponent` /
  `ComponentCategory` types + a built-in starter kit (6 components
  spanning button/input/card/nav/layout/feedback) as PenNode
  templates; `EditorState::instantiate_kit_component` deep-clones
  with fresh ids and translates the whole subtree to the drop point
  (children carry document-absolute coords).
- op-editor-ui: `component_browser_panel.rs` floating draggable
  panel — header (close), category pills (filtered to non-empty),
  3-col card grid with name + scaled preview rect; kit-id + search
  filters applied.
- op-host-native: paint at §11.5 (below the Design-MD panel),
  `dispatch_component_browser_press`, drag lifecycle, shared
  `over_topmost_panel` helper covers both top-most panels across
  wheel / pan / right-press / cursor_hint / layer-hover / 4
  overlay-hover blocks + align hover + stale-hover clear.
- op-host-desktop: View-menu toggle, `drain_component_browser_insert`
  places at the viewport centre.
- op-editor-core: `active_children`/`active_children_mut` /
  `ensure_pages` symmetric `pages: Some([])` fallback —
  inserts land in `doc.children` and survive a subsequent
  `add_page` (which migrates them into Page 1).
- op-i18n: new `componentBrowser.empty` × 15 locales.

Bumps vendor/jian for the rebased `DesignMdSpec` schema + the
`jian pack` designMd-filter for packaged apps.
2026-05-21 21:24:12 +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 f6212e9091 fix(desktop): flush input drafts before a Git-panel reload
`document_fingerprint` only sees the committed document, so an
in-progress text-input draft — a half-typed property field or
variable-row value — was invisible to the pull's edit detection and
to `document_is_dirty`. A reload then dropped the draft silently.

Add `WidgetHostNative::commit_pending_input_pub` (commits property +
variable-row focus) and call it before the reconciliation checks:
in `confirm_document_reload` (covering pull / branch switch / merge),
before the during-pull edit comparison in `poll_git_pull_job`, and
in `save_tracked_document`. A pending draft now counts as an edit
and is saved / discarded / kept by explicit choice.
2026-05-18 22:08:16 +08:00
Kayshen-X 51e8a51bd1 fix(desktop): re-confirm pull reload against edits made during the pull
The Pull confirm ran at spawn time, but a `git pull` resolves
asynchronously on a worker thread — the user can keep editing the
document while it runs. The post-pull reload then discarded those
during-pull edits with no prompt.

Capture a document fingerprint when the pull is spawned; in
`poll_git_pull_job`, compare it against the document at reload time.
An unchanged document reloads silently as before; a document edited
during the pull goes back through the unsaved-changes confirm so the
edits are saved / discarded / kept by explicit choice.
2026-05-18 22:00:54 +08:00
Kayshen-X 1249aba2c8 fix(desktop): reconcile Git panel commit/pull with editor state
The Commit and Pull paths ignored the editor's in-memory document,
so they could act on stale disk state:

- Commit staged the last-saved file. With unsaved edits open, the
  commit captured stale content, not what the user saw. It now saves
  the document first (flushing pending inline edits) and skips the
  commit if that write fails.
- Pull rewrites the tracked .op on disk but never reloaded the
  editor or guarded unsaved edits. It now confirms via the
  unsaved-changes prompt before starting, and `poll_git_pull_job`
  reloads the document after a fast-forward / merge so the editor
  reflects the pulled state (a conflict leaves unparsable markers,
  so the panel shows merge-in-progress instead).

Mirrors the reload discipline already used by branch switch / merge.
2026-05-18 21:53:46 +08:00
Kayshen-X 2b3892a12f feat(desktop): wire the Git panel to a desktop repo session
GitSession binds an op_git::GitRepo to the currently-open document,
rebinding whenever the document path changes — the Git panel reads
it for branch / status / history and drives commits, the worktree
merge orchestrator and diffs through it.

Network- and scan-bound git work (pull, status, diff / show) runs on
worker threads (GitPullJob / GitStatusJob / GitDiffJob) drained on a
later frame, so a large repository or a slow remote never freezes the
UI; an open panel re-snapshots every 2 s to stay current with
external changes. A clean branch merge reloads the document from
disk; a conflicting one surfaces the quarantined ConflictBag in a
dialog. main.rs is split — git_host.rs + keyboard_input.rs — to keep
it under the 800-line cap.
2026-05-18 21:45:32 +08:00
Kayshen-X 0730230ddb feat(desktop): adopt the casement winit fork
`casement` is ZSeven-W's winit fork (sibling repo, referenced by
local path for now). It adds the macOS open-documents Apple-event
hook that upstream winit lacks, needed for Finder double-click open.
The `package = "casement"` key keeps the `winit` import name so all
`use winit::…` stays unchanged.

glutin-winit is dropped: it hard-depends on the upstream `winit`
package, which would pull a second, incompatible winit into the
tree. Its only use — GlWindow::build_surface_attributes — is replaced
by a direct glutin SurfaceAttributesBuilder call in provider.rs.
2026-05-18 21:45:09 +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 ebe0e0bf0f feat(ai): polish chat panel with streaming, tool-calls, thinking, images
Restructure the floating AI chat transcript from flat text bubbles
into a structured view:

- ChatMessage carries thinking text, tool calls and images plus
  per-block collapsed flags and a streaming flag. Image ids come
  from a process-global counter so a fresh ChatState cannot collide
  with the native decode cache.
- ChatPoll splits provider deltas into text / thinking / tool_calls;
  apply_poll_to_message folds them into the in-flight assistant
  message and clears streaming on the terminal Done. The no-provider
  error path also ends the stream and drops staged attachments.
- New RenderBackend::draw_image (default no-op) backed by a bounded
  FIFO decode cache + aspect-fit in the native skia backend; web
  degrades to a framed placeholder.
- New ai_chat_transcript widget: deterministic layout shared by
  paint and hit-test (no live text measurement), collapsible
  thinking / tool-call blocks, a streaming caret + typing-dot
  animation, and image thumbnails.

Reviewed with Codex (3 rounds to clean).
2026-05-18 08:23:31 +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 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 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 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 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 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