diff --git a/code-rs/core/src/config.rs b/code-rs/core/src/config.rs index 4cea21352d5b..979fc5bddfc2 100644 --- a/code-rs/core/src/config.rs +++ b/code-rs/core/src/config.rs @@ -162,6 +162,7 @@ pub struct RemoteInboxConfig { pub bridge_url: Option, pub code_everywhere_url: Option, pub token: Option, + pub host_id: Option, pub host_label: Option, } @@ -1957,6 +1958,7 @@ enabled = true bridge_url = "ws://127.0.0.1:8787/every-code/connect" code_everywhere_url = "http://127.0.0.1:4789" token = "shared-secret" +host_id = "host-mac-studio" host_label = "Mac Studio" "#, ) @@ -1969,6 +1971,7 @@ host_label = "Mac Studio" bridge_url: Some("ws://127.0.0.1:8787/every-code/connect".to_string()), code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: Some("shared-secret".to_string()), + host_id: Some("host-mac-studio".to_string()), host_label: Some("Mac Studio".to_string()), }) ); @@ -1989,6 +1992,7 @@ host_label = "Mac Studio" Some("http://127.0.0.1:4789") ); assert_eq!(resolved.remote_inbox.token.as_deref(), Some("shared-secret")); + assert_eq!(resolved.remote_inbox.host_id.as_deref(), Some("host-mac-studio")); assert_eq!(resolved.remote_inbox.host_label.as_deref(), Some("Mac Studio")); let default_resolved = Config::load_from_base_config_with_overrides( diff --git a/code-rs/tui/src/chatwidget.rs b/code-rs/tui/src/chatwidget.rs index 5789ca4d0525..3bde033b5751 100644 --- a/code-rs/tui/src/chatwidget.rs +++ b/code-rs/tui/src/chatwidget.rs @@ -872,6 +872,19 @@ pub(crate) fn is_test_mode() -> bool { }) } } + +#[cfg(any(test, feature = "test-helpers"))] +fn code_everywhere_pending_work_smoke_enabled() -> bool { + std::env::var("CODE_EVERYWHERE_SMOKE_PENDING_WORK") + .ok() + .map(|value| { + matches!( + value.trim().to_ascii_lowercase().as_str(), + "1" | "true" | "yes" | "on" + ) + }) + .unwrap_or(false) +} use serde_json; use tracing::{debug, info, warn}; // use image::GenericImageView; @@ -13984,6 +13997,7 @@ impl ChatWidget<'_> { // Record session id for potential future fork/backtrack features self.session_id = Some(event.session_id); self.start_remote_inbox_if_needed(event.session_id); + self.seed_code_everywhere_pending_work_if_requested(); self.bottom_pane .set_history_metadata(event.history_log_id, event.history_entry_count); // Record session information at the top of the conversation. @@ -30148,6 +30162,88 @@ Have we met every part of this goal and is there no further work to do?"# ); } + #[cfg(any(test, feature = "test-helpers"))] + fn seed_code_everywhere_pending_work_if_requested(&mut self) { + if !code_everywhere_pending_work_smoke_enabled() { + return; + } + + let Some(client) = &self.remote_inbox_client else { + return; + }; + let turn_id = "ce-smoke-pending-turn"; + client.send_turn_started(turn_id); + + self.handle_exec_approval_now( + "ce-smoke-approval-event".to_string(), + ExecApprovalRequestEvent { + call_id: "ce-smoke-approval".to_string(), + approval_id: Some("ce-smoke-approval".to_string()), + turn_id: turn_id.to_string(), + command: vec!["printf".to_string(), "Code Everywhere approval smoke".to_string()], + cwd: self.config.cwd.clone(), + reason: Some("Code Everywhere approval smoke".to_string()), + network_approval_context: None, + additional_permissions: None, + }, + ); + } + + #[cfg(any(test, feature = "test-helpers"))] + fn seed_code_everywhere_requested_input_smoke(&mut self) { + if !code_everywhere_pending_work_smoke_enabled() + || self.pending_request_user_input.is_some() + { + return; + } + let turn_id = "ce-smoke-pending-turn"; + let input = code_protocol::request_user_input::RequestUserInputEvent { + call_id: "ce-smoke-input".to_string(), + turn_id: turn_id.to_string(), + questions: vec![code_protocol::request_user_input::RequestUserInputQuestion { + id: "mode".to_string(), + header: "Smoke mode".to_string(), + question: "Choose the Code Everywhere smoke mode.".to_string(), + is_other: false, + is_secret: false, + options: Some(vec![ + code_protocol::request_user_input::RequestUserInputQuestionOption { + label: "Continue (Recommended)".to_string(), + description: "Use the deterministic smoke answer.".to_string(), + }, + code_protocol::request_user_input::RequestUserInputQuestionOption { + label: "Pause".to_string(), + description: "Leave this option unused in the smoke.".to_string(), + }, + ]), + }], + }; + self.pending_request_user_input = Some(PendingRequestUserInput { + turn_id: input.turn_id.clone(), + call_id: input.call_id.clone(), + anchor_key: self.next_internal_key(), + questions: input.questions.clone(), + }); + self.bottom_pane + .update_status_text("waiting for user input".to_string()); + self.bottom_pane.set_task_running(true); + self.bottom_pane.ensure_input_focus(); + self.bottom_pane + .show_request_user_input(crate::bottom_pane::RequestUserInputView::new( + input.turn_id.clone(), + input.questions.clone(), + self.app_event_tx.clone(), + )); + self.remote_inbox_send_request_user_input(&input); + self.request_redraw(); + } + + #[cfg(not(any(test, feature = "test-helpers")))] + fn seed_code_everywhere_pending_work_if_requested(&mut self) {} + + #[cfg(not(any(test, feature = "test-helpers")))] + fn seed_code_everywhere_requested_input_smoke(&mut self) {} + pub(crate) fn on_remote_inbox_reply( &mut self, command_id: String, @@ -30269,6 +30365,9 @@ Have we met every part of this goal and is there no further work to do?"# .resolve_approval_decision(&approval_id, decision) { tracing::info!(approval_id, "accepted remote inbox approval decision"); + if matches!(decision, code_core::protocol::ReviewDecision::Approved) { + self.seed_code_everywhere_requested_input_smoke(); + } Ok(()) } else { tracing::warn!(approval_id, "rejected remote inbox approval decision"); diff --git a/code-rs/tui/src/remote_inbox/client.rs b/code-rs/tui/src/remote_inbox/client.rs index 4156b285a977..a49ffce65cd0 100644 --- a/code-rs/tui/src/remote_inbox/client.rs +++ b/code-rs/tui/src/remote_inbox/client.rs @@ -679,6 +679,7 @@ fn spawn_code_everywhere_http_client( command_kind: command.kind(), status: "accepted", reason: None, + accepted_resolution: None, } } else { dispatch_code_everywhere_command(command, &session, &app_event_tx).await @@ -760,6 +761,7 @@ impl RemoteInboxSession { SessionHello { session_id: self.session_id.clone(), session_epoch: self.session_epoch.clone(), + host_id: config.host_id.clone().filter(|id| !id.trim().is_empty()), host_label: config .host_label .clone() @@ -841,8 +843,7 @@ impl CodeEverywhereHttpClient { session: &RemoteInboxSession, outcome: CodeEverywhereCommandOutcome, ) -> Result<(), reqwest::Error> { - self.publish_events(vec![code_everywhere_command_outcome_event(session, outcome)]) - .await + self.publish_events(code_everywhere_command_events(session, outcome)).await } async fn publish_events(&self, events: Vec) -> Result<(), reqwest::Error> { @@ -925,9 +926,8 @@ fn code_everywhere_events_for_client_message( ) -> Vec { let now = chrono::Utc::now().to_rfc3339(); match message { - ClientMessage::Hello(hello) => vec![json!({ - "kind": "session_hello", - "session": { + ClientMessage::Hello(hello) => { + let mut session = json!({ "sessionId": hello.session_id, "sessionEpoch": hello.session_epoch, "hostLabel": hello.host_label, @@ -940,8 +940,16 @@ fn code_everywhere_events_for_client_message( "startedAt": now, "updatedAt": now, "currentTurnId": null, + }); + if let Some(host_id) = hello.host_id { + session["hostId"] = json!(host_id); } - })], + + vec![json!({ + "kind": "session_hello", + "session": session, + })] + } ClientMessage::StatusChanged(status) => status_changed_events(status, now), ClientMessage::TurnStep(step) => vec![turn_step_event(step, now)], ClientMessage::TurnComplete(status) => turn_complete_events(status, now), @@ -1078,9 +1086,22 @@ fn session_status_event(status: SessionStatusEvent, state: &str, updated_at: Str }) } -fn code_everywhere_command_outcome_event( +fn code_everywhere_command_events( session: &RemoteInboxSession, outcome: CodeEverywhereCommandOutcome, +) -> Vec { + let mut events = vec![code_everywhere_command_outcome_event(session, &outcome)]; + if outcome.status == "accepted" { + if let Some(resolution) = outcome.accepted_resolution { + events.push(code_everywhere_pending_work_resolved_event(session, resolution)); + } + } + events +} + +fn code_everywhere_command_outcome_event( + session: &RemoteInboxSession, + outcome: &CodeEverywhereCommandOutcome, ) -> Value { json!({ "kind": "command_outcome", @@ -1096,6 +1117,33 @@ fn code_everywhere_command_outcome_event( }) } +fn code_everywhere_pending_work_resolved_event( + session: &RemoteInboxSession, + resolution: CodeEverywherePendingWorkResolution, +) -> Value { + let resolved_at = chrono::Utc::now().to_rfc3339(); + match resolution { + CodeEverywherePendingWorkResolution::Approval { + approval_id, + decision, + } => json!({ + "kind": "approval_resolved", + "sessionId": session.session_id, + "sessionEpoch": session.session_epoch, + "approvalId": approval_id, + "decision": decision, + "resolvedAt": resolved_at, + }), + CodeEverywherePendingWorkResolution::RequestedInput { input_id } => json!({ + "kind": "user_input_resolved", + "sessionId": session.session_id, + "sessionEpoch": session.session_epoch, + "inputId": input_id, + "resolvedAt": resolved_at, + }), + } +} + fn code_everywhere_question(question: code_protocol::request_user_input::RequestUserInputQuestion) -> Value { json!({ "id": question.id, @@ -1378,6 +1426,8 @@ enum CodeEverywhereCommandPayload { session_id: String, #[serde(rename = "sessionEpoch")] session_epoch: String, + #[serde(rename = "inputId")] + input_id: Option, #[serde(rename = "turnId")] turn_id: String, answers: Vec, @@ -1392,6 +1442,11 @@ struct CodeEverywhereRequestedInputAnswer { } enum CodeEverywhereCommand { + Stale { + id: String, + command_kind: &'static str, + reason: String, + }, Reply { id: String, text: String, @@ -1418,6 +1473,7 @@ enum CodeEverywhereCommand { }, RequestUserInputResponse { id: String, + input_id: Option, turn_id: String, response: RequestUserInputResponse, }, @@ -1428,12 +1484,24 @@ struct CodeEverywhereCommandOutcome { command_kind: &'static str, status: &'static str, reason: Option, + accepted_resolution: Option, +} + +enum CodeEverywherePendingWorkResolution { + Approval { + approval_id: String, + decision: &'static str, + }, + RequestedInput { + input_id: String, + }, } impl CodeEverywhereCommand { fn id(&self) -> &str { match self { - CodeEverywhereCommand::Reply { id, .. } + CodeEverywhereCommand::Stale { id, .. } + | CodeEverywhereCommand::Reply { id, .. } | CodeEverywhereCommand::ContinueAutonomously { id } | CodeEverywhereCommand::PauseCurrentTurn { id } | CodeEverywhereCommand::NewSession { id } @@ -1446,6 +1514,7 @@ impl CodeEverywhereCommand { fn kind(&self) -> &'static str { match self { + CodeEverywhereCommand::Stale { command_kind, .. } => command_kind, CodeEverywhereCommand::Reply { .. } => "reply", CodeEverywhereCommand::ContinueAutonomously { .. } => "continue_autonomously", CodeEverywhereCommand::PauseCurrentTurn { .. } => "pause_current_turn", @@ -1456,57 +1525,90 @@ impl CodeEverywhereCommand { CodeEverywhereCommand::RequestUserInputResponse { .. } => "request_user_input_response", } } + + fn accepted_resolution(&self) -> Option { + match self { + CodeEverywhereCommand::ApprovalDecision { + approval_id, + decision, + .. + } => Some(CodeEverywherePendingWorkResolution::Approval { + approval_id: approval_id.clone(), + decision: match decision { + ReviewDecision::Approved | ReviewDecision::ApprovedForSession => "approve", + ReviewDecision::Denied | ReviewDecision::Abort => "deny", + }, + }), + CodeEverywhereCommand::RequestUserInputResponse { + input_id: Some(input_id), + .. + } => Some(CodeEverywherePendingWorkResolution::RequestedInput { + input_id: input_id.clone(), + }), + _ => None, + } + } } fn parse_code_everywhere_command( record: CodeEverywhereCommandRecord, session: &RemoteInboxSession, ) -> Option { + let command_kind = record.command.kind(); + let (session_id, session_epoch) = record.command.session_scope(); + if !matches_session(session_id, session_epoch, session) { + return Some(CodeEverywhereCommand::Stale { + id: record.id, + command_kind, + reason: stale_command_reason(session_id, session_epoch, session), + }); + } + match record.command { CodeEverywhereCommandPayload::Reply { - session_id, - session_epoch, + session_id: _, + session_epoch: _, content, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::Reply { + } => Some(CodeEverywhereCommand::Reply { id: record.id, text: content, }), CodeEverywhereCommandPayload::ContinueAutonomously { - session_id, - session_epoch, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::ContinueAutonomously { + session_id: _, + session_epoch: _, + } => Some(CodeEverywhereCommand::ContinueAutonomously { id: record.id, }), CodeEverywhereCommandPayload::PauseCurrentTurn { - session_id, - session_epoch, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::PauseCurrentTurn { + session_id: _, + session_epoch: _, + } => Some(CodeEverywhereCommand::PauseCurrentTurn { id: record.id, }), CodeEverywhereCommandPayload::NewSession { - session_id, - session_epoch, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::NewSession { + session_id: _, + session_epoch: _, + } => Some(CodeEverywhereCommand::NewSession { id: record.id, }), CodeEverywhereCommandPayload::EndSession { - session_id, - session_epoch, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::EndSession { + session_id: _, + session_epoch: _, + } => Some(CodeEverywhereCommand::EndSession { id: record.id, }), CodeEverywhereCommandPayload::StatusRequest { - session_id, - session_epoch, - } if matches_session(&session_id, &session_epoch, session) => Some(CodeEverywhereCommand::StatusRequest { + session_id: _, + session_epoch: _, + } => Some(CodeEverywhereCommand::StatusRequest { id: record.id, }), CodeEverywhereCommandPayload::ApprovalDecision { - session_id, - session_epoch, + session_id: _, + session_epoch: _, approval_id, decision, - } if matches_session(&session_id, &session_epoch, session) => { + } => { let decision = match decision.as_str() { "approve" => ReviewDecision::Approved, "deny" => ReviewDecision::Denied, @@ -1519,11 +1621,12 @@ fn parse_code_everywhere_command( }) } CodeEverywhereCommandPayload::RequestUserInputResponse { - session_id, - session_epoch, + session_id: _, + session_epoch: _, + input_id, turn_id, answers, - } if matches_session(&session_id, &session_epoch, session) => { + } => { let response = RequestUserInputResponse { answers: answers .into_iter() @@ -1539,14 +1642,76 @@ fn parse_code_everywhere_command( }; Some(CodeEverywhereCommand::RequestUserInputResponse { id: record.id, + input_id, turn_id, response, }) } - _ => None, } } +impl CodeEverywhereCommandPayload { + fn kind(&self) -> &'static str { + match self { + CodeEverywhereCommandPayload::Reply { .. } => "reply", + CodeEverywhereCommandPayload::ContinueAutonomously { .. } => "continue_autonomously", + CodeEverywhereCommandPayload::PauseCurrentTurn { .. } => "pause_current_turn", + CodeEverywhereCommandPayload::NewSession { .. } => "new_session", + CodeEverywhereCommandPayload::EndSession { .. } => "end_session", + CodeEverywhereCommandPayload::StatusRequest { .. } => "status_request", + CodeEverywhereCommandPayload::ApprovalDecision { .. } => "approval_decision", + CodeEverywhereCommandPayload::RequestUserInputResponse { .. } => "request_user_input_response", + } + } + + fn session_scope(&self) -> (&str, &str) { + match self { + CodeEverywhereCommandPayload::Reply { + session_id, + session_epoch, + .. + } + | CodeEverywhereCommandPayload::ContinueAutonomously { + session_id, + session_epoch, + } + | CodeEverywhereCommandPayload::PauseCurrentTurn { + session_id, + session_epoch, + } + | CodeEverywhereCommandPayload::NewSession { + session_id, + session_epoch, + } + | CodeEverywhereCommandPayload::EndSession { + session_id, + session_epoch, + } + | CodeEverywhereCommandPayload::StatusRequest { + session_id, + session_epoch, + } + | CodeEverywhereCommandPayload::ApprovalDecision { + session_id, + session_epoch, + .. + } + | CodeEverywhereCommandPayload::RequestUserInputResponse { + session_id, + session_epoch, + .. + } => (session_id, session_epoch), + } + } +} + +fn stale_command_reason(session_id: &str, session_epoch: &str, session: &RemoteInboxSession) -> String { + format!( + "stale session scope: command targeted session {session_id} epoch {session_epoch}, active session is {} epoch {}", + session.session_id, session.session_epoch + ) +} + fn matches_session(session_id: &str, session_epoch: &str, session: &RemoteInboxSession) -> bool { if session_id != session.session_id || session_epoch != session.session_epoch { tracing::warn!(session_id, session_epoch, "ignoring stale Code Everywhere command"); @@ -1575,7 +1740,9 @@ async fn dispatch_code_everywhere_command( ) -> CodeEverywhereCommandOutcome { let command_id = command.id().to_string(); let command_kind = command.kind(); + let accepted_resolution = command.accepted_resolution(); let result = match command { + CodeEverywhereCommand::Stale { reason, .. } => Err(reason), CodeEverywhereCommand::Reply { id, text } => { let (response_tx, response_rx) = tokio::sync::oneshot::channel(); if !app_event_tx.send_with_result(AppEvent::RemoteInboxReply { @@ -1653,6 +1820,7 @@ async fn dispatch_code_everywhere_command( } CodeEverywhereCommand::RequestUserInputResponse { id, + input_id: _, turn_id, response, } => { @@ -1678,12 +1846,14 @@ async fn dispatch_code_everywhere_command( command_kind, status: "accepted", reason: None, + accepted_resolution, }, Err(reason) => CodeEverywhereCommandOutcome { command_id, command_kind, status: "rejected", reason: Some(reason), + accepted_resolution: None, }, } } @@ -1723,6 +1893,7 @@ async fn connect_once( &ClientMessage::Hello(SessionHello { session_id: session.session_id.clone(), session_epoch: session.session_epoch.clone(), + host_id: config.host_id.clone().filter(|id| !id.trim().is_empty()), host_label: config .host_label .clone() @@ -2581,6 +2752,7 @@ mod tests { bridge_url: None, code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: None, + host_id: Some("host-mac-studio".to_string()), host_label: Some("Mac Studio".to_string()), }; let hello = ClientMessage::Hello(session.hello(&config)); @@ -2588,6 +2760,7 @@ mod tests { assert_eq!(events[0]["kind"], "session_hello"); assert_eq!(events[0]["session"]["sessionId"], "session-1"); + assert_eq!(events[0]["session"]["hostId"], "host-mac-studio"); assert_eq!(events[0]["session"]["hostLabel"], "Mac Studio"); let approval = ClientMessage::ApprovalRequest(RemoteApprovalRequest { @@ -2613,6 +2786,7 @@ mod tests { bridge_url: None, code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: None, + host_id: None, host_label: Some("Mac Studio".to_string()), }; @@ -2652,6 +2826,7 @@ mod tests { bridge_url: None, code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: None, + host_id: None, host_label: Some("Mac Studio".to_string()), }; let events = code_everywhere_events_for_client_message( @@ -2680,14 +2855,16 @@ mod tests { #[test] fn code_everywhere_maps_command_outcomes() { let session = test_session(); + let outcome = CodeEverywhereCommandOutcome { + command_id: "command-1".to_string(), + command_kind: "status_request", + status: "accepted", + reason: None, + accepted_resolution: None, + }; let event = code_everywhere_command_outcome_event( &session, - CodeEverywhereCommandOutcome { - command_id: "command-1".to_string(), - command_kind: "status_request", - status: "accepted", - reason: None, - }, + &outcome, ); assert_eq!(event["kind"], "command_outcome"); @@ -2699,6 +2876,48 @@ mod tests { assert!(event["outcome"]["reason"].is_null()); } + #[test] + fn code_everywhere_maps_accepted_pending_work_command_resolutions() { + let session = test_session(); + let approval_events = code_everywhere_command_events( + &session, + CodeEverywhereCommandOutcome { + command_id: "command-approval".to_string(), + command_kind: "approval_decision", + status: "accepted", + reason: None, + accepted_resolution: Some(CodeEverywherePendingWorkResolution::Approval { + approval_id: "approval-1".to_string(), + decision: "approve", + }), + }, + ); + + assert_eq!(approval_events.len(), 2); + assert_eq!(approval_events[0]["kind"], "command_outcome"); + assert_eq!(approval_events[1]["kind"], "approval_resolved"); + assert_eq!(approval_events[1]["approvalId"], "approval-1"); + assert_eq!(approval_events[1]["decision"], "approve"); + + let input_events = code_everywhere_command_events( + &session, + CodeEverywhereCommandOutcome { + command_id: "command-input".to_string(), + command_kind: "request_user_input_response", + status: "accepted", + reason: None, + accepted_resolution: Some(CodeEverywherePendingWorkResolution::RequestedInput { + input_id: "input-1".to_string(), + }), + }, + ); + + assert_eq!(input_events.len(), 2); + assert_eq!(input_events[0]["kind"], "command_outcome"); + assert_eq!(input_events[1]["kind"], "user_input_resolved"); + assert_eq!(input_events[1]["inputId"], "input-1"); + } + #[test] fn code_everywhere_republish_includes_latest_status_snapshot() { let session = test_session(); @@ -2707,6 +2926,7 @@ mod tests { bridge_url: None, code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: None, + host_id: None, host_label: Some("Mac Studio".to_string()), }; let latest_status_snapshot = Arc::new(Mutex::new(Some(ClientMessage::TurnComplete( @@ -2732,6 +2952,7 @@ mod tests { bridge_url: None, code_everywhere_url: Some("http://127.0.0.1:4789".to_string()), token: None, + host_id: None, host_label: Some("Mac Studio".to_string()), }; let latest_status_snapshot = Arc::new(Mutex::new(None)); @@ -2765,7 +2986,14 @@ mod tests { parse_code_everywhere_command(reply, &session), Some(CodeEverywhereCommand::Reply { id, text }) if id == "command-1" && text == "keep going" )); - assert!(parse_code_everywhere_command(stale, &session).is_none()); + assert!(matches!( + parse_code_everywhere_command(stale, &session), + Some(CodeEverywhereCommand::Stale { id, command_kind, reason }) + if id == "command-2" + && command_kind == "status_request" + && reason.contains("old-epoch") + && reason.contains("epoch-1") + )); } #[test] @@ -2779,6 +3007,7 @@ mod tests { "kind": "request_user_input_response", "sessionId": "session-1", "sessionEpoch": "epoch-1", + "inputId": "input-1", "turnId": "turn-1", "answers": [ { "questionId": "question-1", "value": "Ship it" } @@ -2791,8 +3020,8 @@ mod tests { assert!(matches!( parse_code_everywhere_command(claim.commands.into_iter().next().unwrap(), &session), - Some(CodeEverywhereCommand::RequestUserInputResponse { response, .. }) - if response.answers.contains_key("question-1") + Some(CodeEverywhereCommand::RequestUserInputResponse { input_id, response, .. }) + if input_id.as_deref() == Some("input-1") && response.answers.contains_key("question-1") )); } diff --git a/code-rs/tui/src/remote_inbox/protocol.rs b/code-rs/tui/src/remote_inbox/protocol.rs index 03b334fc1500..112dc7a87843 100644 --- a/code-rs/tui/src/remote_inbox/protocol.rs +++ b/code-rs/tui/src/remote_inbox/protocol.rs @@ -26,6 +26,8 @@ pub(crate) enum ClientMessage { pub(crate) struct SessionHello { pub session_id: String, pub session_epoch: String, + #[serde(skip_serializing_if = "Option::is_none")] + pub host_id: Option, pub host_label: String, pub cwd: String, pub branch: Option, @@ -176,6 +178,7 @@ mod tests { let message = ClientMessage::Hello(SessionHello { session_id: "session-1".to_string(), session_epoch: "epoch-1".to_string(), + host_id: Some("host-mac-studio".to_string()), host_label: "Mac Studio".to_string(), cwd: "/tmp/project".to_string(), branch: Some("main".to_string()), @@ -189,6 +192,7 @@ mod tests { "type": "hello", "session_id": "session-1", "session_epoch": "epoch-1", + "host_id": "host-mac-studio", "host_label": "Mac Studio", "cwd": "/tmp/project", "branch": "main",