1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
//! `AgentEvent::ToolCall` handler extracted from `run_interactive`.
//!
//! Records the structured tool-call entry (Interrupted until its
//! `ToolResult` lands), feeds the left-panel activity ring, closes any
//! stale chamber (passively — chamber turnover, not a denial), flushes the
//! coalesced response tokens, and paints the rounded chamber TOP border.
//! Behavior is identical to the inline code; pure refactor (dirge-4y4l).
use std::collections::VecDeque;
use std::time::Instant;
use crossterm::style::Color;
use serde_json::Value;
use crate::ui::agent_io::render_agent_stream;
use crate::ui::avatar;
use crate::ui::colors::{c_agent, c_tool};
use crate::ui::events::sanitize_output;
use crate::ui::panel_data::tool_call_label;
use crate::ui::run_handlers::RunCtx;
use crate::ui::tool_display::{close_tool_chamber_passive, format_tool_banner_value};
#[allow(clippy::too_many_arguments)]
pub(crate) fn handle_tool_call(
ctx: &mut RunCtx<'_>,
id: &str,
name: &str,
args: &Value,
was_reasoning: &mut bool,
last_token_render: &mut Option<Instant>,
tool_activity: &mut VecDeque<String>,
activity_cap: usize,
) -> anyhow::Result<()> {
// Feed the left-panel [ACTIVITY] ticker (newest last, bounded ring).
tool_activity.push_back(tool_call_label(name, args));
while tool_activity.len() > activity_cap {
tool_activity.pop_front();
}
// dirge-5h5: log entry state so the parallel-read race can be
// reconstructed offline.
tracing::trace!(
target: "dirge::ui::chamber",
event = "tool_call_in",
id = %id,
name = %name,
last_tool_call_id_before = ?ctx.last_tool_call_id,
tool_chamber_open_before = *ctx.tool_chamber_open,
chamber_top_start_before = ?ctx.chamber_top_start,
chamber_top_end_before = ?ctx.chamber_top_end,
buffer_len = ctx.renderer.buffer_len(),
"ToolCall handler entry"
);
*was_reasoning = false;
// Phase 3: persist as structured entry. Start in Interrupted state so
// that if the user aborts before the result arrives, the saved session
// captures the right state. The matching `ToolResult` flips it to
// Completed.
ctx.tool_calls_buf.push(crate::session::ToolCallEntry {
id: id.to_string(),
name: name.to_string(),
args: args.clone(),
state: crate::session::ToolCallState::Interrupted,
});
// Track for the abort-trailer warning: when the user later hits Ctrl+C /
// Esc, the saved partial reply notes how many tool calls ran (and didn't
// have their results preserved in the message text).
*ctx.tool_calls_this_run = ctx.tool_calls_this_run.saturating_add(1);
ctx.renderer
.set_avatar_state(avatar::AvatarState::from_tool_name(name));
#[cfg(feature = "experimental-ui-terminal-tab")]
ctx.renderer.set_last_tool_name(name);
// If a previous tool's chamber never closed (errored without a
// ToolResult, etc.), close it before opening the new one. Use PASSIVE
// close, not abort: a new ToolCall arriving over a stale chamber is
// chamber turnover, not a denial event — painting "⚠ tool denied" would
// falsely brand a healthy tool call as refused.
close_tool_chamber_passive(
ctx.renderer,
ctx.last_tool_name,
ctx.tool_chamber_open,
ctx.chamber_top_start,
ctx.chamber_top_end,
)?;
*ctx.last_tool_name = Some(name.to_string());
*ctx.last_tool_call_id = Some(id.to_string());
// dirge-ufe0: flush any trailing token the render coalescer skipped (a
// ToolCall queued behind the final tokens leaves them
// caught-up-but-unpainted) before response_buf is cleared, so the
// streamed text is on-screen above the tool chamber.
if !ctx.response_buf.is_empty() {
render_agent_stream(
ctx.response_buf,
ctx.response_start_line,
c_agent(),
ctx.renderer,
)?;
}
*last_token_render = None;
if *ctx.agent_line_started {
ctx.renderer.write_line("", Color::White)?;
*ctx.agent_line_started = false;
}
ctx.response_buf.clear();
*ctx.response_start_line = None;
// Stash the thinking before the tool chamber takes over so Ctrl+O can
// still expand it.
ctx.end_reasoning();
*ctx.reasoning_start_line = None;
// Tool-call line: rounded chamber TOP border with the tool name on it.
// Output lines below get `│ ` chamber rows; closed by `╰────╯` after the
// ToolResult.
let upper = name.to_ascii_uppercase();
// Record the buffer position BEFORE the spacer + header — used by
// passive close to drop the chamber entirely if no body content follows
// (parallel tool calls).
*ctx.chamber_top_start = Some(ctx.renderer.buffer_len());
// Blank line BEFORE the chamber top so the eye has an anchor between
// dense prior output and the new tool chamber.
ctx.renderer.write_line("", Color::White)?;
let raw_value = format_tool_banner_value(name, args);
let raw_value = sanitize_output(&raw_value).into_string();
// dirge-ghpf: chamber TOP as a reflowing block so it re-boxes on resize
// in lockstep with the body below (was a fixed-width raw line).
ctx.renderer.write_chamber_top(upper, raw_value, c_tool())?;
*ctx.chamber_top_end = Some(ctx.renderer.buffer_len());
*ctx.tool_chamber_open = true;
tracing::trace!(
target: "dirge::ui::chamber",
event = "tool_call_painted",
id = %id,
name = %name,
chamber_top_start_after = ?ctx.chamber_top_start,
chamber_top_end_after = ?ctx.chamber_top_end,
buffer_len = ctx.renderer.buffer_len(),
"ToolCall TOP painted"
);
// Note: on-tool-start fires from HookedToolDyn now, around the actual
// tool invocation — the UI no longer dispatches it here.
// ── Editor follow-along (best-effort, opt-in) ─────────────────
if let Some(template) = &ctx.cfg.editor_open_command
&& !template.trim().is_empty()
&& let Some((path, line)) = crate::ui::editor_follow::file_target_for_tool(name, args)
{
let cwd = ctx.session.working_dir.as_str();
let abs_path = crate::permission::checker::resolve_absolute(&path, cwd);
let target = (abs_path.clone(), line);
if *ctx.last_editor_follow != Some(target.clone()) {
let argv = crate::ui::editor_follow::build_editor_open_argv(template, &abs_path, line);
crate::ui::editor_follow::spawn_editor_follow(&argv);
*ctx.last_editor_follow = Some(target);
}
}
Ok(())
}