diff --git a/Cargo.lock b/Cargo.lock index 93dc174f..1832d768 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -528,6 +528,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "cc-commands" +version = "0.1.0" +dependencies = [ + "cc-types", +] + [[package]] name = "cc-compact" version = "0.1.0" @@ -567,6 +574,44 @@ dependencies = [ "uuid", ] +[[package]] +name = "cc-daemon" +version = "0.1.0" +dependencies = [ + "cc-types", +] + +[[package]] +name = "cc-engine" +version = "0.1.0" +dependencies = [ + "anyhow", + "async-trait", + "cc-bootstrap", + "cc-compact", + "cc-config", + "cc-keybindings", + "cc-types", + "chrono", + "futures", + "git2", + "parking_lot", + "serde", + "serde_json", + "serial_test", + "tempfile", + "tokio", + "tracing", + "uuid", +] + +[[package]] +name = "cc-ipc" +version = "0.1.0" +dependencies = [ + "cc-types", +] + [[package]] name = "cc-keybindings" version = "0.1.0" @@ -579,6 +624,13 @@ dependencies = [ "tracing", ] +[[package]] +name = "cc-lsp-service" +version = "0.1.0" +dependencies = [ + "cc-types", +] + [[package]] name = "cc-mcp" version = "0.1.0" @@ -624,6 +676,20 @@ dependencies = [ "url", ] +[[package]] +name = "cc-plugins" +version = "0.1.0" +dependencies = [ + "cc-types", +] + +[[package]] +name = "cc-query" +version = "0.1.0" +dependencies = [ + "cc-types", +] + [[package]] name = "cc-sandbox" version = "0.1.0" @@ -691,6 +757,20 @@ dependencies = [ "serde", ] +[[package]] +name = "cc-teams" +version = "0.1.0" +dependencies = [ + "cc-types", +] + +[[package]] +name = "cc-tools" +version = "0.1.0" +dependencies = [ + "cc-types", +] + [[package]] name = "cc-types" version = "0.1.0" @@ -698,8 +778,10 @@ dependencies = [ "anyhow", "async-trait", "chrono", + "parking_lot", "serde", "serde_json", + "tokio", "uuid", ] @@ -805,6 +887,7 @@ dependencies = [ "cc-compact", "cc-computer-use", "cc-config", + "cc-engine", "cc-keybindings", "cc-mcp", "cc-observability", diff --git a/Cargo.toml b/Cargo.toml index c1b29382..6fb5b05e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -136,6 +136,16 @@ cc-sandbox = { path = "crates/cc-sandbox" } cc-permissions = { path = "crates/cc-permissions" } cc-browser = { path = "crates/cc-browser" } cc-session = { path = "crates/cc-session" } +# Phase 6 / Phase 7 scaffolds (issues #75, #76). +cc-engine = { path = "crates/cc-engine" } +cc-query = { path = "crates/cc-query" } +cc-tools = { path = "crates/cc-tools" } +cc-lsp-service = { path = "crates/cc-lsp-service" } +cc-plugins = { path = "crates/cc-plugins" } +cc-teams = { path = "crates/cc-teams" } +cc-daemon = { path = "crates/cc-daemon" } +cc-commands = { path = "crates/cc-commands" } +cc-ipc = { path = "crates/cc-ipc" } # Daemon HTTP server axum = { version = "0.8", features = ["ws"] } diff --git a/crates/cc-commands/Cargo.toml b/crates/cc-commands/Cargo.toml new file mode 100644 index 00000000..d43c6965 --- /dev/null +++ b/crates/cc-commands/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-commands" +version = "0.1.0" +edition = "2021" +description = "Slash-command implementations (53 commands: /commit, /review, /session, /memory, etc.). Phase 7 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-commands/src/lib.rs b/crates/cc-commands/src/lib.rs new file mode 100644 index 00000000..35f48f22 --- /dev/null +++ b/crates/cc-commands/src/lib.rs @@ -0,0 +1,10 @@ +//! cc-commands — slash-command implementations (Phase 7 scaffold). +//! +//! Issue #76 (`[workspace-split] Phase 7`): target destination for +//! `crates/claude-code-rs/src/commands/` (53 commands, ~12.6k LOC — the +//! largest single-module extraction). This crate implements the +//! `CommandDispatcher` trait defined in cc-types::commands. +//! +//! Downstream deps (after full move): cc-engine, cc-plugins, cc-tools, +//! cc-teams, cc-browser, cc-compact, cc-session, cc-voice, cc-keybindings, +//! cc-mcp, cc-sandbox, cc-auth, cc-bootstrap, cc-skills, cc-utils. diff --git a/crates/cc-daemon/Cargo.toml b/crates/cc-daemon/Cargo.toml new file mode 100644 index 00000000..a008fcbf --- /dev/null +++ b/crates/cc-daemon/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-daemon" +version = "0.1.0" +edition = "2021" +description = "KAIROS daemon: HTTP/SSE server, proactive tick, webhooks, team-memory proxy. Phase 7 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-daemon/src/lib.rs b/crates/cc-daemon/src/lib.rs new file mode 100644 index 00000000..f2f864f7 --- /dev/null +++ b/crates/cc-daemon/src/lib.rs @@ -0,0 +1,5 @@ +//! cc-daemon — KAIROS daemon HTTP/SSE server (Phase 7 scaffold). +//! +//! Issue #76 (`[workspace-split] Phase 7`): target destination for +//! `crates/claude-code-rs/src/daemon/`. Depends on cc-engine, cc-config, and +//! cc-types. diff --git a/crates/cc-engine/Cargo.toml b/crates/cc-engine/Cargo.toml new file mode 100644 index 00000000..53bf84cf --- /dev/null +++ b/crates/cc-engine/Cargo.toml @@ -0,0 +1,50 @@ +[package] +name = "cc-engine" +version = "0.1.0" +edition = "2021" +description = "QueryEngine + query loop + AppState + Tool trait + status-line runner (Phase 6 extraction)." + +# Phase 6 is being landed incrementally: +# +# Step 1 (this crate, landed): publish an empty scaffold wired into the +# workspace. +# +# Step 2 (in progress): move `src/ui/status_line/` and +# `src/types/{tool, app_state, config}.rs` into this crate — these are the +# last runtime-free-ish items blocking `ToolUseContext` / `AppState` from +# leaving the root crate. +# +# Step 3 (follow-up): move `src/engine/` and `src/query/` into this crate. + +[dependencies] +# Async +tokio = { workspace = true } +futures = { workspace = true } +async-trait = { workspace = true } + +# Serialization +serde = { workspace = true } +serde_json = { workspace = true } + +# Errors / logging +anyhow = { workspace = true } +tracing = { workspace = true } + +# Shared primitives +chrono = { workspace = true } +uuid = { workspace = true } +parking_lot = { workspace = true } + +# Git (for status-line payload repo lookups) +git2 = { workspace = true } + +# Workspace siblings (only leaf / already-extracted crates) +cc-types = { workspace = true } +cc-config = { workspace = true } +cc-bootstrap = { workspace = true } +cc-compact = { workspace = true } +cc-keybindings = { workspace = true } + +[dev-dependencies] +tempfile = { workspace = true } +serial_test = { workspace = true } diff --git a/crates/cc-engine/src/lib.rs b/crates/cc-engine/src/lib.rs new file mode 100644 index 00000000..e30c10dc --- /dev/null +++ b/crates/cc-engine/src/lib.rs @@ -0,0 +1,29 @@ +//! cc-engine — QueryEngine, Agent tool, query-loop driver, and engine-level +//! shared state (Phase 6 — in progress). +//! +//! Issue #75 (`[workspace-split] Phase 6`): this crate is the target +//! destination for `crates/claude-code-rs/src/engine/` plus the engine-level +//! shared structures (`AppState`, `Tool` trait, `ToolUseContext`) and the +//! status-line runner. +//! +//! ## What lives here now +//! +//! - [`status_line`] — scriptable status-line payload + runner, moved from +//! `src/ui/status_line/`. Lives here because `AppState` holds a +//! `StatusLineRunner` handle. +//! +//! ## Coming in follow-up PRs +//! +//! - `types::{tool, app_state, config}` from the root crate +//! - The contents of `src/engine/` and `src/query/` +//! +//! See `docs/superpowers/specs/2026-04-20-workspace-split-design.md`. + +pub mod status_line; +pub mod types; + +// Re-export from cc-types so consumers can eventually write +// `use cc_engine::{HookRunner, CommandDispatcher}` once the engine types +// land here too. +#[allow(unused_imports)] +pub use cc_types::hooks::{HookRunner, NoopHookRunner}; diff --git a/crates/claude-code-rs/src/ui/status_line/mod.rs b/crates/cc-engine/src/status_line/mod.rs similarity index 100% rename from crates/claude-code-rs/src/ui/status_line/mod.rs rename to crates/cc-engine/src/status_line/mod.rs diff --git a/crates/claude-code-rs/src/ui/status_line/payload.rs b/crates/cc-engine/src/status_line/payload.rs similarity index 92% rename from crates/claude-code-rs/src/ui/status_line/payload.rs rename to crates/cc-engine/src/status_line/payload.rs index 8bd94f47..a70a1c1a 100644 --- a/crates/claude-code-rs/src/ui/status_line/payload.rs +++ b/crates/cc-engine/src/status_line/payload.rs @@ -14,8 +14,8 @@ use std::path::{Path, PathBuf}; use git2::Repository; use serde::{Deserialize, Serialize}; -use crate::bootstrap::model::ModelSetting; -use crate::compact::auto_compact::get_context_window_size; +use cc_bootstrap::model::ModelSetting; +use cc_compact::auto_compact::get_context_window_size; /// Top-level payload piped to the user's status-line command on stdin. /// @@ -169,6 +169,11 @@ pub struct WorktreeStatus { } /// Inputs needed to assemble a concrete status-line payload snapshot. +/// +/// `resolved_output_style_name` and `worktree` are pre-computed by callers +/// because resolving them requires root-crate modules (`engine::output_style` +/// and `tools::worktree`) that cc-engine cannot reach. Callers that don't +/// need either field pass `None`. pub struct StatusLineSnapshot<'a> { pub session_id: Option, pub model_id: &'a str, @@ -181,8 +186,13 @@ pub struct StatusLineSnapshot<'a> { pub total_cost_usd: f64, pub api_calls: u64, pub session_duration_secs: Option, - pub output_style: Option<&'a str>, + /// Pre-resolved canonical output-style name. Callers compute this by + /// calling `crate::engine::output_style::resolve(style, cwd).name()`. + pub resolved_output_style_name: Option, pub editor_mode: Option<&'a str>, + /// Pre-built worktree status. Callers compute this from + /// `crate::tools::worktree::get_current_worktree_session()`. + pub worktree: Option, pub streaming: bool, pub message_count: usize, } @@ -220,9 +230,9 @@ pub fn build_payload_from_snapshot(snapshot: StatusLineSnapshot<'_>) -> StatusLi api_calls: snapshot.api_calls, session_duration_secs: snapshot.session_duration_secs, }); - payload.output_style = resolve_output_style_name(snapshot.output_style, snapshot.cwd); + payload.output_style = snapshot.resolved_output_style_name; payload.vim = vim_status_from_editor_mode(snapshot.editor_mode); - payload.worktree = current_worktree_status(); + payload.worktree = snapshot.worktree; payload.streaming = snapshot.streaming; payload.message_count = snapshot.message_count; payload @@ -244,16 +254,10 @@ pub fn model_info_from_runtime(model_id: &str, backend: Option<&str>) -> Option< }) } -pub fn resolve_output_style_name(output_style: Option<&str>, cwd: &Path) -> Option { - output_style - .map(str::trim) - .filter(|style| !style.is_empty()) - .map(|style| { - crate::engine::output_style::resolve(style, cwd) - .name() - .to_string() - }) -} +// `resolve_output_style_name` used to live here but touched +// `crate::engine::output_style::resolve` in the root crate. Callers now +// pre-resolve the canonical name and pass it via +// `StatusLineSnapshot::resolved_output_style_name`. pub fn vim_status_from_editor_mode(editor_mode: Option<&str>) -> Option { match editor_mode.map(str::trim) { @@ -316,27 +320,13 @@ pub fn workspace_status_from_path(cwd: &Path) -> Option { }) } -pub fn current_worktree_status() -> Option { - let session = crate::tools::worktree::get_current_worktree_session()?; - let name = session - .worktree_path - .file_name() - .and_then(|name| name.to_str()) - .filter(|name| !name.is_empty()) - .unwrap_or("worktree") - .to_string(); - - Some(WorktreeStatus { - name, - path: session.worktree_path.display().to_string(), - branch: Some(session.branch_name), - original_cwd: session.original_cwd.display().to_string(), - original_branch: None, - }) -} +// `current_worktree_status` used to live here but read the +// `crate::tools::worktree::get_current_worktree_session()` global. +// Callers now build a `WorktreeStatus` themselves and pass it via +// `StatusLineSnapshot::worktree`. fn project_dir_for_statusline(cwd: &Path, git_root: Option<&Path>) -> Option { - let configured = crate::bootstrap::state::project_root(); + let configured = cc_bootstrap::state::project_root(); if !configured.as_os_str().is_empty() { return Some(configured); } @@ -470,8 +460,9 @@ mod tests { total_cost_usd: 0.42, api_calls: 3, session_duration_secs: Some(9), - output_style: Some("default"), + resolved_output_style_name: Some("default".into()), editor_mode: Some("vim"), + worktree: None, streaming: true, message_count: 5, }); diff --git a/crates/claude-code-rs/src/ui/status_line/runner.rs b/crates/cc-engine/src/status_line/runner.rs similarity index 99% rename from crates/claude-code-rs/src/ui/status_line/runner.rs rename to crates/cc-engine/src/status_line/runner.rs index 869dff5e..3cb1588d 100644 --- a/crates/claude-code-rs/src/ui/status_line/runner.rs +++ b/crates/cc-engine/src/status_line/runner.rs @@ -27,7 +27,7 @@ use tokio::io::AsyncWriteExt; use tokio::process::Command; use tokio::task::{AbortHandle, JoinHandle}; -use crate::config::settings::StatusLineSettings; +use cc_config::settings::StatusLineSettings; use super::payload::StatusLinePayload; @@ -310,7 +310,7 @@ pub fn payload_from_value(v: Value) -> Result StatusLineSettings { StatusLineSettings { diff --git a/crates/claude-code-rs/src/types/app_state.rs b/crates/cc-engine/src/types/app_state.rs similarity index 91% rename from crates/claude-code-rs/src/types/app_state.rs rename to crates/cc-engine/src/types/app_state.rs index 0f98ecb8..7785a682 100644 --- a/crates/claude-code-rs/src/types/app_state.rs +++ b/crates/cc-engine/src/types/app_state.rs @@ -38,7 +38,7 @@ pub struct AppState { /// effort 值 pub effort_value: Option, /// Agent Teams 上下文 (feature-gated) - pub team_context: Option, + pub team_context: Option, /// Hook configurations loaded from settings.json (merged config). /// Read by `tools::hooks::load_hook_configs()` and the hook execution pipeline. pub hooks: HashMap, @@ -57,11 +57,11 @@ pub struct AppState { /// Populated at startup from `~/.cc-rust/keybindings.json` (issue #10). /// Multiple UI surfaces (Rust TUI, IPC-driven OpenTUI) share the same /// handle so reloads are observed everywhere. - pub keybindings: crate::keybindings::KeybindingRegistry, + pub keybindings: cc_keybindings::KeybindingRegistry, /// Shared scriptable status-line runner (issue #11). The TUI owns the /// renderer-facing side; `/statusline` and the IPC driver both reach /// into this handle to inspect / reset the subprocess. - pub status_line_runner: crate::ui::status_line::StatusLineRunner, + pub status_line_runner: crate::status_line::StatusLineRunner, } impl Default for AppState { @@ -93,8 +93,8 @@ impl Default for AppState { is_assistant_mode: false, autonomous_tick_ms: None, terminal_focus: true, - keybindings: crate::keybindings::KeybindingRegistry::with_defaults(), - status_line_runner: crate::ui::status_line::StatusLineRunner::new(), + keybindings: cc_keybindings::KeybindingRegistry::with_defaults(), + status_line_runner: crate::status_line::StatusLineRunner::new(), } } } diff --git a/crates/claude-code-rs/src/types/config.rs b/crates/cc-engine/src/types/config.rs similarity index 98% rename from crates/claude-code-rs/src/types/config.rs rename to crates/cc-engine/src/types/config.rs index df0a6379..5d4053cf 100644 --- a/crates/claude-code-rs/src/types/config.rs +++ b/crates/cc-engine/src/types/config.rs @@ -1,6 +1,6 @@ #![allow(dead_code)] #[allow(unused_imports)] -use super::message::{Message, SystemMessage, Usage}; +use cc_types::message::{Message, SystemMessage, Usage}; #[allow(unused_imports)] use super::tool::{QueryChainTracking, ToolUseContext, Tools}; diff --git a/crates/cc-engine/src/types/mod.rs b/crates/cc-engine/src/types/mod.rs new file mode 100644 index 00000000..8248c1bd --- /dev/null +++ b/crates/cc-engine/src/types/mod.rs @@ -0,0 +1,20 @@ +//! Engine-level shared types. +//! +//! Moved from `crates/claude-code-rs/src/types/{app_state, tool, config}.rs` +//! in Phase 6. The pure leaf types (`message`, `state`, `transitions`, +//! `permissions`, `hooks`, `commands`, `agent_*`, `background_agents`, +//! `teams`) live in `cc-types`; the three modules here depend on them plus a +//! handful of runtime-bound sibling crates (`cc-keybindings`, `cc-config`) and +//! this crate's own `status_line` module. +//! +//! The root crate re-exports these via `src/types/mod.rs` so existing +//! `crate::types::{app_state, tool, config}` import paths keep working. + +pub mod app_state; +pub mod config; +pub mod tool; + +// Re-export the pure-data modules from cc-types so a consumer doing +// `use cc_engine::types::message::*` or `use cc_engine::types::state::*` +// lines up with the pre-move shape of `crate::types::*`. +pub use cc_types::{message, permissions, state, transitions}; diff --git a/crates/claude-code-rs/src/types/tool.rs b/crates/cc-engine/src/types/tool.rs similarity index 98% rename from crates/claude-code-rs/src/types/tool.rs rename to crates/cc-engine/src/types/tool.rs index 733e2c0c..94847fa3 100644 --- a/crates/claude-code-rs/src/types/tool.rs +++ b/crates/cc-engine/src/types/tool.rs @@ -9,7 +9,7 @@ use serde_json::Value; use super::app_state::AppState; #[allow(unused_imports)] -use super::message::{AssistantMessage, ContentBlock, Message, ToolResultContent}; +use cc_types::message::{AssistantMessage, ContentBlock, Message, ToolResultContent}; /// Async callback for interactive permission requests. /// @@ -136,7 +136,7 @@ pub struct ToolUseContext { /// Sender for background agent completion results. /// When `Some`, the Agent tool can spawn background tasks. /// When `None`, `run_in_background` falls back to synchronous execution. - pub bg_agent_tx: Option, + pub bg_agent_tx: Option, /// Hook runner used by tools (e.g. the Agent tool fires SubagentStart / /// SubagentStop events through this trait rather than importing /// `crate::tools::hooks` directly). diff --git a/crates/cc-ipc/Cargo.toml b/crates/cc-ipc/Cargo.toml new file mode 100644 index 00000000..14006f85 --- /dev/null +++ b/crates/cc-ipc/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-ipc" +version = "0.1.0" +edition = "2021" +description = "IPC spine: JSONL/stdio (headless) + HTTP/SSE (daemon) protocols for frontend bridging. Phase 7 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-ipc/src/lib.rs b/crates/cc-ipc/src/lib.rs new file mode 100644 index 00000000..4675daac --- /dev/null +++ b/crates/cc-ipc/src/lib.rs @@ -0,0 +1,19 @@ +//! cc-ipc — IPC spine between backend and frontend (Phase 7 scaffold). +//! +//! Issue #76 (`[workspace-split] Phase 7`): target destination for +//! `crates/claude-code-rs/src/ipc/` plus the `tools/system_status.rs` wrapper +//! (which reads `ipc::subsystem_handlers` and therefore moves with the IPC +//! crate to avoid a `cc-tools -> cc-ipc` edge). +//! +//! Cycle-breaking prerequisites (done in this PR): moved agent event / command +//! / channel types from `src/ipc/` to `cc-types::{agent_events, +//! agent_types, agent_channel}`. The files under `src/ipc/` are now thin +//! re-exports so downstream consumers don't break while the physical code +//! move is staged in a follow-up PR. + +#[allow(unused_imports)] +pub use cc_types::{ + agent_channel::{agent_channel, AgentIpcEvent, AgentReceiver, AgentSender}, + agent_events::{AgentCommand, AgentEvent, TeamCommand, TeamEvent}, + agent_types::{AgentInfo, AgentNode, TeamMemberInfo}, +}; diff --git a/crates/cc-lsp-service/Cargo.toml b/crates/cc-lsp-service/Cargo.toml new file mode 100644 index 00000000..06cd92fc --- /dev/null +++ b/crates/cc-lsp-service/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-lsp-service" +version = "0.1.0" +edition = "2021" +description = "LSP client service. Phase 6 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-lsp-service/src/lib.rs b/crates/cc-lsp-service/src/lib.rs new file mode 100644 index 00000000..a550d818 --- /dev/null +++ b/crates/cc-lsp-service/src/lib.rs @@ -0,0 +1,7 @@ +//! cc-lsp-service — LSP client service (Phase 6 scaffold). +//! +//! Issue #75 (`[workspace-split] Phase 6`): target destination for +//! `crates/claude-code-rs/src/lsp_service/` plus the `tools/lsp.rs` tool +//! wrapper. Moving the tool into this crate resolves the +//! `tools::lsp <-> lsp_service` cycle: after the move, the LSP shared types +//! (HoverInfo, SymbolInfo, SourceLocation) live in one place. diff --git a/crates/cc-plugins/Cargo.toml b/crates/cc-plugins/Cargo.toml new file mode 100644 index 00000000..1a76e3d5 --- /dev/null +++ b/crates/cc-plugins/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-plugins" +version = "0.1.0" +edition = "2021" +description = "Plugin loader (discovery, manifest parsing, tool contribution). Phase 7 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-plugins/src/lib.rs b/crates/cc-plugins/src/lib.rs new file mode 100644 index 00000000..ab425d8e --- /dev/null +++ b/crates/cc-plugins/src/lib.rs @@ -0,0 +1,6 @@ +//! cc-plugins — plugin loader (Phase 7 scaffold). +//! +//! Issue #76 (`[workspace-split] Phase 7`): target destination for +//! `crates/claude-code-rs/src/plugins/`. This crate depends on cc-tools for +//! the Tool trait (via cc-types once the trait lands there) and on +//! cc-permissions for decision plumbing. diff --git a/crates/cc-query/Cargo.toml b/crates/cc-query/Cargo.toml new file mode 100644 index 00000000..a04979cc --- /dev/null +++ b/crates/cc-query/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-query" +version = "0.1.0" +edition = "2021" +description = "Async streaming query-loop driver for QueryEngine. Phase 6 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-query/src/lib.rs b/crates/cc-query/src/lib.rs new file mode 100644 index 00000000..58469fc6 --- /dev/null +++ b/crates/cc-query/src/lib.rs @@ -0,0 +1,18 @@ +//! cc-query — async streaming query loop (Phase 6 scaffold). +//! +//! Issue #75 (`[workspace-split] Phase 6`): target destination for +//! `crates/claude-code-rs/src/query/`. The current PR publishes the crate +//! scaffold so the workspace manifest lists every Phase 6/7 crate up-front. +//! +//! Cycle-breaking prerequisites (done in this PR): +//! - `query -> tools` edge removed: query now calls hooks via the +//! `cc_types::hooks::HookRunner` trait exposed through `QueryDeps::hook_runner()`. +//! - `CompletedBackgroundAgent` / `PendingBackgroundResults` moved to +//! `cc-types::background_agents`. +//! +//! Remaining before the source move: hoist `types/tool.rs`, `types/app_state.rs`, +//! and `types/config.rs` into cc-types so `QueryDeps` no longer touches root +//! crate items (`crate::types::{app_state, config, tool}`). + +#[allow(unused_imports)] +pub use cc_types::background_agents::{CompletedBackgroundAgent, PendingBackgroundResults}; diff --git a/crates/cc-teams/Cargo.toml b/crates/cc-teams/Cargo.toml new file mode 100644 index 00000000..6ddc5f5e --- /dev/null +++ b/crates/cc-teams/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-teams" +version = "0.1.0" +edition = "2021" +description = "Team coordination: backend, mailbox, protocol, runner. Phase 7 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-teams/src/lib.rs b/crates/cc-teams/src/lib.rs new file mode 100644 index 00000000..fe515b6b --- /dev/null +++ b/crates/cc-teams/src/lib.rs @@ -0,0 +1,6 @@ +//! cc-teams — team coordination (Phase 7 scaffold). +//! +//! Issue #76 (`[workspace-split] Phase 7`): target destination for +//! `crates/claude-code-rs/src/teams/` plus the two teammate-specific tool +//! wrappers (`tools/send_message.rs`, `tools/team_spawn.rs`) that get pulled +//! in with the team runtime to avoid a `cc-tools -> cc-teams` edge. diff --git a/crates/cc-tools/Cargo.toml b/crates/cc-tools/Cargo.toml new file mode 100644 index 00000000..1aefefc0 --- /dev/null +++ b/crates/cc-tools/Cargo.toml @@ -0,0 +1,8 @@ +[package] +name = "cc-tools" +version = "0.1.0" +edition = "2021" +description = "Tool implementations (FileRead/FileWrite/Bash/Glob/Grep/...). Phase 6 workspace split (scaffold)." + +[dependencies] +cc-types = { workspace = true } diff --git a/crates/cc-tools/src/lib.rs b/crates/cc-tools/src/lib.rs new file mode 100644 index 00000000..23001f78 --- /dev/null +++ b/crates/cc-tools/src/lib.rs @@ -0,0 +1,20 @@ +//! cc-tools — tool implementations (Phase 6 scaffold). +//! +//! Issue #75 (`[workspace-split] Phase 6`): target destination for +//! `crates/claude-code-rs/src/tools/` minus the three cycle-causing tool files +//! that move to their natural homes: +//! +//! - `tools/lsp.rs` -> cc-lsp-service (with the LSP client) +//! - `tools/send_message.rs` + `tools/team_spawn.rs` -> cc-teams +//! - `tools/system_status.rs` -> cc-ipc (with subsystem_handlers) +//! +//! Cycle-breaking prerequisites (done in this PR): +//! - `tools -> engine` removed in Phase 5 (agent tool moved to cc-engine). +//! - `background_agents` types moved to cc-types::background_agents. +//! +//! Remaining before the source move: hoist `types/tool.rs` (Tool trait, +//! ToolUseContext) to cc-types so every tool module can depend only on +//! cc-types instead of the root crate. + +#[allow(unused_imports)] +pub use cc_types::background_agents::{CompletedBackgroundAgent, PendingBackgroundResults}; diff --git a/crates/cc-types/Cargo.toml b/crates/cc-types/Cargo.toml index bac57b54..40621bd8 100644 --- a/crates/cc-types/Cargo.toml +++ b/crates/cc-types/Cargo.toml @@ -11,3 +11,5 @@ uuid = { workspace = true } chrono = { workspace = true } async-trait = { workspace = true } anyhow = { workspace = true } +parking_lot = { workspace = true } +tokio = { workspace = true } diff --git a/crates/cc-types/src/agent_channel.rs b/crates/cc-types/src/agent_channel.rs new file mode 100644 index 00000000..3ec533ca --- /dev/null +++ b/crates/cc-types/src/agent_channel.rs @@ -0,0 +1,27 @@ +//! Agent IPC channel — the dedicated mpsc channel for agent + team events. +//! +//! Moved from `src/ipc/agent_channel.rs` to cc-types in Phase 6 so the +//! `ToolUseContext::bg_agent_tx` field in cc-types::tool can be typed without +//! depending on the future cc-ipc crate. + +#![allow(dead_code)] + +use super::agent_events::{AgentEvent, TeamEvent}; + +/// All events that flow through the agent channel. +#[derive(Debug)] +pub enum AgentIpcEvent { + Agent(AgentEvent), + Team(TeamEvent), +} + +/// Sender half — injected into agent tool and team modules. +pub type AgentSender = tokio::sync::mpsc::UnboundedSender; + +/// Receiver half — consumed by the headless event loop. +pub type AgentReceiver = tokio::sync::mpsc::UnboundedReceiver; + +/// Create a new agent channel pair. +pub fn agent_channel() -> (AgentSender, AgentReceiver) { + tokio::sync::mpsc::unbounded_channel() +} diff --git a/crates/cc-types/src/agent_events.rs b/crates/cc-types/src/agent_events.rs new file mode 100644 index 00000000..82cbc2e1 --- /dev/null +++ b/crates/cc-types/src/agent_events.rs @@ -0,0 +1,127 @@ +//! Agent and team event/command enums for IPC. +//! +//! **Event enums** (Backend -> Frontend): `Serialize + Debug + Clone`, tagged by `"kind"`. +//! **Command enums** (Frontend -> Backend): `Deserialize + Debug`, tagged by `"kind"`. +//! +//! Moved from `src/ipc/agent_events.rs` to cc-types in Phase 6 so the engine +//! crate's `sdk_to_agent_event` helper can depend on these types without a +//! reverse edge into the future cc-ipc crate. + +#![allow(dead_code)] + +use serde::{Deserialize, Serialize}; + +use super::agent_types::*; + +// =========================================================================== +// AgentEvent (Backend → Frontend) +// =========================================================================== + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum AgentEvent { + Spawned { + agent_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + parent_agent_id: Option, + description: String, + #[serde(skip_serializing_if = "Option::is_none")] + agent_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + model: Option, + is_background: bool, + depth: usize, + chain_id: String, + }, + Completed { + agent_id: String, + result_preview: String, + had_error: bool, + duration_ms: u64, + #[serde(skip_serializing_if = "Option::is_none")] + output_tokens: Option, + }, + Error { + agent_id: String, + error: String, + duration_ms: u64, + }, + Aborted { agent_id: String }, + StreamDelta { agent_id: String, text: String }, + ThinkingDelta { agent_id: String, thinking: String }, + ToolUse { + agent_id: String, + tool_use_id: String, + tool_name: String, + input: serde_json::Value, + }, + ToolResult { + agent_id: String, + tool_use_id: String, + output: String, + is_error: bool, + }, + TreeSnapshot { roots: Vec }, +} + +// =========================================================================== +// AgentCommand (Frontend → Backend) +// =========================================================================== + +#[derive(Deserialize, Debug)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum AgentCommand { + AbortAgent { agent_id: String }, + QueryActiveAgents, + QueryAgentOutput { agent_id: String }, +} + +// =========================================================================== +// TeamEvent (Backend → Frontend) +// =========================================================================== + +#[derive(Serialize, Debug, Clone)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum TeamEvent { + MemberJoined { + team_name: String, + agent_id: String, + agent_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + role: Option, + }, + MemberLeft { + team_name: String, + agent_id: String, + agent_name: String, + }, + MessageRouted { + team_name: String, + from: String, + to: String, + text: String, + timestamp: String, + #[serde(skip_serializing_if = "Option::is_none")] + summary: Option, + }, + StatusSnapshot { + team_name: String, + members: Vec, + pending_messages: usize, + }, +} + +// =========================================================================== +// TeamCommand (Frontend → Backend) +// =========================================================================== + +#[derive(Deserialize, Debug)] +#[serde(tag = "kind", rename_all = "snake_case")] +pub enum TeamCommand { + InjectMessage { + team_name: String, + to: String, + text: String, + }, + QueryTeamStatus { team_name: String }, +} diff --git a/crates/cc-types/src/agent_types.rs b/crates/cc-types/src/agent_types.rs new file mode 100644 index 00000000..0ffc48db --- /dev/null +++ b/crates/cc-types/src/agent_types.rs @@ -0,0 +1,61 @@ +//! Shared data types for agent tree, agent info, and team member IPC messages. +//! +//! Moved from `src/ipc/agent_types.rs` to cc-types in Phase 6 so crates that +//! need to reference these types (engine for sdk_to_agent_event, tool context +//! for `bg_agent_tx`) don't have to depend on the future cc-ipc crate. +//! +//! All types are `Serialize + Deserialize + Debug + Clone` so they can flow +//! freely across the JSONL/SSE boundary between the Rust backend and any +//! frontend process. + +#![allow(dead_code)] + +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AgentNode { + pub agent_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_agent_id: Option, + pub description: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub agent_type: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub model: Option, + pub state: String, + pub is_background: bool, + pub depth: usize, + pub chain_id: String, + pub spawned_at: i64, + #[serde(skip_serializing_if = "Option::is_none")] + pub completed_at: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub duration_ms: Option, + #[serde(skip_serializing_if = "Option::is_none")] + pub result_preview: Option, + pub had_error: bool, + pub children: Vec, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct AgentInfo { + pub agent_id: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub parent_agent_id: Option, + pub description: String, + pub state: String, + pub is_background: bool, + pub depth: usize, + #[serde(skip_serializing_if = "Option::is_none")] + pub duration_ms: Option, +} + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub struct TeamMemberInfo { + pub agent_id: String, + pub agent_name: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub role: Option, + pub is_active: bool, + pub unread_messages: usize, +} diff --git a/crates/cc-types/src/background_agents.rs b/crates/cc-types/src/background_agents.rs new file mode 100644 index 00000000..85882d9b --- /dev/null +++ b/crates/cc-types/src/background_agents.rs @@ -0,0 +1,88 @@ +//! Background agent types — shared between the Agent tool, query loop, +//! and the headless/TUI event loop. +//! +//! Moved to cc-types in Phase 6 to break the query -> tools edge. + +use std::sync::Arc; +use std::time::Duration; + +use parking_lot::Mutex; + +/// Result from a completed background agent. +#[derive(Debug, Clone)] +pub struct CompletedBackgroundAgent { + pub agent_id: String, + pub description: String, + pub result_text: String, + pub had_error: bool, + pub duration: Duration, +} + +/// Shared buffer of completed agents waiting to be injected into the query loop. +/// +/// The event loop pushes completed agents here after notifying the frontend. +/// The query loop drains at turn boundaries and injects system messages. +/// Internal `Mutex` means this is safe to clone and share without external locking. +#[derive(Debug, Clone, Default)] +pub struct PendingBackgroundResults { + inner: Arc>>, +} + +impl PendingBackgroundResults { + pub fn new() -> Self { + Self::default() + } + + /// Push a completed agent result (called by event loop). + pub fn push(&self, agent: CompletedBackgroundAgent) { + self.inner.lock().push(agent); + } + + /// Drain all pending results (called by query loop at turn start). + pub fn drain_all(&self) -> Vec { + let mut guard = self.inner.lock(); + std::mem::take(&mut *guard) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + fn make_completed(id: &str, desc: &str) -> CompletedBackgroundAgent { + CompletedBackgroundAgent { + agent_id: id.to_string(), + description: desc.to_string(), + result_text: format!("Result from {}", desc), + had_error: false, + duration: Duration::from_secs(1), + } + } + + #[test] + fn test_pending_results_push_and_drain() { + let pending = PendingBackgroundResults::new(); + assert!(pending.drain_all().is_empty()); + + pending.push(make_completed("a1", "task one")); + pending.push(make_completed("a2", "task two")); + + let drained = pending.drain_all(); + assert_eq!(drained.len(), 2); + assert_eq!(drained[0].agent_id, "a1"); + assert_eq!(drained[1].agent_id, "a2"); + + assert!(pending.drain_all().is_empty()); + } + + #[test] + fn test_pending_results_clone_shares_state() { + let pending1 = PendingBackgroundResults::new(); + let pending2 = pending1.clone(); + + pending1.push(make_completed("a1", "task")); + let drained = pending2.drain_all(); + assert_eq!(drained.len(), 1); + assert_eq!(drained[0].agent_id, "a1"); + } +} diff --git a/crates/cc-types/src/hooks.rs b/crates/cc-types/src/hooks.rs index 61115a18..9197c03e 100644 --- a/crates/cc-types/src/hooks.rs +++ b/crates/cc-types/src/hooks.rs @@ -184,6 +184,15 @@ pub trait HookRunner: Send + Sync { payload: &Value, hook_configs: &[HookEventConfig], ) -> anyhow::Result; + + /// Run Stop lifecycle hooks — called by the query loop after the model + /// stops generating (no tool calls in final assistant message). + /// + /// Returns `StopContinuation` if any hook asked the loop to keep going. + async fn run_stop_hooks( + &self, + hook_configs: &[HookEventConfig], + ) -> anyhow::Result; } // --------------------------------------------------------------------------- @@ -260,4 +269,11 @@ impl HookRunner for NoopHookRunner { ) -> anyhow::Result { Ok(HookOutput::default()) } + + async fn run_stop_hooks( + &self, + _hook_configs: &[HookEventConfig], + ) -> anyhow::Result { + Ok(PostToolHookResult::Continue) + } } diff --git a/crates/cc-types/src/lib.rs b/crates/cc-types/src/lib.rs index e6624283..308ce045 100644 --- a/crates/cc-types/src/lib.rs +++ b/crates/cc-types/src/lib.rs @@ -7,9 +7,14 @@ //! //! See issue #70 (`[workspace-split] Phase 1`) for the rationale behind this //! partial split. +pub mod agent_channel; +pub mod agent_events; +pub mod agent_types; +pub mod background_agents; pub mod commands; pub mod hooks; pub mod message; pub mod permissions; pub mod state; +pub mod teams; pub mod transitions; diff --git a/crates/cc-types/src/teams.rs b/crates/cc-types/src/teams.rs new file mode 100644 index 00000000..a1f0e59a --- /dev/null +++ b/crates/cc-types/src/teams.rs @@ -0,0 +1,92 @@ +//! Team coordination data types shared across cc-rust crates. +//! +//! Only the *pure data* types that `AppState` needs live here. Runtime +//! machinery (backend, mailbox, runner, protocol, in-process task state) stays +//! in the root crate's `teams::` module until cc-teams is extracted in +//! Phase 7. Having `TeamContext` and `TeammateInfo` in cc-types lets +//! `types/app_state.rs` stop reaching into `crate::teams::types::*`, which is +//! the last blocker for moving `AppState` into cc-types. +//! +//! See issue #75 / #76 (workspace split Phase 6/7) and the "Remaining before +//! the source move" section of +//! `docs/superpowers/specs/2026-04-20-workspace-split-design.md`. + +use std::collections::HashMap; + +/// Runtime team context stored in `AppState::team_context` while an Agent +/// Team session is active. +/// +/// Populated by `/team join|leave|...` commands and the `TeamSpawn` tool; read +/// by `send_message` / `team_spawn` tool impls and the `agents_cmd` browser to +/// resolve teammate identities and routing targets. +#[derive(Debug, Clone, Default)] +pub struct TeamContext { + pub team_name: String, + pub team_file_path: String, + pub lead_agent_id: String, + pub self_agent_id: Option, + pub self_agent_name: Option, + pub is_leader: Option, + pub self_agent_color: Option, + pub teammates: HashMap, +} + +/// Runtime info about a spawned teammate, indexed by teammate name inside +/// `TeamContext::teammates`. +#[derive(Debug, Clone)] +pub struct TeammateInfo { + pub name: String, + pub agent_type: Option, + pub color: Option, + pub tmux_session_name: String, + pub tmux_pane_id: String, + pub cwd: String, + pub worktree_path: Option, + pub spawned_at: i64, +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn team_context_default_is_empty() { + let ctx = TeamContext::default(); + assert!(ctx.team_name.is_empty()); + assert!(ctx.team_file_path.is_empty()); + assert!(ctx.lead_agent_id.is_empty()); + assert!(ctx.self_agent_id.is_none()); + assert!(ctx.teammates.is_empty()); + } + + #[test] + fn team_context_holds_teammates() { + let mut ctx = TeamContext { + team_name: "backend".into(), + team_file_path: "/tmp/team.json".into(), + lead_agent_id: "lead@backend".into(), + self_agent_id: Some("self@backend".into()), + self_agent_name: Some("self".into()), + is_leader: Some(false), + self_agent_color: Some("cyan".into()), + teammates: HashMap::new(), + }; + ctx.teammates.insert( + "alice".into(), + TeammateInfo { + name: "alice".into(), + agent_type: Some("reviewer".into()), + color: Some("magenta".into()), + tmux_session_name: "sess".into(), + tmux_pane_id: "%1".into(), + cwd: "/work".into(), + worktree_path: None, + spawned_at: 1713168000, + }, + ); + + let info = ctx.teammates.get("alice").expect("alice present"); + assert_eq!(info.agent_type.as_deref(), Some("reviewer")); + assert_eq!(ctx.teammates.len(), 1); + } +} diff --git a/crates/claude-code-rs/Cargo.toml b/crates/claude-code-rs/Cargo.toml index 1cbbd50e..c6793aaf 100644 --- a/crates/claude-code-rs/Cargo.toml +++ b/crates/claude-code-rs/Cargo.toml @@ -148,6 +148,7 @@ cc-sandbox = { workspace = true } cc-permissions = { workspace = true } cc-browser = { workspace = true } cc-session = { workspace = true } +cc-engine = { workspace = true } # Daemon axum = { workspace = true } diff --git a/crates/claude-code-rs/src/commands/statusline_cmd.rs b/crates/claude-code-rs/src/commands/statusline_cmd.rs index 95f76a6e..1c3f1333 100644 --- a/crates/claude-code-rs/src/commands/statusline_cmd.rs +++ b/crates/claude-code-rs/src/commands/statusline_cmd.rs @@ -355,8 +355,12 @@ fn build_test_payload(ctx: &CommandContext) -> StatusLinePayload { total_cost_usd: usage.total_cost_usd, api_calls: usage.api_calls, session_duration_secs: None, - output_style: ctx.app_state.settings.output_style.as_deref(), + resolved_output_style_name: crate::ui::status_line_resolver::resolve_output_style_name( + ctx.app_state.settings.output_style.as_deref(), + &ctx.cwd, + ), editor_mode: ctx.app_state.settings.editor_mode.as_deref(), + worktree: crate::ui::status_line_resolver::current_worktree_status(), streaming: false, message_count: ctx.messages.len(), }) diff --git a/crates/claude-code-rs/src/engine/agent/dispatch.rs b/crates/claude-code-rs/src/engine/agent/dispatch.rs index f3eda393..9155ef5e 100644 --- a/crates/claude-code-rs/src/engine/agent/dispatch.rs +++ b/crates/claude-code-rs/src/engine/agent/dispatch.rs @@ -51,7 +51,7 @@ impl AgentTool { .as_ref() .map(|t| t.chain_id.clone()) .unwrap_or_default(); - let node = crate::ipc::agent_types::AgentNode { + let node = cc_types::agent_types::AgentNode { agent_id: agent_id.to_string(), parent_agent_id: ctx.agent_id.clone(), description: description.to_string(), @@ -71,8 +71,8 @@ impl AgentTool { crate::ipc::agent_tree::AGENT_TREE.lock().register(node); if let Some(tx) = agent_tx { - let _ = tx.send(crate::ipc::agent_channel::AgentIpcEvent::Agent( - crate::ipc::agent_events::AgentEvent::Spawned { + let _ = tx.send(cc_types::agent_channel::AgentIpcEvent::Agent( + cc_types::agent_events::AgentEvent::Spawned { agent_id: agent_id.to_string(), parent_agent_id: ctx.agent_id.clone(), description: description.to_string(), @@ -85,8 +85,8 @@ impl AgentTool { )); let roots = crate::ipc::agent_tree::AGENT_TREE.lock().build_snapshot(); - let _ = tx.send(crate::ipc::agent_channel::AgentIpcEvent::Agent( - crate::ipc::agent_events::AgentEvent::TreeSnapshot { roots }, + let _ = tx.send(cc_types::agent_channel::AgentIpcEvent::Agent( + cc_types::agent_events::AgentEvent::TreeSnapshot { roots }, )); } } @@ -118,8 +118,8 @@ impl AgentTool { ); if let Some(tx) = agent_tx { - let _ = tx.send(crate::ipc::agent_channel::AgentIpcEvent::Agent( - crate::ipc::agent_events::AgentEvent::Completed { + let _ = tx.send(cc_types::agent_channel::AgentIpcEvent::Agent( + cc_types::agent_events::AgentEvent::Completed { agent_id: agent_id.to_string(), result_preview: preview, had_error, @@ -129,8 +129,8 @@ impl AgentTool { )); let roots = crate::ipc::agent_tree::AGENT_TREE.lock().build_snapshot(); - let _ = tx.send(crate::ipc::agent_channel::AgentIpcEvent::Agent( - crate::ipc::agent_events::AgentEvent::TreeSnapshot { roots }, + let _ = tx.send(cc_types::agent_channel::AgentIpcEvent::Agent( + cc_types::agent_events::AgentEvent::TreeSnapshot { roots }, )); } } diff --git a/crates/claude-code-rs/src/engine/agent/mod.rs b/crates/claude-code-rs/src/engine/agent/mod.rs index ffb67535..1c5c4288 100644 --- a/crates/claude-code-rs/src/engine/agent/mod.rs +++ b/crates/claude-code-rs/src/engine/agent/mod.rs @@ -157,9 +157,9 @@ async fn count_worktree_changes( pub(crate) fn sdk_to_agent_event( sdk_msg: &crate::engine::sdk_types::SdkMessage, agent_id: &str, -) -> Option { +) -> Option { use crate::engine::sdk_types::SdkMessage; - use crate::ipc::agent_events::AgentEvent; + use cc_types::agent_events::AgentEvent; use crate::types::message::{ContentBlock, StreamEvent, ToolResultContent}; match sdk_msg { @@ -285,7 +285,7 @@ async fn collect_stream_result( stream: std::pin::Pin< Box + Send>, >, - ipc: Option<(&crate::ipc::agent_channel::AgentSender, &str)>, + ipc: Option<(&cc_types::agent_channel::AgentSender, &str)>, ) -> (String, bool) { use crate::engine::sdk_types::SdkMessage; use futures::StreamExt; @@ -322,7 +322,7 @@ async fn collect_stream_result( // Forward intermediate events to IPC when a sender is available if let Some((tx, agent_id)) = ipc { if let Some(agent_event) = sdk_to_agent_event(&msg, agent_id) { - let _ = tx.send(crate::ipc::agent_channel::AgentIpcEvent::Agent(agent_event)); + let _ = tx.send(cc_types::agent_channel::AgentIpcEvent::Agent(agent_event)); } } } diff --git a/crates/claude-code-rs/src/engine/agent/tests.rs b/crates/claude-code-rs/src/engine/agent/tests.rs index 45c6ff5c..78da7929 100644 --- a/crates/claude-code-rs/src/engine/agent/tests.rs +++ b/crates/claude-code-rs/src/engine/agent/tests.rs @@ -195,7 +195,7 @@ fn test_sdk_to_agent_event_stream_delta_text() { let msg = make_stream_event(json!({"text": "hello world"})); let event = sdk_to_agent_event(&msg, "a1").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::StreamDelta { agent_id, text } => { + cc_types::agent_events::AgentEvent::StreamDelta { agent_id, text } => { assert_eq!(agent_id, "a1"); assert_eq!(text, "hello world"); } @@ -208,7 +208,7 @@ fn test_sdk_to_agent_event_stream_delta_thinking() { let msg = make_stream_event(json!({"thinking": "let me consider"})); let event = sdk_to_agent_event(&msg, "a2").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ThinkingDelta { agent_id, thinking } => { + cc_types::agent_events::AgentEvent::ThinkingDelta { agent_id, thinking } => { assert_eq!(agent_id, "a2"); assert_eq!(thinking, "let me consider"); } @@ -231,7 +231,7 @@ fn test_sdk_to_agent_event_tool_use() { }]); let event = sdk_to_agent_event(&msg, "a3").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ToolUse { + cc_types::agent_events::AgentEvent::ToolUse { agent_id, tool_use_id, tool_name, @@ -263,7 +263,7 @@ fn test_sdk_to_agent_event_tool_result_text() { }]); let event = sdk_to_agent_event(&msg, "a4").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ToolResult { + cc_types::agent_events::AgentEvent::ToolResult { agent_id, tool_use_id, output, @@ -287,7 +287,7 @@ fn test_sdk_to_agent_event_tool_result_error() { }]); let event = sdk_to_agent_event(&msg, "a5").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ToolResult { is_error, .. } => { + cc_types::agent_events::AgentEvent::ToolResult { is_error, .. } => { assert!(is_error); } other => panic!("expected ToolResult, got {:?}", other), @@ -307,7 +307,7 @@ fn test_sdk_to_agent_event_tool_result_blocks_shows_placeholder() { }]); let event = sdk_to_agent_event(&msg, "a6").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ToolResult { output, .. } => { + cc_types::agent_events::AgentEvent::ToolResult { output, .. } => { assert_eq!(output, "[complex output]"); } other => panic!("expected ToolResult, got {:?}", other), @@ -379,7 +379,7 @@ fn test_sdk_to_agent_event_tool_use_picks_first() { ]); let event = sdk_to_agent_event(&msg, "a7").unwrap(); match event { - crate::ipc::agent_events::AgentEvent::ToolUse { tool_use_id, .. } => { + cc_types::agent_events::AgentEvent::ToolUse { tool_use_id, .. } => { assert_eq!(tool_use_id, "tu-first"); } other => panic!("expected ToolUse, got {:?}", other), diff --git a/crates/claude-code-rs/src/engine/lifecycle/deps.rs b/crates/claude-code-rs/src/engine/lifecycle/deps.rs index a908e1df..a7dd9bbb 100644 --- a/crates/claude-code-rs/src/engine/lifecycle/deps.rs +++ b/crates/claude-code-rs/src/engine/lifecycle/deps.rs @@ -46,7 +46,7 @@ pub(crate) struct QueryEngineDeps { /// Background agent sender — forwarded into ToolUseContext. pub(crate) bg_agent_tx: Option, /// Shared buffer of completed background agents. - pub(crate) pending_bg_results: crate::tools::background_agents::PendingBackgroundResults, + pub(crate) pending_bg_results: cc_types::background_agents::PendingBackgroundResults, /// Hook runner — used via the `HookRunner` trait from `cc-types::hooks` so /// the engine has no direct dependency on `crate::tools::hooks`. pub(crate) hook_runner: Arc, @@ -901,10 +901,14 @@ impl QueryDeps for QueryEngineDeps { fn drain_background_results( &self, - ) -> Vec { + ) -> Vec { self.pending_bg_results.drain_all() } + fn hook_runner(&self) -> Arc { + self.hook_runner.clone() + } + fn audit_context(&self) -> crate::observability::AuditContext { self.audit_ctx.clone() } diff --git a/crates/claude-code-rs/src/engine/lifecycle/mod.rs b/crates/claude-code-rs/src/engine/lifecycle/mod.rs index 81db6d7c..bf306466 100644 --- a/crates/claude-code-rs/src/engine/lifecycle/mod.rs +++ b/crates/claude-code-rs/src/engine/lifecycle/mod.rs @@ -109,7 +109,7 @@ pub struct QueryEngine { pub(crate) has_handled_orphaned_permission: Arc, /// Shared buffer of completed background agents. /// Event loop pushes; query loop drains. - pub(crate) pending_bg_results: crate::tools::background_agents::PendingBackgroundResults, + pub(crate) pending_bg_results: cc_types::background_agents::PendingBackgroundResults, /// Hook runner for the tool-execution hook system. /// /// Defaults to [`cc_types::hooks::NoopHookRunner`]. Call sites that want @@ -167,7 +167,7 @@ impl QueryEngine { })), aborted: Arc::new(AtomicBool::new(false)), has_handled_orphaned_permission: Arc::new(AtomicBool::new(false)), - pending_bg_results: crate::tools::background_agents::PendingBackgroundResults::new(), + pending_bg_results: cc_types::background_agents::PendingBackgroundResults::new(), hook_runner: Arc::new(cc_types::hooks::NoopHookRunner::new()), command_dispatcher: Arc::new(cc_types::commands::NoopCommandDispatcher::new()), } diff --git a/crates/claude-code-rs/src/ipc/agent_channel.rs b/crates/claude-code-rs/src/ipc/agent_channel.rs index dc342f70..98fac1d9 100644 --- a/crates/claude-code-rs/src/ipc/agent_channel.rs +++ b/crates/claude-code-rs/src/ipc/agent_channel.rs @@ -1,58 +1,8 @@ //! Agent IPC channel — the dedicated mpsc channel for agent + team events. +//! +//! The real definitions now live in `cc_types::agent_channel`; this module is +//! a thin re-export so existing `crate::ipc::agent_channel::*` paths keep +//! working. -#![allow(dead_code)] // Types are pre-defined for upcoming agent IPC extension tasks - -use super::agent_events::{AgentEvent, TeamEvent}; - -/// All events that flow through the agent channel. -#[derive(Debug)] -pub enum AgentIpcEvent { - Agent(AgentEvent), - Team(TeamEvent), -} - -/// Sender half — injected into agent tool and team modules. -pub type AgentSender = tokio::sync::mpsc::UnboundedSender; - -/// Receiver half — consumed by the headless event loop. -pub type AgentReceiver = tokio::sync::mpsc::UnboundedReceiver; - -/// Create a new agent channel pair. -pub fn agent_channel() -> (AgentSender, AgentReceiver) { - tokio::sync::mpsc::unbounded_channel() -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::ipc::agent_events::AgentEvent; - - #[test] - fn agent_channel_send_receive() { - let (tx, mut rx) = agent_channel(); - tx.send(AgentIpcEvent::Agent(AgentEvent::Aborted { - agent_id: "a1".into(), - })) - .unwrap(); - let event = rx.try_recv().unwrap(); - assert!(matches!( - event, - AgentIpcEvent::Agent(AgentEvent::Aborted { .. }) - )); - } - - #[test] - fn agent_channel_team_event() { - let (tx, mut rx) = agent_channel(); - tx.send(AgentIpcEvent::Team( - crate::ipc::agent_events::TeamEvent::MemberLeft { - team_name: "t1".into(), - agent_id: "a1".into(), - agent_name: "worker".into(), - }, - )) - .unwrap(); - let event = rx.try_recv().unwrap(); - assert!(matches!(event, AgentIpcEvent::Team(_))); - } -} +#[allow(unused_imports)] +pub use cc_types::agent_channel::{agent_channel, AgentIpcEvent, AgentReceiver, AgentSender}; diff --git a/crates/claude-code-rs/src/ipc/agent_events.rs b/crates/claude-code-rs/src/ipc/agent_events.rs index 73d52b7b..c68e48b9 100644 --- a/crates/claude-code-rs/src/ipc/agent_events.rs +++ b/crates/claude-code-rs/src/ipc/agent_events.rs @@ -1,506 +1,7 @@ //! Agent and team event/command enums for IPC. //! -//! **Event enums** (Backend -> Frontend): `Serialize + Debug + Clone`, tagged by `"kind"`. -//! **Command enums** (Frontend -> Backend): `Deserialize + Debug`, tagged by `"kind"`. -//! -//! These types are consumed by: -//! - `protocol.rs` — wrapped inside `BackendMessage` / `FrontendMessage` variants -//! - `headless.rs` — event dispatch loop -//! - Agent orchestration and team coordination modules - -#![allow(dead_code)] // Types are pre-defined for upcoming agent IPC extension tasks - -use serde::{Deserialize, Serialize}; - -use super::agent_types::*; - -// =========================================================================== -// AgentEvent (Backend → Frontend) -// =========================================================================== - -/// Events emitted by the agent subsystem to notify the frontend about -/// agent lifecycle, streaming output, tool usage, and tree snapshots. -#[derive(Serialize, Debug, Clone)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum AgentEvent { - /// A new agent was spawned. - Spawned { - agent_id: String, - #[serde(skip_serializing_if = "Option::is_none")] - parent_agent_id: Option, - description: String, - #[serde(skip_serializing_if = "Option::is_none")] - agent_type: Option, - #[serde(skip_serializing_if = "Option::is_none")] - model: Option, - is_background: bool, - depth: usize, - chain_id: String, - }, - /// An agent completed successfully. - Completed { - agent_id: String, - result_preview: String, - had_error: bool, - duration_ms: u64, - #[serde(skip_serializing_if = "Option::is_none")] - output_tokens: Option, - }, - /// An agent encountered a fatal error. - Error { - agent_id: String, - error: String, - duration_ms: u64, - }, - /// An agent was aborted by the user or system. - Aborted { agent_id: String }, - /// A streaming text delta from an agent's response. - StreamDelta { agent_id: String, text: String }, - /// A streaming thinking/reasoning delta from an agent. - ThinkingDelta { agent_id: String, thinking: String }, - /// An agent initiated a tool use. - ToolUse { - agent_id: String, - tool_use_id: String, - tool_name: String, - input: serde_json::Value, - }, - /// A tool returned its result to an agent. - ToolResult { - agent_id: String, - tool_use_id: String, - output: String, - is_error: bool, - }, - /// Full snapshot of the agent tree hierarchy. - TreeSnapshot { roots: Vec }, -} - -// =========================================================================== -// AgentCommand (Frontend → Backend) -// =========================================================================== - -/// Commands the frontend can send to control or query agents. -#[derive(Deserialize, Debug)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum AgentCommand { - /// Request to abort a specific agent. - AbortAgent { agent_id: String }, - /// Request the list of all currently active agents. - QueryActiveAgents, - /// Request the full output of a specific agent. - QueryAgentOutput { agent_id: String }, -} - -// =========================================================================== -// TeamEvent (Backend → Frontend) -// =========================================================================== - -/// Events emitted by the team coordination subsystem. -#[derive(Serialize, Debug, Clone)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum TeamEvent { - /// A new member joined a team. - MemberJoined { - team_name: String, - agent_id: String, - agent_name: String, - #[serde(skip_serializing_if = "Option::is_none")] - role: Option, - }, - /// A member left a team. - MemberLeft { - team_name: String, - agent_id: String, - agent_name: String, - }, - /// A message was routed between team members. - MessageRouted { - team_name: String, - from: String, - to: String, - text: String, - timestamp: String, - #[serde(skip_serializing_if = "Option::is_none")] - summary: Option, - }, - /// Full status snapshot of a team. - StatusSnapshot { - team_name: String, - members: Vec, - pending_messages: usize, - }, -} - -// =========================================================================== -// TeamCommand (Frontend → Backend) -// =========================================================================== - -/// Commands the frontend can send to interact with team coordination. -#[derive(Deserialize, Debug)] -#[serde(tag = "kind", rename_all = "snake_case")] -pub enum TeamCommand { - /// Inject a message to a specific team member. - InjectMessage { - team_name: String, - to: String, - text: String, - }, - /// Query the current status of a team. - QueryTeamStatus { team_name: String }, -} - -// =========================================================================== -// Tests -// =========================================================================== - -#[cfg(test)] -mod tests { - use super::*; - - // ----------------------------------------------------------------------- - // AgentEvent serialization - // ----------------------------------------------------------------------- - - #[test] - fn agent_event_spawned_serializes_with_kind() { - let event = AgentEvent::Spawned { - agent_id: "agent-1".to_string(), - parent_agent_id: None, - description: "Implement feature".to_string(), - agent_type: Some("coordinator".to_string()), - model: Some("claude-sonnet-4-20250514".to_string()), - is_background: false, - depth: 0, - chain_id: "chain-abc".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize AgentEvent::Spawned"); - assert_eq!(value["kind"], "spawned"); - assert_eq!(value["agent_id"], "agent-1"); - assert_eq!(value["description"], "Implement feature"); - assert_eq!(value["agent_type"], "coordinator"); - assert_eq!(value["model"], "claude-sonnet-4-20250514"); - assert_eq!(value["is_background"], false); - assert_eq!(value["depth"], 0); - assert_eq!(value["chain_id"], "chain-abc"); - assert!( - value.get("parent_agent_id").is_none(), - "None parent_agent_id should be omitted" - ); - } - - #[test] - fn agent_event_spawned_with_parent_serializes() { - let event = AgentEvent::Spawned { - agent_id: "agent-2".to_string(), - parent_agent_id: Some("agent-1".to_string()), - description: "Run tests".to_string(), - agent_type: None, - model: None, - is_background: true, - depth: 1, - chain_id: "chain-abc".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "spawned"); - assert_eq!(value["parent_agent_id"], "agent-1"); - assert_eq!(value["is_background"], true); - assert!(value.get("agent_type").is_none()); - assert!(value.get("model").is_none()); - } - - #[test] - fn agent_event_completed_serializes_with_kind() { - let event = AgentEvent::Completed { - agent_id: "agent-1".to_string(), - result_preview: "All tests passed".to_string(), - had_error: false, - duration_ms: 5000, - output_tokens: Some(1234), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "completed"); - assert_eq!(value["agent_id"], "agent-1"); - assert_eq!(value["result_preview"], "All tests passed"); - assert_eq!(value["had_error"], false); - assert_eq!(value["duration_ms"], 5000); - assert_eq!(value["output_tokens"], 1234); - } - - #[test] - fn agent_event_completed_without_output_tokens() { - let event = AgentEvent::Completed { - agent_id: "agent-1".to_string(), - result_preview: "Done".to_string(), - had_error: true, - duration_ms: 100, - output_tokens: None, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "completed"); - assert_eq!(value["had_error"], true); - assert!(value.get("output_tokens").is_none()); - } - - #[test] - fn agent_event_error_serializes_with_kind() { - let event = AgentEvent::Error { - agent_id: "agent-3".to_string(), - error: "API timeout".to_string(), - duration_ms: 30000, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "error"); - assert_eq!(value["agent_id"], "agent-3"); - assert_eq!(value["error"], "API timeout"); - assert_eq!(value["duration_ms"], 30000); - } - - #[test] - fn agent_event_aborted_serializes_with_kind() { - let event = AgentEvent::Aborted { - agent_id: "agent-4".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "aborted"); - assert_eq!(value["agent_id"], "agent-4"); - } - - #[test] - fn agent_event_stream_delta_serializes_with_kind() { - let event = AgentEvent::StreamDelta { - agent_id: "agent-1".to_string(), - text: "Hello, world!".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize AgentEvent::StreamDelta"); - assert_eq!(value["kind"], "stream_delta"); - assert_eq!(value["agent_id"], "agent-1"); - assert_eq!(value["text"], "Hello, world!"); - } - - #[test] - fn agent_event_thinking_delta_serializes() { - let event = AgentEvent::ThinkingDelta { - agent_id: "agent-1".to_string(), - thinking: "Let me consider...".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "thinking_delta"); - assert_eq!(value["thinking"], "Let me consider..."); - } - - #[test] - fn agent_event_tool_use_serializes() { - let event = AgentEvent::ToolUse { - agent_id: "agent-1".to_string(), - tool_use_id: "tu-001".to_string(), - tool_name: "Bash".to_string(), - input: serde_json::json!({"command": "ls -la"}), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "tool_use"); - assert_eq!(value["tool_name"], "Bash"); - assert_eq!(value["input"]["command"], "ls -la"); - } - - #[test] - fn agent_event_tool_result_serializes() { - let event = AgentEvent::ToolResult { - agent_id: "agent-1".to_string(), - tool_use_id: "tu-001".to_string(), - output: "file1.rs\nfile2.rs".to_string(), - is_error: false, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "tool_result"); - assert_eq!(value["is_error"], false); - } - - #[test] - fn agent_event_tree_snapshot_serializes_with_kind() { - let node = AgentNode { - agent_id: "agent-root".to_string(), - parent_agent_id: None, - description: "Root agent".to_string(), - agent_type: None, - model: None, - state: "running".to_string(), - is_background: false, - depth: 0, - chain_id: "chain-1".to_string(), - spawned_at: 1713168000000, - completed_at: None, - duration_ms: None, - result_preview: None, - had_error: false, - children: vec![], - }; - let event = AgentEvent::TreeSnapshot { roots: vec![node] }; - let value = serde_json::to_value(&event).expect("serialize AgentEvent::TreeSnapshot"); - assert_eq!(value["kind"], "tree_snapshot"); - let roots = value["roots"].as_array().expect("roots should be array"); - assert_eq!(roots.len(), 1); - assert_eq!(roots[0]["agent_id"], "agent-root"); - } - - // ----------------------------------------------------------------------- - // AgentCommand deserialization - // ----------------------------------------------------------------------- - - #[test] - fn agent_command_abort_agent_deserializes() { - let json = r#"{"kind":"abort_agent","agent_id":"agent-42"}"#; - let cmd: AgentCommand = serde_json::from_str(json).expect("deserialize AgentCommand"); - match cmd { - AgentCommand::AbortAgent { agent_id } => assert_eq!(agent_id, "agent-42"), - other => panic!("unexpected variant: {:?}", other), - } - } - - #[test] - fn agent_command_query_active_agents_deserializes() { - let json = r#"{"kind":"query_active_agents"}"#; - let cmd: AgentCommand = - serde_json::from_str(json).expect("deserialize AgentCommand::QueryActiveAgents"); - assert!(matches!(cmd, AgentCommand::QueryActiveAgents)); - } - - #[test] - fn agent_command_query_agent_output_deserializes() { - let json = r#"{"kind":"query_agent_output","agent_id":"agent-7"}"#; - let cmd: AgentCommand = serde_json::from_str(json).expect("deserialize"); - match cmd { - AgentCommand::QueryAgentOutput { agent_id } => assert_eq!(agent_id, "agent-7"), - other => panic!("unexpected variant: {:?}", other), - } - } - - // ----------------------------------------------------------------------- - // TeamEvent serialization - // ----------------------------------------------------------------------- - - #[test] - fn team_event_member_joined_serializes() { - let event = TeamEvent::MemberJoined { - team_name: "backend-team".to_string(), - agent_id: "agent-10".to_string(), - agent_name: "Alice".to_string(), - role: Some("reviewer".to_string()), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "member_joined"); - assert_eq!(value["team_name"], "backend-team"); - assert_eq!(value["role"], "reviewer"); - } - - #[test] - fn team_event_member_joined_without_role() { - let event = TeamEvent::MemberJoined { - team_name: "team-1".to_string(), - agent_id: "agent-11".to_string(), - agent_name: "Bob".to_string(), - role: None, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "member_joined"); - assert!(value.get("role").is_none()); - } - - #[test] - fn team_event_member_left_serializes() { - let event = TeamEvent::MemberLeft { - team_name: "team-1".to_string(), - agent_id: "agent-10".to_string(), - agent_name: "Alice".to_string(), - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "member_left"); - assert_eq!(value["agent_name"], "Alice"); - } - - #[test] - fn team_event_message_routed_serializes_with_kind() { - let event = TeamEvent::MessageRouted { - team_name: "backend-team".to_string(), - from: "agent-10".to_string(), - to: "agent-11".to_string(), - text: "Please review PR #42".to_string(), - timestamp: "2026-04-15T10:00:00Z".to_string(), - summary: Some("Review request".to_string()), - }; - let value = serde_json::to_value(&event).expect("serialize TeamEvent::MessageRouted"); - assert_eq!(value["kind"], "message_routed"); - assert_eq!(value["team_name"], "backend-team"); - assert_eq!(value["from"], "agent-10"); - assert_eq!(value["to"], "agent-11"); - assert_eq!(value["text"], "Please review PR #42"); - assert_eq!(value["summary"], "Review request"); - } - - #[test] - fn team_event_message_routed_without_summary() { - let event = TeamEvent::MessageRouted { - team_name: "team-1".to_string(), - from: "a".to_string(), - to: "b".to_string(), - text: "hi".to_string(), - timestamp: "2026-04-15T10:00:00Z".to_string(), - summary: None, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "message_routed"); - assert!(value.get("summary").is_none()); - } - - #[test] - fn team_event_status_snapshot_serializes() { - let event = TeamEvent::StatusSnapshot { - team_name: "backend-team".to_string(), - members: vec![TeamMemberInfo { - agent_id: "agent-10".to_string(), - agent_name: "Alice".to_string(), - role: Some("reviewer".to_string()), - is_active: true, - unread_messages: 2, - }], - pending_messages: 5, - }; - let value = serde_json::to_value(&event).expect("serialize"); - assert_eq!(value["kind"], "status_snapshot"); - assert_eq!(value["pending_messages"], 5); - assert_eq!(value["members"].as_array().unwrap().len(), 1); - } - - // ----------------------------------------------------------------------- - // TeamCommand deserialization - // ----------------------------------------------------------------------- - - #[test] - fn team_command_inject_message_deserializes() { - let json = r#"{"kind":"inject_message","team_name":"backend-team","to":"agent-10","text":"hello"}"#; - let cmd: TeamCommand = - serde_json::from_str(json).expect("deserialize TeamCommand::InjectMessage"); - match cmd { - TeamCommand::InjectMessage { - team_name, - to, - text, - } => { - assert_eq!(team_name, "backend-team"); - assert_eq!(to, "agent-10"); - assert_eq!(text, "hello"); - } - other => panic!("unexpected variant: {:?}", other), - } - } +//! The real definitions now live in `cc_types::agent_events`; this module is a +//! thin re-export so existing `crate::ipc::agent_events::*` paths keep working. - #[test] - fn team_command_query_team_status_deserializes() { - let json = r#"{"kind":"query_team_status","team_name":"ops-team"}"#; - let cmd: TeamCommand = serde_json::from_str(json).expect("deserialize"); - match cmd { - TeamCommand::QueryTeamStatus { team_name } => assert_eq!(team_name, "ops-team"), - other => panic!("unexpected variant: {:?}", other), - } - } -} +#[allow(unused_imports)] +pub use cc_types::agent_events::{AgentCommand, AgentEvent, TeamCommand, TeamEvent}; diff --git a/crates/claude-code-rs/src/ipc/agent_types.rs b/crates/claude-code-rs/src/ipc/agent_types.rs index 23edb149..8771faef 100644 --- a/crates/claude-code-rs/src/ipc/agent_types.rs +++ b/crates/claude-code-rs/src/ipc/agent_types.rs @@ -1,291 +1,7 @@ //! Shared data types for agent tree, agent info, and team member IPC messages. //! -//! These types are used by: -//! - Agent-related IPC events and protocol extensions -//! - The `SystemStatus` tool (flat `AgentInfo` variant) -//! - Background agent orchestration and team coordination -//! -//! All types are `Serialize + Deserialize + Debug + Clone` so they can flow -//! freely across the JSONL/SSE boundary between the Rust backend and any -//! frontend process. - -#![allow(dead_code)] // Types are pre-defined for upcoming agent IPC extension tasks - -use serde::{Deserialize, Serialize}; - -// --------------------------------------------------------------------------- -// Agent tree node -// --------------------------------------------------------------------------- - -/// Recursive tree node representing a running or completed agent. -/// -/// Used to build a full agent hierarchy for the frontend, where each node -/// may have zero or more `children` forming an arbitrarily deep tree. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AgentNode { - /// Unique identifier for this agent instance. - pub agent_id: String, - /// Identifier of the parent agent that spawned this one, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub parent_agent_id: Option, - /// Human-readable description of this agent's purpose. - pub description: String, - /// Optional agent type label (e.g. "tool", "coordinator", "worker"). - #[serde(skip_serializing_if = "Option::is_none")] - pub agent_type: Option, - /// Model used by this agent (e.g. "claude-sonnet-4-20250514"). - #[serde(skip_serializing_if = "Option::is_none")] - pub model: Option, - /// Current lifecycle state: "running", "completed", "error", or "aborted". - pub state: String, - /// Whether this agent runs in the background (non-blocking). - pub is_background: bool, - /// Nesting depth in the agent tree (root = 0). - pub depth: usize, - /// Chain identifier grouping related agents in a single execution chain. - pub chain_id: String, - /// Unix timestamp (milliseconds) when this agent was spawned. - pub spawned_at: i64, - /// Unix timestamp (milliseconds) when this agent completed, if finished. - #[serde(skip_serializing_if = "Option::is_none")] - pub completed_at: Option, - /// Wall-clock duration in milliseconds from spawn to completion. - #[serde(skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, - /// Truncated preview of the agent's result or output. - #[serde(skip_serializing_if = "Option::is_none")] - pub result_preview: Option, - /// Whether this agent encountered an error during execution. - pub had_error: bool, - /// Child agents spawned by this agent. - pub children: Vec, -} - -// --------------------------------------------------------------------------- -// Flat agent info (for SystemStatus tool) -// --------------------------------------------------------------------------- - -/// Flat (non-recursive) agent information used by the `SystemStatus` tool. -/// -/// Unlike `AgentNode`, this struct does not carry children — it is intended -/// for simple status listings rather than tree rendering. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct AgentInfo { - /// Unique identifier for this agent instance. - pub agent_id: String, - /// Identifier of the parent agent, if any. - #[serde(skip_serializing_if = "Option::is_none")] - pub parent_agent_id: Option, - /// Human-readable description of this agent's purpose. - pub description: String, - /// Current lifecycle state: "running", "completed", "error", or "aborted". - pub state: String, - /// Whether this agent runs in the background (non-blocking). - pub is_background: bool, - /// Nesting depth in the agent tree (root = 0). - pub depth: usize, - /// Wall-clock duration in milliseconds from spawn to completion. - #[serde(skip_serializing_if = "Option::is_none")] - pub duration_ms: Option, -} - -// --------------------------------------------------------------------------- -// Team member info -// --------------------------------------------------------------------------- - -/// Status information for a team member in a coordinated agent group. -#[derive(Serialize, Deserialize, Debug, Clone)] -pub struct TeamMemberInfo { - /// Unique identifier for this team member's agent. - pub agent_id: String, - /// Display name of this team member. - pub agent_name: String, - /// Optional role label (e.g. "reviewer", "implementer"). - #[serde(skip_serializing_if = "Option::is_none")] - pub role: Option, - /// Whether this team member is currently active/online. - pub is_active: bool, - /// Number of unread messages from this team member. - pub unread_messages: usize, -} - -// --------------------------------------------------------------------------- -// Tests -// --------------------------------------------------------------------------- - -#[cfg(test)] -mod tests { - use super::*; - - #[test] - fn agent_node_with_children_serializes_correctly() { - let child = AgentNode { - agent_id: "agent-child-1".to_string(), - parent_agent_id: Some("agent-root".to_string()), - description: "Run tests".to_string(), - agent_type: None, - model: None, - state: "completed".to_string(), - is_background: false, - depth: 1, - chain_id: "chain-abc".to_string(), - spawned_at: 1713168001000, - completed_at: Some(1713168005000), - duration_ms: Some(4000), - result_preview: Some("All 42 tests passed".to_string()), - had_error: false, - children: vec![], - }; - - let root = AgentNode { - agent_id: "agent-root".to_string(), - parent_agent_id: None, - description: "Implement feature X".to_string(), - agent_type: Some("coordinator".to_string()), - model: Some("claude-sonnet-4-20250514".to_string()), - state: "running".to_string(), - is_background: false, - depth: 0, - chain_id: "chain-abc".to_string(), - spawned_at: 1713168000000, - completed_at: None, - duration_ms: None, - result_preview: None, - had_error: false, - children: vec![child], - }; - - let value = serde_json::to_value(&root).expect("serialize AgentNode"); - - // Root: None fields should be omitted - assert!( - value.get("parent_agent_id").is_none(), - "None parent_agent_id should be omitted" - ); - assert!( - value.get("completed_at").is_none(), - "None completed_at should be omitted" - ); - assert!( - value.get("duration_ms").is_none(), - "None duration_ms should be omitted" - ); - assert!( - value.get("result_preview").is_none(), - "None result_preview should be omitted" - ); - - // Root: present Optional fields should be included - assert_eq!(value["agent_type"], "coordinator"); - assert_eq!(value["model"], "claude-sonnet-4-20250514"); - - // Root: required fields - assert_eq!(value["agent_id"], "agent-root"); - assert_eq!(value["state"], "running"); - assert_eq!(value["depth"], 0); - assert_eq!(value["is_background"], false); - assert_eq!(value["had_error"], false); - - // Children - let children = value["children"] - .as_array() - .expect("children should be array"); - assert_eq!(children.len(), 1); - - let child_val = &children[0]; - assert_eq!(child_val["agent_id"], "agent-child-1"); - assert_eq!(child_val["parent_agent_id"], "agent-root"); - assert_eq!(child_val["completed_at"], 1713168005000_i64); - assert_eq!(child_val["duration_ms"], 4000); - assert_eq!(child_val["result_preview"], "All 42 tests passed"); - // Child: None optional fields omitted - assert!(child_val.get("agent_type").is_none()); - assert!(child_val.get("model").is_none()); - - // Roundtrip - let json = serde_json::to_string(&root).expect("serialize to string"); - let parsed: AgentNode = serde_json::from_str(&json).expect("deserialize AgentNode"); - assert_eq!(parsed.agent_id, "agent-root"); - assert_eq!(parsed.children.len(), 1); - assert_eq!(parsed.children[0].agent_id, "agent-child-1"); - } - - #[test] - fn agent_info_serializes_and_omits_none_fields() { - let info = AgentInfo { - agent_id: "agent-42".to_string(), - parent_agent_id: None, - description: "Background linter".to_string(), - state: "running".to_string(), - is_background: true, - depth: 0, - duration_ms: None, - }; - - let value = serde_json::to_value(&info).expect("serialize AgentInfo"); - assert_eq!(value["agent_id"], "agent-42"); - assert_eq!(value["is_background"], true); - assert!(value.get("parent_agent_id").is_none()); - assert!(value.get("duration_ms").is_none()); - - // With optional fields present - let info_full = AgentInfo { - agent_id: "agent-43".to_string(), - parent_agent_id: Some("agent-42".to_string()), - description: "Sub-task".to_string(), - state: "completed".to_string(), - is_background: false, - depth: 1, - duration_ms: Some(1500), - }; - - let value_full = serde_json::to_value(&info_full).expect("serialize full AgentInfo"); - assert_eq!(value_full["parent_agent_id"], "agent-42"); - assert_eq!(value_full["duration_ms"], 1500); - - // Roundtrip - let json = serde_json::to_string(&info_full).expect("serialize to string"); - let parsed: AgentInfo = serde_json::from_str(&json).expect("deserialize AgentInfo"); - assert_eq!(parsed.agent_id, "agent-43"); - assert_eq!(parsed.duration_ms, Some(1500)); - } - - #[test] - fn team_member_info_serializes_and_omits_none_role() { - let member = TeamMemberInfo { - agent_id: "team-member-1".to_string(), - agent_name: "Alice".to_string(), - role: None, - is_active: true, - unread_messages: 3, - }; - - let value = serde_json::to_value(&member).expect("serialize TeamMemberInfo"); - assert_eq!(value["agent_id"], "team-member-1"); - assert_eq!(value["agent_name"], "Alice"); - assert_eq!(value["is_active"], true); - assert_eq!(value["unread_messages"], 3); - assert!(value.get("role").is_none(), "None role should be omitted"); - - // With role present - let member_with_role = TeamMemberInfo { - agent_id: "team-member-2".to_string(), - agent_name: "Bob".to_string(), - role: Some("reviewer".to_string()), - is_active: false, - unread_messages: 0, - }; - - let value2 = serde_json::to_value(&member_with_role).expect("serialize"); - assert_eq!(value2["role"], "reviewer"); - assert_eq!(value2["is_active"], false); +//! The real definitions now live in `cc_types::agent_types`; this module is a +//! thin re-export so existing `crate::ipc::agent_types::*` paths keep working. - // Roundtrip - let json = serde_json::to_string(&member_with_role).expect("serialize to string"); - let parsed: TeamMemberInfo = - serde_json::from_str(&json).expect("deserialize TeamMemberInfo"); - assert_eq!(parsed.agent_name, "Bob"); - assert_eq!(parsed.role.as_deref(), Some("reviewer")); - assert_eq!(parsed.unread_messages, 0); - } -} +#[allow(unused_imports)] +pub use cc_types::agent_types::{AgentInfo, AgentNode, TeamMemberInfo}; diff --git a/crates/claude-code-rs/src/ipc/sdk_mapper.rs b/crates/claude-code-rs/src/ipc/sdk_mapper.rs index 8b90ba58..54869397 100644 --- a/crates/claude-code-rs/src/ipc/sdk_mapper.rs +++ b/crates/claude-code-rs/src/ipc/sdk_mapper.rs @@ -314,8 +314,12 @@ fn build_status_line_payload( total_cost_usd: result.total_cost_usd, api_calls: result.usage.api_call_count, session_duration_secs: Some(result.duration_ms / 1000), - output_style: app_state.settings.output_style.as_deref(), + resolved_output_style_name: crate::ui::status_line_resolver::resolve_output_style_name( + app_state.settings.output_style.as_deref(), + cwd, + ), editor_mode: app_state.settings.editor_mode.as_deref(), + worktree: crate::ui::status_line_resolver::current_worktree_status(), streaming: false, message_count: engine.messages().len(), }); diff --git a/crates/claude-code-rs/src/lsp_service/conversions/mod.rs b/crates/claude-code-rs/src/lsp_service/conversions/mod.rs index 281a6ce2..ca23f4cc 100644 --- a/crates/claude-code-rs/src/lsp_service/conversions/mod.rs +++ b/crates/claude-code-rs/src/lsp_service/conversions/mod.rs @@ -6,7 +6,7 @@ use anyhow::{bail, Context, Result}; use serde_json::Value; -use crate::tools::lsp::{HoverInfo, SourceLocation, SymbolInfo}; +use crate::lsp_service::types::{HoverInfo, SourceLocation, SymbolInfo}; // --------------------------------------------------------------------------- // URI helpers diff --git a/crates/claude-code-rs/src/lsp_service/mod.rs b/crates/claude-code-rs/src/lsp_service/mod.rs index 0a99b213..e916eb81 100644 --- a/crates/claude-code-rs/src/lsp_service/mod.rs +++ b/crates/claude-code-rs/src/lsp_service/mod.rs @@ -10,6 +10,7 @@ pub mod client; pub mod conversions; +pub mod types; use std::collections::HashMap; use std::path::{Path, PathBuf}; @@ -18,7 +19,7 @@ use std::sync::LazyLock; use anyhow::{Context, Result}; use serde::{Deserialize, Serialize}; -use crate::tools::lsp::{HoverInfo, SourceLocation, SymbolInfo}; +pub use types::{HoverInfo, SourceLocation, SymbolInfo}; pub mod transport; diff --git a/crates/claude-code-rs/src/lsp_service/types.rs b/crates/claude-code-rs/src/lsp_service/types.rs new file mode 100644 index 00000000..b0a9b728 --- /dev/null +++ b/crates/claude-code-rs/src/lsp_service/types.rs @@ -0,0 +1,37 @@ +//! Shared LSP result types used by both the LSP service (this crate) and the +//! `Lsp` tool wrapper in `tools::lsp`. +//! +//! Previously these lived in `tools::lsp`, which produced a +//! `lsp_service -> tools` edge that blocked Phase 6 crate extraction. Moving +//! them here reverses the direction: `tools::lsp` now imports from +//! `lsp_service::types`, which is the natural direction given the tool is a +//! thin wrapper over the service. + +use serde::Serialize; + +/// A location in a source file (simplified LSP Location). +#[derive(Debug, Clone, Serialize)] +pub struct SourceLocation { + pub file_path: String, + pub line: u32, // 1-based + pub character: u32, // 1-based + pub end_line: Option, + pub end_character: Option, +} + +/// A symbol in a document. +#[derive(Debug, Clone, Serialize)] +pub struct SymbolInfo { + pub name: String, + pub kind: String, + pub location: SourceLocation, + #[serde(skip_serializing_if = "Vec::is_empty")] + pub children: Vec, +} + +/// Hover information. +#[derive(Debug, Clone, Serialize)] +pub struct HoverInfo { + pub contents: String, + pub range: Option, +} diff --git a/crates/claude-code-rs/src/query/deps.rs b/crates/claude-code-rs/src/query/deps.rs index edca6dd9..aa880f07 100644 --- a/crates/claude-code-rs/src/query/deps.rs +++ b/crates/claude-code-rs/src/query/deps.rs @@ -146,10 +146,17 @@ pub trait QueryDeps: Send + Sync { /// Default: returns empty vec (no background agent support). fn drain_background_results( &self, - ) -> Vec { + ) -> Vec { vec![] } + /// Hook runner for the query loop (PreCompact / PostCompact / Stop / + /// StopFailure events). Defaults to a no-op runner so tests using the + /// default trait impl don't need to provide one. + fn hook_runner(&self) -> Arc { + Arc::new(cc_types::hooks::NoopHookRunner) + } + /// Get the audit context for this submit. fn audit_context(&self) -> crate::observability::AuditContext { crate::observability::AuditContext::noop("unknown") diff --git a/crates/claude-code-rs/src/query/loop_impl.rs b/crates/claude-code-rs/src/query/loop_impl.rs index 170774c7..25d569ad 100644 --- a/crates/claude-code-rs/src/query/loop_impl.rs +++ b/crates/claude-code-rs/src/query/loop_impl.rs @@ -153,12 +153,13 @@ pub fn query(params: QueryParams, deps: Arc) -> impl Stream) -> impl Stream) -> impl Stream) -> impl Stream) -> impl Stream, @@ -49,9 +50,9 @@ pub async fn run_stop_hooks( return Ok(StopHookResult::AllowStop); } - use crate::tools::hooks::{self, PostToolHookResult}; + use cc_types::hooks::PostToolHookResult; - match hooks::run_stop_hooks(hook_configs).await { + match runner.run_stop_hooks(hook_configs).await { Ok(PostToolHookResult::Continue) => Ok(StopHookResult::AllowStop), Ok(PostToolHookResult::StopContinuation { message }) => Ok(StopHookResult::PreventStop { continuation_message: message, @@ -160,7 +161,10 @@ mod tests { #[tokio::test] async fn test_stop_hooks_allow_by_default() { let msg = make_assistant_message(vec![]); - let result = run_stop_hooks(&msg, &[], None, &[]).await.unwrap(); + let runner = cc_types::hooks::NoopHookRunner; + let result = run_stop_hooks(&runner, &msg, &[], None, &[]) + .await + .unwrap(); assert!(matches!(result, StopHookResult::AllowStop)); } } diff --git a/crates/claude-code-rs/src/teams/types.rs b/crates/claude-code-rs/src/teams/types.rs index fe1d6b2a..2837d30c 100644 --- a/crates/claude-code-rs/src/teams/types.rs +++ b/crates/claude-code-rs/src/teams/types.rs @@ -5,8 +5,6 @@ #![allow(unused)] -use std::collections::HashMap; - use serde::{Deserialize, Serialize}; use crate::types::tool::PermissionMode; @@ -103,32 +101,13 @@ pub struct TeamMember { // --------------------------------------------------------------------------- // TeamContext — AppState extension // --------------------------------------------------------------------------- - -/// Runtime team context stored in AppState. -#[derive(Debug, Clone, Default)] -pub struct TeamContext { - pub team_name: String, - pub team_file_path: String, - pub lead_agent_id: String, - pub self_agent_id: Option, - pub self_agent_name: Option, - pub is_leader: Option, - pub self_agent_color: Option, - pub teammates: HashMap, -} - -/// Runtime info about a spawned teammate. -#[derive(Debug, Clone)] -pub struct TeammateInfo { - pub name: String, - pub agent_type: Option, - pub color: Option, - pub tmux_session_name: String, - pub tmux_pane_id: String, - pub cwd: String, - pub worktree_path: Option, - pub spawned_at: i64, -} +// +// `TeamContext` and `TeammateInfo` are pure data types consumed by +// `types/app_state.rs`. They moved to `cc-types::teams` so cc-types no longer +// needs to reach back into the root crate's `teams::` module. See issue #75 +// ("Remaining before the source move" in the workspace-split design doc). +#[allow(unused_imports)] +pub use cc_types::teams::{TeamContext, TeammateInfo}; // --------------------------------------------------------------------------- // TeammateMessage — mailbox message @@ -330,12 +309,8 @@ mod tests { assert_ne!(TaskStatus::Running, TaskStatus::Completed); } - #[test] - fn test_team_context_default() { - let ctx = TeamContext::default(); - assert!(ctx.team_name.is_empty()); - assert!(ctx.teammates.is_empty()); - } + // `test_team_context_default` moved to `cc-types::teams` together with + // `TeamContext` / `TeammateInfo`. #[test] fn test_idle_reason_serde() { diff --git a/crates/claude-code-rs/src/tools/background_agents.rs b/crates/claude-code-rs/src/tools/background_agents.rs index ac97713f..de77553e 100644 --- a/crates/claude-code-rs/src/tools/background_agents.rs +++ b/crates/claude-code-rs/src/tools/background_agents.rs @@ -1,98 +1,8 @@ -//! Background agent types — shared between the Agent tool, query loop, -//! and the headless/TUI event loop. - -use std::sync::Arc; -use std::time::Duration; - -use parking_lot::Mutex; - -/// Result from a completed background agent. -#[derive(Debug, Clone)] -pub struct CompletedBackgroundAgent { - pub agent_id: String, - pub description: String, - pub result_text: String, - pub had_error: bool, - pub duration: Duration, -} - -/// Shared buffer of completed agents waiting to be injected into the query loop. -/// -/// The event loop pushes completed agents here after notifying the frontend. -/// The query loop drains at turn boundaries and injects system messages. -/// Internal `Mutex` means this is safe to clone and share without external locking. -#[derive(Debug, Clone, Default)] -pub struct PendingBackgroundResults { - inner: Arc>>, -} - -impl PendingBackgroundResults { - pub fn new() -> Self { - Self::default() - } - - /// Push a completed agent result (called by event loop). - pub fn push(&self, agent: CompletedBackgroundAgent) { - self.inner.lock().push(agent); - } - - /// Drain all pending results (called by query loop at turn start). - pub fn drain_all(&self) -> Vec { - let mut guard = self.inner.lock(); - std::mem::take(&mut *guard) - } -} - -#[cfg(test)] -mod tests { - use super::*; - - fn make_completed(id: &str, desc: &str) -> CompletedBackgroundAgent { - CompletedBackgroundAgent { - agent_id: id.to_string(), - description: desc.to_string(), - result_text: format!("Result from {}", desc), - had_error: false, - duration: Duration::from_secs(1), - } - } - - #[test] - fn test_pending_results_push_and_drain() { - let pending = PendingBackgroundResults::new(); - assert!(pending.drain_all().is_empty()); - - pending.push(make_completed("a1", "task one")); - pending.push(make_completed("a2", "task two")); - - let drained = pending.drain_all(); - assert_eq!(drained.len(), 2); - assert_eq!(drained[0].agent_id, "a1"); - assert_eq!(drained[1].agent_id, "a2"); - - // Second drain is empty - assert!(pending.drain_all().is_empty()); - } - - #[test] - fn test_pending_results_clone_shares_state() { - let pending1 = PendingBackgroundResults::new(); - let pending2 = pending1.clone(); - - pending1.push(make_completed("a1", "task")); - let drained = pending2.drain_all(); - assert_eq!(drained.len(), 1); - assert_eq!(drained[0].agent_id, "a1"); - } - - #[test] - fn test_channel_send_recv() { - let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); - tx.send(make_completed("bg1", "background task")).unwrap(); - - let received = rx.try_recv().unwrap(); - assert_eq!(received.agent_id, "bg1"); - assert_eq!(received.description, "background task"); - assert!(!received.had_error); - } -} +//! Background agent types — re-exported from `cc-types::background_agents`. +//! +//! The real definitions moved to cc-types in Phase 6 to break the +//! `query -> tools` edge. This module is kept as a thin re-export so existing +//! call sites (engine, ipc, agent tool) continue to work unchanged. + +#[allow(unused_imports)] +pub use cc_types::background_agents::{CompletedBackgroundAgent, PendingBackgroundResults}; diff --git a/crates/claude-code-rs/src/tools/file_read.rs b/crates/claude-code-rs/src/tools/file_read.rs deleted file mode 100644 index e69de29b..00000000 diff --git a/crates/claude-code-rs/src/tools/hooks/mod.rs b/crates/claude-code-rs/src/tools/hooks/mod.rs index 7469b79e..6e5a8a54 100644 --- a/crates/claude-code-rs/src/tools/hooks/mod.rs +++ b/crates/claude-code-rs/src/tools/hooks/mod.rs @@ -150,6 +150,13 @@ impl HookRunner for ShellHookRunner { ) -> anyhow::Result { run_event_hooks(event_name, payload, hook_configs).await } + + async fn run_stop_hooks( + &self, + hook_configs: &[HookEventConfig], + ) -> anyhow::Result { + run_stop_hooks(hook_configs).await + } } // --------------------------------------------------------------------------- diff --git a/crates/claude-code-rs/src/tools/lsp.rs b/crates/claude-code-rs/src/tools/lsp.rs index 13a4daa3..2b2a50b8 100644 --- a/crates/claude-code-rs/src/tools/lsp.rs +++ b/crates/claude-code-rs/src/tools/lsp.rs @@ -98,33 +98,13 @@ impl LspOperation { // --------------------------------------------------------------------------- // LSP location types // --------------------------------------------------------------------------- - -/// A location in a source file (simplified LSP Location). -#[derive(Debug, Clone, serde::Serialize)] -pub struct SourceLocation { - pub file_path: String, - pub line: u32, // 1-based - pub character: u32, // 1-based - pub end_line: Option, - pub end_character: Option, -} - -/// A symbol in a document. -#[derive(Debug, Clone, serde::Serialize)] -pub struct SymbolInfo { - pub name: String, - pub kind: String, - pub location: SourceLocation, - #[serde(skip_serializing_if = "Vec::is_empty")] - pub children: Vec, -} - -/// Hover information. -#[derive(Debug, Clone, serde::Serialize)] -pub struct HoverInfo { - pub contents: String, - pub range: Option, -} +// +// Moved to `crate::lsp_service::types` in Phase 6 prep so the `lsp_service` +// crate no longer has to depend on `crate::tools::lsp` (breaks the +// `tools::lsp <-> lsp_service` cycle). Re-exported here so existing +// `crate::tools::lsp::{HoverInfo, SourceLocation, SymbolInfo}` call sites +// keep compiling. +pub use crate::lsp_service::types::{HoverInfo, SourceLocation, SymbolInfo}; // --------------------------------------------------------------------------- // Result formatting diff --git a/crates/claude-code-rs/src/types/mod.rs b/crates/claude-code-rs/src/types/mod.rs index 203cf538..0c2ff31f 100644 --- a/crates/claude-code-rs/src/types/mod.rs +++ b/crates/claude-code-rs/src/types/mod.rs @@ -1,13 +1,8 @@ -// `message`, `state`, `transitions` now live in the `cc-types` workspace crate -// (issue #70 — Phase 1 leaf extraction). Re-export them here so existing -// `crate::types::message::*` paths keep resolving across the ~100 call sites -// in this crate. -// -// `app_state`, `tool`, and `config` still depend on teams / ui / config / ipc -// and stay local to the root crate until those subsystems move out. Once they -// do, this file can collapse to a single `pub use cc_types::*;`. -pub use cc_types::{message, state, transitions}; +// `app_state`, `tool`, and `config` moved to `cc-engine::types` in Phase 6 +// (issue #75). `message`, `state`, `transitions` live in `cc-types` (Phase 1). +// This module re-exports both sets so existing +// `crate::types::{app_state, tool, config, message, state, transitions}` +// paths across the root crate keep compiling unchanged. -pub mod app_state; -pub mod config; -pub mod tool; +pub use cc_engine::types::{app_state, config, tool}; +pub use cc_types::{message, state, transitions}; diff --git a/crates/claude-code-rs/src/ui/app.rs b/crates/claude-code-rs/src/ui/app.rs index 4725b96a..c33f4de3 100644 --- a/crates/claude-code-rs/src/ui/app.rs +++ b/crates/claude-code-rs/src/ui/app.rs @@ -607,8 +607,12 @@ impl App { total_cost_usd: self.session_cost_usd, api_calls: self.session_usage.api_calls, session_duration_secs: None, - output_style: self.output_style.as_deref(), + resolved_output_style_name: crate::ui::status_line_resolver::resolve_output_style_name( + self.output_style.as_deref(), + std::path::Path::new(&self.cwd), + ), editor_mode: self.vim.enabled.then_some("vim"), + worktree: crate::ui::status_line_resolver::current_worktree_status(), streaming: self.is_streaming, message_count: self.messages.len(), }); diff --git a/crates/claude-code-rs/src/ui/mod.rs b/crates/claude-code-rs/src/ui/mod.rs index d6a637ed..0bf7649b 100644 --- a/crates/claude-code-rs/src/ui/mod.rs +++ b/crates/claude-code-rs/src/ui/mod.rs @@ -15,7 +15,10 @@ pub mod permissions; pub mod prompt_input; #[allow(dead_code)] pub mod spinner; -pub mod status_line; +// `status_line` moved to `cc-engine` in Phase 6 (issue #75). Downstream +// consumers should import from `cc_engine::status_line` directly. +pub use cc_engine::status_line; +pub mod status_line_resolver; pub mod terminal_env; #[allow(dead_code)] pub mod theme; diff --git a/crates/claude-code-rs/src/ui/status_line_resolver.rs b/crates/claude-code-rs/src/ui/status_line_resolver.rs new file mode 100644 index 00000000..8dd7ff5d --- /dev/null +++ b/crates/claude-code-rs/src/ui/status_line_resolver.rs @@ -0,0 +1,47 @@ +//! Root-crate helpers for pre-resolving fields that `cc_engine::status_line` +//! cannot resolve itself. +//! +//! The `StatusLineSnapshot` struct in cc-engine takes +//! `resolved_output_style_name: Option` and `worktree: Option` +//! as already-resolved values, because the original in-crate helpers touched +//! `crate::engine::output_style` and `crate::tools::worktree` — modules that +//! haven't moved out of the root crate yet. These small helpers perform that +//! resolution at each snapshot-building call site. + +use std::path::Path; + +use cc_engine::status_line::payload::WorktreeStatus; + +/// Resolve an output-style name via the engine's output-style registry. +/// +/// Returns `None` when the input is missing or empty. +pub fn resolve_output_style_name(output_style: Option<&str>, cwd: &Path) -> Option { + output_style + .map(str::trim) + .filter(|style| !style.is_empty()) + .map(|style| { + crate::engine::output_style::resolve(style, cwd) + .name() + .to_string() + }) +} + +/// Build a `WorktreeStatus` from the current worktree session (if any). +pub fn current_worktree_status() -> Option { + let session = crate::tools::worktree::get_current_worktree_session()?; + let name = session + .worktree_path + .file_name() + .and_then(|name| name.to_str()) + .filter(|name| !name.is_empty()) + .unwrap_or("worktree") + .to_string(); + + Some(WorktreeStatus { + name, + path: session.worktree_path.display().to_string(), + branch: Some(session.branch_name), + original_cwd: session.original_cwd.display().to_string(), + original_branch: None, + }) +} diff --git a/docs/superpowers/specs/2026-04-20-workspace-split-design.md b/docs/superpowers/specs/2026-04-20-workspace-split-design.md index 3d3626f3..e74aa774 100644 --- a/docs/superpowers/specs/2026-04-20-workspace-split-design.md +++ b/docs/superpowers/specs/2026-04-20-workspace-split-design.md @@ -222,6 +222,50 @@ Now that cycles are gone: **Estimated effort**: L (2 days) **Risk**: medium — large surface (17k LOC for tools). +**Current status (issue #75 — in progress)**: scaffolding + cycle-breaking landed: + +- All four Phase 6 crates (`cc-engine`, `cc-query`, `cc-tools`, `cc-lsp-service`) + exist as workspace members with stub `lib.rs` files and dependency + declarations. The physical source move is staged in follow-up PRs. +- Phase-5 residual `query -> tools` cycle fully eliminated: the query loop + now drives hooks through `cc_types::hooks::HookRunner` via a new + `QueryDeps::hook_runner()` accessor. `HookRunner` gained a `run_stop_hooks` + method, implemented by both `NoopHookRunner` (cc-types) and + `ShellHookRunner` (root crate). +- `background_agents` (CompletedBackgroundAgent, PendingBackgroundResults) + moved to `cc-types::background_agents` so the engine `QueryDeps` trait can + cite them without touching `crate::tools::*`. +- Agent IPC type trio (`agent_types`, `agent_events`, `agent_channel`) moved + to `cc-types::{agent_types, agent_events, agent_channel}`. Root + `src/ipc/agent_*.rs` are now thin re-exports. This unblocks the + `engine -> ipc` edge (the engine `sdk_to_agent_event` helper now depends + only on cc-types). +- `ToolUseContext::bg_agent_tx` typed against `cc_types::agent_channel::AgentSender` + instead of `crate::ipc::agent_channel::AgentSender`, removing the + `types -> ipc` edge in the Tool trait surface. +- LSP shared types (`HoverInfo`, `SourceLocation`, `SymbolInfo`) moved from + `tools/lsp.rs` into a new `lsp_service/types.rs` module. Tool side now + re-exports. This breaks the `lsp_service -> tools::lsp` cycle; when + `tools/lsp.rs` moves into `cc-lsp-service` in the next pass, the crate is + self-contained. + +**Remaining before the source move**: + +1. Hoist `types/tool.rs` (Tool trait, `ToolUseContext`) to cc-types. Blocker: + `ToolUseContext::get_app_state` returns `AppState`, which lives in + `types/app_state.rs` and transitively reaches into `teams::types::TeamContext` + and `ui::status_line::StatusLineRunner`. Either (a) abstract `AppState` + behind an opaque `Arc` trait, or (b) move `TeamContext` + and `StatusLineRunner` to cc-types first. +2. Move `types/config.rs` (QueryEngineConfig, QueryParams) and the + `types/app_state.rs` residue to cc-types once (1) unblocks. +3. Move the three cycle-causing tool files to their natural homes: + - `tools/lsp.rs` -> cc-lsp-service + - `tools/send_message.rs` + `tools/team_spawn.rs` -> cc-teams + - `tools/system_status.rs` -> cc-ipc +4. Physically move `src/engine/`, `src/query/`, `src/tools/`, `src/lsp_service/` + into their respective crates and rewire `use crate::X` imports. + ### Phase 7 — Extract high-level crates - `cc-plugins` @@ -233,6 +277,12 @@ Now that cycles are gone: **Estimated effort**: L (2 days) **Risk**: medium. +**Current status (issue #76 — scaffold only)**: all five Phase 7 crates exist +as workspace members with stub `lib.rs` files and description comments. The +source move blocks on completion of Phase 6, because every Phase 7 crate +depends transitively on `cc-engine`, `cc-tools`, or the `Tool` trait in +`types/tool.rs`. See Phase 6 remaining items above. + ### Phase 8 — Thin root bin crate After all extractions, `crates/claude-code-rs/src/` contains only: