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.
- package/default/Screenshot_2026-05-29_at_11.16.28_PM.png +0 -0
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/index.js +15 -2
- package/node_modules/@groove-dev/daemon/src/process.js +1 -1
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +7 -1
- package/node_modules/@groove-dev/daemon/src/registry.js +3 -0
- package/node_modules/@groove-dev/daemon/src/routes/files.js +18 -5
- package/node_modules/@groove-dev/daemon/src/state.js +7 -2
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +16 -6
- package/node_modules/@groove-dev/daemon/src/validate.js +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BsCp-oqa.js +1030 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/components/agents/folder-browser.jsx +39 -11
- package/node_modules/@groove-dev/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/node_modules/@groove-dev/gui/src/components/agents/spawn-wizard.jsx +30 -1
- package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +64 -42
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/node_modules/@groove-dev/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/node_modules/@groove-dev/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +64 -33
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +2 -11
- package/node_modules/@groove-dev/gui/src/views/editor.jsx +63 -2
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +2 -1
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/index.js +15 -2
- package/packages/daemon/src/process.js +1 -1
- package/packages/daemon/src/providers/claude-code.js +7 -1
- package/packages/daemon/src/registry.js +3 -0
- package/packages/daemon/src/routes/files.js +18 -5
- package/packages/daemon/src/state.js +7 -2
- package/packages/daemon/src/tunnel-manager.js +16 -6
- package/packages/daemon/src/validate.js +1 -0
- package/packages/gui/dist/assets/index-BrMU-6gi.css +1 -0
- package/packages/gui/dist/assets/index-BsCp-oqa.js +1030 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-feed.jsx +6 -0
- package/packages/gui/src/components/agents/folder-browser.jsx +39 -11
- package/packages/gui/src/components/agents/recommended-team-card.jsx +300 -0
- package/packages/gui/src/components/agents/spawn-wizard.jsx +30 -1
- package/packages/gui/src/components/editor/terminal.jsx +64 -42
- package/packages/gui/src/components/fleet/fleet-content.jsx +18 -4
- package/packages/gui/src/components/fleet/fleet-sidebar.jsx +125 -44
- package/packages/gui/src/components/layout/breadcrumb-bar.jsx +4 -4
- package/packages/gui/src/components/settings/quick-connect.jsx +64 -33
- package/packages/gui/src/components/settings/ssh-wizard.jsx +74 -72
- package/packages/gui/src/views/agents.jsx +2 -11
- package/packages/gui/src/views/editor.jsx +63 -2
- package/packages/gui/src/views/settings.jsx +2 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-BJVNpGIp.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-CSMIQsrG.js +0 -1025
- package/packages/gui/dist/assets/index-BJVNpGIp.css +0 -1
- package/packages/gui/dist/assets/index-CSMIQsrG.js +0 -1025
- package/ssh/error.png +0 -0
- 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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-BrMU-6gi.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -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
|
-
|
|
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
|
|
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
|
-
|
|
170
|
-
|
|
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">
|