Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion crates/codeoid-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down
17 changes: 17 additions & 0 deletions crates/codeoid-protocol/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -88,6 +88,12 @@ pub struct SessionInfo {
/// Rendered as a "⑃ from <name>·t<atTurn>" tag in the session title.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub forked_from: Option<ForkedFrom>,

/// Git worktree backing this session's workdir (fork isolation / bind).
/// Absent = shares its workdir with no git isolation. Rendered as a
/// "⎇ <branch>" tag in the session title.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree: Option<SessionWorktree>,
}

/// Where a forked session came from — the parent id, the parent's name at
Expand All @@ -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)]
Expand Down
5 changes: 5 additions & 0 deletions crates/codeoid-protocol/tests/wire_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
}),
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2225,6 +2225,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
});
state
}
Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/state/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -766,6 +766,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/state/sessions.rs
Original file line number Diff line number Diff line change
Expand Up @@ -129,6 +129,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
}
}

Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/ui/approval.rs
Original file line number Diff line number Diff line change
Expand Up @@ -291,6 +291,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
});
let mut m = msg(
MessageRole::ToolCall,
Expand Down
38 changes: 38 additions & 0 deletions crates/codeoid-tui/src/ui/scrollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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(" "),
])
}
Expand Down Expand Up @@ -631,6 +642,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
}
}

Expand Down Expand Up @@ -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");
Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/ui/worker.rs
Original file line number Diff line number Diff line change
Expand Up @@ -372,6 +372,7 @@ mod tests {
fallback_model: None,
provider_id: None,
forked_from: None,
worktree: None,
});
state.provider_commands.insert(
"s1".into(),
Expand Down
Loading