groove-dev 0.27.168 → 0.27.171

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 (62) hide show
  1. package/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/index.js +15 -2
  5. package/node_modules/@groove-dev/daemon/src/process.js +1 -1
  6. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +7 -1
  7. package/node_modules/@groove-dev/daemon/src/registry.js +3 -0
  8. package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
  9. package/node_modules/@groove-dev/daemon/src/state.js +7 -2
  10. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
  11. package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
  13. package/node_modules/@groove-dev/gui/dist/assets/index-BsCp-oqa.js +1030 -0
  14. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  15. package/node_modules/@groove-dev/gui/package.json +1 -1
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +6 -0
  17. package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
  18. package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
  19. package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +30 -1
  20. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +64 -42
  21. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
  22. package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
  23. package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
  24. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
  25. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
  27. package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
  28. package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
  29. package/package.json +1 -1
  30. package/packages/cli/package.json +1 -1
  31. package/packages/daemon/package.json +1 -1
  32. package/packages/daemon/src/index.js +15 -2
  33. package/packages/daemon/src/process.js +1 -1
  34. package/packages/daemon/src/providers/claude-code.js +7 -1
  35. package/packages/daemon/src/registry.js +3 -0
  36. package/packages/daemon/src/routes/files.js +18 -5
  37. package/packages/daemon/src/state.js +7 -2
  38. package/packages/daemon/src/tunnel-manager.js +16 -6
  39. package/packages/daemon/src/validate.js +1 -0
  40. package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
  41. package/packages/gui/dist/assets/index-BsCp-oqa.js +1030 -0
  42. package/packages/gui/dist/index.html +2 -2
  43. package/packages/gui/package.json +1 -1
  44. package/packages/gui/src/components/agents/agent-feed.jsx +6 -0
  45. package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
  46. package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
  47. package/packages/gui/src/components/agents/spawn-wizard.jsx +30 -1
  48. package/packages/gui/src/components/editor/terminal.jsx +64 -42
  49. package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
  50. package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
  51. package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
  52. package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
  53. package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
  54. package/packages/gui/src/views/agents.jsx +2 -11
  55. package/packages/gui/src/views/editor.jsx +63 -2
  56. package/packages/gui/src/views/settings.jsx +2 -1
  57. package/node_modules/@groove-dev/gui/dist/assets/index-BJVNpGIp.css +0 -1
  58. package/node_modules/@groove-dev/gui/dist/assets/index-CSMIQsrG.js +0 -1025
  59. package/packages/gui/dist/assets/index-BJVNpGIp.css +0 -1
  60. package/packages/gui/dist/assets/index-CSMIQsrG.js +0 -1025
  61. package/ssh/error.png +0 -0
  62. package/terminal/Screenshot_2026-05-19_at_12.20.15_PM.png +0 -0
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-CSMIQsrG.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-BsCp-oqa.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-26L3JoZv.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-DoBZjiHE.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BYKpdS2W.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-BJVNpGIp.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-BrMU-6gi.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.168",
3
+ "version": "0.27.171",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -608,6 +608,12 @@ export function AgentFeed({ agent }) {
608
608
  const isThinking = useGrooveStore((s) => s.thinkingAgents?.has(agent.id));
609
609
  const cachedChatRef = useRef(EMPTY);
610
610
  const cachedActivityRef = useRef(EMPTY);
611
+ const cachedAgentIdRef = useRef(agent.id);
612
+ if (cachedAgentIdRef.current !== agent.id) {
613
+ cachedAgentIdRef.current = agent.id;
614
+ cachedChatRef.current = EMPTY;
615
+ cachedActivityRef.current = EMPTY;
616
+ }
611
617
  if (rawChatHistory.length > 0) cachedChatRef.current = rawChatHistory;
612
618
  if (rawActivityLog.length > 0) cachedActivityRef.current = rawActivityLog;
613
619
  const chatHistory = rawChatHistory.length > 0 ? rawChatHistory : cachedChatRef.current;
@@ -6,7 +6,7 @@ import { api } from '../../lib/api';
6
6
  import { cn } from '../../lib/cn';
7
7
  import {
8
8
  FolderOpen, FolderClosed, ChevronRight, Home, HardDrive,
9
- ArrowUp, Check, Loader2,
9
+ ArrowUp, Check, Loader2, FileKey,
10
10
  } from 'lucide-react';
11
11
 
12
12
  function BreadcrumbPath({ path, onNavigate }) {
@@ -43,29 +43,39 @@ function BreadcrumbPath({ path, onNavigate }) {
43
43
  );
44
44
  }
45
45
 
46
- export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homePath, mandatory = false, title }) {
46
+ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homePath, mandatory = false, title, mode = 'directory' }) {
47
47
  const home = homePath || '/home';
48
48
  const [path, setPath] = useState(currentPath || home);
49
49
  const [entries, setEntries] = useState([]);
50
+ const [files, setFiles] = useState([]);
51
+ const [selectedFile, setSelectedFile] = useState(null);
50
52
  const [loading, setLoading] = useState(false);
51
53
  const [error, setError] = useState(null);
52
54
 
55
+ const isFileMode = mode === 'file';
56
+
53
57
  useEffect(() => {
54
58
  if (open) {
55
- navigateTo(currentPath || home);
59
+ setSelectedFile(null);
60
+ const startPath = currentPath || home;
61
+ navigateTo(isFileMode && startPath.includes('/') ? startPath.split('/').slice(0, -1).join('/') || '/' : startPath);
56
62
  }
57
63
  }, [open]);
58
64
 
59
65
  async function navigateTo(target) {
60
66
  setLoading(true);
61
67
  setError(null);
68
+ setSelectedFile(null);
62
69
  try {
63
- const data = await api.get(`/browse-system?path=${encodeURIComponent(target)}`);
70
+ const params = `path=${encodeURIComponent(target)}${isFileMode ? '&files=true&hidden=true' : ''}`;
71
+ const data = await api.get(`/browse-system?${params}`);
64
72
  setPath(data.current || target);
65
73
  setEntries(data.dirs || []);
74
+ setFiles(isFileMode ? (data.files || []) : []);
66
75
  } catch (err) {
67
76
  setError(err.message);
68
77
  setEntries([]);
78
+ setFiles([]);
69
79
  }
70
80
  setLoading(false);
71
81
  }
@@ -80,7 +90,7 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
80
90
  }
81
91
 
82
92
  function handleSelect() {
83
- onSelect(path);
93
+ onSelect(isFileMode && selectedFile ? selectedFile : path);
84
94
  if (!mandatory) onOpenChange(false);
85
95
  }
86
96
 
@@ -137,9 +147,9 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
137
147
  <p className="text-xs text-danger font-sans">{error}</p>
138
148
  </div>
139
149
  )}
140
- {!loading && !error && entries.length === 0 && (
150
+ {!loading && !error && entries.length === 0 && files.length === 0 && (
141
151
  <div className="px-4 py-6 text-center">
142
- <p className="text-xs text-text-3 font-sans">No subdirectories</p>
152
+ <p className="text-xs text-text-3 font-sans">{isFileMode ? 'No files found' : 'No subdirectories'}</p>
143
153
  </div>
144
154
  )}
145
155
  {!loading && !error && entries.map((entry) => (
@@ -161,13 +171,31 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
161
171
  )}
162
172
  </button>
163
173
  ))}
174
+ {!loading && !error && files.map((file) => (
175
+ <button
176
+ key={file.path}
177
+ onClick={() => setSelectedFile(file.path)}
178
+ className={cn(
179
+ 'w-full flex items-center gap-2.5 px-3.5 py-2 text-left cursor-pointer',
180
+ 'transition-colors border-b border-border-subtle last:border-0',
181
+ selectedFile === file.path ? 'bg-accent/10' : 'hover:bg-surface-4',
182
+ )}
183
+ >
184
+ <FileKey size={15} className={cn('flex-shrink-0', selectedFile === file.path ? 'text-accent' : 'text-text-3')} />
185
+ <span className={cn('text-sm font-sans truncate flex-1', selectedFile === file.path ? 'text-accent font-medium' : 'text-text-0')}>{file.name}</span>
186
+ </button>
187
+ ))}
164
188
  </div>
165
189
  </div>
166
190
 
167
191
  {/* Current selection */}
168
192
  <div className="flex items-center gap-3 bg-surface-4/50 rounded-lg px-3.5 py-2.5 border border-border-subtle">
169
- <FolderOpen size={16} className="text-accent flex-shrink-0" />
170
- <span className="text-xs font-mono text-text-1 truncate flex-1">{path}</span>
193
+ {isFileMode && selectedFile ? (
194
+ <FileKey size={16} className="text-accent flex-shrink-0" />
195
+ ) : (
196
+ <FolderOpen size={16} className="text-accent flex-shrink-0" />
197
+ )}
198
+ <span className="text-xs font-mono text-text-1 truncate flex-1">{isFileMode && selectedFile ? selectedFile : path}</span>
171
199
  </div>
172
200
 
173
201
  {/* Actions */}
@@ -175,8 +203,8 @@ export function FolderBrowser({ open, onOpenChange, currentPath, onSelect, homeP
175
203
  {!mandatory && (
176
204
  <Button variant="ghost" size="md" onClick={() => onOpenChange(false)}>Cancel</Button>
177
205
  )}
178
- <Button variant="primary" size="md" onClick={handleSelect} className="gap-1.5">
179
- <Check size={14} /> Select Folder
206
+ <Button variant="primary" size="md" onClick={handleSelect} disabled={isFileMode && !selectedFile} className="gap-1.5">
207
+ <Check size={14} /> {isFileMode ? 'Select File' : 'Select Folder'}
180
208
  </Button>
181
209
  </div>
182
210
  </div>
@@ -0,0 +1,300 @@
1
+ // FSL-1.1-Apache-2.0 — see LICENSE
2
+ import { useState, useEffect } from 'react';
3
+ import { useGrooveStore } from '../../stores/groove';
4
+ import { cn } from '../../lib/cn';
5
+ import { Button } from '../ui/button';
6
+ import { Select, SelectTrigger, SelectContent, SelectItem } from '../ui/select';
7
+ import { TuningSlider } from '../ui/slider';
8
+ import {
9
+ Rocket, X, ChevronDown, Settings2, Zap, Shield, Server, Monitor, Code2, TestTube, Cpu, Activity, Gauge,
10
+ } from 'lucide-react';
11
+
12
+ const ROLE_ICONS = { backend: Server, frontend: Monitor, fullstack: Code2, testing: TestTube, security: Shield };
13
+ const PROVIDER_TEMP_SUPPORT = new Set(['codex', 'grok', 'local']);
14
+ const NAME_RE = /^[a-zA-Z0-9_-]{1,64}$/;
15
+
16
+ function sanitizeName(raw) {
17
+ return raw.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9_-]/g, '').slice(0, 64);
18
+ }
19
+
20
+ export function RecommendedTeamCard() {
21
+ const recommendedTeam = useGrooveStore((s) => s.recommendedTeam);
22
+ const launchRecommendedTeam = useGrooveStore((s) => s.launchRecommendedTeam);
23
+ const teamLaunchConfig = useGrooveStore((s) => s.teamLaunchConfig);
24
+ const fetchProviders = useGrooveStore((s) => s.fetchProviders);
25
+ const [launching, setLaunching] = useState(false);
26
+ const [editedAgents, setEditedAgents] = useState(null);
27
+ const [settingsOpen, setSettingsOpen] = useState(false);
28
+ const [providers, setProviders] = useState([]);
29
+
30
+ const [tsProvider, setTsProvider] = useState(teamLaunchConfig?.provider || '');
31
+ const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
32
+ const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
33
+ const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
34
+ const [expandedAgent, setExpandedAgent] = useState(null);
35
+
36
+ useEffect(() => {
37
+ fetchProviders().then((list) => {
38
+ if (Array.isArray(list)) setProviders(list.filter((p) => p.installed));
39
+ }).catch(() => {});
40
+ }, []);
41
+
42
+ if (!recommendedTeam?.agents?.length) return null;
43
+
44
+ const agents = recommendedTeam.agents;
45
+ const phase1 = agents.filter((a) => !a.phase || a.phase === 1);
46
+ const phase2 = agents.filter((a) => a.phase === 2);
47
+
48
+ const agentEdits = editedAgents ?? phase1.map((a) => ({ ...a, name: a.name || '' }));
49
+
50
+ const selectedProvider = providers.find((p) => p.id === tsProvider);
51
+ const tsModels = (selectedProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
52
+ const showTemp = PROVIDER_TEMP_SUPPORT.has(tsProvider);
53
+
54
+ function handleNameChange(i, raw) {
55
+ const next = agentEdits.map((a, idx) => idx === i ? { ...a, name: sanitizeName(raw) } : a);
56
+ setEditedAgents(next);
57
+ }
58
+
59
+ function handleAgentField(i, updates) {
60
+ if (typeof updates === 'string') {
61
+ const [field, value] = [updates, arguments[2]];
62
+ setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, [field]: value } : a));
63
+ } else {
64
+ setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, ...updates } : a));
65
+ }
66
+ }
67
+
68
+ function handleTsProviderChange(id) {
69
+ setTsProvider(id);
70
+ const p = providers.find((x) => x.id === id);
71
+ const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
72
+ setTsModel(pModels[0]?.id || '');
73
+ }
74
+
75
+ async function handleLaunch() {
76
+ setLaunching(true);
77
+ useGrooveStore.setState({
78
+ teamLaunchConfig: {
79
+ ...(tsProvider && { provider: tsProvider, model: tsModel }),
80
+ reasoningEffort: tsReasoning,
81
+ ...(showTemp && { temperature: tsTemp }),
82
+ },
83
+ });
84
+ try {
85
+ const modified = [...agentEdits, ...phase2];
86
+ await launchRecommendedTeam(modified);
87
+ } catch { /* toast handles */ }
88
+ setLaunching(false);
89
+ }
90
+
91
+ function handleDismiss() {
92
+ useGrooveStore.setState({ recommendedTeam: null });
93
+ }
94
+
95
+ return (
96
+ <div className="absolute bottom-4 left-1/2 -translate-x-1/2 z-50 w-full max-w-lg">
97
+ <div className="mx-4 rounded-lg border border-accent/30 bg-surface-2/95 backdrop-blur-md shadow-xl shadow-accent/5 overflow-hidden">
98
+ <div className="px-4 py-3 border-b border-border-subtle flex items-center gap-2">
99
+ <Rocket size={16} className="text-accent" />
100
+ <span className="text-sm font-semibold text-text-0 font-sans flex-1">Planner Recommends a Team</span>
101
+ <button onClick={handleDismiss} className="text-text-4 hover:text-text-1 cursor-pointer"><X size={14} /></button>
102
+ </div>
103
+
104
+ {/* Collapsible Team Settings */}
105
+ <div className="border-b border-border-subtle">
106
+ <button
107
+ onClick={() => setSettingsOpen(!settingsOpen)}
108
+ className="w-full flex items-center gap-2 px-4 py-2 text-left cursor-pointer hover:bg-surface-3/50 transition-colors"
109
+ >
110
+ <ChevronDown size={12} className={cn('text-text-4 transition-transform duration-200', !settingsOpen && '-rotate-90')} />
111
+ <Settings2 size={12} className="text-text-3" />
112
+ <span className="text-2xs font-semibold text-text-2 font-sans uppercase tracking-wider">Team Settings</span>
113
+ {tsProvider && (
114
+ <span className="ml-auto text-2xs text-accent font-mono">{tsProvider}{tsModel ? ` / ${tsModel}` : ''}</span>
115
+ )}
116
+ </button>
117
+ {settingsOpen && (
118
+ <div className="px-4 pb-3 space-y-3">
119
+ <div className="flex gap-3">
120
+ <div className="flex-1 space-y-1">
121
+ <label className="text-2xs text-text-3 font-sans">Provider</label>
122
+ <Select value={tsProvider} onValueChange={handleTsProviderChange}>
123
+ <SelectTrigger placeholder="Default" className="bg-surface-4 h-7 text-xs" />
124
+ <SelectContent>
125
+ {providers.map((p) => (
126
+ <SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
127
+ ))}
128
+ </SelectContent>
129
+ </Select>
130
+ </div>
131
+ <div className="flex-1 space-y-1">
132
+ <label className="text-2xs text-text-3 font-sans">Model</label>
133
+ <Select value={tsModel} onValueChange={setTsModel}>
134
+ <SelectTrigger placeholder="Auto" className="bg-surface-4 h-7 text-xs" />
135
+ <SelectContent>
136
+ <SelectItem value="auto">Auto</SelectItem>
137
+ {tsModels.map((m) => (
138
+ <SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
139
+ ))}
140
+ </SelectContent>
141
+ </Select>
142
+ </div>
143
+ </div>
144
+ <TuningSlider
145
+ label="Reasoning"
146
+ value={tsReasoning}
147
+ onChange={setTsReasoning}
148
+ min={0} max={100} step={1}
149
+ />
150
+ {showTemp && (
151
+ <TuningSlider
152
+ label="Temperature"
153
+ value={tsTemp}
154
+ onChange={setTsTemp}
155
+ min={0} max={1} step={0.01}
156
+ formatValue={(v) => v.toFixed(2)}
157
+ />
158
+ )}
159
+ </div>
160
+ )}
161
+ </div>
162
+
163
+ <div className="px-4 py-3 space-y-1.5">
164
+ {agentEdits.map((a, i) => {
165
+ const Icon = ROLE_ICONS[a.role] || Code2;
166
+ const nameValid = !a.name || NAME_RE.test(a.name);
167
+ const isExpanded = expandedAgent === i;
168
+ const agentProvider = providers.find((p) => p.id === (a.provider || tsProvider));
169
+ const agentModels = (agentProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
170
+ return (
171
+ <div key={i} className="rounded-md bg-surface-4 border border-border-subtle overflow-hidden">
172
+ <div
173
+ className="flex items-center gap-2 px-2.5 py-1.5 cursor-pointer hover:bg-surface-5/50 transition-colors"
174
+ onClick={() => setExpandedAgent(isExpanded ? null : i)}
175
+ >
176
+ <Icon size={12} className="text-text-2 shrink-0" />
177
+ <input
178
+ type="text"
179
+ value={a.name}
180
+ onChange={(e) => handleNameChange(i, e.target.value)}
181
+ onClick={(e) => e.stopPropagation()}
182
+ placeholder={a.role}
183
+ className={cn(
184
+ 'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
185
+ !nameValid && 'text-red-400',
186
+ )}
187
+ maxLength={64}
188
+ spellCheck={false}
189
+ />
190
+ {a.provider && a.provider !== tsProvider && (
191
+ <span className="text-2xs text-accent font-mono shrink-0">{a.provider}</span>
192
+ )}
193
+ {a.scope?.length > 0 && (
194
+ <span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
195
+ {a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
196
+ </span>
197
+ )}
198
+ <ChevronDown size={10} className={cn('text-text-4 shrink-0 transition-transform duration-200', !isExpanded && '-rotate-90')} />
199
+ </div>
200
+ {isExpanded && (
201
+ <div className="px-2.5 pb-2.5 pt-1 space-y-2.5 border-t border-border-subtle">
202
+ <div className="flex gap-2">
203
+ <div className="flex-1 space-y-1">
204
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Cpu size={10} />Provider</label>
205
+ <Select value={a.provider || ''} onValueChange={(id) => {
206
+ const p = providers.find((x) => x.id === id);
207
+ const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
208
+ handleAgentField(i, { provider: id, model: pModels[0]?.id || '' });
209
+ }}>
210
+ <SelectTrigger placeholder="Team default" className="bg-surface-3 h-7 text-xs" />
211
+ <SelectContent>
212
+ <SelectItem value="">Team default</SelectItem>
213
+ {providers.map((p) => (
214
+ <SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
215
+ ))}
216
+ </SelectContent>
217
+ </Select>
218
+ </div>
219
+ <div className="flex-1 space-y-1">
220
+ <label className="text-2xs text-text-3 font-sans">Model</label>
221
+ <Select value={a.model || ''} onValueChange={(v) => handleAgentField(i, 'model', v)}>
222
+ <SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
223
+ <SelectContent>
224
+ <SelectItem value="">Auto</SelectItem>
225
+ {agentModels.map((m) => (
226
+ <SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
227
+ ))}
228
+ </SelectContent>
229
+ </Select>
230
+ </div>
231
+ </div>
232
+ <div className="space-y-1">
233
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Activity size={10} />Model Routing</label>
234
+ <div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
235
+ {[{ value: 'fixed', label: 'Fixed' }, { value: 'auto', label: 'Auto' }, { value: 'auto-floor', label: 'Auto + Floor' }].map((opt) => (
236
+ <button
237
+ key={opt.value}
238
+ onClick={() => handleAgentField(i, 'routingMode', opt.value)}
239
+ className={cn(
240
+ 'flex-1 px-2 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
241
+ (a.routingMode || 'auto') === opt.value
242
+ ? 'bg-accent/15 text-accent shadow-sm'
243
+ : 'text-text-3 hover:text-text-1',
244
+ )}
245
+ >
246
+ {opt.label}
247
+ </button>
248
+ ))}
249
+ </div>
250
+ </div>
251
+ <div className="space-y-1">
252
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Gauge size={10} />Effort Level</label>
253
+ <div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
254
+ {[{ value: 'min', label: 'Min' }, { value: 'low', label: 'Low' }, { value: 'default', label: 'Default' }, { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }].map((opt) => (
255
+ <button
256
+ key={opt.value}
257
+ onClick={() => handleAgentField(i, 'effort', opt.value)}
258
+ className={cn(
259
+ 'flex-1 px-1.5 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
260
+ (a.effort || 'default') === opt.value
261
+ ? 'bg-accent/15 text-accent shadow-sm'
262
+ : 'text-text-3 hover:text-text-1',
263
+ )}
264
+ >
265
+ {opt.label}
266
+ </button>
267
+ ))}
268
+ </div>
269
+ </div>
270
+ </div>
271
+ )}
272
+ </div>
273
+ );
274
+ })}
275
+
276
+ {recommendedTeam.projectDir && (
277
+ <div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
278
+ <span className="text-text-4">Project:</span>
279
+ <span className="text-accent">{recommendedTeam.projectDir}/</span>
280
+ </div>
281
+ )}
282
+
283
+ {phase2.length > 0 && (
284
+ <div className="flex items-center gap-1.5 text-2xs text-text-3 font-sans">
285
+ <Shield size={10} />
286
+ <span>{phase2.length} QC agent{phase2.length > 1 ? 's' : ''} will auto-spawn after builders complete</span>
287
+ </div>
288
+ )}
289
+ </div>
290
+
291
+ <div className="px-4 py-3 border-t border-border-subtle">
292
+ <Button variant="primary" size="md" onClick={handleLaunch} disabled={launching} className="w-full gap-2">
293
+ <Zap size={14} />
294
+ {launching ? 'Launching...' : `Launch ${phase1.length} Agent${phase1.length > 1 ? 's' : ''}`}
295
+ </Button>
296
+ </div>
297
+ </div>
298
+ </div>
299
+ );
300
+ }
@@ -83,6 +83,7 @@ export function SpawnWizard() {
83
83
  const [preflightDialog, setPreflightDialog] = useState(null);
84
84
  const [ollamaInstalled, setOllamaInstalled] = useState([]);
85
85
  const [ollamaServerRunning, setOllamaServerRunning] = useState(false);
86
+ const [fast, setFast] = useState(false);
86
87
  const [teamMode, setTeamMode] = useState('new');
87
88
  const [newTeamName, setNewTeamName] = useState('');
88
89
  const [selectedTeamId, setSelectedTeamId] = useState('');
@@ -141,7 +142,7 @@ export function SpawnWizard() {
141
142
  setPersonalities(Array.isArray(data) ? data : data.personalities || []);
142
143
  }).catch(() => {});
143
144
  setRole(''); setCustomRole(''); setName('');
144
- setProvider(_presetProvider); setModel(_presetModel);
145
+ setProvider(_presetProvider); setModel(_presetModel); setFast(false);
145
146
  setTeamMode('new'); setSelectedTeamId('');
146
147
  setNewTeamName(_presetModel
147
148
  ? _presetModel.split(':').pop().replace(/[-_]/g, ' ')
@@ -206,6 +207,7 @@ export function SpawnWizard() {
206
207
  ...(selectedIntegrations.length > 0 && { integrations: selectedIntegrations }),
207
208
  ...(selectedIntegrations.length > 0 && { integrationApproval }),
208
209
  ...(selectedRepos.length > 0 && { repos: selectedRepos }),
210
+ ...(fast && { fast: true }),
209
211
  ...(selectedPersonality && { personality: selectedPersonality }),
210
212
  ...(selectedRole === 'ambassador' && selectedPeerId && { peerId: selectedPeerId }),
211
213
  ...(teamId && { teamId }),
@@ -514,6 +516,33 @@ export function SpawnWizard() {
514
516
  </div>
515
517
  )}
516
518
 
519
+ {/* Fast mode — available for Opus models on Claude Code */}
520
+ {provider === 'claude-code' && model && model.includes('opus') && (
521
+ <label className="flex items-center gap-2.5 cursor-pointer">
522
+ <button
523
+ type="button"
524
+ role="switch"
525
+ aria-checked={fast}
526
+ onClick={() => setFast(!fast)}
527
+ className={cn(
528
+ 'relative w-8 h-[18px] rounded-full transition-colors flex-shrink-0',
529
+ fast ? 'bg-accent' : 'bg-surface-5',
530
+ )}
531
+ >
532
+ <span
533
+ className={cn(
534
+ 'absolute top-[2px] left-[2px] w-[14px] h-[14px] rounded-full bg-white transition-transform',
535
+ fast && 'translate-x-[14px]',
536
+ )}
537
+ />
538
+ </button>
539
+ <div>
540
+ <span className="text-xs font-semibold text-text-0 font-sans">Fast mode</span>
541
+ <span className="text-2xs text-text-3 font-sans ml-1.5">Faster output, same model</span>
542
+ </div>
543
+ </label>
544
+ )}
545
+
517
546
  {/* Ollama model status */}
518
547
  {provider === 'ollama' && model && (
519
548
  <div className="flex items-center gap-2 flex-wrap text-2xs font-sans">