From 8af8a0eda0851d0f91315a9bc4ee9a04001c311f Mon Sep 17 00:00:00 2001 From: Yash Datta Date: Mon, 13 Jul 2026 23:21:57 +0800 Subject: [PATCH] feat: show the isolated-worktree branch in the TUI session title MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit TUI parity for the web WorktreeChip (codeoid daemon now runs forked/isolated sessions in their own git worktree). SessionInfo gains an optional `worktree { path, branch, createdByCodeoid }` (additive, camelCase on the wire), and session_title renders a green "⎇ " tag next to the fork-lineage chip — so a fork running in its own worktree is self-describing. Tests: session_title_shows_worktree (chip present with branch / absent without); wire_format sample now carries a worktree so the no-snake-case-keys check validates the camelCase serde (createdByCodeoid). Updated the SessionInfo test fixtures across the crates for the new field. cargo test --workspace green, fmt clean. Co-Authored-By: Claude Opus 4.8 --- crates/codeoid-protocol/src/lib.rs | 3 +- crates/codeoid-protocol/src/session.rs | 17 +++++++++ crates/codeoid-protocol/tests/wire_format.rs | 5 +++ crates/codeoid-tui/src/app.rs | 1 + crates/codeoid-tui/src/state/mod.rs | 1 + crates/codeoid-tui/src/state/sessions.rs | 1 + crates/codeoid-tui/src/ui/approval.rs | 1 + crates/codeoid-tui/src/ui/scrollback.rs | 38 ++++++++++++++++++++ crates/codeoid-tui/src/ui/worker.rs | 1 + 9 files changed, 67 insertions(+), 1 deletion(-) diff --git a/crates/codeoid-protocol/src/lib.rs b/crates/codeoid-protocol/src/lib.rs index ea4f722..1902206 100644 --- a/crates/codeoid-protocol/src/lib.rs +++ b/crates/codeoid-protocol/src/lib.rs @@ -48,7 +48,8 @@ pub use message::{ ContentPart, IdentityType, MessageIdentity, MessageRole, SessionMessage, SessionMessageDelta, }; pub use session::{ - ForkedFrom, SessionInfo, SessionMode, SessionStatus, SessionUsage, Subagent, TurnUsage, + ForkedFrom, SessionInfo, SessionMode, SessionStatus, SessionUsage, SessionWorktree, Subagent, + TurnUsage, }; pub use tool::{CancelReason, ConfirmedBy, ToolInfo, ToolPhase, ToolState}; diff --git a/crates/codeoid-protocol/src/session.rs b/crates/codeoid-protocol/src/session.rs index 5ac9717..3cdd526 100644 --- a/crates/codeoid-protocol/src/session.rs +++ b/crates/codeoid-protocol/src/session.rs @@ -88,6 +88,12 @@ pub struct SessionInfo { /// Rendered as a "⑃ from ·t" tag in the session title. #[serde(default, skip_serializing_if = "Option::is_none")] pub forked_from: Option, + + /// Git worktree backing this session's workdir (fork isolation / bind). + /// Absent = shares its workdir with no git isolation. Rendered as a + /// "⎇ " tag in the session title. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub worktree: Option, } /// Where a forked session came from — the parent id, the parent's name at @@ -101,6 +107,17 @@ pub struct ForkedFrom { pub at_turn: u32, } +/// A git worktree backing a session's workdir. `created_by_codeoid` marks a +/// worktree codeoid created for fork isolation (removed on destroy) versus one +/// the user bound (never touched). +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct SessionWorktree { + pub path: String, + pub branch: String, + pub created_by_codeoid: bool, +} + /// Rotation telemetry — how many times the backing Claude Code session has /// been rolled over to avoid context compaction. #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/crates/codeoid-protocol/tests/wire_format.rs b/crates/codeoid-protocol/tests/wire_format.rs index b2b305f..5556a83 100644 --- a/crates/codeoid-protocol/tests/wire_format.rs +++ b/crates/codeoid-protocol/tests/wire_format.rs @@ -113,6 +113,11 @@ fn sample_session_info() -> SessionInfo { name: "Sandbox".into(), at_turn: 12, }), + worktree: Some(codeoid_protocol::SessionWorktree { + path: "/repo-worktrees/fix-a1b2".into(), + branch: "codeoid/fix-a1b2".into(), + created_by_codeoid: true, + }), } } diff --git a/crates/codeoid-tui/src/app.rs b/crates/codeoid-tui/src/app.rs index 6b58540..721db61 100644 --- a/crates/codeoid-tui/src/app.rs +++ b/crates/codeoid-tui/src/app.rs @@ -2225,6 +2225,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, }); state } diff --git a/crates/codeoid-tui/src/state/mod.rs b/crates/codeoid-tui/src/state/mod.rs index 8684895..0b48136 100644 --- a/crates/codeoid-tui/src/state/mod.rs +++ b/crates/codeoid-tui/src/state/mod.rs @@ -766,6 +766,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, } } diff --git a/crates/codeoid-tui/src/state/sessions.rs b/crates/codeoid-tui/src/state/sessions.rs index e7a40bc..e7ad9a5 100644 --- a/crates/codeoid-tui/src/state/sessions.rs +++ b/crates/codeoid-tui/src/state/sessions.rs @@ -129,6 +129,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, } } diff --git a/crates/codeoid-tui/src/ui/approval.rs b/crates/codeoid-tui/src/ui/approval.rs index c6e80ed..102113b 100644 --- a/crates/codeoid-tui/src/ui/approval.rs +++ b/crates/codeoid-tui/src/ui/approval.rs @@ -291,6 +291,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, }); let mut m = msg( MessageRole::ToolCall, diff --git a/crates/codeoid-tui/src/ui/scrollback.rs b/crates/codeoid-tui/src/ui/scrollback.rs index 7a51992..87a21ff 100644 --- a/crates/codeoid-tui/src/ui/scrollback.rs +++ b/crates/codeoid-tui/src/ui/scrollback.rs @@ -353,6 +353,17 @@ fn session_title(session: &SessionInfo) -> Line<'static> { .fg(Color::Magenta) .add_modifier(Modifier::ITALIC), ), + // Isolated git worktree — mirrors the web WorktreeChip. Shows the + // branch, so a fork running in its own worktree is self-describing. + Span::styled( + match &session.worktree { + Some(w) => format!(" · ⎇ {}", truncate_name(&w.branch, 24)), + None => String::new(), + }, + Style::default() + .fg(Color::Green) + .add_modifier(Modifier::ITALIC), + ), Span::raw(" "), ]) } @@ -631,6 +642,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, } } @@ -969,6 +981,32 @@ mod tests { assert!(!title.contains("⑃"), "{title}"); } + #[test] + fn session_title_shows_worktree() { + let mut session = mk_session("wt-1"); + session.worktree = Some(codeoid_protocol::SessionWorktree { + path: "/repo-worktrees/fix-a1b2".into(), + branch: "codeoid/fix-a1b2".into(), + created_by_codeoid: true, + }); + let title: String = session_title(&session) + .spans + .iter() + .map(|sp| sp.content.clone().into_owned()) + .collect(); + assert!(title.contains("⎇"), "{title}"); + assert!(title.contains("codeoid/fix-a1b2"), "{title}"); + + // A session without a worktree shows no worktree tag. + let plain = mk_session("s2"); + let title: String = session_title(&plain) + .spans + .iter() + .map(|sp| sp.content.clone().into_owned()) + .collect(); + assert!(!title.contains("⎇"), "{title}"); + } + #[test] fn truncate_name_caps_long_parent_names() { assert_eq!(truncate_name("short", 20), "short"); diff --git a/crates/codeoid-tui/src/ui/worker.rs b/crates/codeoid-tui/src/ui/worker.rs index 3c04dad..7b7760b 100644 --- a/crates/codeoid-tui/src/ui/worker.rs +++ b/crates/codeoid-tui/src/ui/worker.rs @@ -372,6 +372,7 @@ mod tests { fallback_model: None, provider_id: None, forked_from: None, + worktree: None, }); state.provider_commands.insert( "s1".into(),