openpencil/Cargo.toml
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

61 lines
2.7 KiB
TOML
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

[workspace]
resolver = "2"
# 用 glob 而非显式列表Step 0 增量创建 crate 时不需要每次改 membersper implementer
# Phase 1 batch 2 的 workspace masking 反馈:列出未存在的 crate 会让 `cargo build -p X`
# 在 resolve 阶段就挂掉。glob 自动包含 crates/ 下所有 manifest自然支持增量创建。
members = ["crates/*"]
exclude = ["vendor/agent", "vendor/jian", "vendor/skia-safe-op", "node_modules"]
[workspace.package]
version = "0.1.0"
edition = "2021"
rust-version = "1.85"
license = "MIT"
authors = ["ZSeven-W <fini.yang@gmail.com>"]
repository = "https://github.com/ZSeven-W/openpencil"
[workspace.dependencies]
serde = { version = "1", features = ["derive"] }
serde_json = "1"
thiserror = "1"
anyhow = "1"
tracing = "0.1"
# WASM 边界相关per kickoff §1.2 黑名单 = 不出现在这里)
# 故意不在 workspace.dependencies 暴露 tokio / reqwest / agent / pen-agent-cli /
# pen-server / native-tls —— 这些只在 native-only crate 内部用 dev-deps 或 target-specific deps。
# Step 1b §3.2 P0.5B Run path (2026-05-09): wasm32-unknown-unknown skia
# build via `vendor/skia-safe-op` — fork of rust-skia 0.97.0 with a new
# build_support/platform/wasm_unknown.rs target that produces .o files
# free of emscripten runtime imports.
#
# IMPORTANT: `[patch.crates-io]` is workspace-global, NOT target-scoped.
# Every `skia-safe` / `skia-bindings` consumer in this workspace —
# including `openpencil-shell-native` (macOS / Linux / Windows desktop,
# `gl` feature) — now resolves through this fork on every target.
# Cargo does not natively support per-target patches, so this is the
# accepted blast radius.
#
# Compatibility surface vs upstream rust-skia 0.97.0:
# - every upstream platform module (macos / linux / windows /
# emscripten / android / ios / ohos / alpine) is byte-identical;
# only `wasm_unknown` is new
# - the dispatch table adds one new arm for
# `("wasm32", "unknown", "unknown", _)`; every existing target
# tuple still resolves to the same upstream module as before
# - the binary-cache feature URL + skia-binaries archive layout are
# untouched, so native desktop builds continue to download the
# prebuilt artifact instead of compiling Skia from source
#
# Trade-offs accepted:
# - upstream rust-skia patches will not flow until we re-vendor
# - `Cargo.lock` records `path` sources for skia-bindings/skia-safe
# instead of `registry+…`, surfacing the fork in audit tools
#
# When upstream rust-skia ships a wasm32-unknown-unknown platform
# module of its own, this whole [patch] block can go away.
[patch.crates-io]
skia-bindings = { path = "vendor/skia-safe-op/skia-bindings" }
skia-safe = { path = "vendor/skia-safe-op/skia-safe" }