mirror of
https://github.com/openai/codex.git
synced 2026-10-23 18:08:45 +00:00
Simplify TUI session resume and internal event types
This commit is contained in:
@@ -31,7 +31,6 @@ use codex_state::state_db_path;
|
||||
use codex_tui::AppExitInfo;
|
||||
use codex_tui::Cli as TuiCli;
|
||||
use codex_tui::ExitReason;
|
||||
use codex_tui::FinalOutput;
|
||||
use codex_tui::UpdateAction;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
@@ -535,7 +534,7 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
|
||||
|
||||
let mut lines = Vec::new();
|
||||
if !token_usage.is_zero() {
|
||||
lines.push(FinalOutput::from(token_usage).to_string());
|
||||
lines.push(token_usage.to_string());
|
||||
}
|
||||
|
||||
if let Some(resume_cmd) =
|
||||
|
||||
@@ -71,7 +71,6 @@ use crate::test_support::PathBufExt;
|
||||
use crate::test_support::test_path_buf;
|
||||
#[cfg(test)]
|
||||
use crate::test_support::test_path_display;
|
||||
use crate::token_usage::FinalOutput;
|
||||
use crate::token_usage::TokenUsage;
|
||||
use crate::transcript_reflow::TranscriptReflowState;
|
||||
use crate::tui;
|
||||
@@ -377,7 +376,7 @@ fn session_summary(
|
||||
thread_name: Option<String>,
|
||||
rollout_path: Option<&Path>,
|
||||
) -> Option<SessionSummary> {
|
||||
let usage_line = (!token_usage.is_zero()).then(|| FinalOutput::from(token_usage).to_string());
|
||||
let usage_line = (!token_usage.is_zero()).then(|| token_usage.to_string());
|
||||
let thread_id =
|
||||
resumable_thread(thread_id, thread_name, rollout_path).map(|thread| thread.thread_id);
|
||||
let resume_command =
|
||||
|
||||
+72
-194
@@ -57,6 +57,7 @@ use crate::bottom_pane::StatusSurfacePreviewData;
|
||||
use crate::bottom_pane::StatusSurfacePreviewItem;
|
||||
use crate::bottom_pane::TerminalTitleItem;
|
||||
use crate::bottom_pane::TerminalTitleSetupView;
|
||||
use crate::diff_model::FileChange;
|
||||
use crate::legacy_core::DEFAULT_AGENTS_MD_FILENAME;
|
||||
use crate::legacy_core::config::Config;
|
||||
use crate::legacy_core::config::Constrained;
|
||||
@@ -82,23 +83,6 @@ use crate::terminal_title::set_terminal_title;
|
||||
use crate::text_formatting::proper_join;
|
||||
use crate::token_usage::TokenUsage;
|
||||
use crate::token_usage::TokenUsageInfo;
|
||||
use crate::tool_activity::ExecCommandBeginEvent;
|
||||
use crate::tool_activity::ExecCommandEndEvent;
|
||||
use crate::tool_activity::ExecCommandOutputDeltaEvent;
|
||||
use crate::tool_activity::HookCompletedEvent;
|
||||
use crate::tool_activity::HookStartedEvent;
|
||||
use crate::tool_activity::ImageGenerationBeginEvent;
|
||||
use crate::tool_activity::ImageGenerationEndEvent;
|
||||
use crate::tool_activity::McpInvocation;
|
||||
use crate::tool_activity::McpToolCallBeginEvent;
|
||||
use crate::tool_activity::McpToolCallEndEvent;
|
||||
use crate::tool_activity::PatchApplyBeginEvent;
|
||||
use crate::tool_activity::PatchApplyEndEvent;
|
||||
use crate::tool_activity::TerminalInteractionEvent;
|
||||
use crate::tool_activity::ViewImageToolCallEvent;
|
||||
use crate::tool_activity::WebSearchBeginEvent;
|
||||
use crate::tool_activity::WebSearchEndEvent;
|
||||
use crate::turn_state::TurnAbortReason;
|
||||
use crate::version::CODEX_CLI_VERSION;
|
||||
use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
use codex_app_server_protocol::AddCreditsNudgeEmailStatus;
|
||||
@@ -310,6 +294,7 @@ use crate::get_git_diff::get_git_diff;
|
||||
use crate::history_cell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::HookCell;
|
||||
use crate::history_cell::McpInvocation;
|
||||
use crate::history_cell::McpToolCallCell;
|
||||
use crate::history_cell::PlainHistoryCell;
|
||||
use crate::history_cell::WebSearchCell;
|
||||
@@ -336,7 +321,12 @@ use self::goal_status::GoalStatusState;
|
||||
use self::goal_status::goal_status_indicator_from_app_goal;
|
||||
mod goal_menu;
|
||||
mod interrupts;
|
||||
use self::interrupts::ExecCommandBeginEvent;
|
||||
use self::interrupts::ExecCommandEndEvent;
|
||||
use self::interrupts::InterruptManager;
|
||||
use self::interrupts::McpToolCallBeginEvent;
|
||||
use self::interrupts::McpToolCallEndEvent;
|
||||
use self::interrupts::PatchApplyEndEvent;
|
||||
mod keymap_picker;
|
||||
mod mcp_startup;
|
||||
use self::mcp_startup::McpStartupStatus;
|
||||
@@ -1532,6 +1522,12 @@ enum PlanModeNudgeScope {
|
||||
Thread(ThreadId),
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, Eq, PartialEq)]
|
||||
pub(crate) enum TurnAbortReason {
|
||||
Interrupted,
|
||||
BudgetLimited,
|
||||
}
|
||||
|
||||
/// Returns whether `text` contains the standalone word `plan`.
|
||||
///
|
||||
/// This intentionally mirrors the App suggestion heuristic instead of trying to infer broader
|
||||
@@ -1561,58 +1557,6 @@ impl ThreadItemRenderSource {
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_output_entry_from_notification(
|
||||
entry: codex_app_server_protocol::HookOutputEntry,
|
||||
) -> codex_app_server_protocol::HookOutputEntry {
|
||||
codex_app_server_protocol::HookOutputEntry {
|
||||
kind: entry.kind,
|
||||
text: entry.text,
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_run_summary_from_notification(
|
||||
run: codex_app_server_protocol::HookRunSummary,
|
||||
) -> codex_app_server_protocol::HookRunSummary {
|
||||
codex_app_server_protocol::HookRunSummary {
|
||||
id: run.id,
|
||||
event_name: run.event_name,
|
||||
handler_type: run.handler_type,
|
||||
execution_mode: run.execution_mode,
|
||||
scope: run.scope,
|
||||
source_path: run.source_path,
|
||||
source: run.source,
|
||||
display_order: run.display_order,
|
||||
status: run.status,
|
||||
status_message: run.status_message,
|
||||
started_at: run.started_at,
|
||||
completed_at: run.completed_at,
|
||||
duration_ms: run.duration_ms,
|
||||
entries: run
|
||||
.entries
|
||||
.into_iter()
|
||||
.map(hook_output_entry_from_notification)
|
||||
.collect(),
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_started_event_from_notification(
|
||||
notification: codex_app_server_protocol::HookStartedNotification,
|
||||
) -> HookStartedEvent {
|
||||
HookStartedEvent {
|
||||
turn_id: notification.turn_id,
|
||||
run: hook_run_summary_from_notification(notification.run),
|
||||
}
|
||||
}
|
||||
|
||||
fn hook_completed_event_from_notification(
|
||||
notification: codex_app_server_protocol::HookCompletedNotification,
|
||||
) -> HookCompletedEvent {
|
||||
HookCompletedEvent {
|
||||
turn_id: notification.turn_id,
|
||||
run: hook_run_summary_from_notification(notification.run),
|
||||
}
|
||||
}
|
||||
|
||||
fn app_server_request_id_to_mcp_request_id(
|
||||
request_id: &codex_app_server_protocol::RequestId,
|
||||
) -> codex_protocol::mcp::RequestId {
|
||||
@@ -3728,8 +3672,8 @@ impl ChatWidget {
|
||||
self.defer_or_handle(|q| q.push_exec_begin(ev), |s| s.handle_exec_begin_now(ev2));
|
||||
}
|
||||
|
||||
fn on_exec_command_output_delta(&mut self, ev: ExecCommandOutputDeltaEvent) {
|
||||
self.track_unified_exec_output_chunk(&ev.call_id, &ev.chunk);
|
||||
fn on_exec_command_output_delta(&mut self, call_id: &str, delta: &str) {
|
||||
self.track_unified_exec_output_chunk(call_id, delta.as_bytes());
|
||||
if !self.bottom_pane.is_task_running() {
|
||||
return;
|
||||
}
|
||||
@@ -3742,13 +3686,13 @@ impl ChatWidget {
|
||||
return;
|
||||
};
|
||||
|
||||
if cell.append_output(&ev.call_id, std::str::from_utf8(&ev.chunk).unwrap_or("")) {
|
||||
if cell.append_output(call_id, delta) {
|
||||
self.bump_active_cell_revision();
|
||||
self.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
fn on_terminal_interaction(&mut self, ev: TerminalInteractionEvent) {
|
||||
fn on_terminal_interaction(&mut self, process_id: String, stdin: String) {
|
||||
if !self.bottom_pane.is_task_running() {
|
||||
return;
|
||||
}
|
||||
@@ -3756,9 +3700,9 @@ impl ChatWidget {
|
||||
let command_display = self
|
||||
.unified_exec_processes
|
||||
.iter()
|
||||
.find(|process| process.key == ev.process_id)
|
||||
.find(|process| process.key == process_id)
|
||||
.map(|process| process.command_display.clone());
|
||||
if ev.stdin.is_empty() {
|
||||
if stdin.is_empty() {
|
||||
// Empty stdin means we are polling for background output.
|
||||
// Surface this in the status indicator (single "waiting" surface) instead of
|
||||
// the transcript. Keep the header short so the interrupt hint remains visible.
|
||||
@@ -3773,17 +3717,17 @@ impl ChatWidget {
|
||||
/*details_max_lines*/ 1,
|
||||
);
|
||||
match &mut self.unified_exec_wait_streak {
|
||||
Some(wait) if wait.process_id == ev.process_id => {
|
||||
Some(wait) if wait.process_id == process_id => {
|
||||
wait.update_command_display(command_display);
|
||||
}
|
||||
Some(_) => {
|
||||
self.flush_unified_exec_wait_streak();
|
||||
self.unified_exec_wait_streak =
|
||||
Some(UnifiedExecWaitStreak::new(ev.process_id, command_display));
|
||||
Some(UnifiedExecWaitStreak::new(process_id, command_display));
|
||||
}
|
||||
None => {
|
||||
self.unified_exec_wait_streak =
|
||||
Some(UnifiedExecWaitStreak::new(ev.process_id, command_display));
|
||||
Some(UnifiedExecWaitStreak::new(process_id, command_display));
|
||||
}
|
||||
}
|
||||
self.request_redraw();
|
||||
@@ -3791,43 +3735,45 @@ impl ChatWidget {
|
||||
if self
|
||||
.unified_exec_wait_streak
|
||||
.as_ref()
|
||||
.is_some_and(|wait| wait.process_id == ev.process_id)
|
||||
.is_some_and(|wait| wait.process_id == process_id)
|
||||
{
|
||||
self.flush_unified_exec_wait_streak();
|
||||
}
|
||||
self.add_to_history(history_cell::new_unified_exec_interaction(
|
||||
command_display,
|
||||
ev.stdin,
|
||||
stdin,
|
||||
));
|
||||
}
|
||||
}
|
||||
|
||||
fn on_patch_apply_begin(&mut self, event: PatchApplyBeginEvent) {
|
||||
self.add_to_history(history_cell::new_patch_event(
|
||||
event.changes,
|
||||
&self.config.cwd,
|
||||
));
|
||||
fn on_patch_apply_begin(&mut self, changes: HashMap<PathBuf, FileChange>) {
|
||||
self.add_to_history(history_cell::new_patch_event(changes, &self.config.cwd));
|
||||
}
|
||||
|
||||
fn on_view_image_tool_call(&mut self, event: ViewImageToolCallEvent) {
|
||||
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(
|
||||
event.path,
|
||||
path,
|
||||
&self.config.cwd,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_image_generation_begin(&mut self, _event: ImageGenerationBeginEvent) {
|
||||
fn on_image_generation_begin(&mut self) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
}
|
||||
|
||||
fn on_image_generation_end(&mut self, event: ImageGenerationEndEvent) {
|
||||
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(
|
||||
event.call_id,
|
||||
event.revised_prompt,
|
||||
event.saved_path,
|
||||
call_id,
|
||||
revised_prompt,
|
||||
saved_path,
|
||||
));
|
||||
self.request_redraw();
|
||||
}
|
||||
@@ -3939,11 +3885,11 @@ impl ChatWidget {
|
||||
self.defer_or_handle(|q| q.push_mcp_end(ev), |s| s.handle_mcp_end_now(ev2));
|
||||
}
|
||||
|
||||
fn on_web_search_begin(&mut self, ev: WebSearchBeginEvent) {
|
||||
fn on_web_search_begin(&mut self, call_id: String) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_active_cell();
|
||||
self.active_cell = Some(Box::new(history_cell::new_active_web_search_call(
|
||||
ev.call_id,
|
||||
call_id,
|
||||
String::new(),
|
||||
self.config.animations,
|
||||
)));
|
||||
@@ -3951,13 +3897,13 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_web_search_end(&mut self, ev: WebSearchEndEvent) {
|
||||
fn on_web_search_end(
|
||||
&mut self,
|
||||
call_id: String,
|
||||
query: String,
|
||||
action: codex_protocol::models::WebSearchAction,
|
||||
) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
let WebSearchEndEvent {
|
||||
call_id,
|
||||
query,
|
||||
action,
|
||||
} = ev;
|
||||
let mut handled = false;
|
||||
if let Some(cell) = self
|
||||
.active_cell
|
||||
@@ -4047,17 +3993,17 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_hook_started(&mut self, event: HookStartedEvent) {
|
||||
fn on_hook_started(&mut self, run: codex_app_server_protocol::HookRunSummary) {
|
||||
self.flush_answer_stream_with_separator();
|
||||
self.flush_completed_hook_output();
|
||||
match self.active_hook_cell.as_mut() {
|
||||
Some(cell) => {
|
||||
cell.start_run(event.run);
|
||||
cell.start_run(run);
|
||||
self.bump_active_cell_revision();
|
||||
}
|
||||
None => {
|
||||
self.active_hook_cell = Some(history_cell::new_active_hook_cell(
|
||||
event.run,
|
||||
run,
|
||||
self.config.animations,
|
||||
));
|
||||
self.bump_active_cell_revision();
|
||||
@@ -4066,8 +4012,7 @@ impl ChatWidget {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
fn on_hook_completed(&mut self, event: HookCompletedEvent) {
|
||||
let completed = event.run;
|
||||
fn on_hook_completed(&mut self, completed: codex_app_server_protocol::HookRunSummary) {
|
||||
let completed_existing_run = self
|
||||
.active_hook_cell
|
||||
.as_mut()
|
||||
@@ -4667,7 +4612,6 @@ impl ChatWidget {
|
||||
invocation,
|
||||
duration,
|
||||
result,
|
||||
..
|
||||
} = ev;
|
||||
|
||||
let extra_cell = match self
|
||||
@@ -5981,7 +5925,7 @@ impl ChatWidget {
|
||||
ThreadItem::CommandExecution {
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
cwd: _,
|
||||
process_id,
|
||||
source,
|
||||
status,
|
||||
@@ -5997,9 +5941,7 @@ impl ChatWidget {
|
||||
self.on_exec_command_begin(ExecCommandBeginEvent {
|
||||
call_id: id,
|
||||
process_id,
|
||||
turn_id: turn_id.clone(),
|
||||
command: split_command_string(&command),
|
||||
cwd,
|
||||
parsed_cmd: command_actions
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
@@ -6012,43 +5954,25 @@ impl ChatWidget {
|
||||
self.on_exec_command_end(ExecCommandEndEvent {
|
||||
call_id: id,
|
||||
process_id,
|
||||
turn_id: turn_id.clone(),
|
||||
command: split_command_string(&command),
|
||||
cwd,
|
||||
parsed_cmd: command_actions
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: aggregated_output.clone(),
|
||||
exit_code: exit_code.unwrap_or_default(),
|
||||
duration: Duration::from_millis(
|
||||
duration_ms.unwrap_or_default().max(0) as u64
|
||||
),
|
||||
formatted_output: aggregated_output,
|
||||
status: match status {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Completed => {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Completed
|
||||
}
|
||||
codex_app_server_protocol::CommandExecutionStatus::Failed => {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Failed
|
||||
}
|
||||
codex_app_server_protocol::CommandExecutionStatus::Declined => {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Declined
|
||||
}
|
||||
codex_app_server_protocol::CommandExecutionStatus::InProgress => {
|
||||
codex_app_server_protocol::CommandExecutionStatus::Failed
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
ThreadItem::FileChange {
|
||||
id,
|
||||
changes,
|
||||
id: _,
|
||||
changes: _,
|
||||
status,
|
||||
} => {
|
||||
if !matches!(
|
||||
@@ -6056,29 +5980,11 @@ impl ChatWidget {
|
||||
codex_app_server_protocol::PatchApplyStatus::InProgress
|
||||
) {
|
||||
self.on_patch_apply_end(PatchApplyEndEvent {
|
||||
call_id: id,
|
||||
turn_id: turn_id.clone(),
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
success: !matches!(
|
||||
status,
|
||||
codex_app_server_protocol::PatchApplyStatus::Failed
|
||||
),
|
||||
changes: file_update_changes_to_display(changes),
|
||||
status: match status {
|
||||
codex_app_server_protocol::PatchApplyStatus::Completed => {
|
||||
codex_app_server_protocol::PatchApplyStatus::Completed
|
||||
}
|
||||
codex_app_server_protocol::PatchApplyStatus::Failed => {
|
||||
codex_app_server_protocol::PatchApplyStatus::Failed
|
||||
}
|
||||
codex_app_server_protocol::PatchApplyStatus::Declined => {
|
||||
codex_app_server_protocol::PatchApplyStatus::Declined
|
||||
}
|
||||
codex_app_server_protocol::PatchApplyStatus::InProgress => {
|
||||
codex_app_server_protocol::PatchApplyStatus::Failed
|
||||
}
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -6087,7 +5993,6 @@ impl ChatWidget {
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
mcp_app_resource_uri,
|
||||
result,
|
||||
error,
|
||||
duration_ms,
|
||||
@@ -6100,7 +6005,6 @@ impl ChatWidget {
|
||||
tool,
|
||||
arguments: Some(arguments),
|
||||
},
|
||||
mcp_app_resource_uri,
|
||||
duration: Duration::from_millis(duration_ms.unwrap_or_default().max(0) as u64),
|
||||
result: match (result, error) {
|
||||
(_, Some(error)) => Err(error.message),
|
||||
@@ -6118,34 +6022,25 @@ impl ChatWidget {
|
||||
});
|
||||
}
|
||||
ThreadItem::WebSearch { id, query, action } => {
|
||||
self.on_web_search_begin(WebSearchBeginEvent {
|
||||
call_id: id.clone(),
|
||||
});
|
||||
self.on_web_search_end(WebSearchEndEvent {
|
||||
call_id: id,
|
||||
self.on_web_search_begin(id.clone());
|
||||
self.on_web_search_end(
|
||||
id,
|
||||
query,
|
||||
action: action
|
||||
action
|
||||
.map(web_search_action_to_core)
|
||||
.unwrap_or(codex_protocol::models::WebSearchAction::Other),
|
||||
});
|
||||
);
|
||||
}
|
||||
ThreadItem::ImageView { id, path } => {
|
||||
self.on_view_image_tool_call(ViewImageToolCallEvent { call_id: id, path });
|
||||
ThreadItem::ImageView { id: _, path } => {
|
||||
self.on_view_image_tool_call(path);
|
||||
}
|
||||
ThreadItem::ImageGeneration {
|
||||
id,
|
||||
status,
|
||||
revised_prompt,
|
||||
result,
|
||||
saved_path,
|
||||
..
|
||||
} => {
|
||||
self.on_image_generation_end(ImageGenerationEndEvent {
|
||||
call_id: id,
|
||||
result,
|
||||
revised_prompt,
|
||||
status,
|
||||
saved_path,
|
||||
});
|
||||
self.on_image_generation_end(id, revised_prompt, saved_path);
|
||||
}
|
||||
ThreadItem::EnteredReviewMode { review, .. } => {
|
||||
if from_replay {
|
||||
@@ -6311,18 +6206,10 @@ impl ChatWidget {
|
||||
}
|
||||
ServerNotification::ReasoningSummaryPartAdded(_) => self.on_reasoning_section_break(),
|
||||
ServerNotification::TerminalInteraction(notification) => {
|
||||
self.on_terminal_interaction(TerminalInteractionEvent {
|
||||
call_id: notification.item_id,
|
||||
process_id: notification.process_id,
|
||||
stdin: notification.stdin,
|
||||
})
|
||||
self.on_terminal_interaction(notification.process_id, notification.stdin)
|
||||
}
|
||||
ServerNotification::CommandExecutionOutputDelta(notification) => {
|
||||
self.on_exec_command_output_delta(ExecCommandOutputDeltaEvent {
|
||||
call_id: notification.item_id,
|
||||
stream: codex_app_server_protocol::CommandExecOutputStream::Stdout,
|
||||
chunk: notification.delta.into_bytes(),
|
||||
});
|
||||
self.on_exec_command_output_delta(¬ification.item_id, ¬ification.delta);
|
||||
}
|
||||
ServerNotification::FileChangeOutputDelta(notification) => {
|
||||
self.on_patch_apply_output_delta(notification.item_id, notification.delta);
|
||||
@@ -6348,10 +6235,10 @@ impl ChatWidget {
|
||||
})
|
||||
}
|
||||
ServerNotification::HookStarted(notification) => {
|
||||
self.on_hook_started(hook_started_event_from_notification(notification));
|
||||
self.on_hook_started(notification.run);
|
||||
}
|
||||
ServerNotification::HookCompleted(notification) => {
|
||||
self.on_hook_completed(hook_completed_event_from_notification(notification));
|
||||
self.on_hook_completed(notification.run);
|
||||
}
|
||||
ServerNotification::Error(notification) => {
|
||||
if notification.will_retry {
|
||||
@@ -6568,7 +6455,7 @@ impl ChatWidget {
|
||||
ThreadItem::CommandExecution {
|
||||
id,
|
||||
command,
|
||||
cwd,
|
||||
cwd: _,
|
||||
process_id,
|
||||
source,
|
||||
command_actions,
|
||||
@@ -6577,9 +6464,7 @@ impl ChatWidget {
|
||||
self.on_exec_command_begin(ExecCommandBeginEvent {
|
||||
call_id: id,
|
||||
process_id,
|
||||
turn_id: notification.turn_id,
|
||||
command: split_command_string(&command),
|
||||
cwd,
|
||||
parsed_cmd: command_actions
|
||||
.into_iter()
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
@@ -6588,20 +6473,14 @@ impl ChatWidget {
|
||||
interaction_input: None,
|
||||
});
|
||||
}
|
||||
ThreadItem::FileChange { id, changes, .. } => {
|
||||
self.on_patch_apply_begin(PatchApplyBeginEvent {
|
||||
call_id: id,
|
||||
turn_id: notification.turn_id,
|
||||
auto_approved: false,
|
||||
changes: file_update_changes_to_display(changes),
|
||||
});
|
||||
ThreadItem::FileChange { id: _, changes, .. } => {
|
||||
self.on_patch_apply_begin(file_update_changes_to_display(changes));
|
||||
}
|
||||
ThreadItem::McpToolCall {
|
||||
id,
|
||||
server,
|
||||
tool,
|
||||
arguments,
|
||||
mcp_app_resource_uri,
|
||||
..
|
||||
} => {
|
||||
self.on_mcp_tool_call_begin(McpToolCallBeginEvent {
|
||||
@@ -6611,14 +6490,13 @@ impl ChatWidget {
|
||||
tool,
|
||||
arguments: Some(arguments),
|
||||
},
|
||||
mcp_app_resource_uri,
|
||||
});
|
||||
}
|
||||
ThreadItem::WebSearch { id, .. } => {
|
||||
self.on_web_search_begin(WebSearchBeginEvent { call_id: id });
|
||||
self.on_web_search_begin(id);
|
||||
}
|
||||
ThreadItem::ImageGeneration { id, .. } => {
|
||||
self.on_image_generation_begin(ImageGenerationBeginEvent { call_id: id });
|
||||
ThreadItem::ImageGeneration { .. } => {
|
||||
self.on_image_generation_begin();
|
||||
}
|
||||
ThreadItem::CollabAgentToolCall {
|
||||
id,
|
||||
|
||||
@@ -1,19 +1,65 @@
|
||||
//! Queue prompt overlays and deferred tool activity while another interrupt is visible.
|
||||
|
||||
use std::collections::VecDeque;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::app::app_server_requests::ResolvedAppServerRequest;
|
||||
use crate::approval_events::ApplyPatchApprovalRequestEvent;
|
||||
use crate::approval_events::ExecApprovalRequestEvent;
|
||||
use crate::tool_activity::ExecCommandBeginEvent;
|
||||
use crate::tool_activity::ExecCommandEndEvent;
|
||||
use crate::tool_activity::McpToolCallBeginEvent;
|
||||
use crate::tool_activity::McpToolCallEndEvent;
|
||||
use crate::tool_activity::PatchApplyEndEvent;
|
||||
use crate::history_cell::McpInvocation;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_protocol::approvals::ElicitationRequestEvent;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_protocol::request_permissions::RequestPermissionsEvent;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
|
||||
use super::ChatWidget;
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExecCommandBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) process_id: Option<String>,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed_cmd: Vec<ParsedCommand>,
|
||||
pub(crate) source: CommandExecutionSource,
|
||||
pub(crate) interaction_input: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct ExecCommandEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) process_id: Option<String>,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) parsed_cmd: Vec<ParsedCommand>,
|
||||
pub(crate) source: CommandExecutionSource,
|
||||
pub(crate) interaction_input: Option<String>,
|
||||
pub(crate) aggregated_output: String,
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) formatted_output: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct McpToolCallBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) invocation: McpInvocation,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct McpToolCallEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) invocation: McpInvocation,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) result: Result<CallToolResult, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct PatchApplyEndEvent {
|
||||
pub(crate) success: bool,
|
||||
pub(crate) stderr: String,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum QueuedInterrupt {
|
||||
ExecApproval(ExecApprovalRequestEvent),
|
||||
@@ -149,7 +195,6 @@ impl QueuedInterrupt {
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::approval_events::ExecApprovalRequestEvent;
|
||||
use crate::tool_activity::ExecCommandBeginEvent;
|
||||
use codex_app_server_protocol::CommandExecutionSource as ExecCommandSource;
|
||||
use codex_protocol::request_user_input::RequestUserInputEvent;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
@@ -185,9 +230,7 @@ mod tests {
|
||||
ExecCommandBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn".to_string(),
|
||||
command: vec!["true".to_string()],
|
||||
cwd: AbsolutePathBuf::current_dir().expect("current dir"),
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
|
||||
@@ -4,6 +4,8 @@
|
||||
//! snapshot-based so that layout regressions and status/header changes show up as stable,
|
||||
//! reviewable diffs.
|
||||
|
||||
pub(super) use super::interrupts::ExecCommandBeginEvent;
|
||||
pub(super) use super::interrupts::ExecCommandEndEvent;
|
||||
pub(super) use super::*;
|
||||
pub(super) use crate::app_command::AppCommand as Op;
|
||||
pub(super) use crate::app_event::AppEvent;
|
||||
@@ -30,13 +32,6 @@ pub(super) use crate::test_support::test_path_buf;
|
||||
pub(super) use crate::test_support::test_path_display;
|
||||
pub(super) use crate::token_usage::TokenUsage;
|
||||
pub(super) use crate::token_usage::TokenUsageInfo;
|
||||
pub(super) use crate::tool_activity::ExecCommandBeginEvent;
|
||||
pub(super) use crate::tool_activity::ExecCommandEndEvent;
|
||||
pub(super) use crate::tool_activity::ImageGenerationEndEvent;
|
||||
pub(super) use crate::tool_activity::PatchApplyBeginEvent;
|
||||
pub(super) use crate::tool_activity::PatchApplyEndEvent;
|
||||
pub(super) use crate::tool_activity::TerminalInteractionEvent;
|
||||
pub(super) use crate::tool_activity::ViewImageToolCallEvent;
|
||||
pub(super) use crate::tui::FrameRequester;
|
||||
pub(super) use assert_matches::assert_matches;
|
||||
pub(super) use codex_app_server_protocol::AddCreditsNudgeCreditType;
|
||||
|
||||
@@ -348,25 +348,19 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
"echo orphaned".to_string(),
|
||||
];
|
||||
let parsed_cmd = codex_shell_command::parse_command::parse_command(&command);
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
handle_exec_end(
|
||||
&mut chat,
|
||||
ExecCommandEndEvent {
|
||||
call_id: "call-orphan".to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
stdout: "done".to_string(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: "done".to_string(),
|
||||
exit_code: 0,
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: "done".to_string(),
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
},
|
||||
);
|
||||
|
||||
@@ -609,10 +603,7 @@ async fn unified_exec_interaction_after_task_complete_is_suppressed() {
|
||||
/*last_agent_message*/ None, /*duration_ms*/ None, /*from_replay*/ false,
|
||||
);
|
||||
|
||||
handle_terminal_interaction(
|
||||
&mut chat,
|
||||
terminal_interaction_event("call-1", "proc-1", "ls\n"),
|
||||
);
|
||||
terminal_interaction(&mut chat, "call-1", "proc-1", "ls\n");
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
@@ -708,11 +699,7 @@ async fn unified_exec_wait_status_header_updates_on_late_command_display() {
|
||||
recent_chunks: Vec::new(),
|
||||
});
|
||||
|
||||
chat.on_terminal_interaction(TerminalInteractionEvent {
|
||||
call_id: "call-1".to_string(),
|
||||
process_id: "proc-1".to_string(),
|
||||
stdin: String::new(),
|
||||
});
|
||||
terminal_interaction(&mut chat, "call-1", "proc-1", "");
|
||||
|
||||
assert!(chat.active_cell.is_none());
|
||||
assert_eq!(
|
||||
@@ -841,13 +828,7 @@ async fn view_image_tool_call_adds_history_cell() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
let image_path = chat.config.cwd.join("example.png");
|
||||
|
||||
handle_view_image_tool_call(
|
||||
&mut chat,
|
||||
ViewImageToolCallEvent {
|
||||
call_id: "call-image".into(),
|
||||
path: image_path,
|
||||
},
|
||||
);
|
||||
handle_view_image_tool_call(&mut chat, "call-image", image_path);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected a single history cell");
|
||||
@@ -861,13 +842,9 @@ async fn image_generation_call_adds_history_cell() {
|
||||
|
||||
handle_image_generation_end(
|
||||
&mut chat,
|
||||
ImageGenerationEndEvent {
|
||||
call_id: "call-image-generation".into(),
|
||||
status: "completed".into(),
|
||||
revised_prompt: Some("A tiny blue square".into()),
|
||||
result: "Zm9v".into(),
|
||||
saved_path: Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
},
|
||||
"call-image-generation",
|
||||
Some("A tiny blue square".into()),
|
||||
Some(test_path_buf("/tmp/ig-1.png").abs()),
|
||||
);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
@@ -1371,13 +1348,7 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
let begin = PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: true,
|
||||
changes: changes2,
|
||||
};
|
||||
handle_patch_apply_begin(&mut chat, begin);
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", changes2);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
let blob = lines_to_single_string(cells.last().unwrap());
|
||||
@@ -1394,16 +1365,13 @@ async fn apply_patch_events_emit_history_cells() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
let end = PatchApplyEndEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
stdout: "ok\n".into(),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: AppServerPatchApplyStatus::Completed,
|
||||
};
|
||||
handle_patch_apply_end(&mut chat, end);
|
||||
handle_patch_apply_end(
|
||||
&mut chat,
|
||||
"c1",
|
||||
"turn-c1",
|
||||
end_changes,
|
||||
AppServerPatchApplyStatus::Completed,
|
||||
);
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(
|
||||
cells.is_empty(),
|
||||
@@ -1442,15 +1410,7 @@ async fn apply_patch_manual_approval_adjusts_header() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
},
|
||||
);
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert!(!cells.is_empty(), "expected apply block cell to be sent");
|
||||
@@ -1496,15 +1456,7 @@ async fn apply_patch_manual_flow_snapshot() {
|
||||
content: "hello\n".to_string(),
|
||||
},
|
||||
);
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
turn_id: "turn-c1".into(),
|
||||
auto_approved: false,
|
||||
changes: apply_changes,
|
||||
},
|
||||
);
|
||||
handle_patch_apply_begin(&mut chat, "c1", "turn-c1", apply_changes);
|
||||
let approved_lines = drain_insert_history(&mut rx)
|
||||
.pop()
|
||||
.expect("approved patch cell");
|
||||
@@ -1613,15 +1565,7 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
PathBuf::from("pkg.rs"),
|
||||
FileChange::Add { content: "".into() },
|
||||
);
|
||||
handle_patch_apply_begin(
|
||||
&mut chat,
|
||||
PatchApplyBeginEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
auto_approved: false,
|
||||
changes: changes2,
|
||||
},
|
||||
);
|
||||
handle_patch_apply_begin(&mut chat, "call-1", "turn-call-1", changes2);
|
||||
let mut end_changes = HashMap::new();
|
||||
end_changes.insert(
|
||||
PathBuf::from("pkg.rs"),
|
||||
@@ -1629,15 +1573,10 @@ async fn apply_patch_full_flow_integration_like() {
|
||||
);
|
||||
handle_patch_apply_end(
|
||||
&mut chat,
|
||||
PatchApplyEndEvent {
|
||||
call_id: "call-1".into(),
|
||||
turn_id: "turn-call-1".into(),
|
||||
stdout: String::from("ok"),
|
||||
stderr: String::new(),
|
||||
success: true,
|
||||
changes: end_changes,
|
||||
status: AppServerPatchApplyStatus::Completed,
|
||||
},
|
||||
"call-1",
|
||||
"turn-call-1",
|
||||
end_changes,
|
||||
AppServerPatchApplyStatus::Completed,
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -751,14 +751,19 @@ fn file_update_changes_from_tui(changes: HashMap<PathBuf, FileChange>) -> Vec<Fi
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(super) fn handle_patch_apply_begin(chat: &mut ChatWidget, event: PatchApplyBeginEvent) {
|
||||
pub(super) fn handle_patch_apply_begin(
|
||||
chat: &mut ChatWidget,
|
||||
call_id: impl Into<String>,
|
||||
turn_id: impl Into<String>,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
turn_id: turn_id.into(),
|
||||
item: AppServerThreadItem::FileChange {
|
||||
id: event.call_id,
|
||||
changes: file_update_changes_from_tui(event.changes),
|
||||
id: call_id.into(),
|
||||
changes: file_update_changes_from_tui(changes),
|
||||
status: AppServerPatchApplyStatus::InProgress,
|
||||
},
|
||||
}),
|
||||
@@ -766,46 +771,61 @@ pub(super) fn handle_patch_apply_begin(chat: &mut ChatWidget, event: PatchApplyB
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_patch_apply_end(chat: &mut ChatWidget, event: PatchApplyEndEvent) {
|
||||
pub(super) fn handle_patch_apply_end(
|
||||
chat: &mut ChatWidget,
|
||||
call_id: impl Into<String>,
|
||||
turn_id: impl Into<String>,
|
||||
changes: HashMap<PathBuf, FileChange>,
|
||||
status: AppServerPatchApplyStatus,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
turn_id: turn_id.into(),
|
||||
item: AppServerThreadItem::FileChange {
|
||||
id: event.call_id,
|
||||
changes: file_update_changes_from_tui(event.changes),
|
||||
status: event.status,
|
||||
id: call_id.into(),
|
||||
changes: file_update_changes_from_tui(changes),
|
||||
status,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_view_image_tool_call(chat: &mut ChatWidget, event: ViewImageToolCallEvent) {
|
||||
pub(super) fn handle_view_image_tool_call(
|
||||
chat: &mut ChatWidget,
|
||||
call_id: impl Into<String>,
|
||||
path: AbsolutePathBuf,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: AppServerThreadItem::ImageView {
|
||||
id: event.call_id,
|
||||
path: event.path,
|
||||
id: call_id.into(),
|
||||
path,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_image_generation_end(chat: &mut ChatWidget, event: ImageGenerationEndEvent) {
|
||||
pub(super) fn handle_image_generation_end(
|
||||
chat: &mut ChatWidget,
|
||||
call_id: impl Into<String>,
|
||||
revised_prompt: Option<String>,
|
||||
saved_path: Option<AbsolutePathBuf>,
|
||||
) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: "turn-1".to_string(),
|
||||
item: AppServerThreadItem::ImageGeneration {
|
||||
id: event.call_id,
|
||||
status: event.status,
|
||||
revised_prompt: event.revised_prompt,
|
||||
result: event.result,
|
||||
saved_path: event.saved_path,
|
||||
id: call_id.into(),
|
||||
status: "completed".to_string(),
|
||||
revised_prompt,
|
||||
result: String::new(),
|
||||
saved_path,
|
||||
},
|
||||
}),
|
||||
/*replay_kind*/ None,
|
||||
@@ -908,14 +928,11 @@ pub(super) fn begin_exec_with_source(
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()];
|
||||
let parsed_cmd: Vec<ParsedCommand> =
|
||||
codex_shell_command::parse_command::parse_command(&command);
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let interaction_input = None;
|
||||
let event = ExecCommandBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
interaction_input,
|
||||
@@ -931,13 +948,10 @@ pub(super) fn begin_unified_exec_startup(
|
||||
raw_cmd: &str,
|
||||
) -> ExecCommandBeginEvent {
|
||||
let command = vec!["bash".to_string(), "-lc".to_string(), raw_cmd.to_string()];
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
let event = ExecCommandBeginEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: Some(process_id.to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
interaction_input: None,
|
||||
@@ -947,20 +961,24 @@ pub(super) fn begin_unified_exec_startup(
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_begin(chat: &mut ChatWidget, event: ExecCommandBeginEvent) {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
let command_actions = event
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd))
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &cwd))
|
||||
.collect();
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemStarted(ItemStartedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: event.call_id,
|
||||
command: codex_shell_command::parse_command::shlex_join(&event.command),
|
||||
cwd: event.cwd,
|
||||
cwd,
|
||||
process_id: event.process_id,
|
||||
source: event.source,
|
||||
status: AppServerCommandExecutionStatus::InProgress,
|
||||
@@ -997,36 +1015,6 @@ pub(super) fn terminal_interaction(
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_terminal_interaction(chat: &mut ChatWidget, event: TerminalInteractionEvent) {
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::TerminalInteraction(
|
||||
codex_app_server_protocol::TerminalInteractionNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item_id: event.call_id,
|
||||
process_id: event.process_id,
|
||||
stdin: event.stdin,
|
||||
},
|
||||
),
|
||||
/*replay_kind*/ None,
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn terminal_interaction_event(
|
||||
call_id: &str,
|
||||
process_id: &str,
|
||||
stdin: &str,
|
||||
) -> TerminalInteractionEvent {
|
||||
TerminalInteractionEvent {
|
||||
call_id: call_id.to_string(),
|
||||
process_id: process_id.to_string(),
|
||||
stdin: stdin.to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn complete_assistant_message(
|
||||
chat: &mut ChatWidget,
|
||||
item_id: &str,
|
||||
@@ -1181,9 +1169,7 @@ pub(super) fn end_exec(
|
||||
};
|
||||
let ExecCommandBeginEvent {
|
||||
call_id,
|
||||
turn_id,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
interaction_input,
|
||||
@@ -1194,46 +1180,46 @@ pub(super) fn end_exec(
|
||||
ExecCommandEndEvent {
|
||||
call_id,
|
||||
process_id,
|
||||
turn_id,
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
interaction_input,
|
||||
stdout: stdout.to_string(),
|
||||
stderr: stderr.to_string(),
|
||||
aggregated_output: aggregated.clone(),
|
||||
exit_code,
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: aggregated,
|
||||
status: if exit_code == 0 {
|
||||
AppServerCommandExecutionStatus::Completed
|
||||
} else {
|
||||
AppServerCommandExecutionStatus::Failed
|
||||
},
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub(super) fn handle_exec_end(chat: &mut ChatWidget, event: ExecCommandEndEvent) {
|
||||
let cwd = chat.config.cwd.clone();
|
||||
let duration_ms = i64::try_from(event.duration.as_millis()).unwrap_or(i64::MAX);
|
||||
let command_actions = event
|
||||
.parsed_cmd
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &event.cwd))
|
||||
.map(|parsed| AppServerCommandAction::from_core_with_cwd(parsed, &cwd))
|
||||
.collect();
|
||||
let status = if event.exit_code == 0 {
|
||||
AppServerCommandExecutionStatus::Completed
|
||||
} else {
|
||||
AppServerCommandExecutionStatus::Failed
|
||||
};
|
||||
chat.handle_server_notification(
|
||||
ServerNotification::ItemCompleted(ItemCompletedNotification {
|
||||
thread_id: thread_id(chat),
|
||||
turn_id: event.turn_id.clone(),
|
||||
turn_id: chat
|
||||
.last_turn_id
|
||||
.clone()
|
||||
.unwrap_or_else(|| "turn-1".to_string()),
|
||||
item: AppServerThreadItem::CommandExecution {
|
||||
id: event.call_id,
|
||||
command: codex_shell_command::parse_command::shlex_join(&event.command),
|
||||
cwd: event.cwd,
|
||||
cwd,
|
||||
process_id: event.process_id,
|
||||
source: event.source,
|
||||
status: event.status,
|
||||
status,
|
||||
command_actions,
|
||||
aggregated_output: (!event.aggregated_output.is_empty())
|
||||
.then_some(event.aggregated_output),
|
||||
|
||||
@@ -2671,15 +2671,12 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
path: "diff_render.rs".into(),
|
||||
},
|
||||
];
|
||||
let cwd = AbsolutePathBuf::current_dir().expect("current dir");
|
||||
handle_exec_begin(
|
||||
&mut chat,
|
||||
ExecCommandBeginEvent {
|
||||
call_id: "c1".into(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
command: command.clone(),
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
@@ -2690,19 +2687,14 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
ExecCommandEndEvent {
|
||||
call_id: "c1".into(),
|
||||
process_id: None,
|
||||
turn_id: "turn-1".into(),
|
||||
command,
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::Agent,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: String::new(),
|
||||
exit_code: 0,
|
||||
duration: std::time::Duration::from_millis(16000),
|
||||
formatted_output: String::new(),
|
||||
status: AppServerCommandExecutionStatus::Completed,
|
||||
},
|
||||
);
|
||||
handle_turn_started(&mut chat, "turn-1");
|
||||
|
||||
@@ -37,7 +37,6 @@ use crate::test_support::PathBufExt;
|
||||
use crate::test_support::test_path_buf;
|
||||
use crate::text_formatting::format_and_truncate_tool_result;
|
||||
use crate::text_formatting::truncate_text;
|
||||
use crate::tool_activity::McpInvocation;
|
||||
use crate::tooltips;
|
||||
use crate::ui_consts::LIVE_PREFIX_COLS;
|
||||
use crate::update_action::UpdateAction;
|
||||
@@ -1580,6 +1579,13 @@ pub(crate) struct McpToolCallCell {
|
||||
animations_enabled: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) struct McpInvocation {
|
||||
pub(crate) server: String,
|
||||
pub(crate) tool: String,
|
||||
pub(crate) arguments: Option<serde_json::Value>,
|
||||
}
|
||||
|
||||
impl McpToolCallCell {
|
||||
pub(crate) fn new(
|
||||
call_id: String,
|
||||
|
||||
@@ -62,7 +62,6 @@ use std::fs::OpenOptions;
|
||||
use std::path::Path;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
pub use token_usage::FinalOutput;
|
||||
pub use token_usage::TokenUsage;
|
||||
use tracing::Level;
|
||||
use tracing::error;
|
||||
@@ -169,11 +168,9 @@ mod terminal_title;
|
||||
mod text_formatting;
|
||||
mod theme_picker;
|
||||
mod token_usage;
|
||||
mod tool_activity;
|
||||
mod tooltips;
|
||||
mod transcript_reflow;
|
||||
mod tui;
|
||||
mod turn_state;
|
||||
mod ui_consts;
|
||||
pub(crate) mod update_action;
|
||||
pub use update_action::UpdateAction;
|
||||
|
||||
@@ -6,7 +6,6 @@ use codex_config::LoaderOverrides;
|
||||
use codex_tui::AppExitInfo;
|
||||
use codex_tui::Cli;
|
||||
use codex_tui::ExitReason;
|
||||
use codex_tui::FinalOutput;
|
||||
use codex_tui::run_main;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use supports_color::Stream;
|
||||
@@ -20,7 +19,7 @@ fn format_exit_messages(exit_info: AppExitInfo, color_enabled: bool) -> Vec<Stri
|
||||
|
||||
let mut lines = Vec::new();
|
||||
if !token_usage.is_zero() {
|
||||
lines.push(FinalOutput::from(token_usage).to_string());
|
||||
lines.push(token_usage.to_string());
|
||||
}
|
||||
|
||||
if let Some(resume_cmd) =
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
//! Resolve saved-session state needed before resuming or forking a thread.
|
||||
//!
|
||||
//! The app-server API owns normal thread lifecycle data. This module handles the local fallback
|
||||
//! path used before a thread has been resumed, where TUI may need the saved cwd or model from the
|
||||
//! local rollout JSONL to rebuild config or render an inactive session accurately.
|
||||
//! The app-server API owns normal thread lifecycle data. This module coordinates
|
||||
//! the TUI-specific cwd prompt and falls back to local rollout metadata only
|
||||
//! before the app server has resumed the selected thread.
|
||||
|
||||
use std::io;
|
||||
use std::path::Path;
|
||||
@@ -21,6 +21,13 @@ use serde::Deserialize;
|
||||
use serde_json::Value;
|
||||
use tokio::io::AsyncBufReadExt;
|
||||
|
||||
#[derive(Default)]
|
||||
struct RolloutResumeState {
|
||||
thread_id: Option<ThreadId>,
|
||||
cwd: Option<PathBuf>,
|
||||
model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct SessionMetadata {
|
||||
id: ThreadId,
|
||||
@@ -28,7 +35,7 @@ struct SessionMetadata {
|
||||
}
|
||||
|
||||
#[derive(Deserialize)]
|
||||
struct LatestTurnContext {
|
||||
struct TurnContextResumeState {
|
||||
cwd: PathBuf,
|
||||
model: String,
|
||||
}
|
||||
@@ -51,10 +58,10 @@ pub(crate) async fn resolve_session_thread_id(
|
||||
) -> Option<ThreadId> {
|
||||
match id_str_if_uuid {
|
||||
Some(id_str) => ThreadId::from_string(id_str).ok(),
|
||||
None => read_session_metadata(path)
|
||||
None => read_rollout_resume_state(path)
|
||||
.await
|
||||
.ok()
|
||||
.map(|metadata| metadata.id),
|
||||
.and_then(|state| state.thread_id),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -71,7 +78,10 @@ pub(crate) async fn read_session_model(
|
||||
}
|
||||
|
||||
let path = path?;
|
||||
read_latest_turn_context(path).await.map(|item| item.model)
|
||||
read_rollout_resume_state(path)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|state| state.model)
|
||||
}
|
||||
|
||||
pub(crate) async fn resolve_cwd_for_resume_or_fork(
|
||||
@@ -113,17 +123,9 @@ async fn read_session_cwd(
|
||||
return Some(metadata.cwd);
|
||||
}
|
||||
|
||||
// Prefer the latest TurnContext cwd so resume/fork reflects the most recent
|
||||
// session directory (for the changed-cwd prompt) when DB data is unavailable.
|
||||
// The alternative would be mutating the session metadata line when the session cwd
|
||||
// changes, but the rollout is an append-only JSONL log and rewriting the head
|
||||
// would be error-prone.
|
||||
let path = path?;
|
||||
if let Some(cwd) = read_latest_turn_context(path).await.map(|item| item.cwd) {
|
||||
return Some(cwd);
|
||||
}
|
||||
match read_session_metadata(path).await {
|
||||
Ok(metadata) => Some(metadata.cwd),
|
||||
match read_rollout_resume_state(path).await {
|
||||
Ok(state) => state.cwd,
|
||||
Err(err) => {
|
||||
let rollout_path = path.display().to_string();
|
||||
tracing::warn!(
|
||||
@@ -140,86 +142,68 @@ pub(crate) fn cwds_differ(current_cwd: &Path, session_cwd: &Path) -> bool {
|
||||
!path_utils::paths_match_after_normalization(current_cwd, session_cwd)
|
||||
}
|
||||
|
||||
async fn read_session_metadata(path: &Path) -> io::Result<SessionMetadata> {
|
||||
async fn read_rollout_resume_state(path: &Path) -> io::Result<RolloutResumeState> {
|
||||
let file = tokio::fs::File::open(path).await?;
|
||||
let reader = tokio::io::BufReader::new(file);
|
||||
let mut lines = reader.lines();
|
||||
let mut state = RolloutResumeState::default();
|
||||
let mut saw_record = false;
|
||||
|
||||
while let Some(line) = lines.next_line().await? {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
saw_record = true;
|
||||
let record = serde_json::from_str::<RawRecord>(trimmed).map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to parse rollout line in {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
if record.item_type != "session_meta" {
|
||||
return Err(io::Error::other(format!(
|
||||
"rollout at {} does not start with session metadata",
|
||||
path.display()
|
||||
)));
|
||||
}
|
||||
let payload = record.payload.ok_or_else(|| {
|
||||
io::Error::other(format!(
|
||||
"session metadata in {} is missing a payload",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
return serde_json::from_value(payload).map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to parse session metadata in {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
});
|
||||
}
|
||||
|
||||
Err(io::Error::other(format!(
|
||||
"rollout at {} is empty",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
|
||||
async fn read_latest_turn_context(path: &Path) -> Option<LatestTurnContext> {
|
||||
let text = tokio::fs::read_to_string(path).await.ok()?;
|
||||
for line in text.lines().rev() {
|
||||
let trimmed = line.trim();
|
||||
if trimmed.is_empty() {
|
||||
continue;
|
||||
}
|
||||
let Ok(record) = serde_json::from_str::<RawRecord>(trimmed) else {
|
||||
continue;
|
||||
};
|
||||
if record.item_type != "turn_context" {
|
||||
continue;
|
||||
}
|
||||
let Some(payload) = record.payload else {
|
||||
continue;
|
||||
};
|
||||
if let Ok(item) = serde_json::from_value(payload) {
|
||||
return Some(item);
|
||||
|
||||
match record.item_type.as_str() {
|
||||
"session_meta" if state.thread_id.is_none() => {
|
||||
let metadata =
|
||||
serde_json::from_value::<SessionMetadata>(payload).map_err(|err| {
|
||||
io::Error::other(format!(
|
||||
"failed to parse session metadata in {}: {err}",
|
||||
path.display()
|
||||
))
|
||||
})?;
|
||||
state.thread_id = Some(metadata.id);
|
||||
state.cwd.get_or_insert(metadata.cwd);
|
||||
}
|
||||
"turn_context" => {
|
||||
if let Ok(turn_context) = serde_json::from_value::<TurnContextResumeState>(payload)
|
||||
{
|
||||
state.cwd = Some(turn_context.cwd);
|
||||
state.model = Some(turn_context.model);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
None
|
||||
|
||||
if saw_record {
|
||||
Ok(state)
|
||||
} else {
|
||||
Err(io::Error::other(format!(
|
||||
"rollout at {} is empty",
|
||||
path.display()
|
||||
)))
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::legacy_core::config::ConfigBuilder;
|
||||
use codex_features::Feature;
|
||||
use pretty_assertions::assert_eq;
|
||||
use tempfile::TempDir;
|
||||
|
||||
async fn build_config(temp_dir: &TempDir) -> std::io::Result<Config> {
|
||||
ConfigBuilder::default()
|
||||
.codex_home(temp_dir.path().to_path_buf())
|
||||
.build()
|
||||
.await
|
||||
}
|
||||
|
||||
fn rollout_line(
|
||||
timestamp: &str,
|
||||
item_type: &str,
|
||||
@@ -232,35 +216,6 @@ mod tests {
|
||||
})
|
||||
}
|
||||
|
||||
fn turn_context_line(config: &Config, cwd: PathBuf, timestamp: &str) -> serde_json::Value {
|
||||
let model = config
|
||||
.model
|
||||
.clone()
|
||||
.unwrap_or_else(|| "gpt-5.1".to_string());
|
||||
rollout_line(
|
||||
timestamp,
|
||||
"turn_context",
|
||||
serde_json::json!({
|
||||
"cwd": cwd,
|
||||
"model": model,
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn session_meta_line(thread_id: ThreadId, cwd: PathBuf, timestamp: &str) -> serde_json::Value {
|
||||
rollout_line(
|
||||
timestamp,
|
||||
"session_meta",
|
||||
serde_json::json!({
|
||||
"id": thread_id.to_string(),
|
||||
"timestamp": timestamp,
|
||||
"cwd": cwd,
|
||||
"originator": "test",
|
||||
"cli_version": "test",
|
||||
}),
|
||||
)
|
||||
}
|
||||
|
||||
fn write_rollout_lines(path: &Path, lines: &[serde_json::Value]) -> std::io::Result<()> {
|
||||
let mut text = String::new();
|
||||
for line in lines {
|
||||
@@ -271,162 +226,71 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_returns_none_without_sqlite_or_rollout_path() -> std::io::Result<()> {
|
||||
async fn rollout_resume_state_prefers_latest_turn_context() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
|
||||
let cwd = read_session_cwd(&config, ThreadId::new(), /*path*/ None).await;
|
||||
|
||||
assert_eq!(cwd, None);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_prefers_latest_turn_context() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let first = temp_dir.path().join("first");
|
||||
let second = temp_dir.path().join("second");
|
||||
std::fs::create_dir_all(&first)?;
|
||||
std::fs::create_dir_all(&second)?;
|
||||
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
write_rollout_lines(
|
||||
&rollout_path,
|
||||
&[
|
||||
turn_context_line(&config, first, "t0"),
|
||||
turn_context_line(&config, second.clone(), "t1"),
|
||||
],
|
||||
)?;
|
||||
|
||||
let cwd = read_session_cwd(&config, ThreadId::new(), Some(&rollout_path))
|
||||
.await
|
||||
.expect("expected cwd");
|
||||
assert_eq!(cwd, second);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn should_prompt_when_meta_matches_current_but_latest_turn_differs() -> std::io::Result<()>
|
||||
{
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let current = temp_dir.path().join("current");
|
||||
let thread_id = ThreadId::new();
|
||||
let original = temp_dir.path().join("original");
|
||||
let latest = temp_dir.path().join("latest");
|
||||
std::fs::create_dir_all(¤t)?;
|
||||
std::fs::create_dir_all(&latest)?;
|
||||
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
write_rollout_lines(
|
||||
&rollout_path,
|
||||
&[
|
||||
session_meta_line(ThreadId::new(), current.clone(), "t0"),
|
||||
turn_context_line(&config, latest.clone(), "t1"),
|
||||
rollout_line(
|
||||
"t0",
|
||||
"session_meta",
|
||||
serde_json::json!({
|
||||
"id": thread_id,
|
||||
"cwd": original,
|
||||
"originator": "test",
|
||||
"cli_version": "test",
|
||||
}),
|
||||
),
|
||||
rollout_line(
|
||||
"t1",
|
||||
"turn_context",
|
||||
serde_json::json!({ "cwd": temp_dir.path().join("middle"), "model": "middle" }),
|
||||
),
|
||||
rollout_line(
|
||||
"t2",
|
||||
"turn_context",
|
||||
serde_json::json!({ "cwd": latest.clone(), "model": "latest" }),
|
||||
),
|
||||
],
|
||||
)?;
|
||||
|
||||
let session_cwd = read_session_cwd(&config, ThreadId::new(), Some(&rollout_path))
|
||||
.await
|
||||
.expect("expected cwd");
|
||||
assert_eq!(session_cwd, latest);
|
||||
assert!(cwds_differ(¤t, &session_cwd));
|
||||
let state = read_rollout_resume_state(&rollout_path).await?;
|
||||
|
||||
assert_eq!(state.thread_id, Some(thread_id));
|
||||
assert_eq!(state.cwd, Some(latest));
|
||||
assert_eq!(state.model, Some("latest".to_string()));
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_falls_back_to_session_meta() -> std::io::Result<()> {
|
||||
async fn rollout_resume_state_falls_back_to_session_meta() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let config = build_config(&temp_dir).await?;
|
||||
let session_cwd = temp_dir.path().join("session");
|
||||
std::fs::create_dir_all(&session_cwd)?;
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
let cwd = temp_dir.path().join("session");
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
write_rollout_lines(
|
||||
&rollout_path,
|
||||
&[session_meta_line(
|
||||
ThreadId::new(),
|
||||
session_cwd.clone(),
|
||||
&[rollout_line(
|
||||
"t0",
|
||||
"session_meta",
|
||||
serde_json::json!({
|
||||
"id": thread_id,
|
||||
"cwd": cwd.clone(),
|
||||
"originator": "test",
|
||||
"cli_version": "test",
|
||||
}),
|
||||
)],
|
||||
)?;
|
||||
|
||||
let cwd = read_session_cwd(&config, ThreadId::new(), Some(&rollout_path))
|
||||
.await
|
||||
.expect("expected cwd");
|
||||
assert_eq!(cwd, session_cwd);
|
||||
Ok(())
|
||||
}
|
||||
let state = read_rollout_resume_state(&rollout_path).await?;
|
||||
|
||||
#[tokio::test]
|
||||
async fn read_session_cwd_prefers_sqlite_when_thread_id_present() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let mut config = build_config(&temp_dir).await?;
|
||||
config
|
||||
.features
|
||||
.enable(Feature::Sqlite)
|
||||
.expect("test config should allow sqlite");
|
||||
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_cwd = temp_dir.path().join("rollout-cwd");
|
||||
let sqlite_cwd = temp_dir.path().join("sqlite-cwd");
|
||||
std::fs::create_dir_all(&rollout_cwd)?;
|
||||
std::fs::create_dir_all(&sqlite_cwd)?;
|
||||
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
write_rollout_lines(
|
||||
&rollout_path,
|
||||
&[turn_context_line(&config, rollout_cwd, "t0")],
|
||||
)?;
|
||||
|
||||
let runtime = codex_state::StateRuntime::init(
|
||||
config.codex_home.to_path_buf(),
|
||||
config.model_provider_id.clone(),
|
||||
)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
runtime
|
||||
.mark_backfill_complete(/*last_watermark*/ None)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let mut builder = codex_state::ThreadMetadataBuilder::new(
|
||||
thread_id,
|
||||
rollout_path.clone(),
|
||||
chrono::Utc::now(),
|
||||
serde_json::from_value(serde_json::json!("cli"))
|
||||
.expect("cli session source should deserialize"),
|
||||
);
|
||||
builder.cwd = sqlite_cwd.clone();
|
||||
let metadata = builder.build(config.model_provider_id.as_str());
|
||||
runtime
|
||||
.upsert_thread(&metadata)
|
||||
.await
|
||||
.map_err(std::io::Error::other)?;
|
||||
|
||||
let cwd = read_session_cwd(&config, thread_id, Some(&rollout_path))
|
||||
.await
|
||||
.expect("expected cwd");
|
||||
assert_eq!(cwd, sqlite_cwd);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn resolve_session_thread_id_reads_minimal_session_metadata() -> std::io::Result<()> {
|
||||
let temp_dir = TempDir::new()?;
|
||||
let thread_id = ThreadId::new();
|
||||
let rollout_path = temp_dir.path().join("rollout.jsonl");
|
||||
write_rollout_lines(
|
||||
&rollout_path,
|
||||
&[session_meta_line(
|
||||
thread_id,
|
||||
temp_dir.path().to_path_buf(),
|
||||
"t0",
|
||||
)],
|
||||
)?;
|
||||
|
||||
let resolved = resolve_session_thread_id(&rollout_path, /*id_str_if_uuid*/ None).await;
|
||||
|
||||
assert_eq!(resolved, Some(thread_id));
|
||||
assert_eq!(state.thread_id, Some(thread_id));
|
||||
assert_eq!(state.cwd, Some(cwd));
|
||||
assert_eq!(state.model, None);
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,7 +1,8 @@
|
||||
//! TUI token usage and final-output formatting models.
|
||||
//! TUI token usage models and display formatting.
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use codex_protocol::num_format::format_with_separators;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
@@ -57,38 +58,26 @@ pub(crate) struct TokenUsageInfo {
|
||||
pub(crate) model_context_window: Option<i64>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct FinalOutput {
|
||||
pub token_usage: TokenUsage,
|
||||
}
|
||||
|
||||
impl From<TokenUsage> for FinalOutput {
|
||||
fn from(token_usage: TokenUsage) -> Self {
|
||||
Self { token_usage }
|
||||
}
|
||||
}
|
||||
|
||||
impl fmt::Display for FinalOutput {
|
||||
impl fmt::Display for TokenUsage {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
let token_usage = &self.token_usage;
|
||||
write!(
|
||||
f,
|
||||
"Token usage: total={} input={}{} output={}{}",
|
||||
format_with_separators(token_usage.blended_total()),
|
||||
format_with_separators(token_usage.non_cached_input()),
|
||||
if token_usage.cached_input() > 0 {
|
||||
format_with_separators(self.blended_total()),
|
||||
format_with_separators(self.non_cached_input()),
|
||||
if self.cached_input() > 0 {
|
||||
format!(
|
||||
" (+ {} cached)",
|
||||
format_with_separators(token_usage.cached_input())
|
||||
format_with_separators(self.cached_input())
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
},
|
||||
format_with_separators(token_usage.output_tokens),
|
||||
if token_usage.reasoning_output_tokens > 0 {
|
||||
format_with_separators(self.output_tokens),
|
||||
if self.reasoning_output_tokens > 0 {
|
||||
format!(
|
||||
" (reasoning {})",
|
||||
format_with_separators(token_usage.reasoning_output_tokens)
|
||||
format_with_separators(self.reasoning_output_tokens)
|
||||
)
|
||||
} else {
|
||||
String::new()
|
||||
@@ -96,17 +85,3 @@ impl fmt::Display for FinalOutput {
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fn format_with_separators(value: i64) -> String {
|
||||
let sign = if value < 0 { "-" } else { "" };
|
||||
let digits = value.abs().to_string();
|
||||
let mut out = String::new();
|
||||
for (i, ch) in digits.chars().rev().enumerate() {
|
||||
if i > 0 && i % 3 == 0 {
|
||||
out.push(',');
|
||||
}
|
||||
out.push(ch);
|
||||
}
|
||||
let grouped: String = out.chars().rev().collect();
|
||||
format!("{sign}{grouped}")
|
||||
}
|
||||
|
||||
@@ -1,163 +0,0 @@
|
||||
//! Internal tool activity models used by chat cells and interrupt queueing.
|
||||
|
||||
use std::collections::HashMap;
|
||||
use std::path::PathBuf;
|
||||
use std::time::Duration;
|
||||
|
||||
use crate::diff_model::FileChange;
|
||||
use codex_app_server_protocol::CommandExecOutputStream;
|
||||
use codex_app_server_protocol::CommandExecutionSource;
|
||||
use codex_app_server_protocol::CommandExecutionStatus;
|
||||
use codex_app_server_protocol::HookRunSummary;
|
||||
use codex_app_server_protocol::PatchApplyStatus;
|
||||
use codex_protocol::mcp::CallToolResult;
|
||||
use codex_protocol::parse_command::ParsedCommand;
|
||||
use codex_utils_absolute_path::AbsolutePathBuf;
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
use serde_json::Value;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct McpInvocation {
|
||||
pub(crate) server: String,
|
||||
pub(crate) tool: String,
|
||||
pub(crate) arguments: Option<Value>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct McpToolCallBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) invocation: McpInvocation,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) mcp_app_resource_uri: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct McpToolCallEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) invocation: McpInvocation,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) mcp_app_resource_uri: Option<String>,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) result: Result<CallToolResult, String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct WebSearchBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct WebSearchEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) query: String,
|
||||
pub(crate) action: codex_protocol::models::WebSearchAction,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ImageGenerationBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ImageGenerationEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) status: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) revised_prompt: Option<String>,
|
||||
pub(crate) result: String,
|
||||
#[serde(skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) saved_path: Option<AbsolutePathBuf>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ExecCommandBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) process_id: Option<String>,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) parsed_cmd: Vec<ParsedCommand>,
|
||||
#[serde(default)]
|
||||
pub(crate) source: CommandExecutionSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) interaction_input: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ExecCommandEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) process_id: Option<String>,
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) command: Vec<String>,
|
||||
pub(crate) cwd: AbsolutePathBuf,
|
||||
pub(crate) parsed_cmd: Vec<ParsedCommand>,
|
||||
#[serde(default)]
|
||||
pub(crate) source: CommandExecutionSource,
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub(crate) interaction_input: Option<String>,
|
||||
pub(crate) stdout: String,
|
||||
pub(crate) stderr: String,
|
||||
#[serde(default)]
|
||||
pub(crate) aggregated_output: String,
|
||||
pub(crate) exit_code: i32,
|
||||
pub(crate) duration: Duration,
|
||||
pub(crate) formatted_output: String,
|
||||
pub(crate) status: CommandExecutionStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct ViewImageToolCallEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) path: AbsolutePathBuf,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct ExecCommandOutputDeltaEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) stream: CommandExecOutputStream,
|
||||
pub(crate) chunk: Vec<u8>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct TerminalInteractionEvent {
|
||||
pub(crate) call_id: String,
|
||||
pub(crate) process_id: String,
|
||||
pub(crate) stdin: String,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PatchApplyBeginEvent {
|
||||
pub(crate) call_id: String,
|
||||
#[serde(default)]
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) auto_approved: bool,
|
||||
pub(crate) changes: HashMap<PathBuf, FileChange>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub(crate) struct PatchApplyEndEvent {
|
||||
pub(crate) call_id: String,
|
||||
#[serde(default)]
|
||||
pub(crate) turn_id: String,
|
||||
pub(crate) stdout: String,
|
||||
pub(crate) stderr: String,
|
||||
pub(crate) success: bool,
|
||||
#[serde(default)]
|
||||
pub(crate) changes: HashMap<PathBuf, FileChange>,
|
||||
pub(crate) status: PatchApplyStatus,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct HookStartedEvent {
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) run: HookRunSummary,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
pub(crate) struct HookCompletedEvent {
|
||||
pub(crate) turn_id: Option<String>,
|
||||
pub(crate) run: HookRunSummary,
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
//! Turn lifecycle state used by TUI interruption and completion handling.
|
||||
|
||||
use serde::Deserialize;
|
||||
use serde::Serialize;
|
||||
|
||||
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
|
||||
#[serde(rename_all = "snake_case")]
|
||||
pub(crate) enum TurnAbortReason {
|
||||
Interrupted,
|
||||
BudgetLimited,
|
||||
}
|
||||
Reference in New Issue
Block a user