diff --git a/codex-rs/app-server-protocol/schema/json/ServerNotification.json b/codex-rs/app-server-protocol/schema/json/ServerNotification.json index 4f5410c782..16a030e3da 100644 --- a/codex-rs/app-server-protocol/schema/json/ServerNotification.json +++ b/codex-rs/app-server-protocol/schema/json/ServerNotification.json @@ -2865,6 +2865,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json index e4ced5070c..02715050df 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.schemas.json @@ -13127,6 +13127,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json index d1f3cbda55..e74d3568e4 100644 --- a/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json +++ b/codex-rs/app-server-protocol/schema/json/codex_app_server_protocol.v2.schemas.json @@ -10982,6 +10982,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json index 2883670c88..5bf26c1396 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemCompletedNotification.json @@ -684,6 +684,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json index c2e71ccba9..ef4b58811e 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ItemStartedNotification.json @@ -684,6 +684,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json index a7fe2e8d60..75a3a34717 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ReviewStartResponse.json @@ -827,6 +827,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json index 9b1870198a..efb40b5219 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadForkResponse.json @@ -1341,6 +1341,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json index 426f34ce35..27aa211fa1 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadListResponse.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json index c869a79749..22095e8605 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadMetadataUpdateResponse.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json index 9569860c38..805598de49 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadReadResponse.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json index 5143545ec1..b6c1fe9361 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadResumeResponse.json @@ -1341,6 +1341,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json index 502dd3961f..da605b1ddb 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadRollbackResponse.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json index c07b4d8258..4e5a2e9f70 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartResponse.json @@ -1341,6 +1341,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json index ff87af2069..e239288d56 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadStartedNotification.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json index daf821c374..f0b7c1b8f6 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/ThreadUnarchiveResponse.json @@ -1099,6 +1099,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json index 82c2b3c76c..b80896d8ca 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnCompletedNotification.json @@ -827,6 +827,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json index ebb2065cb8..b2bf7887ed 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartResponse.json @@ -827,6 +827,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json index 8b7c2bc410..338b1d57e3 100644 --- a/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json +++ b/codex-rs/app-server-protocol/schema/json/v2/TurnStartedNotification.json @@ -827,6 +827,14 @@ "null" ] }, + "hostId": { + "default": null, + "description": "The configured exec host, when the command ran outside the local host.", + "type": [ + "string", + "null" + ] + }, "id": { "type": "string" }, diff --git a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts index 9202f3728f..a47c30cbcc 100644 --- a/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts +++ b/codex-rs/app-server-protocol/schema/typescript/v2/ThreadItem.ts @@ -34,7 +34,11 @@ cwd: string, /** * Identifier for the underlying PTY process (when available). */ -processId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus, +processId: string | null, +/** + * The configured exec host, when the command ran outside the local host. + */ +hostId: string | null, source: CommandExecutionSource, status: CommandExecutionStatus, /** * A best-effort parsing of the command to understand the action(s) it will perform. * This returns a list of CommandAction objects because a single shell command may diff --git a/codex-rs/app-server-protocol/src/protocol/item_builders.rs b/codex-rs/app-server-protocol/src/protocol/item_builders.rs index 9853f69f40..80520b13ad 100644 --- a/codex-rs/app-server-protocol/src/protocol/item_builders.rs +++ b/codex-rs/app-server-protocol/src/protocol/item_builders.rs @@ -71,6 +71,7 @@ pub fn build_command_execution_approval_request_item( command: shlex_join(&payload.command), cwd: payload.cwd.clone(), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::InProgress, command_actions: payload @@ -91,6 +92,7 @@ pub fn build_command_execution_begin_item(payload: &ExecCommandBeginEvent) -> Th command: shlex_join(&payload.command), cwd: payload.cwd.clone(), process_id: payload.process_id.clone(), + host_id: payload.host_id.clone(), source: payload.source.into(), status: CommandExecutionStatus::InProgress, command_actions: payload @@ -118,6 +120,7 @@ pub fn build_command_execution_end_item(payload: &ExecCommandEndEvent) -> Thread command: shlex_join(&payload.command), cwd: payload.cwd.clone(), process_id: payload.process_id.clone(), + host_id: payload.host_id.clone(), source: payload.source.into(), status: (&payload.status).into(), command_actions: payload @@ -151,6 +154,7 @@ pub fn build_item_from_guardian_event( command, cwd: cwd.clone(), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status, command_actions, @@ -183,6 +187,7 @@ pub fn build_item_from_guardian_event( command, cwd: cwd.clone(), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status, command_actions, diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 80ad17ba77..24770b425b 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -1837,6 +1837,7 @@ mod tests { command: "echo 'hello world'".into(), cwd: PathBuf::from("/tmp"), process_id: Some("pid-1".into()), + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Completed, command_actions: vec![CommandAction::Unknown { @@ -2050,6 +2051,7 @@ mod tests { command: "ls".into(), cwd: PathBuf::from("/tmp"), process_id: Some("pid-2".into()), + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Declined, command_actions: vec![CommandAction::Unknown { @@ -2135,6 +2137,7 @@ mod tests { command: "rm -rf /tmp/guardian".into(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Declined, command_actions: vec![CommandAction::Unknown { @@ -2194,6 +2197,7 @@ mod tests { command: "/bin/rm -f /tmp/file.sqlite".into(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::InProgress, command_actions: vec![CommandAction::Unknown { @@ -2284,6 +2288,7 @@ mod tests { command: "echo done".into(), cwd: PathBuf::from("/tmp"), process_id: Some("pid-42".into()), + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::Completed, command_actions: vec![CommandAction::Unknown { diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 66ac1a6020..13b01cce4c 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -4409,6 +4409,9 @@ pub enum ThreadItem { cwd: PathBuf, /// Identifier for the underlying PTY process (when available). process_id: Option, + /// The configured exec host, when the command ran outside the local host. + #[serde(default)] + host_id: Option, #[serde(default)] source: CommandExecutionSource, status: CommandExecutionStatus, diff --git a/codex-rs/app-server-protocol/src/schema_fixtures.rs b/codex-rs/app-server-protocol/src/schema_fixtures.rs index 56dcf33a4d..2ecdfe80ec 100644 --- a/codex-rs/app-server-protocol/src/schema_fixtures.rs +++ b/codex-rs/app-server-protocol/src/schema_fixtures.rs @@ -133,7 +133,7 @@ fn read_file_bytes(path: &Path) -> Result> { // fixture test is platform-independent. let text = String::from_utf8(bytes) .with_context(|| format!("expected UTF-8 TypeScript in {}", path.display()))?; - let text = text.replace("\r\n", "\n").replace('\r', "\n"); + let text = normalize_typescript_fixture_text(&text); // Fixture comparisons care about schema content, not whether the generator // re-prepended the standard banner to every TypeScript file. let text = text @@ -276,10 +276,7 @@ fn collect_typescript_fixture_file( let contents = T::export_to_string().context("export TypeScript fixture content")?; let output_path = normalize_relative_fixture_path(&output_path); - files.insert( - output_path, - contents.replace("\r\n", "\n").replace('\r', "\n"), - ); + files.insert(output_path, normalize_typescript_fixture_text(&contents)); let mut visitor = TypeScriptFixtureCollector { files, @@ -294,6 +291,20 @@ fn collect_typescript_fixture_file( Ok(()) } +fn normalize_typescript_fixture_text(text: &str) -> String { + let text = text.replace("\r\n", "\n").replace('\r', "\n"); + let mut normalized = String::with_capacity(text.len()); + for line in text.split_inclusive('\n') { + if let Some(line) = line.strip_suffix('\n') { + normalized.push_str(line.trim_end_matches(|ch| ch == ' ' || ch == '\t')); + normalized.push('\n'); + } else { + normalized.push_str(line.trim_end_matches(|ch| ch == ' ' || ch == '\t')); + } + } + normalized +} + fn normalize_relative_fixture_path(path: &Path) -> PathBuf { path.components().collect() } diff --git a/codex-rs/app-server/src/bespoke_event_handling.rs b/codex-rs/app-server/src/bespoke_event_handling.rs index ce71e8974d..7dd2427af9 100644 --- a/codex-rs/app-server/src/bespoke_event_handling.rs +++ b/codex-rs/app-server/src/bespoke_event_handling.rs @@ -1618,6 +1618,7 @@ pub(crate) async fn apply_bespoke_event_handling( let command = shlex_join(&exec_command_begin_event.command); let cwd = exec_command_begin_event.cwd; let process_id = exec_command_begin_event.process_id; + let host_id = exec_command_begin_event.host_id; let first_start = { let mut state = thread_state.lock().await; state @@ -1631,6 +1632,7 @@ pub(crate) async fn apply_bespoke_event_handling( command, cwd, process_id, + host_id, source: exec_command_begin_event.source.into(), status: CommandExecutionStatus::InProgress, command_actions, @@ -2002,6 +2004,7 @@ async fn start_command_execution_item( command, cwd, process_id: None, + host_id: None, source, status: CommandExecutionStatus::InProgress, command_actions, @@ -2046,6 +2049,7 @@ async fn complete_command_execution_item( command, cwd, process_id, + host_id: None, source, status, command_actions, @@ -3228,6 +3232,7 @@ mod tests { command: completion_item.command.clone(), cwd: completion_item.cwd.clone(), process_id: None, + host_id: None, source: CommandExecutionSource::Agent, status: CommandExecutionStatus::InProgress, command_actions: completion_item.command_actions.clone(), diff --git a/codex-rs/tui/src/app/app_server_adapter.rs b/codex-rs/tui/src/app/app_server_adapter.rs index e1e62c6d62..190a1f0c37 100644 --- a/codex-rs/tui/src/app/app_server_adapter.rs +++ b/codex-rs/tui/src/app/app_server_adapter.rs @@ -900,6 +900,7 @@ fn command_execution_started_event(turn_id: &str, item: &ThreadItem) -> Option Option Option command, cwd, process_id, + host_id, source, status, command_actions, @@ -983,7 +985,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, + host_id: host_id.clone(), interaction_input: None, stdout: String::new(), stderr: String::new(), @@ -1183,6 +1185,7 @@ mod tests { command: "printf 'hello world\\n'".to_string(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: CommandExecutionSource::UserShell, status: CommandExecutionStatus::InProgress, command_actions: vec![CommandAction::Unknown { @@ -1239,6 +1242,7 @@ mod tests { command: "printf 'hello world\\n'".to_string(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: CommandExecutionSource::UserShell, status: CommandExecutionStatus::Completed, command_actions: vec![CommandAction::Unknown { @@ -1276,6 +1280,7 @@ mod tests { command: r#"C:\Program Files\Git\bin\bash.exe -lc "echo hi""#.to_string(), cwd: PathBuf::from("C:\\repo"), process_id: None, + host_id: None, source: CommandExecutionSource::UserShell, status: CommandExecutionStatus::InProgress, command_actions: vec![], @@ -1324,6 +1329,7 @@ mod tests { command: "printf 'hello world\\n'".to_string(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: CommandExecutionSource::UserShell, status: CommandExecutionStatus::Completed, command_actions: vec![CommandAction::Unknown { diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index e4e8d93457..c33edceba4 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -6195,6 +6195,7 @@ impl ChatWidget { command, cwd, process_id, + host_id, source, status, command_actions, @@ -6217,7 +6218,7 @@ impl ChatWidget { .map(codex_app_server_protocol::CommandAction::into_core) .collect(), source: source.to_core(), - host_id: None, + host_id, interaction_input: None, }); } else { @@ -6233,7 +6234,7 @@ impl ChatWidget { .map(codex_app_server_protocol::CommandAction::into_core) .collect(), source: source.to_core(), - host_id: None, + host_id, interaction_input: None, stdout: String::new(), stderr: String::new(), @@ -6790,6 +6791,7 @@ impl ChatWidget { command, cwd, process_id, + host_id, source, command_actions, .. @@ -6805,7 +6807,7 @@ impl ChatWidget { .map(codex_app_server_protocol::CommandAction::into_core) .collect(), source: source.to_core(), - host_id: None, + host_id, interaction_input: None, }); } diff --git a/codex-rs/tui/src/chatwidget/tests/app_server.rs b/codex-rs/tui/src/chatwidget/tests/app_server.rs index 085e4976d4..825ae5062f 100644 --- a/codex-rs/tui/src/chatwidget/tests/app_server.rs +++ b/codex-rs/tui/src/chatwidget/tests/app_server.rs @@ -193,6 +193,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { command: command.clone(), cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: AppServerCommandExecutionSource::UserShell, status: AppServerCommandExecutionStatus::InProgress, command_actions: vec![AppServerCommandAction::Unknown { @@ -214,6 +215,7 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { command, cwd: PathBuf::from("/tmp"), process_id: None, + host_id: None, source: AppServerCommandExecutionSource::UserShell, status: AppServerCommandExecutionStatus::Completed, command_actions: vec![AppServerCommandAction::Unknown { @@ -240,6 +242,51 @@ async fn live_app_server_command_execution_strips_shell_wrapper() { ); } +#[tokio::test] +async fn live_app_server_command_execution_shows_remote_host() { + let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.on_task_started(); + + let script = "echo remote"; + let command = + shlex::try_join(["/bin/zsh", "-lc", script]).expect("round-trippable shell wrapper"); + + chat.handle_server_notification( + ServerNotification::ItemCompleted(ItemCompletedNotification { + thread_id: "thread-1".to_string(), + turn_id: "turn-1".to_string(), + item: AppServerThreadItem::CommandExecution { + id: "cmd-remote".to_string(), + command, + cwd: PathBuf::from("/Users/nlieb"), + process_id: Some("proc-remote".to_string()), + host_id: Some("mac".to_string()), + source: AppServerCommandExecutionSource::UnifiedExecStartup, + status: AppServerCommandExecutionStatus::Completed, + command_actions: vec![AppServerCommandAction::Unknown { + command: script.to_string(), + }], + aggregated_output: Some("remote\n".to_string()), + exit_code: Some(0), + duration_ms: Some(5), + }, + }), + /*replay_kind*/ None, + ); + + let cells = drain_insert_history(&mut rx); + assert_eq!( + cells.len(), + 1, + "expected one completed command history cell" + ); + let blob = lines_to_single_string(cells.first().expect("command cell")); + assert!( + blob.contains("• Ran on mac echo remote"), + "expected remote host in app-server command row: {blob:?}" + ); +} + #[test] fn app_server_patch_changes_to_core_preserves_diffs() { let changes = app_server_patch_changes_to_core(vec![FileUpdateChange {