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 模式".