story-kit: start 147_bug_activity_indicator_still_only_shows_thinking_despite_bug_140_fix
This commit is contained in:
@@ -96,6 +96,13 @@ impl ClaudeCodeProvider {
|
||||
}
|
||||
}
|
||||
|
||||
// Drain any remaining activity messages that were buffered when the
|
||||
// token channel closed. The select! loop breaks on token_rx → None,
|
||||
// but activity_rx may still hold signals sent in the same instant.
|
||||
while let Ok(name) = activity_rx.try_recv() {
|
||||
on_activity(&name);
|
||||
}
|
||||
|
||||
pty_handle
|
||||
.await
|
||||
.map_err(|e| format!("PTY task panicked: {e}"))??;
|
||||
@@ -154,6 +161,11 @@ fn run_pty_session(
|
||||
cmd.arg("--output-format");
|
||||
cmd.arg("stream-json");
|
||||
cmd.arg("--verbose");
|
||||
// Enable partial streaming events so we receive stream_event messages
|
||||
// containing raw API events (content_block_start, content_block_delta,
|
||||
// etc.). Without this flag, only complete assistant/user/result events
|
||||
// are emitted and tool-start activity signals never fire.
|
||||
cmd.arg("--include-partial-messages");
|
||||
// Delegate permission decisions to the MCP prompt_permission tool.
|
||||
// Claude Code will call this tool via the story-kit MCP server when
|
||||
// a tool requires user approval, instead of using PTY stdin/stdout.
|
||||
@@ -166,7 +178,7 @@ fn run_pty_session(
|
||||
cmd.env("CLAUDECODE", "");
|
||||
|
||||
slog!(
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose --permission-prompt-tool mcp__story-kit__prompt_permission",
|
||||
"[pty-debug] Spawning: claude -p \"{}\" {} --output-format stream-json --verbose --include-partial-messages --permission-prompt-tool mcp__story-kit__prompt_permission",
|
||||
user_message,
|
||||
resume_session_id
|
||||
.map(|s| format!("--resume {s}"))
|
||||
@@ -255,39 +267,18 @@ fn run_pty_session(
|
||||
Err(std::sync::mpsc::RecvTimeoutError::Timeout) => {
|
||||
// Check if child has exited
|
||||
if let Ok(Some(_status)) = child.try_wait() {
|
||||
// Drain remaining lines
|
||||
// Drain remaining lines through the same dispatch path
|
||||
// (process_json_event) so activity signals fire correctly.
|
||||
while let Ok(Some(line)) = line_rx.try_recv() {
|
||||
let trimmed = line.trim();
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed)
|
||||
&& let Some(event_type) =
|
||||
json.get("type").and_then(|t| t.as_str())
|
||||
{
|
||||
match event_type {
|
||||
"stream_event" => {
|
||||
if let Some(event) = json.get("event") {
|
||||
handle_stream_event(event, &token_tx, &activity_tx);
|
||||
}
|
||||
}
|
||||
"assistant" => {
|
||||
if let Some(message) = json.get("message")
|
||||
&& let Some(content) = message
|
||||
.get("content")
|
||||
.and_then(|c| c.as_array())
|
||||
{
|
||||
parse_assistant_message(content, &msg_tx);
|
||||
}
|
||||
}
|
||||
"user" => {
|
||||
if let Some(message) = json.get("message")
|
||||
&& let Some(content) = message
|
||||
.get("content")
|
||||
.and_then(|c| c.as_array())
|
||||
{
|
||||
parse_tool_results(content, &msg_tx);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if let Ok(json) = serde_json::from_str::<serde_json::Value>(trimmed) {
|
||||
process_json_event(
|
||||
&json,
|
||||
&token_tx,
|
||||
&activity_tx,
|
||||
&msg_tx,
|
||||
&mut sid_tx,
|
||||
);
|
||||
}
|
||||
}
|
||||
break;
|
||||
@@ -356,6 +347,17 @@ fn process_json_event(
|
||||
if let Some(message) = json.get("message")
|
||||
&& let Some(content) = message.get("content").and_then(|c| c.as_array())
|
||||
{
|
||||
// Fire activity signals for tool_use blocks as a fallback path.
|
||||
// The primary path is via stream_event → content_block_start (real-time),
|
||||
// but assistant events also carry tool_use blocks and serve as a reliable
|
||||
// backup if stream_event delivery is delayed or missed.
|
||||
for block in content {
|
||||
if block.get("type").and_then(|t| t.as_str()) == Some("tool_use")
|
||||
&& let Some(name) = block.get("name").and_then(|n| n.as_str())
|
||||
{
|
||||
let _ = activity_tx.send(name.to_string());
|
||||
}
|
||||
}
|
||||
parse_assistant_message(content, msg_tx);
|
||||
}
|
||||
false
|
||||
@@ -943,6 +945,105 @@ mod tests {
|
||||
assert_eq!(tokens, vec!["word"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_json_event_stream_event_tool_use_fires_activity() {
|
||||
// This is the primary activity path: stream_event wrapping content_block_start
|
||||
// with a tool_use block. Requires --include-partial-messages to be enabled.
|
||||
let (tok_tx, _tok_rx, act_tx, mut act_rx, msg_tx, _msg_rx) = make_channels();
|
||||
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
|
||||
let json = json!({
|
||||
"type": "stream_event",
|
||||
"session_id": "s1",
|
||||
"event": {
|
||||
"type": "content_block_start",
|
||||
"index": 1,
|
||||
"content_block": {"type": "tool_use", "id": "toolu_abc", "name": "Bash", "input": {}}
|
||||
}
|
||||
});
|
||||
assert!(!process_json_event(&json, &tok_tx, &act_tx, &msg_tx, &mut sid_tx));
|
||||
drop(act_tx);
|
||||
let activities: Vec<String> = {
|
||||
let mut v = vec![];
|
||||
while let Ok(a) = act_rx.try_recv() {
|
||||
v.push(a);
|
||||
}
|
||||
v
|
||||
};
|
||||
assert_eq!(activities, vec!["Bash"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_json_event_assistant_with_tool_use_fires_activity() {
|
||||
let (tok_tx, _tok_rx, act_tx, mut act_rx, msg_tx, _msg_rx) = make_channels();
|
||||
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
|
||||
let json = json!({
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"content": [
|
||||
{"type": "text", "text": "Let me read that file."},
|
||||
{"type": "tool_use", "id": "toolu_1", "name": "Read", "input": {"file_path": "/foo.rs"}}
|
||||
]
|
||||
}
|
||||
});
|
||||
assert!(!process_json_event(&json, &tok_tx, &act_tx, &msg_tx, &mut sid_tx));
|
||||
drop(act_tx);
|
||||
let activities: Vec<String> = {
|
||||
let mut v = vec![];
|
||||
while let Ok(a) = act_rx.try_recv() {
|
||||
v.push(a);
|
||||
}
|
||||
v
|
||||
};
|
||||
assert_eq!(activities, vec!["Read"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_json_event_assistant_with_multiple_tool_uses_fires_all_activities() {
|
||||
let (tok_tx, _tok_rx, act_tx, mut act_rx, msg_tx, _msg_rx) = make_channels();
|
||||
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
|
||||
let json = json!({
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"content": [
|
||||
{"type": "tool_use", "id": "id1", "name": "Glob", "input": {}},
|
||||
{"type": "tool_use", "id": "id2", "name": "Bash", "input": {}}
|
||||
]
|
||||
}
|
||||
});
|
||||
assert!(!process_json_event(&json, &tok_tx, &act_tx, &msg_tx, &mut sid_tx));
|
||||
drop(act_tx);
|
||||
let activities: Vec<String> = {
|
||||
let mut v = vec![];
|
||||
while let Ok(a) = act_rx.try_recv() {
|
||||
v.push(a);
|
||||
}
|
||||
v
|
||||
};
|
||||
assert_eq!(activities, vec!["Glob", "Bash"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_json_event_assistant_text_only_no_activity() {
|
||||
let (tok_tx, _tok_rx, act_tx, mut act_rx, msg_tx, _msg_rx) = make_channels();
|
||||
let mut sid_tx = None::<tokio::sync::oneshot::Sender<String>>;
|
||||
let json = json!({
|
||||
"type": "assistant",
|
||||
"message": {
|
||||
"content": [{"type": "text", "text": "Just text, no tools."}]
|
||||
}
|
||||
});
|
||||
assert!(!process_json_event(&json, &tok_tx, &act_tx, &msg_tx, &mut sid_tx));
|
||||
drop(act_tx);
|
||||
let activities: Vec<String> = {
|
||||
let mut v = vec![];
|
||||
while let Ok(a) = act_rx.try_recv() {
|
||||
v.push(a);
|
||||
}
|
||||
v
|
||||
};
|
||||
assert!(activities.is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn process_json_event_assistant_event_parses_message() {
|
||||
let (tok_tx, _tok_rx, act_tx, _act_rx, msg_tx, msg_rx) = make_channels();
|
||||
|
||||
Reference in New Issue
Block a user