@saadnvd1/agent-os 0.3.821 → 0.3.825

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (70) hide show
  1. package/app/api/fork/cancel/route.ts +19 -0
  2. package/app/api/fork/children/route.ts +52 -0
  3. package/app/api/fork/result/route.ts +29 -0
  4. package/app/api/fork/return/route.ts +20 -0
  5. package/app/api/fork/route.ts +32 -0
  6. package/app/api/fork/wait/route.ts +27 -0
  7. package/app/api/load/heavy/lease/route.ts +26 -0
  8. package/app/api/sessions/[id]/archive/route.ts +17 -4
  9. package/app/api/sessions/[id]/route.ts +2 -3
  10. package/app/page.tsx +0 -5
  11. package/components/Chat/ChatPanel.tsx +33 -206
  12. package/components/Chat/Forks.tsx +278 -0
  13. package/components/Chat/ReplyingTo.tsx +24 -0
  14. package/components/Chat/TimelineRows.tsx +194 -0
  15. package/components/Pane/DesktopTabBar.tsx +0 -6
  16. package/components/Pane/MobileTabBar.tsx +0 -6
  17. package/components/Pane/MobileViewPicker.test.ts +1 -8
  18. package/components/Pane/MobileViewPicker.tsx +0 -11
  19. package/components/Pane/PaneViewToggle.test.ts +0 -2
  20. package/components/Pane/PaneViewToggle.tsx +0 -17
  21. package/components/Pane/index.tsx +0 -50
  22. package/components/SessionList/useDoneAction.ts +18 -3
  23. package/content/whats-new.json +16 -0
  24. package/contexts/PaneContext.tsx +1 -1
  25. package/data/done/index.ts +26 -2
  26. package/data/fork/index.ts +83 -0
  27. package/lib/chat/commands.ts +15 -0
  28. package/lib/chat/drivers/copilot-ask.ts +38 -5
  29. package/lib/chat/runner.ts +8 -2
  30. package/lib/chat/worker/extras.ts +7 -1
  31. package/lib/chat/worker/host.ts +2 -0
  32. package/lib/config/client-config-types.ts +7 -0
  33. package/lib/config/client-config.ts +2 -0
  34. package/lib/db/migrations/20261102T0100_forks.ts +87 -0
  35. package/lib/done/archive.ts +21 -1
  36. package/lib/experiments/registry.ts +8 -0
  37. package/lib/fork/api.ts +10 -0
  38. package/lib/fork/brief.ts +63 -0
  39. package/lib/fork/conversation.ts +75 -0
  40. package/lib/fork/deliver.ts +91 -0
  41. package/lib/fork/extras.ts +53 -0
  42. package/lib/fork/guard.ts +323 -0
  43. package/lib/fork/heavy-lease.ts +104 -0
  44. package/lib/fork/held.ts +26 -0
  45. package/lib/fork/ledger.ts +324 -0
  46. package/lib/fork/options.ts +46 -0
  47. package/lib/fork/point.ts +41 -0
  48. package/lib/fork/scratch.ts +67 -0
  49. package/lib/fork/settle.ts +215 -0
  50. package/lib/fork/snapshot.ts +69 -0
  51. package/lib/fork/start.ts +273 -0
  52. package/lib/fork/tick.ts +54 -0
  53. package/lib/fork/types.ts +79 -0
  54. package/lib/fork/view.ts +116 -0
  55. package/lib/fork/waits.ts +83 -0
  56. package/lib/fork/worktree.ts +115 -0
  57. package/lib/orchestrator/shell-guard.ts +6 -3
  58. package/lib/orchestrator/write-guard.ts +1 -1
  59. package/lib/security/demo.ts +1 -2
  60. package/lib/sessions/end.ts +5 -0
  61. package/package.json +1 -1
  62. package/server.ts +2 -0
  63. package/app/api/orchestrate/spawn/route.ts +0 -54
  64. package/app/api/orchestrate/workers/[id]/route.ts +0 -89
  65. package/app/api/orchestrate/workers/route.ts +0 -31
  66. package/app/api/sessions/[id]/mcp-config/route.ts +0 -34
  67. package/components/ConductorPanel.tsx +0 -258
  68. package/lib/mcp-config.ts +0 -80
  69. package/lib/orchestration.ts +0 -463
  70. package/mcp/orchestration-server.ts +0 -441
@@ -0,0 +1,278 @@
1
+ "use client";
2
+
3
+ import { useState } from "react";
4
+ import { GitFork, Plus, Square } from "lucide-react";
5
+ import { toast } from "sonner";
6
+ import { useExperiment } from "@/data/experiments";
7
+ import { useClientConfig } from "@/data/client-config/queries";
8
+ import {
9
+ useCancelForks,
10
+ useForkChildren,
11
+ useStartForks,
12
+ type ChildView,
13
+ } from "@/data/fork";
14
+ import { Button } from "@/components/ui/button";
15
+ import {
16
+ Dialog,
17
+ DialogContent,
18
+ DialogDescription,
19
+ DialogFooter,
20
+ DialogHeader,
21
+ DialogTitle,
22
+ } from "@/components/ui/dialog";
23
+ import { StatusPill } from "@/components/ui/status-pill";
24
+ import { Switch } from "@/components/ui/switch";
25
+ import { Textarea } from "@/components/ui/textarea";
26
+ import { SessionAnchor } from "./SessionAnchor";
27
+
28
+ type Tone = "amber" | "muted" | "green" | "primary" | "red";
29
+
30
+ export function stateLabel(c: ChildView): { text: string; tone: Tone } {
31
+ if (c.state === "queued") return { text: "Waiting", tone: "muted" };
32
+ if (c.state === "starting") return { text: "Starting", tone: "muted" };
33
+ if (c.state === "running") return { text: "Working", tone: "primary" };
34
+ if (c.state === "needs_input") return { text: "Needs you", tone: "amber" };
35
+ if (c.outcome === "failed") return { text: "Failed", tone: "red" };
36
+ if (c.outcome === "cancelled") return { text: "Stopped", tone: "muted" };
37
+ return { text: "Done", tone: "green" };
38
+ }
39
+
40
+ function Row({
41
+ c,
42
+ onStop,
43
+ stopping,
44
+ }: {
45
+ c: ChildView;
46
+ onStop: () => void;
47
+ stopping: boolean;
48
+ }) {
49
+ const { text, tone } = stateLabel(c);
50
+ const open = c.state !== "settled";
51
+ const note =
52
+ c.summary ??
53
+ (c.asks
54
+ ? `Asks: ${c.asks}`
55
+ : c.state === "queued"
56
+ ? "Waiting for room"
57
+ : null);
58
+ return (
59
+ <li className="flex min-h-11 items-center gap-2 px-3 py-1.5">
60
+ <StatusPill tone={tone} className="shrink-0">
61
+ {text}
62
+ </StatusPill>
63
+ <div className="min-w-0 flex-1">
64
+ <p className="truncate text-sm font-medium">
65
+ {c.n}. {c.label.replace(/^\d+\.\s*/, "")}
66
+ </p>
67
+ {note && (
68
+ <p className="text-muted-foreground truncate text-xs">
69
+ {note}
70
+ {c.resultKind === "implicit" && c.outcome === "ok"
71
+ ? " · ended without reporting"
72
+ : ""}
73
+ </p>
74
+ )}
75
+ </div>
76
+ <SessionAnchor
77
+ sessionId={c.id}
78
+ className="text-primary flex min-h-11 shrink-0 items-center px-2 text-sm"
79
+ >
80
+ Open
81
+ </SessionAnchor>
82
+ {open && (
83
+ <Button
84
+ size="sm"
85
+ variant="ghost"
86
+ aria-label={`Stop fork ${c.n}`}
87
+ className="size-11 shrink-0"
88
+ disabled={stopping}
89
+ onClick={onStop}
90
+ >
91
+ <Square aria-hidden className="size-4" />
92
+ </Button>
93
+ )}
94
+ </li>
95
+ );
96
+ }
97
+
98
+ // Above the composer: this session's forks and where each stands.
99
+ export function ForksStrip({ sessionId }: { sessionId: string }) {
100
+ const on = useExperiment("forks");
101
+ const { data } = useForkChildren(sessionId, on);
102
+ const cancel = useCancelForks(sessionId);
103
+ const [collapsed, setCollapsed] = useState(false);
104
+ if (!on || !data) return null;
105
+ if (data.asChild) return <ForkOfBanner info={data.asChild} />;
106
+ const { children } = data;
107
+ if (!children.length) return null;
108
+ const open = children.filter((c) => c.state !== "settled").length;
109
+ const stop = (ref?: string) =>
110
+ cancel.mutate(ref ? { ref } : { all: true }, {
111
+ onError: (e) => toast.error(e.message),
112
+ });
113
+ return (
114
+ <section
115
+ aria-label="Forks"
116
+ className="bg-card popover-surface mb-2 overflow-hidden rounded-2xl"
117
+ >
118
+ <div className="flex min-h-11 items-center gap-2 px-3">
119
+ <GitFork
120
+ aria-hidden
121
+ className="text-muted-foreground size-4 shrink-0"
122
+ />
123
+ <button
124
+ type="button"
125
+ aria-expanded={!collapsed}
126
+ onClick={() => setCollapsed((v) => !v)}
127
+ className="min-h-11 min-w-0 flex-1 text-left text-sm font-medium"
128
+ >
129
+ Forks
130
+ <span className="text-muted-foreground ml-2 font-normal">
131
+ {open ? `${open} working` : "all done"} · {children.length} total
132
+ </span>
133
+ </button>
134
+ {open > 0 && (
135
+ <Button
136
+ size="sm"
137
+ variant="ghost"
138
+ className="min-h-11"
139
+ disabled={cancel.isPending}
140
+ onClick={() => stop()}
141
+ >
142
+ Stop all
143
+ </Button>
144
+ )}
145
+ </div>
146
+ {!collapsed && (
147
+ <ul className="border-border max-h-60 divide-y overflow-y-auto border-t">
148
+ {children.map((c) => (
149
+ <Row
150
+ key={c.id}
151
+ c={c}
152
+ stopping={cancel.isPending}
153
+ onStop={() => stop(String(c.n))}
154
+ />
155
+ ))}
156
+ </ul>
157
+ )}
158
+ </section>
159
+ );
160
+ }
161
+
162
+ function ForkOfBanner({
163
+ info,
164
+ }: {
165
+ info: NonNullable<
166
+ NonNullable<ReturnType<typeof useForkChildren>["data"]>["asChild"]
167
+ >;
168
+ }) {
169
+ return (
170
+ <section
171
+ aria-label="Fork of"
172
+ className="bg-card popover-surface mb-2 flex min-h-11 items-center gap-2 rounded-2xl px-3 py-1.5"
173
+ >
174
+ <GitFork aria-hidden className="text-muted-foreground size-4 shrink-0" />
175
+ <p className="min-w-0 flex-1 text-sm">
176
+ Fork {info.n} of{" "}
177
+ <SessionAnchor sessionId={info.parentId} className="text-primary">
178
+ {info.parentName}
179
+ </SessionAnchor>
180
+ <span className="text-muted-foreground block text-xs">
181
+ {info.worktree
182
+ ? "Works in its own checkout. "
183
+ : "Reads the parent's checkout, doesn't change it. "}
184
+ Finishes with a result for the parent.
185
+ </span>
186
+ </p>
187
+ </section>
188
+ );
189
+ }
190
+
191
+ // "/fork": instructions for N copies of this session.
192
+ export function ForkDialog({
193
+ sessionId,
194
+ open,
195
+ onOpenChange,
196
+ }: {
197
+ sessionId: string;
198
+ open: boolean;
199
+ onOpenChange: (open: boolean) => void;
200
+ }) {
201
+ const start = useStartForks(sessionId);
202
+ const max = useClientConfig().data?.forks?.maxPerFork ?? 8;
203
+ const [texts, setTexts] = useState<string[]>([""]);
204
+ const [worktree, setWorktree] = useState(false);
205
+ const filled = texts.map((t) => t.trim()).filter(Boolean);
206
+ const submit = () =>
207
+ start.mutate(
208
+ { instructions: filled, worktree },
209
+ {
210
+ onSuccess: (r) => {
211
+ toast.success(
212
+ `Started ${r.children.length} ${r.children.length === 1 ? "fork" : "forks"}`
213
+ );
214
+ setTexts([""]);
215
+ setWorktree(false);
216
+ onOpenChange(false);
217
+ },
218
+ onError: (e) => toast.error(e.message),
219
+ }
220
+ );
221
+ return (
222
+ <Dialog open={open} onOpenChange={onOpenChange}>
223
+ <DialogContent className="max-h-[90dvh] overflow-y-auto sm:max-w-lg">
224
+ <DialogHeader>
225
+ <DialogTitle>Fork this session</DialogTitle>
226
+ <DialogDescription>
227
+ Each copy knows what this session knows and works in the background.
228
+ You keep going; results are added to your next message.
229
+ </DialogDescription>
230
+ </DialogHeader>
231
+ <div className="space-y-2">
232
+ {texts.map((t, i) => (
233
+ <Textarea
234
+ key={i}
235
+ value={t}
236
+ rows={2}
237
+ aria-label={`Fork ${i + 1}`}
238
+ placeholder={`What should fork ${i + 1} do?`}
239
+ onChange={(e) =>
240
+ setTexts(texts.map((x, j) => (j === i ? e.target.value : x)))
241
+ }
242
+ />
243
+ ))}
244
+ {texts.length < max && (
245
+ <Button
246
+ type="button"
247
+ variant="ghost"
248
+ size="sm"
249
+ className="min-h-11"
250
+ onClick={() => setTexts([...texts, ""])}
251
+ >
252
+ <Plus aria-hidden className="size-4" /> Another
253
+ </Button>
254
+ )}
255
+ <label className="flex min-h-11 items-center justify-between gap-3 text-sm">
256
+ <span>
257
+ Own checkout
258
+ <span className="text-muted-foreground block text-xs">
259
+ For forks that edit code. Off: they read this checkout and leave
260
+ it alone.
261
+ </span>
262
+ </span>
263
+ <Switch checked={worktree} onCheckedChange={setWorktree} />
264
+ </label>
265
+ </div>
266
+ <DialogFooter>
267
+ <Button
268
+ className="min-h-11"
269
+ disabled={!filled.length || start.isPending}
270
+ onClick={submit}
271
+ >
272
+ {filled.length > 1 ? `Start ${filled.length} forks` : "Start fork"}
273
+ </Button>
274
+ </DialogFooter>
275
+ </DialogContent>
276
+ </Dialog>
277
+ );
278
+ }
@@ -0,0 +1,24 @@
1
+ "use client";
2
+
3
+ // Above the composer while it answers a question from the orchestrator's strip.
4
+ export function ReplyingTo({
5
+ text,
6
+ onStop,
7
+ }: {
8
+ text: string;
9
+ onStop: () => void;
10
+ }) {
11
+ return (
12
+ <div className="text-muted-foreground mb-2 flex min-h-11 items-center gap-2 rounded-lg px-2 text-xs">
13
+ <span className="min-w-0 flex-1 truncate">Answering: {text}</span>
14
+ <button
15
+ type="button"
16
+ onClick={onStop}
17
+ aria-label="Stop answering"
18
+ className="hover:text-foreground flex size-11 shrink-0 items-center justify-center"
19
+ >
20
+ ×
21
+ </button>
22
+ </div>
23
+ );
24
+ }
@@ -0,0 +1,194 @@
1
+ "use client";
2
+
3
+ import type { StepCounts } from "@/lib/chat/tool-outcome";
4
+ import { Fragment, memo, useMemo } from "react";
5
+ import {
6
+ groupTimeline,
7
+ sameBlock,
8
+ STANDALONE_TOOLS,
9
+ type TimelineBlock,
10
+ } from "@/lib/chat/group";
11
+ import type { ApprovalDecision, ChatItem } from "@/lib/chat/events";
12
+ import { Approval } from "./Approval";
13
+ import { SubagentCard } from "./Subagent";
14
+ import { UndoneBlock } from "./Undo";
15
+ import { ToolGroup } from "./Tools";
16
+ import { McpServers } from "./McpServers";
17
+ import { ArtifactCard } from "./Artifact";
18
+ import { PlanCard } from "./PlanCard";
19
+ import { UserTurn } from "./EventRow";
20
+ import { EventGroup } from "./EventTurns";
21
+ import { NewDivider } from "./NewDivider";
22
+ import {
23
+ AssistantMessage,
24
+ CommandOutput,
25
+ Compacted,
26
+ SkillChip,
27
+ ErrorMessage,
28
+ NoteLine,
29
+ Reasoning,
30
+ Todos,
31
+ TurnEnd,
32
+ } from "./Messages";
33
+
34
+ export interface ItemActions {
35
+ respond: (id: string, answer: ApprovalDecision) => void;
36
+ // Absent inside undone messages; hidden while the agent works (ChatIdle).
37
+ onCarryPlan?: (id: string) => void;
38
+ onKeepPlanning?: () => void;
39
+ // Absent inside undone messages; hidden while the agent works (ChatIdle).
40
+ onUndo?: (id: string) => void;
41
+ onQuote?: (text: string) => void;
42
+ }
43
+
44
+ function Item({
45
+ item,
46
+ actions,
47
+ steps,
48
+ }: {
49
+ item: ChatItem;
50
+ actions: ItemActions;
51
+ steps?: StepCounts;
52
+ }) {
53
+ switch (item.kind) {
54
+ case "user":
55
+ return (
56
+ <UserTurn
57
+ item={item}
58
+ onUndo={
59
+ item.checkpoint && actions.onUndo
60
+ ? () => actions.onUndo?.(item.id)
61
+ : undefined
62
+ }
63
+ />
64
+ );
65
+ case "assistant":
66
+ return <AssistantMessage item={item} onQuote={actions.onQuote} />;
67
+ case "reasoning":
68
+ return <Reasoning item={item} />;
69
+ case "todos":
70
+ return <Todos item={item} />;
71
+ case "turn_end":
72
+ return <TurnEnd item={item} steps={steps} />;
73
+ case "error":
74
+ return <ErrorMessage item={item} />;
75
+ case "command_output":
76
+ return <CommandOutput item={item} />;
77
+ case "mcp":
78
+ return <McpServers item={item} />;
79
+ case "compacted":
80
+ return <Compacted item={item} />;
81
+ case "note":
82
+ return <NoteLine item={item} />;
83
+ case "artifact":
84
+ return <ArtifactCard item={item} />;
85
+ case "approval":
86
+ return <Approval item={item} respond={actions.respond} />;
87
+ case "plan":
88
+ return (
89
+ <PlanCard
90
+ item={item}
91
+ onCarryOut={
92
+ actions.onCarryPlan
93
+ ? () => actions.onCarryPlan?.(item.id)
94
+ : undefined
95
+ }
96
+ onKeepPlanning={() => actions.onKeepPlanning?.()}
97
+ />
98
+ );
99
+ case "tool":
100
+ return item.name === "Skill" ? (
101
+ <SkillChip item={item} />
102
+ ) : STANDALONE_TOOLS.has(item.name) ? (
103
+ <SubagentCard item={item} />
104
+ ) : null;
105
+ default:
106
+ return null;
107
+ }
108
+ }
109
+
110
+ function Timeline({
111
+ blocks,
112
+ actions,
113
+ }: {
114
+ blocks: TimelineBlock[];
115
+ actions: ItemActions;
116
+ }) {
117
+ return blocks.map((b) => (
118
+ <Block
119
+ key={b.type === "item" ? b.item.id : b.id}
120
+ block={b}
121
+ actions={actions}
122
+ />
123
+ ));
124
+ }
125
+
126
+ const blockKey = (b: TimelineBlock) => (b.type === "item" ? b.item.id : b.id);
127
+
128
+ // One block, re-rendered only when its items or the actions change: a long
129
+ // conversation doesn't redraw every message for each streamed word.
130
+ const Block = memo(
131
+ function Block({
132
+ block: b,
133
+ actions,
134
+ }: {
135
+ block: TimelineBlock;
136
+ actions: ItemActions;
137
+ }) {
138
+ const respond = actions.respond;
139
+ const undoneActions = useMemo(
140
+ () => ({ respond, onQuote: actions.onQuote }),
141
+ [respond, actions.onQuote]
142
+ );
143
+ return b.type === "tools" ? (
144
+ <ToolGroup tools={b.tools} />
145
+ ) : b.type === "events" ? (
146
+ <EventGroup
147
+ id={b.id}
148
+ turns={b.turns}
149
+ renderReplies={(items) => (
150
+ <Timeline blocks={groupTimeline(items)} actions={undoneActions} />
151
+ )}
152
+ />
153
+ ) : b.type === "undone" ? (
154
+ <UndoneBlock
155
+ undo={b.undo}
156
+ count={b.items.filter((i) => i.kind === "user").length}
157
+ >
158
+ <Timeline blocks={groupTimeline(b.items)} actions={undoneActions} />
159
+ </UndoneBlock>
160
+ ) : (
161
+ <div data-item={b.item.id}>
162
+ <Item item={b.item} actions={actions} steps={b.steps} />
163
+ </div>
164
+ );
165
+ },
166
+ (prev, next) =>
167
+ prev.actions === next.actions && sameBlock(prev.block, next.block)
168
+ );
169
+
170
+ // The conversation's rows in the page's flow, not a virtualized list: a
171
+ // selection follows the DOM, and a list that recycles and re-orders its rows
172
+ // breaks it (docs/decisions/2026-10-11-chat-timeline-in-flow.md).
173
+ export function TimelineRows({
174
+ blocks,
175
+ actions,
176
+ newFrom,
177
+ }: {
178
+ blocks: TimelineBlock[];
179
+ actions: ItemActions;
180
+ // The block the reader hasn't seen yet begins at, when they were away.
181
+ newFrom?: string | null;
182
+ }) {
183
+ return blocks.map((b) => (
184
+ <Fragment key={blockKey(b)}>
185
+ {newFrom === blockKey(b) && <NewDivider />}
186
+ <div
187
+ data-block={blockKey(b)}
188
+ className="chat-row mx-auto w-full max-w-3xl px-4 pb-4"
189
+ >
190
+ <Block block={b} actions={actions} />
191
+ </div>
192
+ </Fragment>
193
+ ));
194
+ }
@@ -36,10 +36,8 @@ interface DesktopTabBarProps {
36
36
  sessions: Session[];
37
37
  viewMode: ViewMode;
38
38
  isFocused: boolean;
39
- isConductor: boolean;
40
39
  hasLog?: boolean;
41
40
  hasReport?: boolean;
42
- workerCount: number;
43
41
  canSplit: boolean;
44
42
  canClose: boolean;
45
43
  hasAttachedTmux: boolean;
@@ -68,10 +66,8 @@ export function DesktopTabBar({
68
66
  sessions,
69
67
  viewMode,
70
68
  isFocused,
71
- isConductor,
72
69
  hasLog,
73
70
  hasReport,
74
- workerCount,
75
71
  canSplit,
76
72
  canClose,
77
73
  hasAttachedTmux,
@@ -184,10 +180,8 @@ export function DesktopTabBar({
184
180
  isChat={session.view === "chat"}
185
181
  viewMode={viewMode}
186
182
  isLocalSession={isLocalSession}
187
- isConductor={isConductor}
188
183
  hasLog={hasLog}
189
184
  hasReport={hasReport}
190
- workerCount={workerCount}
191
185
  showGit={showGit}
192
186
  gitDrawerOpen={gitDrawerOpen}
193
187
  shellDrawerOpen={shellDrawerOpen}
@@ -54,10 +54,8 @@ interface MobileTabBarProps {
54
54
  sessions: Session[];
55
55
  projects: Project[];
56
56
  viewMode: ViewMode;
57
- isConductor: boolean;
58
57
  hasLog?: boolean;
59
58
  hasReport?: boolean;
60
- workerCount: number;
61
59
  // Local, and the folder is not known to be outside a git repository.
62
60
  showGit: boolean;
63
61
  onMenuClick?: () => void;
@@ -75,10 +73,8 @@ export function MobileTabBar({
75
73
  sessions,
76
74
  projects,
77
75
  viewMode,
78
- isConductor,
79
76
  hasLog,
80
77
  hasReport,
81
- workerCount,
82
78
  showGit,
83
79
  onMenuClick,
84
80
  onViewModeChange,
@@ -403,8 +399,6 @@ export function MobileTabBar({
403
399
  showGit={showGit}
404
400
  hasLog={hasLog}
405
401
  hasReport={hasReport}
406
- isConductor={isConductor}
407
- workerCount={workerCount}
408
402
  />
409
403
  )}
410
404
  </div>
@@ -14,8 +14,6 @@ const all = {
14
14
  isLocalSession: true,
15
15
  showGit: true,
16
16
  hasLog: true,
17
- isConductor: true,
18
- workerCount: 3,
19
17
  };
20
18
 
21
19
  describe("mobileViews", () => {
@@ -26,9 +24,7 @@ describe("mobileViews", () => {
26
24
  "Git",
27
25
  "Artifacts",
28
26
  "Log",
29
- "Workers",
30
27
  ]);
31
- expect(mobileViews(all).at(-1)?.count).toBe(3);
32
28
  });
33
29
 
34
30
  it("hides views that do not apply", () => {
@@ -36,15 +32,13 @@ describe("mobileViews", () => {
36
32
  mobileViews({
37
33
  isLocalSession: false,
38
34
  showGit: false,
39
- isConductor: false,
40
- workerCount: 0,
41
35
  }).map((v) => v.label)
42
36
  ).toEqual(["Chat", "Artifacts"]);
43
37
  });
44
38
 
45
39
  it("drops only Git when the folder is not a repo", () => {
46
40
  expect(mobileViews({ ...all, showGit: false }).map((v) => v.label)).toEqual(
47
- ["Chat", "Files", "Artifacts", "Log", "Workers"]
41
+ ["Chat", "Files", "Artifacts", "Log"]
48
42
  );
49
43
  });
50
44
  });
@@ -82,7 +76,6 @@ describe("MobileViewPicker", () => {
82
76
  "Git",
83
77
  "Artifacts",
84
78
  "Log",
85
- "Workers3",
86
79
  ]);
87
80
  expect(items[3].getAttribute("aria-current")).toBe("true");
88
81
  });
@@ -9,7 +9,6 @@ import {
9
9
  ScrollText,
10
10
  ClipboardList,
11
11
  Terminal as TerminalIcon,
12
- Users,
13
12
  type LucideIcon,
14
13
  } from "lucide-react";
15
14
  import {
@@ -33,8 +32,6 @@ export interface MobileViewAvailability {
33
32
  showGit: boolean;
34
33
  hasLog?: boolean;
35
34
  hasReport?: boolean;
36
- isConductor: boolean;
37
- workerCount: number;
38
35
  }
39
36
 
40
37
  // The one list of views on a phone; the picker renders it.
@@ -43,8 +40,6 @@ export function mobileViews({
43
40
  showGit,
44
41
  hasLog,
45
42
  hasReport,
46
- isConductor,
47
- workerCount,
48
43
  }: MobileViewAvailability): MobileView[] {
49
44
  const views: (MobileView | false)[] = [
50
45
  { mode: "terminal", label: "Chat", icon: TerminalIcon },
@@ -53,12 +48,6 @@ export function mobileViews({
53
48
  { mode: "artifacts", label: "Artifacts", icon: Package },
54
49
  !!hasReport && { mode: "report", label: "Report", icon: ClipboardList },
55
50
  !!hasLog && { mode: "log", label: "Log", icon: ScrollText },
56
- isConductor && {
57
- mode: "workers",
58
- label: "Workers",
59
- icon: Users,
60
- count: workerCount,
61
- },
62
51
  ];
63
52
  return views.filter((v): v is MobileView => !!v);
64
53
  }
@@ -56,8 +56,6 @@ describe("PaneViewToggle", () => {
56
56
  viewMode: "terminal",
57
57
  isLocalSession: true,
58
58
  showGit,
59
- isConductor: false,
60
- workerCount: 0,
61
59
  gitDrawerOpen: false,
62
60
  shellDrawerOpen: false,
63
61
  onViewModeChange: () => {},
@@ -7,7 +7,6 @@ import {
7
7
  Package,
8
8
  ScrollText,
9
9
  ClipboardList,
10
- Users,
11
10
  } from "lucide-react";
12
11
  import {
13
12
  Tooltip,
@@ -25,11 +24,9 @@ interface PaneViewToggleProps {
25
24
  isLocalSession: boolean;
26
25
  // Local, and the folder is not known to be outside a git repository.
27
26
  showGit: boolean;
28
- isConductor: boolean;
29
27
  // The task merged and has a log.
30
28
  hasLog?: boolean;
31
29
  hasReport?: boolean;
32
- workerCount: number;
33
30
  gitDrawerOpen: boolean;
34
31
  shellDrawerOpen: boolean;
35
32
  onViewModeChange: (mode: ViewMode) => void;
@@ -85,10 +82,8 @@ export function PaneViewToggle({
85
82
  viewMode,
86
83
  isLocalSession,
87
84
  showGit,
88
- isConductor,
89
85
  hasLog,
90
86
  hasReport,
91
- workerCount,
92
87
  gitDrawerOpen,
93
88
  shellDrawerOpen,
94
89
  onViewModeChange,
@@ -159,18 +154,6 @@ export function PaneViewToggle({
159
154
  <ScrollText className="h-3.5 w-3.5" />
160
155
  </ToggleButton>
161
156
  )}
162
- {isConductor && (
163
- <ToggleButton
164
- label="Workers"
165
- active={viewMode === "workers"}
166
- onClick={() => onViewModeChange("workers")}
167
- >
168
- <Users className="h-3.5 w-3.5" />
169
- <span className="bg-primary text-primary-foreground absolute -top-1 -right-1 flex h-3.5 min-w-3.5 items-center justify-center rounded-full text-[9px] font-medium">
170
- {workerCount}
171
- </span>
172
- </ToggleButton>
173
- )}
174
157
  </div>
175
158
  );
176
159
  }