mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
8fe0ecb045
## 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.
265 lines
8.6 KiB
Rust
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),
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|