1use serde::{Deserialize, Serialize};
2
3macro_rules! id_type {
4 ($name:ident) => {
5 #[derive(
6 Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize,
7 )]
8 #[serde(transparent)]
9 pub struct $name(pub u32);
10
11 impl $name {
12 pub const fn new(value: u32) -> Self {
13 Self(value)
14 }
15
16 pub const fn raw(self) -> u32 {
17 self.0
18 }
19 }
20 };
21}
22
23id_type!(WorkspaceId);
24id_type!(HarnessId);
25id_type!(SubagentId);
26
27#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
32#[serde(rename_all = "kebab-case")]
33pub enum HarnessKind {
34 ClaudeCode,
35 OpenCode,
36 Pi,
37 Shell,
38}
39
40impl HarnessKind {
41 pub fn badge(self) -> &'static str {
42 match self {
43 HarnessKind::ClaudeCode => "CC",
44 HarnessKind::OpenCode => "OC",
45 HarnessKind::Pi => "PI",
46 HarnessKind::Shell => "SH",
47 }
48 }
49
50 pub fn label(self) -> &'static str {
51 match self {
52 HarnessKind::ClaudeCode => "Claude Code",
53 HarnessKind::OpenCode => "opencode",
54 HarnessKind::Pi => "pi",
55 HarnessKind::Shell => "shell",
56 }
57 }
58
59 pub fn default_command(self) -> &'static str {
60 match self {
61 HarnessKind::ClaudeCode => "claude",
62 HarnessKind::OpenCode => "opencode",
63 HarnessKind::Pi => "pi",
64 HarnessKind::Shell => "$SHELL",
65 }
66 }
67
68 pub const ALL: [HarnessKind; 4] = [
69 HarnessKind::ClaudeCode,
70 HarnessKind::OpenCode,
71 HarnessKind::Pi,
72 HarnessKind::Shell,
73 ];
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
77#[serde(rename_all = "kebab-case")]
78pub enum HarnessStatus {
79 Working,
81 Idle,
83 NeedsInput,
85 Error,
87}
88
89impl HarnessStatus {
90 pub fn label(self) -> &'static str {
91 match self {
92 HarnessStatus::Working => "working",
93 HarnessStatus::Idle => "idle",
94 HarnessStatus::NeedsInput => "needs input",
95 HarnessStatus::Error => "error",
96 }
97 }
98}
99
100#[derive(Debug, Clone, Serialize, Deserialize)]
103pub struct Subagent {
104 pub id: SubagentId,
105 pub label: String,
106 pub detail: String,
107 pub elapsed: String,
108 pub working: bool,
109}
110
111#[derive(Debug, Clone, Serialize, Deserialize)]
112pub struct Harness {
113 pub id: HarnessId,
114 pub workspace: WorkspaceId,
115 pub name: String,
116 pub kind: HarnessKind,
117 pub cwd: String,
119 pub status: HarnessStatus,
120 #[serde(default)]
121 pub subagents: Vec<Subagent>,
122 #[serde(default)]
123 pub unread: u16,
124 #[serde(default)]
126 pub tag: String,
127 #[serde(default)]
129 pub title: String,
130 #[serde(default)]
132 pub pronouns: String,
133 #[serde(default)]
135 pub color: Option<usize>,
136 #[serde(default = "default_true")]
140 pub announced: bool,
141 #[serde(default = "default_true")]
143 pub started: bool,
144}
145
146fn default_true() -> bool {
147 true
148}
149
150impl Harness {
151 pub fn working_subagents(&self) -> usize {
152 self.subagents.iter().filter(|agent| agent.working).count()
153 }
154
155 pub fn has_live_subagents(&self) -> bool {
156 self.working_subagents() > 0
157 }
158
159 pub fn subagent_count(&self) -> usize {
160 self.subagents.len()
161 }
162}
163
164#[derive(Debug, Clone, Serialize, Deserialize)]
167pub struct Workspace {
168 pub id: WorkspaceId,
169 pub name: String,
170 pub path: String,
171 pub accent: usize,
173 pub expanded: bool,
174 pub harnesses: Vec<Harness>,
175}
176
177impl Workspace {
178 pub fn harness(&self, id: HarnessId) -> Option<&Harness> {
179 self.harnesses.iter().find(|harness| harness.id == id)
180 }
181
182 pub fn harness_mut(&mut self, id: HarnessId) -> Option<&mut Harness> {
183 self.harnesses.iter_mut().find(|harness| harness.id == id)
184 }
185
186 pub fn working(&self) -> usize {
187 self.harnesses
188 .iter()
189 .filter(|harness| harness.status == HarnessStatus::Working)
190 .count()
191 }
192
193 pub fn unread(&self) -> u16 {
194 self.harnesses.iter().map(|harness| harness.unread).sum()
195 }
196}
197
198#[derive(Debug, Clone, Serialize, Deserialize)]
200pub struct Mail {
201 pub to: HarnessId,
202 pub from: String,
205 pub text: String,
206 #[serde(default)]
207 pub read: bool,
208}
209
210#[derive(Debug, Clone, Serialize, Deserialize)]
212pub struct Activity {
213 pub time: String,
215 pub name: String,
216 pub message: String,
217 pub accent: usize,
218 #[serde(default)]
220 pub to: Option<(String, usize)>,
221 #[serde(default)]
223 pub harness: Option<HarnessId>,
224}
225
226#[derive(Debug, Clone, Serialize, Deserialize)]
227pub struct ChoiceOption {
228 pub label: String,
229 pub description: String,
230 pub image: Option<std::path::PathBuf>,
231}
232
233#[derive(Debug, Clone, Serialize, Deserialize)]
235pub struct Choice {
236 pub from: HarnessId,
237 pub title: String,
238 pub question: String,
239 pub options: Vec<ChoiceOption>,
240}
241
242pub const AGENT_COLORS: [usize; 6] = [0, 1, 3, 4, 5, 6];
245
246pub fn assign_agent_colors(workspaces: &mut [Workspace], mut seed: u64) {
249 let mut used: Vec<usize> = workspaces
250 .iter()
251 .flat_map(|w| &w.harnesses)
252 .filter_map(|h| h.color)
253 .collect();
254 for harness in workspaces.iter_mut().flat_map(|w| &mut w.harnesses) {
255 if harness.color.is_some() {
256 continue;
257 }
258 let mut free: Vec<usize> = AGENT_COLORS
259 .into_iter()
260 .filter(|c| !used.contains(c))
261 .collect();
262 if free.is_empty() {
263 free = AGENT_COLORS.to_vec();
264 }
265 seed = seed
266 .wrapping_mul(6364136223846793005)
267 .wrapping_add(1442695040888963407);
268 let color = free[(seed >> 33) as usize % free.len()];
269 harness.color = Some(color);
270 used.push(color);
271 }
272}
273
274#[cfg(test)]
275mod color_tests {
276 use super::*;
277
278 #[test]
279 fn colors_are_unique_while_free_and_allowed() {
280 let mut ws = vec![Workspace {
281 id: WorkspaceId::new(1),
282 name: String::new(),
283 path: String::new(),
284 accent: 0,
285 expanded: true,
286 harnesses: (1..=6)
287 .map(|i| Harness {
288 id: HarnessId::new(i),
289 workspace: WorkspaceId::new(1),
290 name: String::new(),
291 kind: HarnessKind::Shell,
292 cwd: String::new(),
293 status: HarnessStatus::Idle,
294 subagents: Vec::new(),
295 unread: 0,
296 tag: String::new(),
297 title: String::new(),
298 pronouns: String::new(),
299 color: None,
300 announced: true,
301 started: true,
302 })
303 .collect(),
304 }];
305 assign_agent_colors(&mut ws, 42);
306 let mut got: Vec<_> = ws[0].harnesses.iter().map(|h| h.color.unwrap()).collect();
307 got.sort();
308 assert_eq!(got, AGENT_COLORS);
309 }
310}