Skip to content

Commit 80997c1

Browse files
committed
feat(ccb): align TUI event flow + tool surface to ccb internals
Complete the ccb-alignment window: TUI event flow refactor, tool userFacingName convention, component extraction, and fuzzy matching. TUI (crates/tui): - translate_event / apply_event reducer split (#13) - content_buffer mirror fix for streaming chunks (#22) - Renderable trait + component extraction: Header, MessageList, InputArea, BottomBar, StatusLine (Phase 1) - AppEvent bus + FrameRequester (Phase 2) - Overlay trait + stack: CommandPalette, HistorySearch, ModelPicker, Transcript, GlobalSearch, ApprovalQueue (Phases 3-7) - EventBroker pause/resume for external editor (Phase 5) - Theme system consolidation with new role fields (#12, #21) - Delete ContextualKeybindings dead code (#17, #18) - Delete response_started + render_thinking_state dead code (#23, #24) Tools (crates/tools): - Adopt ccb userFacingName convention: Bash->Run, Grep->Search, Read->Read (path . lines N-M), PowerShell, etc. - Extract str_utils::truncate_chars for codepoint-safe truncation - Fix byte-slice panics in tool summary truncation (#10, #20) Core + API + Agent + CLI: - Wire ApprovalQueue + ModelPicker SwitchSession (#4) - Dedupe strip_trailing_tool_json + TOOL_ARG_INDEX_BASE (#5) - Fix BLOCKER: translate_agent_event empty tool name (#9) - Fix run_repl missing registry arg under --no-default-features Dependencies: - Add nucleo-matcher 0.3 for fuzzy matching (#11, #15) Docs: - Bump test count to 3900+ and crate count to 17 in README/README.zh-CN Verified end-to-end: cargo build + test (3934+ pass) + clippy clean, non-interactive -p mode + live interactive TUI via winpty driving real Windows conpty, all tool-use cells rendering correct ccb wrappers.
1 parent a555c87 commit 80997c1

87 files changed

Lines changed: 6607 additions & 1042 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

Cargo.lock

Lines changed: 120 additions & 101 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

Cargo.toml

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -107,6 +107,7 @@ unicode-width = "0.2"
107107
strip-ansi-escapes = "0.2"
108108
jsonc-parser = { version = "0.32", features = ["serde"] }
109109
schemars = "1"
110+
nucleo-matcher = "0.3"
110111

111112
# ─── 测试 ───
112113
tempfile = "3"

README.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
---
1919

20-
> **Status: Active Development** -- 49 built-in tools, 6 permission modes, extended thinking, multi-agent coordination, structured message TUI with 187 spinner verbs, 3800+ tests across 17 crates, 110k+ LOC. Zero `todo!()` macros.
20+
> **Status: Active Development** -- 49 built-in tools, 6 permission modes, extended thinking, multi-agent coordination, structured message TUI with 187 spinner verbs, 3900+ tests across 17 crates, 110k+ LOC. Zero `todo!()` macros.
2121
2222
## What is Crab Code?
2323

@@ -153,7 +153,7 @@ Plus tool-level filtering with `--allowedTools` / `--disallowedTools` supporting
153153

154154
## Architecture
155155

156-
4-layer, 16-crate Rust workspace:
156+
4-layer, 17-crate Rust workspace:
157157

158158
```
159159
Layer 4 (Entry) cli daemon xtask
@@ -209,7 +209,7 @@ crab auth login # Configure authentication
209209

210210
```bash
211211
cargo build --workspace # Build all
212-
cargo test --workspace # Run all tests (3800+)
212+
cargo test --workspace # Run all tests (3900+)
213213
cargo clippy --workspace -- -D warnings # Lint
214214
cargo fmt --all --check # Check formatting
215215
cargo run --bin crab # Run CLI

README.zh-CN.md

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@
1717

1818
---
1919

20-
> **状态:积极开发中** -- 49 个内置工具、6 种权限模式、扩展思维、多 Agent 协调、结构化消息模型 TUI(187 spinner verbs),17 个 crate 共 3800+ 测试、11 万行代码。零 `todo!()` 残留。
20+
> **状态:积极开发中** -- 49 个内置工具、6 种权限模式、扩展思维、多 Agent 协调、结构化消息模型 TUI(187 spinner verbs),17 个 crate 共 3900+ 测试、11 万行代码。零 `todo!()` 残留。
2121
2222
## 什么是 Crab Code?
2323

@@ -153,7 +153,7 @@ Crab Code 支持 Claude Code 的 `settings.json` 格式,包括 `env` 字段:
153153

154154
## 架构
155155

156-
4 层 16 crate 的 Rust workspace:
156+
4 层 17 crate 的 Rust workspace:
157157

158158
```
159159
第 4 层(入口) cli daemon xtask
@@ -209,7 +209,7 @@ crab auth login # 配置认证
209209

210210
```bash
211211
cargo build --workspace # 构建全部
212-
cargo test --workspace # 运行所有测试(3800+)
212+
cargo test --workspace # 运行所有测试(3900+)
213213
cargo clippy --workspace -- -D warnings # Lint 检查
214214
cargo fmt --all --check # 格式检查
215215
cargo run --bin crab # 运行 CLI

crates/agent/src/engine/tool_orchestration.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub async fn execute_tool_calls(
7575
.send(Event::ToolUseStart {
7676
id: id.clone(),
7777
name: name.clone(),
78+
input: input.clone(),
7879
})
7980
.await;
8081
let result = executor.execute(&name, input, ctx).await;
@@ -111,6 +112,7 @@ pub async fn execute_tool_calls(
111112
.send(Event::ToolUseStart {
112113
id: id.clone(),
113114
name: name.clone(),
115+
input: input.clone(),
114116
})
115117
.await;
116118
let output = ToolOutput::error(
@@ -146,6 +148,7 @@ pub async fn execute_tool_calls(
146148
.send(Event::ToolUseStart {
147149
id: id.clone(),
148150
name: name.clone(),
151+
input: input.clone(),
149152
})
150153
.await;
151154
let output = ToolOutput::error(format!("<hook-blocked> {msg}"));
@@ -174,6 +177,7 @@ pub async fn execute_tool_calls(
174177
.send(Event::ToolUseStart {
175178
id: id.clone(),
176179
name: name.clone(),
180+
input: input.clone(),
177181
})
178182
.await;
179183

crates/agent/src/query_loop.rs

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -462,6 +462,7 @@ async fn execute_tool_calls(
462462
.send(Event::ToolUseStart {
463463
id: id.clone(),
464464
name: name.clone(),
465+
input: input.clone(),
465466
})
466467
.await;
467468
let result = executor.execute(&name, input, ctx).await;
@@ -499,6 +500,7 @@ async fn execute_tool_calls(
499500
.send(Event::ToolUseStart {
500501
id: id.clone(),
501502
name: name.clone(),
503+
input: input.clone(),
502504
})
503505
.await;
504506
let output = ToolOutput::error(
@@ -534,6 +536,7 @@ async fn execute_tool_calls(
534536
.send(Event::ToolUseStart {
535537
id: id.clone(),
536538
name: name.clone(),
539+
input: input.clone(),
537540
})
538541
.await;
539542
let output = ToolOutput::error(format!("<hook-blocked> {msg}"));
@@ -562,6 +565,7 @@ async fn execute_tool_calls(
562565
.send(Event::ToolUseStart {
563566
id: id.clone(),
564567
name: name.clone(),
568+
input: input.clone(),
565569
})
566570
.await;
567571

crates/api/src/openai/convert.rs

Lines changed: 13 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
//! Conversion between Chat Completions API types and internal types.
22
3+
use crab_core::event::TOOL_ARG_INDEX_BASE;
34
use crab_core::message::{ContentBlock, Message, Role};
45
use crab_core::model::TokenUsage;
56

@@ -248,8 +249,18 @@ pub fn chunk_to_stream_event(chunk: &ChatCompletionChunk) -> Vec<StreamEvent> {
248249
let mut events = Vec::new();
249250

250251
for choice in &chunk.choices {
252+
let has_tool_calls = choice
253+
.delta
254+
.tool_calls
255+
.as_ref()
256+
.is_some_and(|tc| !tc.is_empty());
257+
258+
// When tool_calls are present alongside content, the content is
259+
// redundant tool-parameter JSON (DeepSeek behaviour). Skip it to
260+
// avoid polluting assistant text with raw JSON.
251261
if let Some(content) = &choice.delta.content
252262
&& !content.is_empty()
263+
&& !has_tool_calls
253264
{
254265
events.push(StreamEvent::ContentDelta {
255266
index: choice.index,
@@ -259,8 +270,8 @@ pub fn chunk_to_stream_event(chunk: &ChatCompletionChunk) -> Vec<StreamEvent> {
259270

260271
if let Some(tool_calls) = &choice.delta.tool_calls {
261272
for tc in tool_calls {
262-
// Use index offset 1000+ to avoid colliding with text content block indices
263-
let tool_index = 1000 + tc.index;
273+
// Use index offset to avoid colliding with text content block indices
274+
let tool_index = TOOL_ARG_INDEX_BASE + tc.index;
264275

265276
// First chunk for a tool call: has id + function.name
266277
if tc.id.is_some() {

crates/cli/src/commands/run.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -328,11 +328,12 @@ fn event_to_value(event: &Event) -> serde_json::Value {
328328
"delta": delta,
329329
})
330330
}
331-
Event::ToolUseStart { id, name } => {
331+
Event::ToolUseStart { id, name, input } => {
332332
serde_json::json!({
333333
"type": "tool_use_start",
334334
"id": id,
335335
"name": name,
336+
"input": input,
336337
})
337338
}
338339
Event::ToolResult { id, output } => {
@@ -429,6 +430,7 @@ mod tests {
429430
let event = Event::ToolUseStart {
430431
id: "tu_1".into(),
431432
name: "bash".into(),
433+
input: serde_json::json!({"command": "ls"}),
432434
};
433435
let value = event_to_value(&event);
434436
assert_eq!(value["type"], "tool_use_start");

crates/cli/src/main.rs

Lines changed: 26 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -891,7 +891,8 @@ async fn run_single_shot(
891891
output_format: OutputFormat,
892892
) -> anyhow::Result<()> {
893893
let event_rx = take_event_rx(session);
894-
let printer = tokio::spawn(print_events(event_rx, output_format));
894+
let registry = session.executor.registry_arc();
895+
let printer = tokio::spawn(print_events(event_rx, output_format, registry));
895896

896897
let result = session.handle_user_input(prompt).await;
897898
// Replace the event_tx with a dummy so the printer's rx sees all senders dropped.
@@ -943,7 +944,8 @@ async fn run_repl(
943944
let effective_input = resolve_slash_command(input, skill_registry);
944945

945946
let event_rx = take_event_rx(session);
946-
let printer = tokio::spawn(print_events(event_rx, OutputFormat::Text));
947+
let registry = session.executor.registry_arc();
948+
let printer = tokio::spawn(print_events(event_rx, OutputFormat::Text, registry));
947949

948950
match session.handle_user_input(&effective_input).await {
949951
Ok(()) => {}
@@ -975,7 +977,11 @@ fn take_event_rx(session: &mut AgentSession) -> mpsc::Receiver<Event> {
975977
///
976978
/// `OutputFormat::Json` and `StreamJson` emit NDJSON to stdout.
977979
/// `OutputFormat::Text` uses colored human-readable output.
978-
async fn print_events(mut rx: mpsc::Receiver<Event>, output_format: OutputFormat) {
980+
async fn print_events(
981+
mut rx: mpsc::Receiver<Event>,
982+
output_format: OutputFormat,
983+
registry: Arc<crab_tools::registry::ToolRegistry>,
984+
) {
979985
let mut stdout = std::io::stdout();
980986
let mut spinner: Option<Spinner> = None;
981987

@@ -993,18 +999,25 @@ async fn print_events(mut rx: mpsc::Receiver<Event>, output_format: OutputFormat
993999
}
9941000

9951001
match event {
996-
Event::ContentDelta { delta, .. } => {
997-
if let Some(mut s) = spinner.take() {
998-
s.stop();
1002+
Event::ContentDelta { index, delta } => {
1003+
// Only print text content (index 0), not tool arguments (index 1000+)
1004+
if index == 0 {
1005+
if let Some(mut s) = spinner.take() {
1006+
s.stop();
1007+
}
1008+
print!("{delta}");
1009+
let _ = stdout.flush();
9991010
}
1000-
print!("{delta}");
1001-
let _ = stdout.flush();
10021011
}
1003-
Event::ToolUseStart { name, .. } => {
1012+
Event::ToolUseStart { name, input, .. } => {
10041013
if let Some(mut s) = spinner.take() {
10051014
s.stop();
10061015
}
1007-
eprintln!("{} {}", "tool:".cyan().bold(), name.cyan());
1016+
let summary = registry
1017+
.get(&name)
1018+
.and_then(|tool| tool.format_use_summary(&input))
1019+
.unwrap_or_else(|| name.clone());
1020+
eprintln!("{} {}", "tool:".cyan().bold(), summary.cyan());
10081021
spinner = Some(Spinner::start(&format!("running {name}...")));
10091022
}
10101023
Event::ToolOutputDelta { id: _, delta } => {
@@ -1161,10 +1174,11 @@ fn event_to_json(event: &Event) -> Option<Value> {
11611174
"cache_creation_tokens": usage.cache_creation_tokens,
11621175
},
11631176
})),
1164-
Event::ToolUseStart { name, id } => Some(json!({
1177+
Event::ToolUseStart { name, id, input } => Some(json!({
11651178
"type": "tool_use_start",
11661179
"tool": name,
11671180
"id": id,
1181+
"input": input,
11681182
})),
11691183
Event::ToolUseInput { id, input } => Some(json!({
11701184
"type": "tool_use_input",
@@ -1622,6 +1636,7 @@ mod tests {
16221636
Event::ToolUseStart {
16231637
id: "t".into(),
16241638
name: "n".into(),
1639+
input: Value::Null,
16251640
},
16261641
Event::ToolUseInput {
16271642
id: "t".into(),

crates/core/src/event.rs

Lines changed: 28 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,17 @@ use crate::model::TokenUsage;
22
use crate::tool::ToolOutput;
33
use serde_json::Value;
44

5+
/// Streaming content-block index offset for `tool_calls` entries.
6+
///
7+
/// Used by `OpenAI`-compatible providers (`DeepSeek` in particular) to
8+
/// encode `tool_calls` entries. Tool-call chunks arrive with indices
9+
/// `>= TOOL_ARG_INDEX_BASE` so they can be multiplexed with text `content`
10+
/// blocks (indices `0..TOOL_ARG_INDEX_BASE`) in a single streaming event.
11+
/// Consumers of `Event::ContentDelta` that render text should filter
12+
/// indices `>= TOOL_ARG_INDEX_BASE` to avoid leaking raw tool-call JSON
13+
/// into the message body.
14+
pub const TOOL_ARG_INDEX_BASE: usize = 1000;
15+
516
/// Domain events for agent-to-UI communication.
617
///
718
/// All variants are `Clone + Send + 'static` to support
@@ -29,7 +40,13 @@ pub enum Event {
2940

3041
// ─── Tool execution ───
3142
/// A tool call has started.
32-
ToolUseStart { id: String, name: String },
43+
ToolUseStart {
44+
id: String,
45+
name: String,
46+
/// Tool input parameters (for rendering hooks).
47+
#[serde(default)]
48+
input: Value,
49+
},
3350

3451
/// Incremental tool input (streaming).
3552
ToolUseInput { id: String, input: Value },
@@ -300,6 +317,7 @@ mod tests {
300317
serde_roundtrip(&Event::ToolUseStart {
301318
id: "tu_1".into(),
302319
name: "bash".into(),
320+
input: serde_json::json!({"command": "ls"}),
303321
});
304322
}
305323

@@ -450,4 +468,13 @@ mod tests {
450468
delta: "Step 1: analyze the problem".into(),
451469
});
452470
}
471+
472+
#[test]
473+
fn tool_arg_index_base_is_stable() {
474+
// Locks the constant to 1000. If a future change raises this value,
475+
// it must be a conscious review — the constant is load-bearing for
476+
// both the OpenAI-compatible streaming producer (crab-api) and the
477+
// text-rendering consumers (crab-tui).
478+
assert_eq!(TOOL_ARG_INDEX_BASE, 1000);
479+
}
453480
}

0 commit comments

Comments
 (0)