Commit graph

20 commits

Author SHA1 Message Date
Kayshen-X 5b6cf39775 chore: vendor anthropic-agent-sdk out of crates/ 2026-05-16 13:14:20 +08:00
Kayshen-X 5dac22a76e feat(desktop/chat): Claude Code adapter via anthropic-agent-sdk
First of the per-CLI ChatProvider adapters that replace the
hand-rolled stream-JSON parser in chat_subprocess.rs. This one wires
`anthropic_agent_sdk::query` (the in-workspace fork of
bartolli/anthropic-agent-sdk) into the OP chat-panel plumbing.

`crates/openpencil-desktop/src/chat_claude.rs`:
  - `ClaudeCodeProvider` impls `ChatProvider`. Constructs trivially
    via `new()` (SDK defaults) or `with_options(ClaudeAgentOptions)`
    when the settings modal has user overrides (system prompt, model
    pick, allowed-tools list, MCP servers, sandbox config — all 30+
    SDK option fields).
  - `send()` spawns the shared tokio runtime task, calls
    `anthropic_agent_sdk::query(prompt, options)`, drains its async
    `Stream<Item = Result<Message>>`, and dispatches each Message
    through `handle_message`:
      - `Message::Assistant.content` Vec<ContentBlock> is unpacked
        per block: `Text { text }` → `ChatDelta::TextDelta`,
        `Thinking { thinking, .. }` → `Thinking`, `ToolUse { name,
        input, .. }` → `ToolUse { name, args = input.to_string() }`,
        `ToolResult` swallowed (already part of conversation history
        the CLI tracks).
      - `Message::Result { subtype, is_error, .. }` is the turn
        terminator. `is_error` → `StopReason::Aborted`; otherwise
        `map_result_subtype` maps "success" → EndTurn,
        "error_max_turns" → MaxTokens, error variants → Aborted,
        unknown → EndTurn.
      - `System` / `User` / `StreamEvent` swallowed (init / context /
        partial-stream payloads the chat widget doesn't surface yet).
  - Receiver-drop short-circuit: every iteration checks
    `tx.is_closed()` so chat-panel teardown stops the SDK stream
    promptly without waiting for the CLI to flush more output.
  - Always emits a terminal `Done` — `Result` message → mapped stop
    reason; stream EOF without a Result → `EndTurn` fallback.

`crates/openpencil-desktop/Cargo.toml`:
  - Adds `anthropic-agent-sdk = { path = "../anthropic-agent-sdk" }`
    + `copilot-sdk = { path = "../copilot-sdk" }`. Copilot dep
    declared now even though `chat_copilot.rs` lands in a follow-up,
    so Cargo.lock resolves the whole graph in one pass.

`crates/openpencil-desktop/src/main.rs`:
  - `mod chat_claude;` between `mod chat_runtime` and
    `mod chat_subprocess` so the alphabetical mod-list rule holds.

Tests (3 added, all pass):
  - `map_result_subtype_table` covers the success / error_max_turns /
    error_during_execution / error / unknown table.
  - `provider_label_is_human_readable` asserts the chat widget gets
    "Claude Code" as the displayed label.
  - `provider_constructs_as_chat_provider_trait_object` is the
    compile-time type-check that `ClaudeCodeProvider` satisfies the
    `Send + Sync` bounds so it can live behind `Arc<dyn ChatProvider>`
    in the widget host.

End-to-end smoke testing requires an actual `claude` binary on PATH.
The 3 tests here verify the wiring + type contracts but not the live
CLI interaction; that lands when the settings modal exposes the
"connect" button + we have a real session to drive.

46 openpencil-desktop tests pass (was 43 before this commit).
Next: chat_copilot.rs over `copilot_sdk::Client + Session`, then
chat_http_server.rs for Codex / OpenCode `serve` mode per the user's
"opencode 和 codex 我们调用 http server, 通过 ipc 启动本地的 server 模式".
2026-05-14 16:52:17 +08:00
Kayshen-X f3f57081da chore(crates): move vendored SDKs into workspace as forkable crates
Per user direction "可以不放在 vendor 里面,我们移动到自己的工程,
后面就和他们分叉" — promote the two community SDKs from vendor/ to
crates/ so they become first-class OP workspace members we own and
evolve, instead of read-only vendored snapshots.

Moves:
  vendor/anthropic-agent-sdk/  →  crates/anthropic-agent-sdk/
  vendor/copilot-sdk-rust/     →  crates/copilot-sdk/

Workspace integration:
  - Root `Cargo.toml` exclude list drops both vendor entries; the
    existing `members = ["crates/*"]` glob auto-includes them.
  - `crates/copilot-sdk/Cargo.toml`: stripped all `[[example]]`
    blocks (22 of them) — the examples/ dir was already removed
    during the import, and leaving the entries broke
    `cargo test --workspace --no-run`.
  - `crates/anthropic-agent-sdk/Cargo.toml`: already had its
    `[[example]]` blocks pruned in the previous commit.

Lockfile pins (workspace `Cargo.lock`):
  Pulling reqwest 0.12.28 (via anthropic-agent-sdk) into the
  unified workspace dep graph re-resolved several `icu_*` crates to
  the 2.2 line, which requires rustc 1.86. OP's toolchain is 1.85
  (locked to stay compatible with the skia-safe-op fork). Pinned:
    icu_collections      2.2.0 → 2.1.1
    icu_locale_core      2.2.0 → 2.1.1
    icu_normalizer       2.2.0 → 2.1.1
    icu_normalizer_data  2.2.0 → 2.1.1
    icu_properties       2.2.0 → 2.1.2
    icu_properties_data  2.2.0 → 2.1.2
    icu_provider         2.2.0 → 2.1.1
    idna_adapter         1.2.2 → 1.2.1
  All eight pins are the latest versions on each crate's 2.1.x /
  1.2.x line that compile on rustc 1.85.

Verification:
  - `cargo check -p anthropic-agent-sdk` ✓
  - `cargo check -p copilot-sdk` ✓
  - `cargo test --workspace --no-run` ✓
  - `cargo test -p openpencil-shell-core --lib` → 250 pass
  - `cargo test -p openpencil-desktop chat_` → 16 pass

Next: replace the hand-rolled subprocess parser in chat_subprocess.rs
with thin per-CLI adapters that route Claude Code through
`anthropic_agent_sdk::SubprocessTransport` and Copilot through
`copilot_sdk::Client + Session`. Gemini stays on the generic stdin
bridge until an upstream Rust SDK exists. Codex + OpenCode get an
HttpServerProvider that spawns `<bin> serve` then connects via a
local HTTP client.
2026-05-14 16:49:17 +08:00
Kayshen-X cb5104af78 chore(vendor): add anthropic-agent-sdk + copilot-sdk-rust as IPC source-of-truth
User direction: instead of hand-rolling subprocess JSON bridges in
`chat_subprocess.rs`, pull the community SDKs into vendor/ + adapt
them. Both repos are MIT-licensed Rust SDKs purpose-built for their
respective CLIs and ship more capable wire-protocol parsers than the
generic line-based approach in this branch's HEAD.

`vendor/anthropic-agent-sdk/` (was bartolli/anthropic-agent-sdk @ main,
2026-05-14):
  - SubprocessTransport for `claude --print --verbose --output-format
    stream-json -- <prompt>`
  - Recognized message envelope (system / user / assistant / result
    shapes per Claude Code's documented headless protocol)
  - Binary-lookup fallback through ~/.npm-global/bin, /usr/local/bin,
    ~/.local/bin, ~/node_modules/.bin, ~/.yarn/bin (via `which` +
    manual probe)
  - Dangerous-env-var scrub (LD_PRELOAD / DYLD_INSERT_LIBRARIES /
    NODE_OPTIONS / ...) for spawn safety
  - CancellationToken-based abort wiring
  - Trimmed locally: removed examples/, demos/, tests/, docs/, .git/.
    Inner `[workspace]` block stripped so OP's root workspace owns the
    build. `typed-builder` pinned to `=0.21.0` because upstream's
    `0.23.2` uses stable `let`-chains (Rust 1.88+) and OP rust-toolchain
    is 1.85 to stay compatible with the skia-safe-op fork.

`vendor/copilot-sdk-rust/` (was copilot-community-sdk/copilot-sdk-rust
@ main, 2026-05-14):
  - LSP-style Content-Length-framed JSON-RPC over stdio for
    `gh copilot` (the new community CLI that succeeds the legacy
    `gh-copilot suggest` subcommand)
  - Client + Session abstraction with event subscription
    (`AssistantMessage` / `SessionIdle` / tool-use events)
  - Trimmed: examples/, tests/, .git/ removed. Cargo.toml unchanged
    (already 2021 edition + 1.85 rust-version + no problematic deps).

Workspace integration:
  - Both directories appear in OP root Cargo.toml's `exclude` list so
    `cargo build --workspace` doesn't try to compile them (each
    declares its own `edition` / `rust-version` distinct from OP).
  - openpencil-desktop will consume them via target-gated path deps
    in the next commit + replace the hand-rolled provider in
    `chat_subprocess.rs` with thin adapters that route per CliName:
      Claude Code → anthropic_agent_sdk::SubprocessTransport
      Copilot     → copilot_sdk::Client + Session
      Gemini      → keep the generic stdin/stdout bridge (no upstream
                    Rust SDK exists yet for the gemini CLI)
      Codex /
      OpenCode    → HttpServer bridge (separate, spawn `<bin> serve`
                    + connect to local 127.0.0.1:port)

Per the user clarification "opencode 和 codex 我们调用 http server, 通过
ipc 启动本地的 server 模式": Codex + OpenCode stay on the HttpServer
path even though they're also spawned subprocesses — the local
server is what we IPC with via HTTP, not their stdio.

Standalone build verified for both vendored crates: ✓ check passes
on rustc 1.85.1 (this host).
2026-05-14 16:36:27 +08:00
Kayshen-X 4b8bb136e7 chore(workspace): patch skia-bindings + skia-safe to vendor/skia-safe-op
Wires the workspace at vendor/skia-safe-op (committed in the previous
commit) via [patch.crates-io] so every consumer of skia-safe /
skia-bindings — both the wasm32-unknown-unknown shell-web bundle and
the macOS / Linux / Windows shell-native desktop binary — resolves
through the fork on every target.

[patch.crates-io] is workspace-global, NOT target-scoped; cargo does
not natively support per-target patches, so this is the accepted
blast radius. The fork is byte-identical to upstream rust-skia 0.97.0
except for the new `wasm_unknown` platform module + its single new
dispatch arm; native builds resolve to the same upstream platform
modules they did before. Verified `cargo check -p
openpencil-shell-native` builds through the fork unchanged.

Trade-off: upstream rust-skia patches no longer flow until we
re-vendor; Cargo.lock records `path` sources for skia-bindings /
skia-safe rather than `registry+...`. The full rationale block is
inline in Cargo.toml.

The Cargo.lock delta also pins js-sys 0.3.97 → 0.3.94 / web-sys
0.3.97 → 0.3.94 — this is the transitive consequence of pinning
wasm-bindgen = "=0.2.117" on shell-web (last 0.2.x release that
compiles on the workspace's Rust 1.85 toolchain; 0.2.120+ requires
1.86). Documented in shell-web/Cargo.toml.

Step 1b §3.2 P0.5B Run path, sub-phase C-hard.1.
2026-05-09 21:06:00 +08:00
Fini af9292d8f5 fix(ai): classify Type 0 components as non-mobile to skip phone chrome
Why: "Design a profile card" through MiniMax-M2.7 produced a 375×803 mobile
screen with auto-injected status bar, because the planner skill listed
"profiles" as a Type 2 single-task screen and the orchestrator's
isMobileScreen heuristic ran on width≤480 alone.

What: design-type.md + decomposition.md add Type 0 (single component:
card / badge / chip / modal) with width=400 height=0 1 subtask no chrome.
isMobileFullScreen helper extracted to orchestrator-plan-classify.ts and
required by both orchestrator.ts and orchestrator-sub-agent.ts so the
two paths can't drift on what "mobile" means (Codex review caught this
when only orchestrator.ts had the new check).

Verified with same MiniMax + same prompt: 400×320 component, 8 nodes,
firstChildRole=card, no status-bar.
2026-05-09 21:00:00 +08:00
Kayshen-X b74f8f8f32 chore(workspace): patch skia-bindings + skia-safe to vendor/skia-safe-op
Wires the workspace at vendor/skia-safe-op (committed in the previous
commit) via [patch.crates-io] so every consumer of skia-safe /
skia-bindings — both the wasm32-unknown-unknown shell-web bundle and
the macOS / Linux / Windows shell-native desktop binary — resolves
through the fork on every target.

[patch.crates-io] is workspace-global, NOT target-scoped; cargo does
not natively support per-target patches, so this is the accepted
blast radius. The fork is byte-identical to upstream rust-skia 0.97.0
except for the new `wasm_unknown` platform module + its single new
dispatch arm; native builds resolve to the same upstream platform
modules they did before. Verified `cargo check -p
openpencil-shell-native` builds through the fork unchanged.

Trade-off: upstream rust-skia patches no longer flow until we
re-vendor; Cargo.lock records `path` sources for skia-bindings /
skia-safe rather than `registry+...`. The full rationale block is
inline in Cargo.toml.

The Cargo.lock delta also pins js-sys 0.3.97 → 0.3.94 / web-sys
0.3.97 → 0.3.94 — this is the transitive consequence of pinning
wasm-bindgen = "=0.2.117" on shell-web (last 0.2.x release that
compiles on the workspace's Rust 1.85 toolchain; 0.2.120+ requires
1.86). Documented in shell-web/Cargo.toml.

Step 1b §3.2 P0.5B Run path, sub-phase C-hard.1.
2026-05-09 20:59:20 +08:00
Kayshen-X cf50616db1 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 22:12:00 +08:00
Kayshen-X c55807e432 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 22:09:00 +08:00
Kayshen-X 7fb674d928 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 21:24:00 +08:00
Kayshen-X 2f60bd49f8 feat(workspace): pin Jian submodule and shell wrapper deps (Step 1a Task 1)
Anchor v19 pivot at the workspace level: vendor Jian as a git submodule
pinned to fork commit ad13ce6 (P0.5 mini-gate GO; skia-safe 0.78 → 0.97 +
new pub draw_on_canvas adapter), wire jian-core / jian-skia / jian-host-desktop
as path deps with explicit version per spec §12.2, and re-export the
Jian render/geometry/scene types from shell-core so shell-native can
translate the OP RenderBackend facade into jian DrawOp commands.

shell-core stays wasm32-clean: only jian-core (already wasm32-validated
in P0.5) plus glam / bitflags / thiserror / tracing land here.
shell-native picks up the full P0-pinned GL stack (skia-safe 0.97.0,
glutin 0.32.3, glutin-winit 0.5.0, glow 0.17.0, winit 0.30.13,
raw-window-handle 0.6.2, scopeguard 1.2) plus jian-skia (textlayout)
and target-gated jian-host-desktop (default-features = false, no `run`
feature so we skip Jian's softbuffer raster present path — OP owns its
own GPU swap_buffers per spec §3.6).

Adds OP RenderBackend trait + Rect / Color (with RED/GREEN/BLUE/BLACK/
WHITE/TRANSPARENT named constants per spec §5.2) + TextLayout facade
that wraps jian_core::render::TextRun explicitly (TextRun has no Default
impl, fields enumerated to honour spec §5.2 round-2 CONCERN-1 fix).

Boundary checks all pass:
- wasm32 shell-web metadata: no jian-host-desktop / jian-skia
- aarch64-linux-android shell-native metadata: no jian-host-desktop
- shell-core src: no glutin / skia_safe / winit / glow imports

Tasks 2-4 (SharedSkiaContext + NativeBackend + ShellEvent mapping +
acceptance) follow per plan v7.
2026-05-05 21:00:00 +08:00
Kayshen-X 22003f9b0c chore(shell-native): add transient P0 probe gate (Step 1a)
Drives the three-OS CI matrix verification of the skia-safe + glutin +
glow + winit dep stack per Step 1a spec §7.

- examples/p0_probe.rs: stencil_visibility + readback chain runner (must
  own a real OS main thread because winit on macOS rejects
  EventLoop::new() from cargo test worker threads).
- tests/p0_probe.rs: subprocess-invoke wrapper, gated
  #[ignore = "P0_PROBE_GATE"] so default cargo test stays untouched.
- Cargo.toml: add transient [target.'cfg(not(target_arch = "wasm32"))'.
  dev-dependencies] block (skia-safe 0.97 + glutin 0.32.3 + glutin-winit
  0.5.0 + glow 0.17.0 + raw-window-handle 0.6.2 + scopeguard 1.2.0 +
  winit defaults). Pinned to versions resolved in /tmp/skia-glow-probe.
- .github/workflows/rust-check.yml: install Linux GL prereqs (xvfb,
  mesa, libxkbcommon, libwayland) and add a P0-probe-gate step running
  cargo test --ignored on each OS (Linux through xvfb-run; Windows
  early-returns per spec §8.2 WINDOWS_GPU_DEFERRED_NO_RUNNER).

All three artefacts are TRANSIENT — reverted in a follow-up cleanup
commit after CI is green and the loader-compat notes commit lands.
Task 1 owns the permanent integration.
2026-05-05 12:23:49 +08:00
Kayshen-X db0b50f7b5 style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 12:23:34 +08:00
Kayshen-X d5011547f1 ci: remove TS/Electron workflows (build-electron / ci / docker / publish-cli)
Rust-ification 阶段,CI 只保留 Rust 相关:
- rust-check.yml: cargo fmt + build + test (with STEP1A_REQUIRE_GPU=1 on Linux) + clippy + cargo-deny
- wasm-bundle-check.yml: wasm32 target check

删除:
- build-electron.yml: Electron desktop build (Rust 化后用 openpencil-shell-native)
- ci.yml: TS type-check + Vitest + web build (Rust 化后已废)
- docker.yml: TS Docker image (Rust 化后重做)
- publish-cli.yml: npm packages (Rust 化后改 cargo publish)
2026-05-05 12:23:33 +08:00
Kayshen-X e6d6b1bd6f style: apply formatter + bump vendor/agent submodule + ignore vendors in oxfmt
- .prettierignore: 加 vendor/agent + vendor/jian + target/(submodule 不在本仓 format 范围)
- vendor/agent: 62c4bad(cosmetic format-only delta in agent-rs)
- root + shell-native + shell-web Cargo.toml / deny.toml / README.md / wasm-bundle-check workflow: oxfmt auto-style
2026-05-05 12:23:18 +08:00
Kayshen-X 2dcc8a96d3 feat(workspace): pin Jian submodule and shell wrapper deps (Step 1a Task 1)
Anchor v19 pivot at the workspace level: vendor Jian as a git submodule
pinned to fork commit ad13ce6 (P0.5 mini-gate GO; skia-safe 0.78 → 0.97 +
new pub draw_on_canvas adapter), wire jian-core / jian-skia / jian-host-desktop
as path deps with explicit version per spec §12.2, and re-export the
Jian render/geometry/scene types from shell-core so shell-native can
translate the OP RenderBackend facade into jian DrawOp commands.

shell-core stays wasm32-clean: only jian-core (already wasm32-validated
in P0.5) plus glam / bitflags / thiserror / tracing land here.
shell-native picks up the full P0-pinned GL stack (skia-safe 0.97.0,
glutin 0.32.3, glutin-winit 0.5.0, glow 0.17.0, winit 0.30.13,
raw-window-handle 0.6.2, scopeguard 1.2) plus jian-skia (textlayout)
and target-gated jian-host-desktop (default-features = false, no `run`
feature so we skip Jian's softbuffer raster present path — OP owns its
own GPU swap_buffers per spec §3.6).

Adds OP RenderBackend trait + Rect / Color (with RED/GREEN/BLUE/BLACK/
WHITE/TRANSPARENT named constants per spec §5.2) + TextLayout facade
that wraps jian_core::render::TextRun explicitly (TextRun has no Default
impl, fields enumerated to honour spec §5.2 round-2 CONCERN-1 fix).

Boundary checks all pass:
- wasm32 shell-web metadata: no jian-host-desktop / jian-skia
- aarch64-linux-android shell-native metadata: no jian-host-desktop
- shell-core src: no glutin / skia_safe / winit / glow imports

Tasks 2-4 (SharedSkiaContext + NativeBackend + ShellEvent mapping +
acceptance) follow per plan v7.
2026-05-05 12:23:10 +08:00
Kayshen-X 535a405dab chore(workspace): bump rust-toolchain 1.82 → 1.85 (cargo-deny edition2024 fix)
Phase 2 Gate codex round 1 BLOCK: cargo-deny check fails on
1.82 because wit-bindgen v0.57.1 requires edition2024 manifest
parsing (introduced in Rust 1.85). Bumping to 1.85 unblocks
both `cargo deny check` and `cargo deny --target wasm32 check
bans` — both now exit 0 (advisories ok, bans ok, licenses ok,
sources ok / bans ok).

Also drops `imports_granularity` + `group_imports` from
rustfmt.toml (nightly-only; were emitting warnings under
stable toolchain). Comment preserved to remind future
nightly-pinning to re-enable.

Cargo.lock regenerated under 1.85 (drops the litemap precise
pin from Phase 1 Task 1.4 — no longer needed).

Verification on 1.85:
- cargo build --workspace: PASS
- cargo test --workspace: PASS (skeleton tests)
- cargo clippy --workspace --all-targets -- -D warnings: PASS
- cargo fmt --all -- --check: PASS (with two nightly warnings now removed)
- cargo check --target wasm32 -p {shell-web --no-default --features web | pen-types/-core/-engine/-codegen/-figma}: PASS
- cargo check --target wasm32 -p openpencil-shell-native: FAIL with compile_error guard text (correct)
- cargo deny check: PASS
- cargo deny --target wasm32 check bans: PASS
2026-05-03 23:40:00 +08:00
Kayshen-X 2ea9b23b66 chore(workspace): bump rust-toolchain 1.80 → 1.82
Phase 1 batch 3 implementer found 1.80 incompatible with current
crates.io ecosystem: parley → fontique → litemap 0.7.5 needs 1.81;
accesskit chain → indexmap 2.14 → hashbrown 0.17 needs edition2024
(1.85); skia-safe 0.75+ → home 0.5.12 needs 1.88. 1.82 is the sweet
spot that fixes litemap (and matches what Task 0.4 actually probed
with — 1.95).

shell-native dep set deviation (winit only, skia-safe + accesskit
deferred to Step 1 kill-spike when actually used) is documented in
the Phase 1 review trail. compile_error guard for wasm32 still fires
correctly — the load-bearing §1.2 invariant is satisfied.
2026-05-03 22:30:00 +08:00
Kayshen-X 8fc97c0a48 chore(workspace): switch members to crates/* glob (avoid masking when adding crates incrementally) 2026-05-03 22:15:00 +08:00
Kayshen-X 025d1763a5 chore(workspace): bootstrap Cargo workspace + toolchain pin 2026-05-03 21:55:00 +08:00