Merge remote-tracking branch 'origin/rcc/thinking' into dev/rust

# Conflicts:
#	rust/crates/commands/src/lib.rs
#	rust/crates/rusty-claude-cli/src/main.rs
This commit is contained in:
Yeachan-Heo
2026-04-01 01:11:06 +00:00
9 changed files with 372 additions and 259 deletions

View File

@@ -164,7 +164,7 @@ fn summarize_messages(messages: &[ConversationMessage]) -> String {
.filter_map(|block| match block {
ContentBlock::ToolUse { name, .. } => Some(name.as_str()),
ContentBlock::ToolResult { tool_name, .. } => Some(tool_name.as_str()),
ContentBlock::Text { .. } => None,
ContentBlock::Text { .. } | ContentBlock::Thinking { .. } => None,
})
.collect::<Vec<_>>();
tool_names.sort_unstable();
@@ -234,6 +234,7 @@ fn summarize_messages(messages: &[ConversationMessage]) -> String {
fn summarize_block(block: &ContentBlock) -> String {
let raw = match block {
ContentBlock::Text { text } => text.clone(),
ContentBlock::Thinking { text, .. } => format!("thinking: {text}"),
ContentBlock::ToolUse { name, input, .. } => format!("tool_use {name}({input})"),
ContentBlock::ToolResult {
tool_name,
@@ -292,7 +293,7 @@ fn collect_key_files(messages: &[ConversationMessage]) -> Vec<String> {
.iter()
.flat_map(|message| message.blocks.iter())
.map(|block| match block {
ContentBlock::Text { text } => text.as_str(),
ContentBlock::Text { text } | ContentBlock::Thinking { text, .. } => text.as_str(),
ContentBlock::ToolUse { input, .. } => input.as_str(),
ContentBlock::ToolResult { output, .. } => output.as_str(),
})
@@ -314,10 +315,15 @@ fn infer_current_work(messages: &[ConversationMessage]) -> Option<String> {
fn first_text_block(message: &ConversationMessage) -> Option<&str> {
message.blocks.iter().find_map(|block| match block {
ContentBlock::Text { text } if !text.trim().is_empty() => Some(text.as_str()),
ContentBlock::Text { text } | ContentBlock::Thinking { text, .. }
if !text.trim().is_empty() =>
{
Some(text.as_str())
}
ContentBlock::ToolUse { .. }
| ContentBlock::ToolResult { .. }
| ContentBlock::Text { .. } => None,
| ContentBlock::Text { .. }
| ContentBlock::Thinking { .. } => None,
})
}
@@ -362,7 +368,7 @@ fn estimate_message_tokens(message: &ConversationMessage) -> usize {
.blocks
.iter()
.map(|block| match block {
ContentBlock::Text { text } => text.len() / 4 + 1,
ContentBlock::Text { text } | ContentBlock::Thinking { text, .. } => text.len() / 4 + 1,
ContentBlock::ToolUse { name, input, .. } => (name.len() + input.len()) / 4 + 1,
ContentBlock::ToolResult {
tool_name, output, ..

View File

@@ -17,6 +17,8 @@ pub struct ApiRequest {
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum AssistantEvent {
TextDelta(String),
ThinkingDelta(String),
ThinkingSignature(String),
ToolUse {
id: String,
name: String,
@@ -247,15 +249,26 @@ fn build_assistant_message(
events: Vec<AssistantEvent>,
) -> Result<(ConversationMessage, Option<TokenUsage>), RuntimeError> {
let mut text = String::new();
let mut thinking = String::new();
let mut thinking_signature: Option<String> = None;
let mut blocks = Vec::new();
let mut finished = false;
let mut usage = None;
for event in events {
match event {
AssistantEvent::TextDelta(delta) => text.push_str(&delta),
AssistantEvent::TextDelta(delta) => {
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
text.push_str(&delta);
}
AssistantEvent::ThinkingDelta(delta) => {
flush_text_block(&mut text, &mut blocks);
thinking.push_str(&delta);
}
AssistantEvent::ThinkingSignature(signature) => thinking_signature = Some(signature),
AssistantEvent::ToolUse { id, name, input } => {
flush_text_block(&mut text, &mut blocks);
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
blocks.push(ContentBlock::ToolUse { id, name, input });
}
AssistantEvent::Usage(value) => usage = Some(value),
@@ -266,6 +279,7 @@ fn build_assistant_message(
}
flush_text_block(&mut text, &mut blocks);
flush_thinking_block(&mut thinking, &mut thinking_signature, &mut blocks);
if !finished {
return Err(RuntimeError::new(
@@ -290,6 +304,19 @@ fn flush_text_block(text: &mut String, blocks: &mut Vec<ContentBlock>) {
}
}
fn flush_thinking_block(
thinking: &mut String,
signature: &mut Option<String>,
blocks: &mut Vec<ContentBlock>,
) {
if !thinking.is_empty() || signature.is_some() {
blocks.push(ContentBlock::Thinking {
text: std::mem::take(thinking),
signature: signature.take(),
});
}
}
type ToolHandler = Box<dyn FnMut(&str) -> Result<String, ToolError>>;
#[derive(Default)]
@@ -325,8 +352,8 @@ impl ToolExecutor for StaticToolExecutor {
#[cfg(test)]
mod tests {
use super::{
ApiClient, ApiRequest, AssistantEvent, ConversationRuntime, RuntimeError,
StaticToolExecutor,
build_assistant_message, ApiClient, ApiRequest, AssistantEvent, ConversationRuntime,
RuntimeError, StaticToolExecutor,
};
use crate::compact::CompactionConfig;
use crate::permissions::{
@@ -503,6 +530,29 @@ mod tests {
));
}
#[test]
fn thinking_blocks_are_preserved_separately_from_text() {
let (message, usage) = build_assistant_message(vec![
AssistantEvent::ThinkingDelta("first ".to_string()),
AssistantEvent::ThinkingDelta("second".to_string()),
AssistantEvent::ThinkingSignature("sig-1".to_string()),
AssistantEvent::TextDelta("final".to_string()),
AssistantEvent::MessageStop,
])
.expect("assistant message should build");
assert_eq!(usage, None);
assert!(matches!(
&message.blocks[0],
ContentBlock::Thinking { text, signature }
if text == "first second" && signature.as_deref() == Some("sig-1")
));
assert!(matches!(
&message.blocks[1],
ContentBlock::Text { text } if text == "final"
));
}
#[test]
fn reconstructs_usage_tracker_from_restored_session() {
struct SimpleApi;

View File

@@ -19,6 +19,10 @@ pub enum ContentBlock {
Text {
text: String,
},
Thinking {
text: String,
signature: Option<String>,
},
ToolUse {
id: String,
name: String,
@@ -313,6 +317,19 @@ impl ContentBlock {
object.insert("type".to_string(), JsonValue::String("text".to_string()));
object.insert("text".to_string(), JsonValue::String(text.clone()));
}
Self::Thinking { text, signature } => {
object.insert(
"type".to_string(),
JsonValue::String("thinking".to_string()),
);
object.insert("text".to_string(), JsonValue::String(text.clone()));
if let Some(signature) = signature {
object.insert(
"signature".to_string(),
JsonValue::String(signature.clone()),
);
}
}
Self::ToolUse { id, name, input } => {
object.insert(
"type".to_string(),
@@ -359,6 +376,13 @@ impl ContentBlock {
"text" => Ok(Self::Text {
text: required_string(object, "text")?,
}),
"thinking" => Ok(Self::Thinking {
text: required_string(object, "text")?,
signature: object
.get("signature")
.and_then(JsonValue::as_str)
.map(ToOwned::to_owned),
}),
"tool_use" => Ok(Self::ToolUse {
id: required_string(object, "id")?,
name: required_string(object, "name")?,