diff --git a/crates/maestro-cli/src/sessions.rs b/crates/maestro-cli/src/sessions.rs index b5a8652..21296a5 100644 --- a/crates/maestro-cli/src/sessions.rs +++ b/crates/maestro-cli/src/sessions.rs @@ -254,6 +254,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } diff --git a/crates/maestro-daemon/src/session.rs b/crates/maestro-daemon/src/session.rs index 7d17d38..e8f6a4e 100644 --- a/crates/maestro-daemon/src/session.rs +++ b/crates/maestro-daemon/src/session.rs @@ -478,6 +478,7 @@ impl SessionManager { // fresh tree has nothing in the way anyway. blocked_work: None, last_focused_at: None, + acp_session_id: None, view: req.view, }; @@ -684,6 +685,7 @@ impl SessionManager { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, // Unknown on adoption without a sidecar; matches every // other field's forward-compat default. view: maestro_protocol::SessionView::Terminal, diff --git a/crates/maestro-daemon/tests/sidecar.rs b/crates/maestro-daemon/tests/sidecar.rs index 4aa7ff2..f942e2d 100644 --- a/crates/maestro-daemon/tests/sidecar.rs +++ b/crates/maestro-daemon/tests/sidecar.rs @@ -32,6 +32,7 @@ fn sample(id: Uuid) -> Session { current_cwd: Some(PathBuf::from("/tmp/work/crates")), blocked_work: None, last_focused_at: None, + acp_session_id: None, view: SessionView::Terminal, } } diff --git a/crates/maestro-protocol/src/display.rs b/crates/maestro-protocol/src/display.rs index f7d38b8..d535c69 100644 --- a/crates/maestro-protocol/src/display.rs +++ b/crates/maestro-protocol/src/display.rs @@ -285,6 +285,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } diff --git a/crates/maestro-protocol/src/lib.rs b/crates/maestro-protocol/src/lib.rs index ead867b..ad38b97 100644 --- a/crates/maestro-protocol/src/lib.rs +++ b/crates/maestro-protocol/src/lib.rs @@ -123,6 +123,15 @@ pub struct Session { /// daemon that predates the field keeps working untouched. #[serde(default)] pub view: SessionView, + /// The ACP session id the `claude-code-acp` adapter handed back from + /// `session/new` (#134). Only a [`SessionView::Structured`] session has + /// one, and only once its handshake succeeded; a later respawn passes it + /// to `session/load` so the conversation continues (#135). Distinct from + /// the hook-reported Claude Code conversation id (#161): that one is what + /// `claude --resume` takes, this one is what the ACP adapter takes, and + /// the two are minted by different processes with different lifetimes. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub acp_session_id: Option, } /// A session's process shape (#133): a plain terminal, or a structured agent @@ -886,6 +895,7 @@ mod tests { blocked_work: None, last_focused_at: None, view: SessionView::Terminal, + acp_session_id: None, } } @@ -1134,6 +1144,29 @@ mod tests { assert_eq!(structured, round_trip(&structured)); } + #[test] + fn acp_session_id_defaults_to_none_and_round_trips() { + // A payload from before #134 carries no `acp_session_id`; it must + // read as `None`, stay off the wire while unset, and round-trip + // once the adapter has handed one back. + let json = serde_json::json!({ + "id": Uuid::from_u128(1), + "cwd": "/tmp", + "command": "bash", + "status": "Running", + "created_at": "2026-05-24T12:00:00Z" + }); + let s: Session = serde_json::from_value(json).unwrap(); + assert_eq!(s.acp_session_id, None); + let serialized = serde_json::to_value(&s).unwrap(); + assert!(serialized.get("acp_session_id").is_none()); + + let mut structured = sample_session(); + structured.view = SessionView::Structured; + structured.acp_session_id = Some("sess_01HZX".into()); + assert_eq!(structured, round_trip(&structured)); + } + /// A client that predates the new/existing discriminator sends no /// `create_branch`; the daemon must read that as "create a new branch" — /// exactly what every payload meant before the field existed (#56). diff --git a/crates/maestro-testing/src/lib.rs b/crates/maestro-testing/src/lib.rs index 35c233d..30c8535 100644 --- a/crates/maestro-testing/src/lib.rs +++ b/crates/maestro-testing/src/lib.rs @@ -48,6 +48,7 @@ pub fn session(id: Uuid, cwd: &str, command: &str) -> Session { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } diff --git a/crates/maestro-tui/src/app.rs b/crates/maestro-tui/src/app.rs index b52d4fd..d62a605 100644 --- a/crates/maestro-tui/src/app.rs +++ b/crates/maestro-tui/src/app.rs @@ -1899,6 +1899,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, }) .collect(), }); @@ -2081,6 +2082,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, }], }); app.dispatch(Action::SidebarNext); @@ -2109,6 +2111,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } @@ -3219,6 +3222,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: focused, + acp_session_id: None, }; let older = Utc::now() - chrono::Duration::seconds(60); app.catalog.apply_event(CatalogEvent::Snapshot { @@ -3498,6 +3502,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, }], }); app.dispatch(Action::KillHighlighted); @@ -3534,6 +3539,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, }], }); } diff --git a/crates/maestro-tui/src/catalog.rs b/crates/maestro-tui/src/catalog.rs index 9a78c84..4a42e4f 100644 --- a/crates/maestro-tui/src/catalog.rs +++ b/crates/maestro-tui/src/catalog.rs @@ -224,6 +224,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } diff --git a/crates/maestro-tui/src/command_bar.rs b/crates/maestro-tui/src/command_bar.rs index 883e3d1..095dee7 100644 --- a/crates/maestro-tui/src/command_bar.rs +++ b/crates/maestro-tui/src/command_bar.rs @@ -810,6 +810,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } diff --git a/crates/maestro-tui/src/sidebar.rs b/crates/maestro-tui/src/sidebar.rs index c22d3aa..c91e94f 100644 --- a/crates/maestro-tui/src/sidebar.rs +++ b/crates/maestro-tui/src/sidebar.rs @@ -501,6 +501,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, } } @@ -1268,6 +1269,7 @@ mod tests { current_cwd: None, blocked_work: None, last_focused_at: None, + acp_session_id: None, }; let sb = Sidebar { sessions: vec![s], diff --git a/web/src/types.ts b/web/src/types.ts index 091883e..3870c7d 100644 --- a/web/src/types.ts +++ b/web/src/types.ts @@ -56,6 +56,10 @@ export interface Session { * how the command bar's empty query sorts a never-focused session after every * session that has one (`docs/COMMAND-BAR.md`). */ last_focused_at?: IsoTimestamp | null; + /** The ACP session id the `claude-code-acp` adapter returned from + * `session/new` (#134). Only a `Structured` session whose handshake + * succeeded has one; a respawn passes it to `session/load` (#135). */ + acp_session_id?: string | null; } /** A session's daemon-created worktree (`SessionWorktree` on the wire). */