diff --git a/crates/codeoid-protocol/src/lib.rs b/crates/codeoid-protocol/src/lib.rs index 4b3cfdf..aafdd83 100644 --- a/crates/codeoid-protocol/src/lib.rs +++ b/crates/codeoid-protocol/src/lib.rs @@ -52,8 +52,8 @@ pub use message::{ ContentPart, IdentityType, MessageIdentity, MessageRole, SessionMessage, SessionMessageDelta, }; pub use session::{ - CollaborationConfig, CollaborationRole, ForkedFrom, SessionInfo, SessionMode, SessionStatus, - SessionUsage, SessionWorktree, Subagent, TurnUsage, + CollaborationConfig, CollaborationRole, CollaborationRoleRef, 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 953f5da..1384456 100644 --- a/crates/codeoid-protocol/src/session.rs +++ b/crates/codeoid-protocol/src/session.rs @@ -101,6 +101,12 @@ pub struct SessionInfo { /// restart the way `role`/`provider_id` already do. #[serde(default, skip_serializing_if = "Option::is_none")] pub collaboration: Option, + + /// Set when this session is a role-CHILD of a collaborative session. + /// Absent = not a collaboration child. Pairs with `collaboration` above, + /// which marks the orchestrating parent. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub collaboration_role: Option, } /// Where a forked session came from — the parent id, the parent's name at @@ -148,6 +154,32 @@ pub struct CollaborationRole { /// What this role is for; surfaced in the child's brief. #[serde(default, skip_serializing_if = "Option::is_none")] pub purpose: Option, + /// Whether this role's children may modify the workspace. + /// + /// `None`/`false` = read-only, and that default is load-bearing: the + /// daemon gives a read-only role a leaf identity with no write scope at + /// all, so a reviewer provably cannot write rather than being asked not + /// to. Write authority is opt-in per role. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub write: Option, +} + +/// Set on a role-CHILD of a collaborative session: which collaboration it +/// belongs to and which role it plays. +/// +/// The mirror of [`SessionInfo::collaboration`] (set on the orchestrating +/// parent), so a client can group a fleet without inferring it from names. +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct CollaborationRoleRef { + /// Session id of the orchestrating parent. + pub parent_session_id: String, + /// Role name from the parent's config (already lowercased by the daemon). + pub role_name: String, + /// 1-based index within this role's fan-out (`review` ×3 → 1, 2, 3). + pub ordinal: u32, + /// Whether this child's identity carries write authority. + pub write: bool, } /// Collaborative-session config: one goal worked by several role-children on diff --git a/crates/codeoid-protocol/tests/wire_format.rs b/crates/codeoid-protocol/tests/wire_format.rs index 65055cd..e698fd6 100644 --- a/crates/codeoid-protocol/tests/wire_format.rs +++ b/crates/codeoid-protocol/tests/wire_format.rs @@ -127,6 +127,7 @@ fn sample_session_info() -> SessionInfo { model: None, count: None, purpose: None, + write: None, }, codeoid_protocol::CollaborationRole { name: "review".into(), @@ -134,9 +135,18 @@ fn sample_session_info() -> SessionInfo { model: Some("gemini-2.5-pro".into()), count: Some(3), purpose: Some("independent critique".into()), + write: Some(false), }, ], }), + // A real session is either a parent or a child, never both; populated + // here anyway so the camelCase walker visits this struct's keys too. + collaboration_role: Some(codeoid_protocol::CollaborationRoleRef { + parent_session_id: "parent-1".into(), + role_name: "review".into(), + ordinal: 2, + write: false, + }), } } diff --git a/crates/codeoid-tui/src/app.rs b/crates/codeoid-tui/src/app.rs index 3510607..2d941d1 100644 --- a/crates/codeoid-tui/src/app.rs +++ b/crates/codeoid-tui/src/app.rs @@ -2653,6 +2653,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, }); state } diff --git a/crates/codeoid-tui/src/state/mod.rs b/crates/codeoid-tui/src/state/mod.rs index 684e181..f94df8d 100644 --- a/crates/codeoid-tui/src/state/mod.rs +++ b/crates/codeoid-tui/src/state/mod.rs @@ -1156,6 +1156,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/state/sessions.rs b/crates/codeoid-tui/src/state/sessions.rs index 50b8f6c..1ac3458 100644 --- a/crates/codeoid-tui/src/state/sessions.rs +++ b/crates/codeoid-tui/src/state/sessions.rs @@ -131,6 +131,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/ui/approval.rs b/crates/codeoid-tui/src/ui/approval.rs index 9619d59..51c0758 100644 --- a/crates/codeoid-tui/src/ui/approval.rs +++ b/crates/codeoid-tui/src/ui/approval.rs @@ -293,6 +293,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: 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 9368ed3..fbca6dd 100644 --- a/crates/codeoid-tui/src/ui/scrollback.rs +++ b/crates/codeoid-tui/src/ui/scrollback.rs @@ -644,6 +644,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, } } diff --git a/crates/codeoid-tui/src/ui/worker.rs b/crates/codeoid-tui/src/ui/worker.rs index c0a3c4d..51c539e 100644 --- a/crates/codeoid-tui/src/ui/worker.rs +++ b/crates/codeoid-tui/src/ui/worker.rs @@ -441,6 +441,7 @@ mod tests { forked_from: None, worktree: None, collaboration: None, + collaboration_role: None, }); state.provider_commands.insert( "s1".into(),