mirror of
https://github.com/openai/codex.git
synced 2026-10-27 18:48:37 +00:00
show remote host in exec tui cells
This commit is contained in:
@@ -1790,6 +1790,7 @@ mod tests {
|
||||
cmd: "echo hello world".into(),
|
||||
}],
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
@@ -2007,6 +2008,7 @@ mod tests {
|
||||
cwd: PathBuf::from("/tmp"),
|
||||
parsed_cmd: vec![ParsedCommand::Unknown { cmd: "ls".into() }],
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: "exec command rejected by user".into(),
|
||||
@@ -2247,6 +2249,7 @@ mod tests {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: "done\n".into(),
|
||||
stderr: String::new(),
|
||||
@@ -2336,6 +2339,7 @@ mod tests {
|
||||
cmd: "echo done".into(),
|
||||
}],
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: "done\n".into(),
|
||||
stderr: String::new(),
|
||||
|
||||
@@ -152,6 +152,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
cwd: cwd.to_path_buf(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
}),
|
||||
)
|
||||
@@ -220,6 +221,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
cwd: cwd.to_path_buf(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: aborted_message.clone(),
|
||||
@@ -244,6 +246,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
cwd: cwd.to_path_buf(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: output.stdout.text.clone(),
|
||||
stderr: output.stderr.text.clone(),
|
||||
@@ -288,6 +291,7 @@ pub(crate) async fn execute_user_shell_command(
|
||||
cwd: cwd.to_path_buf(),
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: exec_output.stdout.text.clone(),
|
||||
stderr: exec_output.stderr.text.clone(),
|
||||
|
||||
@@ -61,27 +61,20 @@ pub(crate) enum ToolEventFailure {
|
||||
Rejected(String),
|
||||
}
|
||||
|
||||
pub(crate) async fn emit_exec_command_begin(
|
||||
ctx: ToolEventCtx<'_>,
|
||||
command: &[String],
|
||||
cwd: &Path,
|
||||
parsed_cmd: &[ParsedCommand],
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<String>,
|
||||
process_id: Option<&str>,
|
||||
) {
|
||||
async fn emit_exec_command_begin(ctx: ToolEventCtx<'_>, exec_input: &ExecCommandInput<'_>) {
|
||||
ctx.session
|
||||
.send_event(
|
||||
ctx.turn,
|
||||
EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: ctx.call_id.to_string(),
|
||||
process_id: process_id.map(str::to_owned),
|
||||
process_id: exec_input.process_id.map(str::to_owned),
|
||||
turn_id: ctx.turn.sub_id.clone(),
|
||||
command: command.to_vec(),
|
||||
cwd: cwd.to_path_buf(),
|
||||
parsed_cmd: parsed_cmd.to_vec(),
|
||||
source,
|
||||
interaction_input,
|
||||
command: exec_input.command.to_vec(),
|
||||
cwd: exec_input.cwd.to_path_buf(),
|
||||
parsed_cmd: exec_input.parsed_cmd.to_vec(),
|
||||
source: exec_input.source,
|
||||
host_id: exec_input.host_id.map(str::to_owned),
|
||||
interaction_input: exec_input.interaction_input.map(str::to_owned),
|
||||
}),
|
||||
)
|
||||
.await;
|
||||
@@ -105,6 +98,7 @@ pub(crate) enum ToolEmitter {
|
||||
source: ExecCommandSource,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
process_id: Option<String>,
|
||||
host_id: Option<String>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -137,6 +131,7 @@ impl ToolEmitter {
|
||||
cwd: PathBuf,
|
||||
source: ExecCommandSource,
|
||||
process_id: Option<String>,
|
||||
host_id: Option<String>,
|
||||
) -> Self {
|
||||
let parsed_cmd = parse_command(command);
|
||||
Self::UnifiedExec {
|
||||
@@ -145,6 +140,7 @@ impl ToolEmitter {
|
||||
source,
|
||||
parsed_cmd,
|
||||
process_id,
|
||||
host_id,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -169,6 +165,7 @@ impl ToolEmitter {
|
||||
*source,
|
||||
/*interaction_input*/ None,
|
||||
/*process_id*/ None,
|
||||
/*host_id*/ None,
|
||||
),
|
||||
stage,
|
||||
)
|
||||
@@ -266,6 +263,7 @@ impl ToolEmitter {
|
||||
source,
|
||||
parsed_cmd,
|
||||
process_id,
|
||||
host_id,
|
||||
},
|
||||
stage,
|
||||
) => {
|
||||
@@ -278,6 +276,7 @@ impl ToolEmitter {
|
||||
*source,
|
||||
/*interaction_input*/ None,
|
||||
process_id.as_deref(),
|
||||
host_id.as_deref(),
|
||||
),
|
||||
stage,
|
||||
)
|
||||
@@ -370,6 +369,7 @@ struct ExecCommandInput<'a> {
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
process_id: Option<&'a str>,
|
||||
host_id: Option<&'a str>,
|
||||
}
|
||||
|
||||
impl<'a> ExecCommandInput<'a> {
|
||||
@@ -380,6 +380,7 @@ impl<'a> ExecCommandInput<'a> {
|
||||
source: ExecCommandSource,
|
||||
interaction_input: Option<&'a str>,
|
||||
process_id: Option<&'a str>,
|
||||
host_id: Option<&'a str>,
|
||||
) -> Self {
|
||||
Self {
|
||||
command,
|
||||
@@ -388,6 +389,7 @@ impl<'a> ExecCommandInput<'a> {
|
||||
source,
|
||||
interaction_input,
|
||||
process_id,
|
||||
host_id,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -409,16 +411,7 @@ async fn emit_exec_stage(
|
||||
) {
|
||||
match stage {
|
||||
ToolEventStage::Begin => {
|
||||
emit_exec_command_begin(
|
||||
ctx,
|
||||
exec_input.command,
|
||||
exec_input.cwd,
|
||||
exec_input.parsed_cmd,
|
||||
exec_input.source,
|
||||
exec_input.interaction_input.map(str::to_owned),
|
||||
exec_input.process_id,
|
||||
)
|
||||
.await;
|
||||
emit_exec_command_begin(ctx, &exec_input).await;
|
||||
}
|
||||
ToolEventStage::Success(output)
|
||||
| ToolEventStage::Failure(ToolEventFailure::Output(output)) => {
|
||||
@@ -482,6 +475,7 @@ async fn emit_exec_end(
|
||||
cwd: exec_input.cwd.to_path_buf(),
|
||||
parsed_cmd: exec_input.parsed_cmd.to_vec(),
|
||||
source: exec_input.source,
|
||||
host_id: exec_input.host_id.map(str::to_owned),
|
||||
interaction_input: exec_input.interaction_input.map(str::to_owned),
|
||||
stdout: exec_result.stdout,
|
||||
stderr: exec_result.stderr,
|
||||
|
||||
@@ -185,12 +185,13 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
"exec_command" => {
|
||||
let cwd = resolve_workdir_base_path(&arguments, &context.turn.cwd)?;
|
||||
let args: ExecCommandArgs = parse_arguments_with_base_path(&arguments, &cwd)?;
|
||||
let environment = resolve_exec_environment(
|
||||
let resolved_environment = resolve_exec_environment(
|
||||
session.as_ref(),
|
||||
turn.as_ref(),
|
||||
args.host_id.as_deref(),
|
||||
)
|
||||
.await?;
|
||||
let environment = resolved_environment.environment;
|
||||
let default_host_cwd = if environment.is_remote() && args.workdir.is_none() {
|
||||
environment
|
||||
.default_cwd()
|
||||
@@ -232,7 +233,7 @@ impl ToolHandler for UnifiedExecHandler {
|
||||
)
|
||||
.map_err(FunctionCallError::RespondToModel)?;
|
||||
let command_for_display = codex_shell_command::parse_command::shlex_join(&command);
|
||||
let request_host_id = args.host_id.clone();
|
||||
let request_host_id = resolved_environment.host_id;
|
||||
|
||||
let ExecCommandArgs {
|
||||
workdir,
|
||||
@@ -409,11 +410,16 @@ fn emit_unified_exec_tty_metric(session_telemetry: &SessionTelemetry, tty: bool)
|
||||
);
|
||||
}
|
||||
|
||||
struct ResolvedExecEnvironment {
|
||||
environment: Arc<codex_exec_server::Environment>,
|
||||
host_id: Option<String>,
|
||||
}
|
||||
|
||||
async fn resolve_exec_environment(
|
||||
session: &crate::codex::Session,
|
||||
turn: &crate::codex::TurnContext,
|
||||
host_id: Option<&str>,
|
||||
) -> Result<Arc<codex_exec_server::Environment>, FunctionCallError> {
|
||||
) -> Result<ResolvedExecEnvironment, FunctionCallError> {
|
||||
let environment = match host_id {
|
||||
Some(host_id) => session
|
||||
.services
|
||||
@@ -428,8 +434,28 @@ async fn resolve_exec_environment(
|
||||
None => turn.environment.clone(),
|
||||
};
|
||||
|
||||
environment.ok_or_else(|| {
|
||||
let environment = environment.ok_or_else(|| {
|
||||
FunctionCallError::RespondToModel("unified exec is unavailable in this session".to_string())
|
||||
})?;
|
||||
let host_id = if environment.is_remote() {
|
||||
Some(match host_id {
|
||||
Some(host_id) => host_id.to_string(),
|
||||
None => {
|
||||
let default_host_id = session.services.environment_manager.default_host_id();
|
||||
if default_host_id == codex_exec_server::LOCAL_HOST_ID {
|
||||
"remote".to_string()
|
||||
} else {
|
||||
default_host_id
|
||||
}
|
||||
}
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
Ok(ResolvedExecEnvironment {
|
||||
environment,
|
||||
host_id,
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -110,6 +110,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
host_id: Option<String>,
|
||||
cwd: PathBuf,
|
||||
process_id: i32,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
@@ -129,6 +130,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
turn_ref,
|
||||
call_id,
|
||||
command,
|
||||
host_id,
|
||||
cwd,
|
||||
Some(process_id.to_string()),
|
||||
transcript,
|
||||
@@ -143,6 +145,7 @@ pub(crate) fn spawn_exit_watcher(
|
||||
turn_ref,
|
||||
call_id,
|
||||
command,
|
||||
host_id,
|
||||
cwd,
|
||||
Some(process_id.to_string()),
|
||||
transcript,
|
||||
@@ -196,6 +199,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
host_id: Option<String>,
|
||||
cwd: PathBuf,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
@@ -223,6 +227,7 @@ pub(crate) async fn emit_exec_end_for_unified_exec(
|
||||
cwd,
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
process_id,
|
||||
host_id,
|
||||
);
|
||||
emitter
|
||||
.emit(event_ctx, ToolEventStage::Success(output))
|
||||
@@ -235,6 +240,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
|
||||
turn_ref: Arc<TurnContext>,
|
||||
call_id: String,
|
||||
command: Vec<String>,
|
||||
host_id: Option<String>,
|
||||
cwd: PathBuf,
|
||||
process_id: Option<String>,
|
||||
transcript: Arc<Mutex<HeadTailBuffer>>,
|
||||
@@ -266,6 +272,7 @@ pub(crate) async fn emit_failed_exec_end_for_unified_exec(
|
||||
cwd,
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
process_id,
|
||||
host_id,
|
||||
);
|
||||
emitter
|
||||
.emit(
|
||||
|
||||
@@ -192,6 +192,7 @@ impl UnifiedExecProcessManager {
|
||||
cwd.to_path_buf(),
|
||||
ExecCommandSource::UnifiedExecStartup,
|
||||
Some(request.process_id.to_string()),
|
||||
request.host_id.clone(),
|
||||
);
|
||||
emitter.emit(event_ctx, ToolEventStage::Begin).await;
|
||||
|
||||
@@ -208,6 +209,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&process),
|
||||
context,
|
||||
&request.command,
|
||||
request.host_id.clone(),
|
||||
cwd.clone(),
|
||||
start,
|
||||
request.process_id,
|
||||
@@ -255,6 +257,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
request.host_id.clone(),
|
||||
cwd.to_path_buf(),
|
||||
Some(request.process_id.to_string()),
|
||||
Arc::clone(&transcript),
|
||||
@@ -298,6 +301,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
request.command.clone(),
|
||||
request.host_id.clone(),
|
||||
cwd.to_path_buf(),
|
||||
Some(process_id.to_string()),
|
||||
Arc::clone(&transcript),
|
||||
@@ -526,6 +530,7 @@ impl UnifiedExecProcessManager {
|
||||
process: Arc<UnifiedExecProcess>,
|
||||
context: &UnifiedExecContext,
|
||||
command: &[String],
|
||||
host_id: Option<String>,
|
||||
cwd: AbsolutePathBuf,
|
||||
started_at: Instant,
|
||||
process_id: i32,
|
||||
@@ -572,6 +577,7 @@ impl UnifiedExecProcessManager {
|
||||
Arc::clone(&context.turn),
|
||||
context.call_id.clone(),
|
||||
command.to_vec(),
|
||||
host_id,
|
||||
cwd.to_path_buf(),
|
||||
process_id,
|
||||
transcript,
|
||||
|
||||
@@ -414,7 +414,14 @@ impl Environment {
|
||||
|
||||
match exec_server_url {
|
||||
Some(exec_server_url) => {
|
||||
Self::create_remote(exec_server_url, None, None, None, "codex-environment").await
|
||||
Self::create_remote(
|
||||
exec_server_url,
|
||||
/*default_cwd*/ None,
|
||||
/*default_shell*/ None,
|
||||
/*ssh_exec_server*/ None,
|
||||
"codex-environment",
|
||||
)
|
||||
.await
|
||||
}
|
||||
None => Ok(Self::default()),
|
||||
}
|
||||
@@ -426,9 +433,9 @@ impl Environment {
|
||||
HostConnection::ExecServerUrl { url } => {
|
||||
Self::create_remote(
|
||||
url.clone(),
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
/*default_cwd*/ None,
|
||||
/*default_shell*/ None,
|
||||
/*ssh_exec_server*/ None,
|
||||
format!("codex-environment-{}", host_config.id),
|
||||
)
|
||||
.await
|
||||
|
||||
@@ -3027,6 +3027,10 @@ pub struct ExecCommandBeginEvent {
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
pub source: ExecCommandSource,
|
||||
/// Remote host id when the command is executed outside the local environment.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub host_id: Option<String>,
|
||||
/// Raw input sent to a unified exec session (if this is an interaction event).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
@@ -3051,6 +3055,10 @@ pub struct ExecCommandEndEvent {
|
||||
/// Where the command originated. Defaults to Agent for backward compatibility.
|
||||
#[serde(default)]
|
||||
pub source: ExecCommandSource,
|
||||
/// Remote host id when the command is executed outside the local environment.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
pub host_id: Option<String>,
|
||||
/// Raw input sent to a unified exec session (if this is an interaction event).
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[ts(optional)]
|
||||
|
||||
@@ -922,6 +922,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option<V
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: source.to_core(),
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
}),
|
||||
}])
|
||||
@@ -982,6 +983,7 @@ fn command_execution_completed_event(turn_id: &str, item: &ThreadItem) -> Option
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: source.to_core(),
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
|
||||
@@ -397,6 +397,7 @@ struct RunningCommand {
|
||||
command: Vec<String>,
|
||||
parsed_cmd: Vec<ParsedCommand>,
|
||||
source: ExecCommandSource,
|
||||
host_id: Option<String>,
|
||||
}
|
||||
|
||||
struct UnifiedExecProcessSummary {
|
||||
@@ -4319,9 +4320,14 @@ impl ChatWidget {
|
||||
if self.suppressed_exec_calls.remove(&ev.call_id) {
|
||||
return;
|
||||
}
|
||||
let (command, parsed, source) = match running {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd, rc.source),
|
||||
None => (ev.command.clone(), ev.parsed_cmd.clone(), ev.source),
|
||||
let (command, parsed, source, host_id) = match running {
|
||||
Some(rc) => (rc.command, rc.parsed_cmd, rc.source, rc.host_id),
|
||||
None => (
|
||||
ev.command.clone(),
|
||||
ev.parsed_cmd.clone(),
|
||||
ev.source,
|
||||
ev.host_id.clone(),
|
||||
),
|
||||
};
|
||||
let parsed = self.annotate_skill_reads_in_parsed_cmd(parsed);
|
||||
let is_unified_exec_interaction =
|
||||
@@ -4382,6 +4388,7 @@ impl ChatWidget {
|
||||
command,
|
||||
parsed,
|
||||
source,
|
||||
host_id,
|
||||
ev.interaction_input.clone(),
|
||||
self.config.animations,
|
||||
);
|
||||
@@ -4403,6 +4410,7 @@ impl ChatWidget {
|
||||
command,
|
||||
parsed,
|
||||
source,
|
||||
host_id,
|
||||
ev.interaction_input.clone(),
|
||||
self.config.animations,
|
||||
);
|
||||
@@ -4550,6 +4558,7 @@ impl ChatWidget {
|
||||
command: ev.command.clone(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ev.source,
|
||||
host_id: ev.host_id.clone(),
|
||||
},
|
||||
);
|
||||
let is_wait_interaction = matches!(ev.source, ExecCommandSource::UnifiedExecInteraction)
|
||||
@@ -4583,6 +4592,7 @@ impl ChatWidget {
|
||||
ev.command.clone(),
|
||||
parsed_cmd.clone(),
|
||||
ev.source,
|
||||
ev.host_id.clone(),
|
||||
interaction_input.clone(),
|
||||
)
|
||||
{
|
||||
@@ -4596,6 +4606,7 @@ impl ChatWidget {
|
||||
ev.command.clone(),
|
||||
parsed_cmd,
|
||||
ev.source,
|
||||
ev.host_id,
|
||||
interaction_input,
|
||||
self.config.animations,
|
||||
)));
|
||||
@@ -6206,6 +6217,7 @@ impl ChatWidget {
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: source.to_core(),
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
});
|
||||
} else {
|
||||
@@ -6221,6 +6233,7 @@ impl ChatWidget {
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: source.to_core(),
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
@@ -6792,6 +6805,7 @@ impl ChatWidget {
|
||||
.map(codex_app_server_protocol::CommandAction::into_core)
|
||||
.collect(),
|
||||
source: source.to_core(),
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -346,6 +346,82 @@ async fn exec_history_cell_shows_working_then_failed() {
|
||||
assert!(blob.to_lowercase().contains("bloop"), "expected error text");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_history_cell_shows_remote_host_while_running() {
|
||||
let (mut chat, _rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
let command = vec![
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"printf remote".to_string(),
|
||||
];
|
||||
let parsed_cmd = vec![ParsedCommand::Unknown {
|
||||
cmd: "printf remote".to_string(),
|
||||
}];
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-remote-running".to_string(),
|
||||
msg: EventMsg::ExecCommandBegin(ExecCommandBeginEvent {
|
||||
call_id: "call-remote-running".to_string(),
|
||||
process_id: Some("proc-remote".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: Some("remote-host".to_string()),
|
||||
interaction_input: None,
|
||||
}),
|
||||
});
|
||||
|
||||
let blob = active_blob(&chat);
|
||||
assert!(
|
||||
blob.contains("• Running on remote-host"),
|
||||
"expected remote host in running header: {blob:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_history_cell_shows_remote_host_after_completion() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
chat.on_task_started();
|
||||
|
||||
let command = vec![
|
||||
"bash".to_string(),
|
||||
"-lc".to_string(),
|
||||
"echo remote".to_string(),
|
||||
];
|
||||
let parsed_cmd = codex_shell_command::parse_command::parse_command(&command);
|
||||
chat.handle_codex_event(Event {
|
||||
id: "call-remote-complete".to_string(),
|
||||
msg: EventMsg::ExecCommandEnd(ExecCommandEndEvent {
|
||||
call_id: "call-remote-complete".to_string(),
|
||||
process_id: Some("proc-remote".to_string()),
|
||||
turn_id: "turn-1".to_string(),
|
||||
command,
|
||||
cwd: std::env::current_dir().unwrap_or_else(|_| PathBuf::from(".")),
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
host_id: Some("remote-host".to_string()),
|
||||
interaction_input: None,
|
||||
stdout: "remote\n".to_string(),
|
||||
stderr: String::new(),
|
||||
aggregated_output: "remote\n".to_string(),
|
||||
exit_code: 0,
|
||||
duration: std::time::Duration::from_millis(5),
|
||||
formatted_output: "remote\n".to_string(),
|
||||
status: CoreExecCommandStatus::Completed,
|
||||
}),
|
||||
});
|
||||
|
||||
let cells = drain_insert_history(&mut rx);
|
||||
assert_eq!(cells.len(), 1, "expected finalized remote exec cell");
|
||||
let blob = lines_to_single_string(&cells[0]);
|
||||
assert!(
|
||||
blob.contains("• Ran on remote-host echo remote"),
|
||||
"expected remote host in completed header: {blob:?}"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn exec_end_without_begin_uses_event_command() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -366,6 +442,7 @@ async fn exec_end_without_begin_uses_event_command() {
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: "done".to_string(),
|
||||
stderr: String::new(),
|
||||
|
||||
@@ -493,6 +493,7 @@ pub(super) fn begin_exec_with_source(
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
host_id: None,
|
||||
interaction_input,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -518,6 +519,7 @@ pub(super) fn begin_unified_exec_startup(
|
||||
cwd,
|
||||
parsed_cmd: Vec::new(),
|
||||
source: ExecCommandSource::UnifiedExecStartup,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
};
|
||||
chat.handle_codex_event(Event {
|
||||
@@ -632,6 +634,7 @@ pub(super) fn end_exec(
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
host_id,
|
||||
interaction_input,
|
||||
process_id,
|
||||
} = begin_event;
|
||||
@@ -645,6 +648,7 @@ pub(super) fn end_exec(
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source,
|
||||
host_id,
|
||||
interaction_input,
|
||||
stdout: stdout.to_string(),
|
||||
stderr: stderr.to_string(),
|
||||
|
||||
@@ -1566,6 +1566,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
cwd: cwd.clone(),
|
||||
parsed_cmd: parsed_cmd.clone(),
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
}),
|
||||
});
|
||||
@@ -1579,6 +1580,7 @@ async fn chatwidget_exec_and_status_layout_vt100_snapshot() {
|
||||
cwd,
|
||||
parsed_cmd,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
interaction_input: None,
|
||||
stdout: String::new(),
|
||||
stderr: String::new(),
|
||||
|
||||
@@ -27,6 +27,7 @@ pub(crate) struct ExecCall {
|
||||
pub(crate) parsed: Vec<ParsedCommand>,
|
||||
pub(crate) output: Option<CommandOutput>,
|
||||
pub(crate) source: ExecCommandSource,
|
||||
pub(crate) host_id: Option<String>,
|
||||
pub(crate) start_time: Option<Instant>,
|
||||
pub(crate) duration: Option<Duration>,
|
||||
pub(crate) interaction_input: Option<String>,
|
||||
@@ -52,6 +53,7 @@ impl ExecCell {
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
source: ExecCommandSource,
|
||||
host_id: Option<String>,
|
||||
interaction_input: Option<String>,
|
||||
) -> Option<Self> {
|
||||
let call = ExecCall {
|
||||
@@ -60,11 +62,18 @@ impl ExecCell {
|
||||
parsed,
|
||||
output: None,
|
||||
source,
|
||||
host_id,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input,
|
||||
};
|
||||
if self.is_exploring_cell() && Self::is_exploring_call(&call) {
|
||||
if self.is_exploring_cell()
|
||||
&& Self::is_exploring_call(&call)
|
||||
&& self
|
||||
.calls
|
||||
.iter()
|
||||
.all(|existing| existing.host_id == call.host_id)
|
||||
{
|
||||
Some(Self {
|
||||
calls: [self.calls.clone(), vec![call]].concat(),
|
||||
animations_enabled: self.animations_enabled,
|
||||
|
||||
@@ -43,6 +43,7 @@ pub(crate) fn new_active_exec_command(
|
||||
command: Vec<String>,
|
||||
parsed: Vec<ParsedCommand>,
|
||||
source: ExecCommandSource,
|
||||
host_id: Option<String>,
|
||||
interaction_input: Option<String>,
|
||||
animations_enabled: bool,
|
||||
) -> ExecCell {
|
||||
@@ -53,6 +54,7 @@ pub(crate) fn new_active_exec_command(
|
||||
parsed,
|
||||
output: None,
|
||||
source,
|
||||
host_id,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input,
|
||||
@@ -61,6 +63,13 @@ pub(crate) fn new_active_exec_command(
|
||||
)
|
||||
}
|
||||
|
||||
fn remote_host_title_suffix(host_id: Option<&str>) -> String {
|
||||
host_id
|
||||
.filter(|host_id| !host_id.trim().is_empty())
|
||||
.map(|host_id| format!(" on {host_id}"))
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn format_unified_exec_interaction(command: &[String], input: Option<&str>) -> String {
|
||||
let command_display = if let Some((_, script)) = extract_bash_command(command) {
|
||||
script.to_string()
|
||||
@@ -261,6 +270,8 @@ impl ExecCell {
|
||||
|
||||
fn exploring_display_lines(&self, width: u16) -> Vec<Line<'static>> {
|
||||
let mut out: Vec<Line<'static>> = Vec::new();
|
||||
let host_suffix =
|
||||
remote_host_title_suffix(self.calls.first().and_then(|call| call.host_id.as_deref()));
|
||||
out.push(Line::from(vec![
|
||||
if self.is_active() {
|
||||
spinner(self.active_start_time(), self.animations_enabled())
|
||||
@@ -269,9 +280,9 @@ impl ExecCell {
|
||||
},
|
||||
" ".into(),
|
||||
if self.is_active() {
|
||||
"Exploring".bold()
|
||||
format!("Exploring{host_suffix}").bold()
|
||||
} else {
|
||||
"Explored".bold()
|
||||
format!("Explored{host_suffix}").bold()
|
||||
},
|
||||
]));
|
||||
|
||||
@@ -375,13 +386,16 @@ impl ExecCell {
|
||||
};
|
||||
let is_interaction = call.is_unified_exec_interaction();
|
||||
let title = if is_interaction {
|
||||
""
|
||||
String::new()
|
||||
} else if self.is_active() {
|
||||
"Running"
|
||||
format!(
|
||||
"Running{}",
|
||||
remote_host_title_suffix(call.host_id.as_deref())
|
||||
)
|
||||
} else if call.is_user_shell_command() {
|
||||
"You ran"
|
||||
"You ran".to_string()
|
||||
} else {
|
||||
"Ran"
|
||||
format!("Ran{}", remote_host_title_suffix(call.host_id.as_deref()))
|
||||
};
|
||||
|
||||
let mut header_line = if is_interaction {
|
||||
@@ -784,6 +798,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: Some(output),
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
start_time: None,
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -933,6 +948,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
start_time: None,
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -970,6 +986,7 @@ mod tests {
|
||||
}],
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: None,
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -1011,6 +1028,7 @@ mod tests {
|
||||
aggregated_output: url.to_string(),
|
||||
}),
|
||||
source: ExecCommandSource::UserShell,
|
||||
host_id: None,
|
||||
start_time: None,
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -1048,6 +1066,7 @@ mod tests {
|
||||
aggregated_output: url.to_string(),
|
||||
}),
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: None,
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
|
||||
@@ -3959,6 +3959,7 @@ mod tests {
|
||||
],
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -3986,6 +3987,7 @@ mod tests {
|
||||
}],
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4005,6 +4007,7 @@ mod tests {
|
||||
path: "shimmer.rs".into(),
|
||||
}],
|
||||
ExecCommandSource::Agent,
|
||||
/*host_id*/ None,
|
||||
/*interaction_input*/ None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -4020,6 +4023,7 @@ mod tests {
|
||||
path: "status_indicator_widget.rs".into(),
|
||||
}],
|
||||
ExecCommandSource::Agent,
|
||||
/*host_id*/ None,
|
||||
/*interaction_input*/ None,
|
||||
)
|
||||
.unwrap();
|
||||
@@ -4055,6 +4059,7 @@ mod tests {
|
||||
],
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4079,6 +4084,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4105,6 +4111,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4129,6 +4136,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4152,6 +4160,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4176,6 +4185,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4200,6 +4210,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
@@ -4250,6 +4261,7 @@ mod tests {
|
||||
parsed: Vec::new(),
|
||||
output: None,
|
||||
source: ExecCommandSource::Agent,
|
||||
host_id: None,
|
||||
start_time: Some(Instant::now()),
|
||||
duration: None,
|
||||
interaction_input: None,
|
||||
|
||||
@@ -1012,6 +1012,7 @@ mod tests {
|
||||
vec!["bash".into(), "-lc".into(), "ls".into()],
|
||||
vec![ParsedCommand::Unknown { cmd: "ls".into() }],
|
||||
ExecCommandSource::Agent,
|
||||
/*host_id*/ None,
|
||||
/*interaction_input*/ None,
|
||||
/*animations_enabled*/ true,
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user