Files
codex/codex-rs/tui/src/chatwidget/tool_lifecycle.rs
T
Eric Traut 8fe0ecb045 Refactor chatwidget protocol flows into modules (phase 3) (#22433)
## Why

`chatwidget.rs` is still carrying too many unrelated responsibilities in
one file. #22269 started a five-phase cleanup to move coherent behavior
domains into focused modules while keeping `chatwidget.rs` as the
composition layer. #22407 completed phase 2 by extracting input and
submission flow.

This PR is phase 3. It keeps moving high-churn event handling out of the
central widget by extracting protocol, replay, streaming, and tool
lifecycle handling without changing the visible behavior those flows
already provide. This is once again just a mechanical movement of
existing functions. No functional changes.

## What Changed

- Added focused modules for protocol request dispatch, replay rendering,
assistant/plan/reasoning streaming, turn runtime bookkeeping, hook
lifecycle handling, command lifecycle handling, tool lifecycle
rendering, and interactive tool request prompts.
- Kept active-cell grouping, transcript invalidation, interrupt
deferral, and final-message separator behavior in the same flows, just
moved into smaller files.
- Added module header comments to the new files so the ownership
boundaries are explicit.
- Left `codex-rs/tui/src/chatwidget.rs` as the registration and
orchestration surface for these extracted behaviors.

## Cleanup Phases

The five-phase cleanup plan from #22269 is:

1. Phase 1: mechanical helper and state moves. Completed in #22269.
2. Phase 2: extract input and submission flow, including queued user
messages, shell prompt submission, pending steer restoration, and thread
input snapshot/restore behavior. Completed in #22407.
3. Phase 3: extract protocol, replay, streaming, and tool lifecycle
handling, while preserving active-cell grouping, transcript
invalidation, interrupt deferral, and final-message separator behavior.
This PR.
4. Phase 4: extract settings, popups, and status surfaces, including
model/reasoning/collaboration/personality popups, permission prompts,
rate-limit UI, and connectors helpers.
5. Phase 5: clean up the remaining constructor and orchestration code
once the larger behavior domains have moved out, leaving `chatwidget.rs`
as the composition layer.
2026-05-13 08:52:56 -07:00

265 lines
8.6 KiB
Rust

//! Non-command tool lifecycle rendering for `ChatWidget`.
//!
//! This module handles patch, MCP, web search, image, and collaborator tool
//! events as transcript cells.
use super::*;
impl ChatWidget {
pub(super) fn on_patch_apply_begin(&mut self, changes: HashMap<PathBuf, FileChange>) {
self.add_to_history(history_cell::new_patch_event(changes, &self.config.cwd));
}
pub(super) fn on_view_image_tool_call(&mut self, path: AbsolutePathBuf) {
self.flush_answer_stream_with_separator();
self.add_to_history(history_cell::new_view_image_tool_call(
path,
&self.config.cwd,
));
self.request_redraw();
}
pub(super) fn on_image_generation_begin(&mut self) {
self.flush_answer_stream_with_separator();
}
pub(super) fn on_image_generation_end(
&mut self,
call_id: String,
revised_prompt: Option<String>,
saved_path: Option<AbsolutePathBuf>,
) {
self.flush_answer_stream_with_separator();
self.add_to_history(history_cell::new_image_generation_call(
call_id,
revised_prompt,
saved_path,
));
self.request_redraw();
}
pub(super) fn on_file_change_completed(&mut self, item: ThreadItem) {
let item2 = item.clone();
self.defer_or_handle(
|q| q.push_item_completed(item),
|s| s.handle_file_change_completed_now(item2),
);
}
pub(super) fn on_mcp_tool_call_started(&mut self, item: ThreadItem) {
let item2 = item.clone();
self.defer_or_handle(
|q| q.push_item_started(item),
|s| s.handle_mcp_tool_call_started_now(item2),
);
}
pub(super) fn on_mcp_tool_call_completed(&mut self, item: ThreadItem) {
let item2 = item.clone();
self.defer_or_handle(
|q| q.push_item_completed(item),
|s| s.handle_mcp_tool_call_completed_now(item2),
);
}
pub(super) fn on_web_search_begin(&mut self, call_id: String) {
self.flush_answer_stream_with_separator();
self.flush_active_cell();
self.transcript.active_cell = Some(Box::new(history_cell::new_active_web_search_call(
call_id,
String::new(),
self.config.animations,
)));
self.bump_active_cell_revision();
self.request_redraw();
}
pub(super) fn on_web_search_end(
&mut self,
call_id: String,
query: String,
action: codex_app_server_protocol::WebSearchAction,
) {
self.flush_answer_stream_with_separator();
let mut handled = false;
if let Some(cell) = self
.transcript
.active_cell
.as_mut()
.and_then(|cell| cell.as_any_mut().downcast_mut::<WebSearchCell>())
&& cell.call_id() == call_id
{
cell.update(action.clone(), query.clone());
cell.complete();
self.bump_active_cell_revision();
self.flush_active_cell();
handled = true;
}
if !handled {
self.add_to_history(history_cell::new_web_search_call(call_id, query, action));
}
self.transcript.had_work_activity = true;
}
pub(super) fn on_collab_event(&mut self, cell: PlainHistoryCell) {
self.flush_answer_stream_with_separator();
self.add_to_history(cell);
self.request_redraw();
}
pub(super) fn on_collab_agent_tool_call(&mut self, item: ThreadItem) {
let ThreadItem::CollabAgentToolCall {
id, tool, status, ..
} = &item
else {
return;
};
if matches!(tool, CollabAgentTool::SpawnAgent)
&& let Some(spawn_request) = multi_agents::spawn_request_summary(&item)
{
self.pending_collab_spawn_requests
.insert(id.clone(), spawn_request);
}
let cached_spawn_request = if matches!(tool, CollabAgentTool::SpawnAgent)
&& !matches!(status, CollabAgentToolCallStatus::InProgress)
{
self.pending_collab_spawn_requests.remove(id)
} else {
None
};
if let Some(cell) = multi_agents::tool_call_history_cell(
&item,
cached_spawn_request.as_ref(),
|thread_id| self.collab_agent_metadata(thread_id),
) {
self.on_collab_event(cell);
}
}
pub(crate) fn handle_file_change_completed_now(&mut self, item: ThreadItem) {
let ThreadItem::FileChange { status, .. } = item else {
return;
};
// If the patch was successful, just let the "Edited" block stand.
// Otherwise, add a failure block.
if matches!(status, codex_app_server_protocol::PatchApplyStatus::Failed) {
self.add_to_history(history_cell::new_patch_apply_failure(String::new()));
}
// Mark that actual work was done (patch applied)
self.transcript.had_work_activity = true;
}
pub(crate) fn handle_mcp_tool_call_started_now(&mut self, item: ThreadItem) {
let ThreadItem::McpToolCall {
id,
server,
tool,
arguments,
..
} = item
else {
return;
};
self.flush_answer_stream_with_separator();
self.flush_active_cell();
self.transcript.active_cell = Some(Box::new(history_cell::new_active_mcp_tool_call(
id,
McpInvocation {
server,
tool,
arguments: Some(arguments),
},
self.config.animations,
)));
self.bump_active_cell_revision();
self.request_redraw();
}
pub(crate) fn handle_mcp_tool_call_completed_now(&mut self, item: ThreadItem) {
self.flush_answer_stream_with_separator();
let ThreadItem::McpToolCall {
id,
server,
tool,
arguments,
result,
error,
duration_ms,
..
} = item
else {
return;
};
let invocation = McpInvocation {
server,
tool,
arguments: Some(arguments),
};
let duration = Duration::from_millis(duration_ms.unwrap_or_default().max(0) as u64);
let result = match (result, error) {
(_, Some(error)) => Err(error.message),
(Some(result), None) => {
let result = *result;
Ok(codex_protocol::mcp::CallToolResult {
content: result.content,
structured_content: result.structured_content,
is_error: Some(false),
meta: None,
})
}
(None, None) => Err("MCP tool call completed without a result".to_string()),
};
let extra_cell = match self
.transcript
.active_cell
.as_mut()
.and_then(|cell| cell.as_any_mut().downcast_mut::<McpToolCallCell>())
{
Some(cell) if cell.call_id() == id => cell.complete(duration, result),
_ => {
self.flush_active_cell();
let mut cell =
history_cell::new_active_mcp_tool_call(id, invocation, self.config.animations);
let extra_cell = cell.complete(duration, result);
self.transcript.active_cell = Some(Box::new(cell));
extra_cell
}
};
self.flush_active_cell();
if let Some(extra) = extra_cell {
self.add_boxed_history(extra);
}
// Mark that actual work was done (MCP tool call)
self.transcript.had_work_activity = true;
}
pub(crate) fn handle_queued_item_started_now(&mut self, item: ThreadItem) {
match item {
item @ ThreadItem::CommandExecution { .. } => {
self.handle_command_execution_started_now(item);
}
item @ ThreadItem::McpToolCall { .. } => {
self.handle_mcp_tool_call_started_now(item);
}
_ => {}
}
}
pub(crate) fn handle_queued_item_completed_now(&mut self, item: ThreadItem) {
match item {
item @ ThreadItem::CommandExecution { .. } => {
self.handle_command_execution_completed_now(item);
}
item @ ThreadItem::FileChange { .. } => self.handle_file_change_completed_now(item),
item @ ThreadItem::McpToolCall { .. } => self.handle_mcp_tool_call_completed_now(item),
_ => {}
}
}
}