groove-dev 0.8.0

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 (84) hide show
  1. package/CLAUDE.md +197 -0
  2. package/LICENSE +40 -0
  3. package/README.md +115 -0
  4. package/docs/GUI_DESIGN_SPEC.md +402 -0
  5. package/favicon.png +0 -0
  6. package/groove-logo-short.png +0 -0
  7. package/groove-logo.png +0 -0
  8. package/package.json +70 -0
  9. package/packages/cli/bin/groove.js +98 -0
  10. package/packages/cli/package.json +15 -0
  11. package/packages/cli/src/client.js +25 -0
  12. package/packages/cli/src/commands/agents.js +38 -0
  13. package/packages/cli/src/commands/approve.js +50 -0
  14. package/packages/cli/src/commands/config.js +35 -0
  15. package/packages/cli/src/commands/kill.js +15 -0
  16. package/packages/cli/src/commands/nuke.js +19 -0
  17. package/packages/cli/src/commands/providers.js +40 -0
  18. package/packages/cli/src/commands/rotate.js +16 -0
  19. package/packages/cli/src/commands/spawn.js +91 -0
  20. package/packages/cli/src/commands/start.js +31 -0
  21. package/packages/cli/src/commands/status.js +38 -0
  22. package/packages/cli/src/commands/stop.js +15 -0
  23. package/packages/cli/src/commands/team.js +77 -0
  24. package/packages/daemon/package.json +18 -0
  25. package/packages/daemon/src/adaptive.js +237 -0
  26. package/packages/daemon/src/api.js +533 -0
  27. package/packages/daemon/src/classifier.js +126 -0
  28. package/packages/daemon/src/credentials.js +121 -0
  29. package/packages/daemon/src/firstrun.js +93 -0
  30. package/packages/daemon/src/index.js +208 -0
  31. package/packages/daemon/src/introducer.js +238 -0
  32. package/packages/daemon/src/journalist.js +600 -0
  33. package/packages/daemon/src/lockmanager.js +58 -0
  34. package/packages/daemon/src/pm.js +108 -0
  35. package/packages/daemon/src/process.js +361 -0
  36. package/packages/daemon/src/providers/aider.js +72 -0
  37. package/packages/daemon/src/providers/base.js +38 -0
  38. package/packages/daemon/src/providers/claude-code.js +167 -0
  39. package/packages/daemon/src/providers/codex.js +68 -0
  40. package/packages/daemon/src/providers/gemini.js +62 -0
  41. package/packages/daemon/src/providers/index.js +38 -0
  42. package/packages/daemon/src/providers/ollama.js +94 -0
  43. package/packages/daemon/src/registry.js +89 -0
  44. package/packages/daemon/src/rotator.js +185 -0
  45. package/packages/daemon/src/router.js +132 -0
  46. package/packages/daemon/src/state.js +34 -0
  47. package/packages/daemon/src/supervisor.js +178 -0
  48. package/packages/daemon/src/teams.js +203 -0
  49. package/packages/daemon/src/terminal/base.js +27 -0
  50. package/packages/daemon/src/terminal/generic.js +27 -0
  51. package/packages/daemon/src/terminal/tmux.js +64 -0
  52. package/packages/daemon/src/tokentracker.js +124 -0
  53. package/packages/daemon/src/validate.js +122 -0
  54. package/packages/daemon/templates/api-builder.json +18 -0
  55. package/packages/daemon/templates/fullstack.json +18 -0
  56. package/packages/daemon/templates/monorepo.json +24 -0
  57. package/packages/gui/dist/assets/index-BO95Rm1F.js +73 -0
  58. package/packages/gui/dist/assets/index-CPzm9ZE9.css +1 -0
  59. package/packages/gui/dist/favicon.png +0 -0
  60. package/packages/gui/dist/groove-logo-short.png +0 -0
  61. package/packages/gui/dist/groove-logo.png +0 -0
  62. package/packages/gui/dist/index.html +13 -0
  63. package/packages/gui/index.html +12 -0
  64. package/packages/gui/package.json +22 -0
  65. package/packages/gui/public/favicon.png +0 -0
  66. package/packages/gui/public/groove-logo-short.png +0 -0
  67. package/packages/gui/public/groove-logo.png +0 -0
  68. package/packages/gui/src/App.jsx +215 -0
  69. package/packages/gui/src/components/AgentActions.jsx +347 -0
  70. package/packages/gui/src/components/AgentChat.jsx +479 -0
  71. package/packages/gui/src/components/AgentNode.jsx +117 -0
  72. package/packages/gui/src/components/AgentPanel.jsx +115 -0
  73. package/packages/gui/src/components/AgentStats.jsx +333 -0
  74. package/packages/gui/src/components/ApprovalQueue.jsx +156 -0
  75. package/packages/gui/src/components/EmptyState.jsx +100 -0
  76. package/packages/gui/src/components/SpawnPanel.jsx +515 -0
  77. package/packages/gui/src/components/TeamSelector.jsx +162 -0
  78. package/packages/gui/src/main.jsx +9 -0
  79. package/packages/gui/src/stores/groove.js +247 -0
  80. package/packages/gui/src/theme.css +67 -0
  81. package/packages/gui/src/views/AgentTree.jsx +148 -0
  82. package/packages/gui/src/views/CommandCenter.jsx +620 -0
  83. package/packages/gui/src/views/JournalistFeed.jsx +149 -0
  84. package/packages/gui/vite.config.js +19 -0
@@ -0,0 +1,100 @@
1
+ // GROOVE GUI — Empty State
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React from 'react';
5
+ import { useGrooveStore } from '../stores/groove';
6
+
7
+ export default function EmptyState() {
8
+ const openDetail = useGrooveStore((s) => s.openDetail);
9
+ const connected = useGrooveStore((s) => s.connected);
10
+
11
+ return (
12
+ <div style={styles.container}>
13
+ <div style={styles.inner}>
14
+ {!connected ? (
15
+ <>
16
+ <div style={styles.pulseRing}>
17
+ <div style={styles.pulseCore} />
18
+ </div>
19
+ <div style={styles.title}>Connecting to daemon...</div>
20
+ <div style={styles.hint}>
21
+ Make sure the GROOVE daemon is running
22
+ </div>
23
+ <code style={styles.code}>groove start</code>
24
+ </>
25
+ ) : (
26
+ <>
27
+ <div style={styles.readyIcon}>
28
+ <svg width="40" height="40" viewBox="0 0 40 40">
29
+ <circle cx="20" cy="20" r="18" fill="none" stroke="#2c313a" strokeWidth="1" />
30
+ <circle cx="20" cy="20" r="18" fill="none" stroke="#33afbc" strokeWidth="1" strokeDasharray="113" strokeDashoffset="28" strokeLinecap="round">
31
+ <animateTransform attributeName="transform" type="rotate" from="0 20 20" to="360 20 20" dur="3s" repeatCount="indefinite" />
32
+ </circle>
33
+ <text x="20" y="24" textAnchor="middle" fill="#33afbc" fontSize="14" fontWeight="700" fontFamily="JetBrains Mono, monospace">+</text>
34
+ </svg>
35
+ </div>
36
+ <div style={styles.title}>Ready to orchestrate</div>
37
+ <div style={styles.hint}>
38
+ Spawn your first agent to start building
39
+ </div>
40
+ <button onClick={() => openDetail({ type: 'spawn' })} style={styles.spawnBtn}>
41
+ Spawn Agent
42
+ </button>
43
+ </>
44
+ )}
45
+ </div>
46
+ </div>
47
+ );
48
+ }
49
+
50
+ const styles = {
51
+ container: {
52
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
53
+ height: '100%', background: '#1a1e25',
54
+ },
55
+ inner: {
56
+ display: 'flex', flexDirection: 'column',
57
+ alignItems: 'center', gap: 12,
58
+ padding: 40,
59
+ },
60
+ pulseRing: {
61
+ width: 40, height: 40,
62
+ borderRadius: '50%',
63
+ border: '1px solid #33afbc',
64
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
65
+ animation: 'pulse 2s infinite',
66
+ },
67
+ pulseCore: {
68
+ width: 8, height: 8, borderRadius: '50%',
69
+ background: '#33afbc',
70
+ boxShadow: '0 0 12px rgba(51, 175, 188, 0.5)',
71
+ },
72
+ readyIcon: {
73
+ marginBottom: 4,
74
+ },
75
+ title: {
76
+ fontSize: 14, color: '#e6e6e6', fontWeight: 600,
77
+ letterSpacing: 0.5,
78
+ },
79
+ hint: {
80
+ fontSize: 12, color: '#6b7280', textAlign: 'center',
81
+ lineHeight: 1.6, maxWidth: 360,
82
+ },
83
+ spawnBtn: {
84
+ padding: '8px 24px',
85
+ background: 'rgba(51, 175, 188, 0.1)',
86
+ border: '1px solid #33afbc',
87
+ color: '#33afbc', fontSize: 12, fontWeight: 600,
88
+ fontFamily: "'JetBrains Mono', monospace",
89
+ cursor: 'pointer', marginTop: 4,
90
+ transition: 'background 0.2s',
91
+ },
92
+ divider: {
93
+ fontSize: 11, color: '#3e4451', margin: '4px 0',
94
+ },
95
+ code: {
96
+ background: '#252a33', padding: '8px 16px',
97
+ fontSize: 11, color: '#33afbc',
98
+ border: '1px solid #2c313a',
99
+ },
100
+ };
@@ -0,0 +1,515 @@
1
+ // GROOVE GUI — Spawn Panel (detail sidebar)
2
+ // FSL-1.1-Apache-2.0 — see LICENSE
3
+
4
+ import React, { useState, useEffect } from 'react';
5
+ import { useGrooveStore } from '../stores/groove';
6
+
7
+ const ROLE_PRESETS = [
8
+ { id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'] },
9
+ { id: 'frontend', label: 'Frontend', desc: 'UI components, views, styles', scope: ['src/components/**', 'src/views/**', 'src/pages/**', 'src/styles/**'] },
10
+ { id: 'fullstack', label: 'Fullstack', desc: 'Full codebase access', scope: [] },
11
+ { id: 'planner', label: 'Planner', desc: 'Architecture, research, planning', scope: [] },
12
+ { id: 'testing', label: 'Testing', desc: 'Tests, specs, coverage', scope: ['tests/**', 'test/**', '**/*.test.*', '**/*.spec.*'] },
13
+ { id: 'devops', label: 'DevOps', desc: 'Docker, CI/CD, infra', scope: ['Dockerfile*', 'docker-compose*', '.github/**', 'infra/**'] },
14
+ { id: 'docs', label: 'Docs', desc: 'Documentation, READMEs', scope: ['docs/**', '*.md'] },
15
+ ];
16
+
17
+ const PERMISSION_LEVELS = [
18
+ { id: 'auto', label: 'Auto', desc: 'AI PM reviews risky operations before they happen', icon: '~' },
19
+ { id: 'full', label: 'Full Send', desc: 'No reviews, maximum speed', icon: '>' },
20
+ ];
21
+
22
+ export default function SpawnPanel() {
23
+ const spawnAgent = useGrooveStore((s) => s.spawnAgent);
24
+ const closeDetail = useGrooveStore((s) => s.closeDetail);
25
+
26
+ const [role, setRole] = useState('');
27
+ const [customRole, setCustomRole] = useState('');
28
+ const [scope, setScope] = useState('');
29
+ const [prompt, setPrompt] = useState('');
30
+ const [permission, setPermission] = useState('auto');
31
+ const [provider, setProvider] = useState('claude-code');
32
+ const [model, setModel] = useState('auto');
33
+ const [providerList, setProviderList] = useState([]);
34
+ const [submitting, setSubmitting] = useState(false);
35
+ const [error, setError] = useState('');
36
+ const [showAdvanced, setShowAdvanced] = useState(false);
37
+ const [connectingProvider, setConnectingProvider] = useState(null);
38
+ const [apiKeyInput, setApiKeyInput] = useState('');
39
+ const [keySaving, setKeySaving] = useState(false);
40
+
41
+ useEffect(() => {
42
+ fetchProviders();
43
+ }, []);
44
+
45
+ async function fetchProviders() {
46
+ try {
47
+ const res = await fetch('/api/providers');
48
+ setProviderList(await res.json());
49
+ } catch { /* ignore */ }
50
+ }
51
+
52
+ const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
53
+ const effectiveScope = role === 'custom'
54
+ ? scope
55
+ : selectedPreset?.scope.join(', ') || '';
56
+
57
+ const isPlanner = role === 'planner';
58
+
59
+ function handleProviderClick(p) {
60
+ if (p.installed && (p.authType === 'subscription' || p.authType === 'local' || p.hasKey)) {
61
+ // Ready to use
62
+ setProvider(p.id);
63
+ setModel('auto');
64
+ setConnectingProvider(null);
65
+ return;
66
+ }
67
+ // Needs setup — expand connection flow
68
+ setConnectingProvider(p.id);
69
+ setApiKeyInput('');
70
+ }
71
+
72
+ async function handleSaveKey() {
73
+ if (!apiKeyInput.trim() || !connectingProvider) return;
74
+ setKeySaving(true);
75
+ try {
76
+ await fetch(`/api/credentials/${connectingProvider}`, {
77
+ method: 'POST',
78
+ headers: { 'Content-Type': 'application/json' },
79
+ body: JSON.stringify({ key: apiKeyInput.trim() }),
80
+ });
81
+ setApiKeyInput('');
82
+ setConnectingProvider(null);
83
+ setProvider(connectingProvider);
84
+ setModel('auto');
85
+ await fetchProviders(); // Refresh to show updated hasKey status
86
+ } catch {
87
+ // ignore
88
+ }
89
+ setKeySaving(false);
90
+ }
91
+
92
+ async function handleSubmit(e) {
93
+ e.preventDefault();
94
+ const finalRole = role === 'custom' ? customRole : role;
95
+ if (!finalRole) { setError('Select a role'); return; }
96
+
97
+ setSubmitting(true);
98
+ setError('');
99
+
100
+ try {
101
+ const scopeArr = effectiveScope
102
+ ? effectiveScope.split(',').map((s) => s.trim()).filter(Boolean)
103
+ : [];
104
+
105
+ const finalPrompt = prompt || null;
106
+ // Role-specific prompt prefixes (e.g., planner constraints) are now
107
+ // applied daemon-side in process.js for consistency across all spawn paths
108
+
109
+ await spawnAgent({
110
+ role: finalRole,
111
+ scope: scopeArr,
112
+ prompt: finalPrompt,
113
+ model: model || 'auto',
114
+ provider,
115
+ permission,
116
+ });
117
+ closeDetail();
118
+ } catch (err) {
119
+ setError(err.message);
120
+ } finally {
121
+ setSubmitting(false);
122
+ }
123
+ }
124
+
125
+ function getProviderStatus(p) {
126
+ if (!p.installed) return 'not installed';
127
+ if (p.authType === 'api-key' && !p.hasKey) return 'needs key';
128
+ if (p.authType === 'subscription') return 'subscription';
129
+ if (p.authType === 'local') return 'local';
130
+ return 'ready';
131
+ }
132
+
133
+ function isProviderReady(p) {
134
+ if (!p.installed) return false;
135
+ if (p.authType === 'api-key' && !p.hasKey) return false;
136
+ return true;
137
+ }
138
+
139
+ return (
140
+ <div style={{ paddingTop: 4 }}>
141
+ <div style={styles.title}>SPAWN AGENT</div>
142
+
143
+ <form onSubmit={handleSubmit}>
144
+ {/* Role picker */}
145
+ <div style={styles.label}>ROLE</div>
146
+ <div style={styles.roleGrid}>
147
+ {ROLE_PRESETS.map((preset) => (
148
+ <button
149
+ key={preset.id}
150
+ type="button"
151
+ onClick={() => setRole(preset.id)}
152
+ style={{
153
+ ...styles.roleBtn,
154
+ ...(role === preset.id ? styles.roleBtnActive : {}),
155
+ }}
156
+ title={preset.desc}
157
+ >
158
+ {preset.label}
159
+ </button>
160
+ ))}
161
+ <button
162
+ type="button"
163
+ onClick={() => setRole('custom')}
164
+ style={{
165
+ ...styles.roleBtn,
166
+ ...(role === 'custom' ? styles.roleBtnActive : {}),
167
+ }}
168
+ >
169
+ Custom
170
+ </button>
171
+ </div>
172
+
173
+ {selectedPreset && (
174
+ <div style={styles.roleDesc}>{selectedPreset.desc}</div>
175
+ )}
176
+
177
+ {role === 'custom' && (
178
+ <input
179
+ style={{ ...styles.input, marginTop: 6 }}
180
+ placeholder="Custom role name..."
181
+ value={customRole}
182
+ onChange={(e) => setCustomRole(e.target.value)}
183
+ autoFocus
184
+ />
185
+ )}
186
+
187
+ {/* Prompt */}
188
+ <div style={styles.label}>
189
+ {isPlanner ? 'WHAT TO PLAN' : 'TASK PROMPT'}
190
+ </div>
191
+ <textarea
192
+ style={styles.textarea}
193
+ placeholder={isPlanner
194
+ ? 'What should this agent research or plan?'
195
+ : 'What should this agent work on?'}
196
+ value={prompt}
197
+ onChange={(e) => setPrompt(e.target.value)}
198
+ rows={3}
199
+ />
200
+
201
+ {/* Permissions */}
202
+ <div style={styles.label}>PERMISSIONS</div>
203
+ <div style={styles.permGrid}>
204
+ {PERMISSION_LEVELS.map((perm) => (
205
+ <button
206
+ key={perm.id}
207
+ type="button"
208
+ onClick={() => setPermission(perm.id)}
209
+ style={{
210
+ ...styles.permBtn,
211
+ ...(permission === perm.id ? styles.permBtnActive : {}),
212
+ }}
213
+ >
214
+ <span style={styles.permIcon}>{perm.icon}</span>
215
+ <div>
216
+ <div style={styles.permLabel}>{perm.label}</div>
217
+ <div style={styles.permDesc}>{perm.desc}</div>
218
+ </div>
219
+ </button>
220
+ ))}
221
+ </div>
222
+
223
+ {/* Advanced toggle */}
224
+ <button
225
+ type="button"
226
+ onClick={() => setShowAdvanced(!showAdvanced)}
227
+ style={styles.advancedToggle}
228
+ >
229
+ {showAdvanced ? '- hide advanced' : '+ advanced options'}
230
+ </button>
231
+
232
+ {showAdvanced && (
233
+ <>
234
+ {/* Provider selector with connection flow */}
235
+ <div style={styles.label}>PROVIDER</div>
236
+ {providerList.map((p) => {
237
+ const ready = isProviderReady(p);
238
+ const status = getProviderStatus(p);
239
+ const isSelected = provider === p.id;
240
+ const isConnecting = connectingProvider === p.id;
241
+
242
+ return (
243
+ <div key={p.id} style={{ marginBottom: 2 }}>
244
+ <button
245
+ type="button"
246
+ onClick={() => handleProviderClick(p)}
247
+ style={{
248
+ ...styles.providerBtn,
249
+ borderColor: isSelected ? 'var(--accent)' : 'var(--border)',
250
+ opacity: ready || isConnecting ? 1 : 0.6,
251
+ }}
252
+ >
253
+ <div style={{ flex: 1 }}>
254
+ <span style={{
255
+ fontSize: 12, fontWeight: 600,
256
+ color: isSelected ? 'var(--text-bright)' : 'var(--text-primary)',
257
+ }}>
258
+ {p.name}
259
+ </span>
260
+ <span style={styles.providerModels}>
261
+ {p.models.map((m) => m.name).join(', ')}
262
+ </span>
263
+ </div>
264
+ <span style={{
265
+ fontSize: 10,
266
+ color: ready ? 'var(--green)' : status === 'not installed' ? 'var(--text-muted)' : 'var(--amber)',
267
+ }}>
268
+ {ready ? (isSelected ? 'active' : 'ready') : status}
269
+ </span>
270
+ </button>
271
+
272
+ {/* Connection flow — inline expand */}
273
+ {isConnecting && (
274
+ <div style={styles.connectBox}>
275
+ {!p.installed && (
276
+ <div>
277
+ <div style={styles.connectLabel}>Install first:</div>
278
+ <code style={styles.connectCode}>{p.installCommand}</code>
279
+ <div style={styles.connectHint}>Run this in your terminal, then click the provider again</div>
280
+ </div>
281
+ )}
282
+ {p.installed && p.authType === 'api-key' && !p.hasKey && (
283
+ <div>
284
+ <div style={styles.connectLabel}>
285
+ API Key {p.envKey ? `(${p.envKey})` : ''}
286
+ </div>
287
+ <div style={{ display: 'flex', gap: 4 }}>
288
+ <input
289
+ type="password"
290
+ style={styles.input}
291
+ placeholder="sk-..."
292
+ value={apiKeyInput}
293
+ onChange={(e) => setApiKeyInput(e.target.value)}
294
+ onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleSaveKey())}
295
+ />
296
+ <button
297
+ type="button"
298
+ onClick={handleSaveKey}
299
+ disabled={keySaving || !apiKeyInput.trim()}
300
+ style={styles.connectSaveBtn}
301
+ >
302
+ {keySaving ? '...' : 'Save'}
303
+ </button>
304
+ </div>
305
+ <div style={styles.connectHint}>
306
+ Encrypted locally. Never sent to GROOVE servers.
307
+ </div>
308
+ </div>
309
+ )}
310
+ {p.installed && p.authType === 'subscription' && (
311
+ <div>
312
+ <div style={styles.connectLabel}>Subscription auth</div>
313
+ <div style={styles.connectHint}>
314
+ {p.name} uses your existing subscription. Make sure you're logged in via the CLI.
315
+ </div>
316
+ </div>
317
+ )}
318
+ {p.installed && p.authType === 'local' && (
319
+ <div>
320
+ <div style={styles.connectLabel}>Local model</div>
321
+ <div style={styles.connectHint}>
322
+ Make sure {p.name} is running locally. No API key needed.
323
+ </div>
324
+ </div>
325
+ )}
326
+ <button
327
+ type="button"
328
+ onClick={() => setConnectingProvider(null)}
329
+ style={styles.connectCancel}
330
+ >
331
+ cancel
332
+ </button>
333
+ </div>
334
+ )}
335
+ </div>
336
+ );
337
+ })}
338
+
339
+ {/* Model selector */}
340
+ {(() => {
341
+ const currentProvider = providerList.find((p) => p.id === provider);
342
+ const models = currentProvider?.models || [];
343
+ if (models.length === 0) return null;
344
+ return (
345
+ <>
346
+ <div style={styles.label}>MODEL</div>
347
+ <select
348
+ style={styles.input}
349
+ value={model}
350
+ onChange={(e) => setModel(e.target.value)}
351
+ >
352
+ <option value="auto">Auto (recommended)</option>
353
+ {models.map((m) => (
354
+ <option key={m.id} value={m.id}>
355
+ {m.name} ({m.tier})
356
+ </option>
357
+ ))}
358
+ </select>
359
+ </>
360
+ );
361
+ })()}
362
+
363
+ {/* Scope */}
364
+ <div style={styles.label}>FILE SCOPE</div>
365
+ <input
366
+ style={styles.input}
367
+ placeholder="e.g. src/api/**, src/lib/**"
368
+ value={role === 'custom' ? scope : effectiveScope}
369
+ onChange={(e) => { if (role === 'custom') setScope(e.target.value); }}
370
+ readOnly={role !== 'custom'}
371
+ />
372
+ <div style={styles.hint}>
373
+ {role === 'custom'
374
+ ? 'Comma-separated glob patterns'
375
+ : 'Auto-set by role preset'}
376
+ </div>
377
+ </>
378
+ )}
379
+
380
+ {error && <div style={styles.error}>{error}</div>}
381
+
382
+ <button
383
+ type="submit"
384
+ disabled={submitting}
385
+ style={{
386
+ ...styles.submitBtn,
387
+ opacity: submitting ? 0.5 : 1,
388
+ }}
389
+ >
390
+ {submitting ? 'spawning...' : isPlanner ? 'Start Planning' : 'Spawn Agent'}
391
+ </button>
392
+ </form>
393
+ </div>
394
+ );
395
+ }
396
+
397
+ const styles = {
398
+ title: {
399
+ fontSize: 11, fontWeight: 600, color: 'var(--text-dim)',
400
+ textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 14,
401
+ },
402
+ label: {
403
+ fontSize: 11, color: 'var(--text-dim)',
404
+ marginBottom: 4, marginTop: 12,
405
+ textTransform: 'uppercase', letterSpacing: 1.5, fontWeight: 600,
406
+ },
407
+ roleGrid: {
408
+ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4,
409
+ },
410
+ roleBtn: {
411
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
412
+ borderRadius: 2, padding: '6px 4px',
413
+ color: 'var(--text-primary)', fontSize: 11, cursor: 'pointer',
414
+ fontFamily: 'var(--font)',
415
+ transition: 'color 0.1s, border-color 0.1s',
416
+ },
417
+ roleBtnActive: {
418
+ borderColor: 'var(--accent)',
419
+ color: 'var(--text-bright)',
420
+ },
421
+ roleDesc: {
422
+ fontSize: 10, color: 'var(--text-dim)', marginTop: 4, fontStyle: 'italic',
423
+ },
424
+ permGrid: {
425
+ display: 'flex', flexDirection: 'column', gap: 4,
426
+ },
427
+ permBtn: {
428
+ display: 'flex', alignItems: 'center', gap: 10,
429
+ padding: '8px 10px', width: '100%',
430
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
431
+ borderRadius: 2, cursor: 'pointer', textAlign: 'left',
432
+ fontFamily: 'var(--font)',
433
+ },
434
+ permBtnActive: {
435
+ borderColor: 'var(--accent)',
436
+ },
437
+ permIcon: {
438
+ fontSize: 14, fontWeight: 700, color: 'var(--accent)',
439
+ width: 18, textAlign: 'center', flexShrink: 0,
440
+ },
441
+ permLabel: {
442
+ fontSize: 11, color: 'var(--text-bright)', fontWeight: 600,
443
+ },
444
+ permDesc: {
445
+ fontSize: 10, color: 'var(--text-dim)',
446
+ },
447
+ advancedToggle: {
448
+ background: 'none', border: 'none', color: 'var(--text-dim)',
449
+ fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
450
+ padding: '8px 0', marginTop: 8,
451
+ },
452
+ providerBtn: {
453
+ width: '100%', display: 'flex', alignItems: 'center', gap: 8,
454
+ padding: '8px 10px',
455
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
456
+ borderRadius: 2, cursor: 'pointer', textAlign: 'left',
457
+ fontFamily: 'var(--font)',
458
+ },
459
+ providerModels: {
460
+ fontSize: 10, color: 'var(--text-dim)', marginLeft: 6,
461
+ },
462
+ connectBox: {
463
+ padding: '8px 10px', margin: '2px 0 4px',
464
+ background: 'var(--bg-base)', border: '1px solid var(--border)',
465
+ borderRadius: 2,
466
+ },
467
+ connectLabel: {
468
+ fontSize: 11, color: 'var(--text-primary)', fontWeight: 600, marginBottom: 4,
469
+ },
470
+ connectCode: {
471
+ display: 'block', padding: '6px 8px',
472
+ background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2,
473
+ fontSize: 11, color: 'var(--accent)', wordBreak: 'break-all',
474
+ },
475
+ connectHint: {
476
+ fontSize: 10, color: 'var(--text-dim)', marginTop: 4,
477
+ },
478
+ connectSaveBtn: {
479
+ padding: '6px 12px', flexShrink: 0,
480
+ background: 'transparent', border: '1px solid var(--accent)',
481
+ borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
482
+ fontFamily: 'var(--font)', cursor: 'pointer',
483
+ },
484
+ connectCancel: {
485
+ background: 'none', border: 'none', color: 'var(--text-dim)',
486
+ fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
487
+ padding: '4px 0', marginTop: 4,
488
+ },
489
+ input: {
490
+ width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
491
+ borderRadius: 2, padding: '6px 8px',
492
+ color: 'var(--text-primary)', fontSize: 12, outline: 'none',
493
+ fontFamily: 'var(--font)',
494
+ },
495
+ textarea: {
496
+ width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
497
+ borderRadius: 2, padding: '6px 8px',
498
+ color: 'var(--text-primary)', fontSize: 12, outline: 'none',
499
+ fontFamily: 'var(--font)', resize: 'vertical',
500
+ },
501
+ hint: {
502
+ fontSize: 10, color: 'var(--text-muted)', marginTop: 3,
503
+ },
504
+ error: {
505
+ color: 'var(--red)', fontSize: 11, marginTop: 8,
506
+ },
507
+ submitBtn: {
508
+ width: '100%', marginTop: 14, padding: '8px',
509
+ background: 'transparent', border: '1px solid var(--accent)',
510
+ borderRadius: 2,
511
+ color: 'var(--accent)', fontSize: 12, fontWeight: 600,
512
+ fontFamily: 'var(--font)',
513
+ cursor: 'pointer',
514
+ },
515
+ };