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
9 changes: 8 additions & 1 deletion crates/codeoid-protocol/src/client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
use serde::{Deserialize, Serialize};
use serde_json;

use crate::session::SessionMode;
use crate::session::{CollaborationConfig, SessionMode};

/// Tagged union of every message a client can send the daemon.
#[derive(Debug, Clone, Serialize, Deserialize)]
Expand All @@ -30,6 +30,13 @@ pub enum ClientMessage {
/// daemon fail-closes on unknown ids. Absent = daemon default.
#[serde(default, skip_serializing_if = "Option::is_none")]
provider_id: Option<String>,
/// Make this a collaborative session: one goal worked by several
/// role-children on their own backends. Absent = a normal session.
/// The daemon validates the semantics (provider registered, exactly
/// one orchestrator, claude-only orchestrator in v1) and answers
/// with a specific `invalid_request` error.
#[serde(default, skip_serializing_if = "Option::is_none")]
collaboration: Option<CollaborationConfig>,
},

#[serde(rename = "session.list", rename_all = "camelCase")]
Expand Down
4 changes: 2 additions & 2 deletions crates/codeoid-protocol/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -52,8 +52,8 @@ pub use message::{
ContentPart, IdentityType, MessageIdentity, MessageRole, SessionMessage, SessionMessageDelta,
};
pub use session::{
ForkedFrom, SessionInfo, SessionMode, SessionStatus, SessionUsage, SessionWorktree, Subagent,
TurnUsage,
CollaborationConfig, CollaborationRole, ForkedFrom, SessionInfo, SessionMode, SessionStatus,
SessionUsage, SessionWorktree, Subagent, TurnUsage,
};
pub use tool::{CancelReason, ConfirmedBy, ToolInfo, ToolPhase, ToolState};

Expand Down
46 changes: 46 additions & 0 deletions crates/codeoid-protocol/src/session.rs
Original file line number Diff line number Diff line change
Expand Up @@ -94,6 +94,13 @@ pub struct SessionInfo {
/// "⎇ <branch>" tag in the session title.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub worktree: Option<SessionWorktree>,

/// Collaboration this session orchestrates — goal + role→backend
/// bindings — when it was created with the Collaborative toggle.
/// Absent = a normal session. Persisted daemon-side, so it survives a
/// restart the way `role`/`provider_id` already do.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub collaboration: Option<CollaborationConfig>,
}

/// Where a forked session came from — the parent id, the parent's name at
Expand All @@ -118,6 +125,45 @@ pub struct SessionWorktree {
pub created_by_codeoid: bool,
}

/// One role in a collaborative session — a `{backend, model}` binding chosen
/// per purpose.
///
/// `name` is a free-form string, not an enum: the role taxonomy is data, so
/// the daemon can add "security-reviewer" as a config change and this crate
/// keeps parsing it without a release.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollaborationRole {
pub name: String,
/// Backend this role's children run on. The daemon fail-closes on an id
/// it does not have registered.
pub provider_id: String,
/// Model within that backend. `None` = the backend's own default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// How many children to fan out for this role. `None` = 1; >1 is what
/// makes a review panel a panel.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub count: Option<u32>,
/// What this role is for; surfaced in the child's brief.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub purpose: Option<String>,
}

/// Collaborative-session config: one goal worked by several role-children on
/// possibly different backends. Sent on `session.create` and echoed back on
/// [`SessionInfo`].
///
/// Exactly one role must be named `orchestrator`, and in v1 it must sit on
/// the claude backend. The daemon enforces both and answers with a specific
/// error, so this crate carries only the shape.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct CollaborationConfig {
pub goal: String,
pub roles: Vec<CollaborationRole>,
}

/// 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
20 changes: 20 additions & 0 deletions crates/codeoid-protocol/tests/wire_format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,25 @@ fn sample_session_info() -> SessionInfo {
branch: "codeoid/fix-a1b2".into(),
created_by_codeoid: true,
}),
collaboration: Some(codeoid_protocol::CollaborationConfig {
goal: "Add rate limiting to the public API".into(),
roles: vec![
codeoid_protocol::CollaborationRole {
name: "orchestrator".into(),
provider_id: "claude".into(),
model: None,
count: None,
purpose: None,
},
codeoid_protocol::CollaborationRole {
name: "review".into(),
provider_id: "gemini".into(),
model: Some("gemini-2.5-pro".into()),
count: Some(3),
purpose: Some("independent critique".into()),
},
],
}),
}
}

Expand Down Expand Up @@ -179,6 +198,7 @@ fn client_messages_are_camel_case_on_wire() {
name: "n".into(),
workdir: "/".into(),
provider_id: Some("pi".into()),
collaboration: None,
},
),
("SessionList", ClientMessage::SessionList { id: "1".into() }),
Expand Down
4 changes: 4 additions & 0 deletions crates/codeoid-tui/src/app.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2447,6 +2447,9 @@ fn session_create_message(
name,
workdir,
provider_id,
// The TUI has no collaborative-create surface yet (that lands with
// the extended create dialog); omitted means a normal session.
collaboration: None,
}
}

Expand Down Expand Up @@ -2581,6 +2584,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: 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 @@ -958,6 +958,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: 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 @@ -130,6 +130,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: 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 @@ -292,6 +292,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: None,
});
let mut m = msg(
MessageRole::ToolCall,
Expand Down
1 change: 1 addition & 0 deletions crates/codeoid-tui/src/ui/scrollback.rs
Original file line number Diff line number Diff line change
Expand Up @@ -643,6 +643,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: None,
}
}

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 @@ -373,6 +373,7 @@ mod tests {
provider_id: None,
forked_from: None,
worktree: None,
collaboration: None,
});
state.provider_commands.insert(
"s1".into(),
Expand Down
Loading