groove-dev 0.17.7 → 0.18.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 (112) hide show
  1. package/CLAUDE.md +0 -7
  2. package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
  3. package/node_modules/@groove-dev/daemon/integrations-registry.json +10 -48
  4. package/node_modules/@groove-dev/daemon/src/api.js +103 -12
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +94 -16
  6. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
  7. package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
  8. package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
  9. package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
  10. package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
  11. package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
  12. package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
  13. package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
  14. package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
  15. package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
  16. package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
  17. package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +182 -0
  18. package/node_modules/@groove-dev/gui/dist/index.html +1 -1
  19. package/node_modules/@groove-dev/gui/src/App.jsx +27 -4
  20. package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +1 -1
  21. package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +839 -586
  22. package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
  23. package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +157 -42
  24. package/package.json +1 -2
  25. package/packages/daemon/integrations-registry.json +10 -48
  26. package/packages/daemon/src/api.js +103 -12
  27. package/packages/daemon/src/integrations.js +94 -16
  28. package/packages/daemon/src/providers/claude-code.js +4 -0
  29. package/packages/gui/dist/assets/index-DXkccbmd.js +182 -0
  30. package/packages/gui/dist/index.html +1 -1
  31. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
  32. package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
  33. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
  34. package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
  35. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
  36. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
  37. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
  38. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
  39. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
  40. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
  41. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
  42. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
  43. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
  44. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
  45. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
  46. package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
  47. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
  48. package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
  49. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
  50. package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
  51. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
  52. package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
  53. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
  54. package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
  55. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
  56. package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
  57. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
  58. package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
  59. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
  60. package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
  61. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
  62. package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
  63. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
  64. package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
  65. package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
  66. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
  67. package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
  68. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
  69. package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
  70. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
  71. package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
  72. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
  73. package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
  74. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
  75. package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
  76. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
  77. package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
  78. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
  79. package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
  80. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
  81. package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
  82. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
  83. package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
  84. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
  85. package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
  86. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
  87. package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
  88. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
  89. package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
  90. package/packages/gui/node_modules/.vite/deps/package.json +3 -0
  91. package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
  92. package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
  93. package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
  94. package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
  95. package/packages/gui/node_modules/.vite/deps/react.js +5 -0
  96. package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
  97. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
  98. package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
  99. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
  100. package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
  101. package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
  102. package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
  103. package/packages/gui/src/App.jsx +27 -4
  104. package/packages/gui/src/components/AgentChat.jsx +1 -1
  105. package/packages/gui/src/components/SpawnPanel.jsx +839 -586
  106. package/packages/gui/src/views/FileEditor.jsx +85 -1
  107. package/packages/gui/src/views/IntegrationsStore.jsx +157 -42
  108. package/docs/FILE-EDITOR-PLAN.md +0 -253
  109. package/docs/GUI_DESIGN_SPEC.md +0 -402
  110. package/docs/SKILLS-API-SPEC.md +0 -277
  111. package/node_modules/@groove-dev/gui/dist/assets/index-CsymvgNh.js +0 -156
  112. package/packages/gui/dist/assets/index-CsymvgNh.js +0 -156
@@ -1,12 +1,12 @@
1
- // GROOVE GUI — Spawn Panel (detail sidebar)
1
+ // GROOVE GUI — Spawn Panel (full-screen agent configurator)
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import React, { useState, useEffect } from 'react';
4
+ import React, { useState, useEffect, useRef } from 'react';
5
5
  import { useGrooveStore } from '../stores/groove';
6
6
  import DirPicker from './DirPicker';
7
+ import { FormattedText } from './AgentChat';
7
8
 
8
9
  const ROLE_PRESETS = [
9
- // Coding roles
10
10
  { id: 'backend', label: 'Backend', desc: 'APIs, server logic, database', scope: ['src/api/**', 'src/server/**', 'src/lib/**', 'src/db/**'], category: 'coding' },
11
11
  { id: 'frontend', label: 'Frontend', desc: 'UI components, views, styles', scope: ['src/components/**', 'src/views/**', 'src/pages/**', 'src/styles/**'], category: 'coding' },
12
12
  { id: 'fullstack', label: 'Fullstack', desc: 'Full codebase access', scope: [], category: 'coding' },
@@ -14,7 +14,6 @@ const ROLE_PRESETS = [
14
14
  { id: 'testing', label: 'Testing', desc: 'Tests, specs, coverage', scope: ['tests/**', 'test/**', '**/*.test.*', '**/*.spec.*'], category: 'coding' },
15
15
  { id: 'devops', label: 'DevOps', desc: 'Docker, CI/CD, infra', scope: ['Dockerfile*', 'docker-compose*', '.github/**', 'infra/**'], category: 'coding' },
16
16
  { id: 'docs', label: 'Docs', desc: 'Documentation, READMEs', scope: ['docs/**', '*.md'], category: 'coding' },
17
- // Business roles
18
17
  { id: 'cmo', label: 'CMO', desc: 'Marketing, social media, content', scope: [], category: 'business', integrations: ['slack', 'brave-search'] },
19
18
  { id: 'cfo', label: 'CFO', desc: 'Finance, billing, revenue', scope: [], category: 'business', integrations: ['stripe', 'google-drive'] },
20
19
  { id: 'ea', label: 'EA', desc: 'Scheduling, email, comms', scope: [], category: 'business', integrations: ['gmail', 'google-calendar', 'slack'] },
@@ -24,14 +23,25 @@ const ROLE_PRESETS = [
24
23
  ];
25
24
 
26
25
  const PERMISSION_LEVELS = [
27
- { id: 'auto', label: 'Auto', desc: 'AI PM reviews risky operations before they happen', icon: '~' },
28
- { id: 'full', label: 'Full Send', desc: 'No reviews, maximum speed', icon: '>' },
26
+ { id: 'auto', label: 'Auto', desc: 'AI PM reviews risky operations', icon: '~' },
27
+ { id: 'full', label: 'Full Send', desc: 'No reviews, max speed', icon: '>' },
28
+ ];
29
+
30
+ const CRON_PRESETS = [
31
+ { value: '*/30 * * * *', label: 'Every 30 min' },
32
+ { value: '0 * * * *', label: 'Every hour' },
33
+ { value: '0 */6 * * *', label: 'Every 6 hours' },
34
+ { value: '0 9 * * *', label: 'Daily 9 AM' },
35
+ { value: '0 9 * * 1-5', label: 'Weekdays 9 AM' },
36
+ { value: '0 0 * * 1', label: 'Weekly Mon' },
37
+ { value: '0 0 1 * *', label: 'Monthly' },
29
38
  ];
30
39
 
31
40
  export default function SpawnPanel() {
32
41
  const spawnAgent = useGrooveStore((s) => s.spawnAgent);
33
42
  const closeDetail = useGrooveStore((s) => s.closeDetail);
34
43
 
44
+ // Config state
35
45
  const [role, setRole] = useState('');
36
46
  const [customRole, setCustomRole] = useState('');
37
47
  const [scope, setScope] = useState('');
@@ -39,6 +49,7 @@ export default function SpawnPanel() {
39
49
  const [permission, setPermission] = useState('auto');
40
50
  const [provider, setProvider] = useState('claude-code');
41
51
  const [model, setModel] = useState('auto');
52
+ const [effort, setEffort] = useState('high');
42
53
  const [providerList, setProviderList] = useState([]);
43
54
  const [submitting, setSubmitting] = useState(false);
44
55
  const [error, setError] = useState('');
@@ -54,55 +65,53 @@ export default function SpawnPanel() {
54
65
  const [installedIntegrations, setInstalledIntegrations] = useState([]);
55
66
  const [selectedIntegrations, setSelectedIntegrations] = useState([]);
56
67
 
68
+ // API key state
69
+ const [hasApiKey, setHasApiKey] = useState(false);
70
+ const [showApiKeyInput, setShowApiKeyInput] = useState(false);
71
+ const [apiKeyValue, setApiKeyValue] = useState('');
72
+ const [apiKeySaving, setApiKeySaving] = useState(false);
73
+
74
+ // Schedule state
75
+ const [scheduleEnabled, setScheduleEnabled] = useState(false);
76
+ const [scheduleCron, setScheduleCron] = useState('0 9 * * *');
77
+ const [scheduleName, setScheduleName] = useState('');
78
+
79
+ // Plan chat state
80
+ const [planMode, setPlanMode] = useState(false);
81
+ const [planMessages, setPlanMessages] = useState([]);
82
+ const [planInput, setPlanInput] = useState('');
83
+ const [planLoading, setPlanLoading] = useState(false);
84
+ const [planResearching, setPlanResearching] = useState(false);
85
+ const chatEndRef = useRef(null);
86
+
57
87
  useEffect(() => {
58
88
  fetchProviders();
59
89
  fetchWorkspaces();
60
90
  fetchInstalledSkills();
61
91
  fetchInstalledIntegrations();
92
+ fetch('/api/anthropic-key/status').then((r) => r.json()).then((d) => setHasApiKey(d.configured)).catch(() => {});
62
93
  }, []);
63
94
 
95
+ useEffect(() => {
96
+ chatEndRef.current?.scrollIntoView({ behavior: 'smooth' });
97
+ }, [planMessages, planLoading]);
98
+
64
99
  async function fetchProviders() {
65
- try {
66
- const res = await fetch('/api/providers');
67
- setProviderList(await res.json());
68
- } catch { /* ignore */ }
100
+ try { const res = await fetch('/api/providers'); setProviderList(await res.json()); } catch { /* */ }
69
101
  }
70
-
71
102
  async function fetchWorkspaces() {
72
- try {
73
- const res = await fetch('/api/indexer/workspaces');
74
- const data = await res.json();
75
- setWorkspaces(data.workspaces || []);
76
- } catch { /* ignore */ }
103
+ try { const res = await fetch('/api/indexer/workspaces'); const d = await res.json(); setWorkspaces(d.workspaces || []); } catch { /* */ }
77
104
  }
78
-
79
105
  async function fetchInstalledSkills() {
80
- try {
81
- const res = await fetch('/api/skills/installed');
82
- setInstalledSkills(await res.json());
83
- } catch { /* ignore */ }
106
+ try { const res = await fetch('/api/skills/installed'); setInstalledSkills(await res.json()); } catch { /* */ }
84
107
  }
85
-
86
108
  async function fetchInstalledIntegrations() {
87
- try {
88
- const res = await fetch('/api/integrations/installed');
89
- setInstalledIntegrations(await res.json());
90
- } catch { /* ignore */ }
91
- }
92
-
93
- function toggleSkill(skillId) {
94
- setSelectedSkills((prev) =>
95
- prev.includes(skillId) ? prev.filter((s) => s !== skillId) : [...prev, skillId]
96
- );
109
+ try { const res = await fetch('/api/integrations/installed'); setInstalledIntegrations(await res.json()); } catch { /* */ }
97
110
  }
98
111
 
99
- function toggleIntegration(integrationId) {
100
- setSelectedIntegrations((prev) =>
101
- prev.includes(integrationId) ? prev.filter((s) => s !== integrationId) : [...prev, integrationId]
102
- );
103
- }
112
+ function toggleSkill(id) { setSelectedSkills((p) => p.includes(id) ? p.filter((s) => s !== id) : [...p, id]); }
113
+ function toggleIntegration(id) { setSelectedIntegrations((p) => p.includes(id) ? p.filter((s) => s !== id) : [...p, id]); }
104
114
 
105
- // Auto-select integrations when a business role is chosen
106
115
  useEffect(() => {
107
116
  const preset = ROLE_PRESETS.find((p) => p.id === role);
108
117
  if (preset?.integrations && installedIntegrations.length > 0) {
@@ -114,23 +123,14 @@ export default function SpawnPanel() {
114
123
  }, [role, installedIntegrations]);
115
124
 
116
125
  const selectedPreset = ROLE_PRESETS.find((p) => p.id === role);
117
- const effectiveScope = role === 'custom'
118
- ? scope
119
- : selectedPreset?.scope.join(', ') || '';
120
-
126
+ const effectiveScope = role === 'custom' ? scope : selectedPreset?.scope.join(', ') || '';
121
127
  const isPlanner = role === 'planner';
122
128
 
123
129
  function handleProviderClick(p) {
124
130
  if (p.installed && (p.authType === 'subscription' || p.authType === 'local' || p.hasKey)) {
125
- // Ready to use
126
- setProvider(p.id);
127
- setModel('auto');
128
- setConnectingProvider(null);
129
- return;
131
+ setProvider(p.id); setModel('auto'); setConnectingProvider(null); return;
130
132
  }
131
- // Needs setup — expand connection flow
132
- setConnectingProvider(p.id);
133
- setApiKeyInput('');
133
+ setConnectingProvider(p.id); setApiKeyInput('');
134
134
  }
135
135
 
136
136
  async function handleSaveKey() {
@@ -138,630 +138,883 @@ export default function SpawnPanel() {
138
138
  setKeySaving(true);
139
139
  try {
140
140
  await fetch(`/api/credentials/${connectingProvider}`, {
141
- method: 'POST',
142
- headers: { 'Content-Type': 'application/json' },
141
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
143
142
  body: JSON.stringify({ key: apiKeyInput.trim() }),
144
143
  });
145
- setApiKeyInput('');
146
- setConnectingProvider(null);
147
- setProvider(connectingProvider);
148
- setModel('auto');
149
- await fetchProviders(); // Refresh to show updated hasKey status
144
+ setApiKeyInput(''); setConnectingProvider(null); setProvider(connectingProvider); setModel('auto');
145
+ await fetchProviders();
146
+ } catch { /* */ }
147
+ setKeySaving(false);
148
+ }
149
+
150
+ function getProviderStatus(p) {
151
+ if (!p.installed) return 'not installed';
152
+ if (p.authType === 'api-key' && !p.hasKey) return 'needs key';
153
+ if (p.authType === 'subscription') return 'subscription';
154
+ if (p.authType === 'local') return 'local';
155
+ return 'ready';
156
+ }
157
+ function isProviderReady(p) {
158
+ if (!p.installed) return false;
159
+ if (p.authType === 'api-key' && !p.hasKey) return false;
160
+ return true;
161
+ }
162
+
163
+ // --- Plan chat (Haiku triages: fast for chat, deep for research) ---
164
+ async function handlePlanSend() {
165
+ if (!planInput.trim() || planLoading) return;
166
+ const userMsg = planInput.trim();
167
+ setPlanInput('');
168
+ setPlanMessages((prev) => [...prev, { from: 'user', text: userMsg }]);
169
+ setPlanLoading(true);
170
+ setPlanResearching(false);
171
+
172
+ try {
173
+ const finalRole = role === 'custom' ? customRole : role;
174
+ const context = [
175
+ finalRole ? `Agent role: ${finalRole}` : null,
176
+ prompt ? `Current task prompt: ${prompt}` : null,
177
+ ].filter(Boolean).join('\n');
178
+
179
+ const history = planMessages.map((m) => `${m.from === 'user' ? 'User' : 'AI'}: ${m.text}`).join('\n');
180
+ const res = await fetch('/api/journalist/query', {
181
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
182
+ body: JSON.stringify({ prompt: `${context}\n\n${history}\nUser: ${userMsg}\n\nRespond helpfully:` }),
183
+ });
184
+ const data = await res.json();
185
+
186
+ if (data.mode === 'research') {
187
+ // Research mode took longer but we got the deep response
188
+ setPlanMessages((prev) => [...prev, { from: 'ai', text: data.response || 'No response', mode: 'research' }]);
189
+ } else {
190
+ setPlanMessages((prev) => [...prev, { from: 'ai', text: data.response || data.error || 'No response' }]);
191
+ }
150
192
  } catch {
151
- // ignore
193
+ setPlanMessages((prev) => [...prev, { from: 'ai', text: 'Failed to reach AI. Write your prompt directly.' }]);
152
194
  }
153
- setKeySaving(false);
195
+ setPlanLoading(false);
196
+ setPlanResearching(false);
154
197
  }
155
198
 
199
+ async function applyPlanToPrompt() {
200
+ // Ask AI to synthesize the conversation into a clean, actionable agent prompt
201
+ setPlanLoading(true);
202
+ try {
203
+ const conversation = planMessages.map((m) => `${m.from === 'user' ? 'User' : 'AI'}: ${m.text}`).join('\n\n');
204
+ const finalRole = role === 'custom' ? customRole : role;
205
+ const res = await fetch('/api/journalist/query', {
206
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
207
+ body: JSON.stringify({
208
+ prompt: `Synthesize the following planning conversation into a clean, structured prompt for a ${finalRole || 'coding'} agent. Output ONLY the prompt — no preamble, no explanation, just the actual content the agent should receive.
209
+
210
+ CRITICAL: The prompt MUST include the SPECIFIC TASK or feature to work on — extracted from the conversation. Don't just define the agent's role. Tell it exactly what to build/plan/do. Structure it as:
211
+ 1. Brief role context (2-3 sentences max)
212
+ 2. The specific task/feature (this is the main content)
213
+ 3. Requirements, constraints, acceptance criteria
214
+ 4. Any relevant details discussed
215
+
216
+ Conversation:\n${conversation}`,
217
+ }),
218
+ });
219
+ const data = await res.json();
220
+ if (data.response) {
221
+ setPrompt(data.response);
222
+ setPlanMode(false);
223
+ }
224
+ } catch {
225
+ // Fallback: use last AI message
226
+ const lastAi = [...planMessages].reverse().find((m) => m.from === 'ai');
227
+ if (lastAi) setPrompt(lastAi.text);
228
+ setPlanMode(false);
229
+ }
230
+ setPlanLoading(false);
231
+ }
232
+
233
+ // --- Submit ---
156
234
  async function handleSubmit(e) {
157
235
  e.preventDefault();
158
236
  const finalRole = role === 'custom' ? customRole : role;
159
237
  if (!finalRole) { setError('Select a role'); return; }
160
-
161
- setSubmitting(true);
162
- setError('');
238
+ setSubmitting(true); setError('');
163
239
 
164
240
  try {
165
- const scopeArr = effectiveScope
166
- ? effectiveScope.split(',').map((s) => s.trim()).filter(Boolean)
167
- : [];
168
-
169
- const finalPrompt = prompt || null;
170
- // Role-specific prompt prefixes (e.g., planner constraints) are now
171
- // applied daemon-side in process.js for consistency across all spawn paths
172
-
173
- await spawnAgent({
174
- role: finalRole,
175
- scope: scopeArr,
176
- prompt: finalPrompt,
177
- model: model || 'auto',
178
- provider,
179
- permission,
241
+ const scopeArr = effectiveScope ? effectiveScope.split(',').map((s) => s.trim()).filter(Boolean) : [];
242
+ const agentConfig = {
243
+ role: finalRole, scope: scopeArr, prompt: prompt || null,
244
+ model: model || 'auto', provider, permission, effort,
180
245
  ...(workingDir.trim() ? { workingDir: workingDir.trim() } : {}),
181
246
  ...(selectedSkills.length > 0 ? { skills: selectedSkills } : {}),
182
247
  ...(selectedIntegrations.length > 0 ? { integrations: selectedIntegrations } : {}),
183
- });
248
+ };
249
+
250
+ if (scheduleEnabled) {
251
+ await fetch('/api/schedules', {
252
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
253
+ body: JSON.stringify({
254
+ name: scheduleName.trim() || `${finalRole}-schedule`,
255
+ cron: scheduleCron,
256
+ agentConfig: { role: finalRole, prompt: prompt || null },
257
+ }),
258
+ });
259
+ }
260
+ await spawnAgent(agentConfig);
184
261
  closeDetail();
185
- } catch (err) {
186
- setError(err.message);
187
- } finally {
188
- setSubmitting(false);
189
- }
190
- }
191
-
192
- function getProviderStatus(p) {
193
- if (!p.installed) return 'not installed';
194
- if (p.authType === 'api-key' && !p.hasKey) return 'needs key';
195
- if (p.authType === 'subscription') return 'subscription';
196
- if (p.authType === 'local') return 'local';
197
- return 'ready';
262
+ } catch (err) { setError(err.message); }
263
+ finally { setSubmitting(false); }
198
264
  }
199
265
 
200
- function isProviderReady(p) {
201
- if (!p.installed) return false;
202
- if (p.authType === 'api-key' && !p.hasKey) return false;
203
- return true;
204
- }
266
+ // ========== RENDER ==========
205
267
 
206
268
  return (
207
- <div style={{ paddingTop: 4 }}>
208
- <div style={styles.title}>SPAWN AGENT</div>
209
-
210
- <form onSubmit={handleSubmit}>
211
- {/* Role picker */}
212
- <div style={styles.label}>ROLE</div>
213
- <div style={styles.roleGrid}>
214
- {ROLE_PRESETS.map((preset) => (
215
- <button
216
- key={preset.id}
217
- type="button"
218
- onClick={() => setRole(preset.id)}
219
- style={{
220
- ...styles.roleBtn,
221
- ...(role === preset.id ? styles.roleBtnActive : {}),
222
- }}
223
- title={preset.desc}
224
- >
225
- {preset.label}
226
- </button>
227
- ))}
228
- <button
229
- type="button"
230
- onClick={() => setRole('custom')}
231
- style={{
232
- ...styles.roleBtn,
233
- ...(role === 'custom' ? styles.roleBtnActive : {}),
234
- }}
235
- >
236
- Custom
237
- </button>
238
- </div>
239
-
240
- {selectedPreset && (
241
- <div style={styles.roleDesc}>{selectedPreset.desc}</div>
242
- )}
243
-
244
- {role === 'custom' && (
245
- <input
246
- style={{ ...styles.input, marginTop: 6 }}
247
- placeholder="Custom role name..."
248
- value={customRole}
249
- onChange={(e) => setCustomRole(e.target.value)}
250
- autoFocus
251
- />
252
- )}
253
-
254
- {/* Prompt */}
255
- <div style={styles.label}>
256
- {isPlanner ? 'WHAT TO PLAN' : 'TASK PROMPT'}
257
- </div>
258
- <textarea
259
- style={styles.textarea}
260
- placeholder={isPlanner
261
- ? 'What should this agent research or plan?'
262
- : 'What should this agent work on?'}
263
- value={prompt}
264
- onChange={(e) => setPrompt(e.target.value)}
265
- rows={3}
266
- />
267
-
268
- {/* Directory picker */}
269
- <div style={styles.label}>DIRECTORY</div>
270
- <div style={styles.wsRow}>
271
- <button
272
- type="button"
273
- onClick={() => setWorkingDir('')}
274
- style={{
275
- ...styles.wsBtn,
276
- ...(!workingDir ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
277
- }}
278
- >
279
- project root
280
- </button>
281
- {workspaces.map((ws) => (
282
- <button
283
- key={ws.path}
284
- type="button"
285
- onClick={() => setWorkingDir(ws.path)}
286
- style={{
287
- ...styles.wsBtn,
288
- ...(workingDir === ws.path ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}),
289
- }}
290
- title={`${ws.name} (${ws.files} files)`}
291
- >
292
- {ws.path}
293
- </button>
294
- ))}
295
- <button
296
- type="button"
297
- onClick={() => setShowDirPicker(true)}
298
- style={styles.browseBtn}
299
- >
300
- Browse...
301
- </button>
302
- </div>
303
- {workingDir && (
304
- <div style={styles.hint}>{workingDir}</div>
305
- )}
306
-
307
- {showDirPicker && (
308
- <DirPicker
309
- initial={workingDir}
310
- onSelect={(path) => setWorkingDir(path)}
311
- onClose={() => setShowDirPicker(false)}
312
- />
313
- )}
314
-
315
- {/* Permissions */}
316
- <div style={styles.label}>PERMISSIONS</div>
317
- <div style={styles.permGrid}>
318
- {PERMISSION_LEVELS.map((perm) => (
269
+ <div style={S.overlay}>
270
+ <div style={S.container}>
271
+ {/* Header bar */}
272
+ <div style={S.header}>
273
+ <div style={S.headerTitle}>Spawn Agent</div>
274
+ <div style={S.headerRight}>
275
+ {error && <span style={S.headerError}>{error}</span>}
319
276
  <button
320
- key={perm.id}
321
- type="button"
322
- onClick={() => setPermission(perm.id)}
323
- style={{
324
- ...styles.permBtn,
325
- ...(permission === perm.id ? styles.permBtnActive : {}),
326
- }}
277
+ onClick={handleSubmit}
278
+ disabled={submitting || !role}
279
+ style={{ ...S.spawnBtn, opacity: submitting || !role ? 0.4 : 1 }}
327
280
  >
328
- <span style={styles.permIcon}>{perm.icon}</span>
329
- <div>
330
- <div style={styles.permLabel}>{perm.label}</div>
331
- <div style={styles.permDesc}>{perm.desc}</div>
332
- </div>
281
+ {submitting ? 'Spawning...'
282
+ : scheduleEnabled ? 'Spawn + Schedule'
283
+ : isPlanner ? 'Start Planning' : 'Spawn Agent'}
333
284
  </button>
334
- ))}
285
+ <button onClick={closeDetail} style={S.closeBtn}>&times;</button>
286
+ </div>
335
287
  </div>
336
288
 
337
- {/* Integrations picker */}
338
- {installedIntegrations.length > 0 && (
339
- <>
340
- <div style={styles.label}>INTEGRATIONS</div>
341
- <div style={styles.skillsGrid}>
342
- {installedIntegrations.map((item) => {
343
- const active = selectedIntegrations.includes(item.id);
344
- const ready = item.configured;
345
- return (
346
- <button
347
- key={item.id}
348
- type="button"
349
- onClick={() => ready && toggleIntegration(item.id)}
350
- title={ready ? item.description : 'Configure credentials first'}
351
- style={{
352
- ...styles.skillBtn,
353
- borderColor: active ? 'var(--accent)' : !ready ? 'var(--amber)' : 'var(--border)',
354
- background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
355
- opacity: ready ? 1 : 0.5,
356
- cursor: ready ? 'pointer' : 'not-allowed',
357
- }}
358
- >
359
- <span style={{
360
- ...styles.skillIcon,
361
- background: active ? 'var(--accent)' : !ready ? 'var(--amber)' : 'var(--bg-active)',
362
- color: active ? 'var(--bg-base)' : 'var(--text-dim)',
363
- }}>
364
- {(item.name || '?').charAt(0)}
365
- </span>
366
- <div style={{ flex: 1, minWidth: 0 }}>
367
- <div style={{
368
- fontSize: 11, fontWeight: 600,
369
- color: active ? 'var(--text-bright)' : 'var(--text-primary)',
370
- }}>
371
- {item.name}
372
- </div>
373
- <div style={{ fontSize: 9, color: ready ? 'var(--green)' : 'var(--amber)' }}>
374
- {ready ? 'connected' : 'needs setup'}
289
+ {/* Two-panel body */}
290
+ <div style={S.body}>
291
+ {/* LEFT — Config */}
292
+ <div style={S.left}>
293
+ <div style={S.leftScroll}>
294
+ {/* Roles */}
295
+ <Section label="Role">
296
+ <div style={S.roleGrid}>
297
+ {ROLE_PRESETS.filter((p) => p.category === 'coding').map((p) => (
298
+ <RoleBtn key={p.id} preset={p} active={role === p.id} onClick={() => setRole(p.id)} />
299
+ ))}
300
+ </div>
301
+ <div style={{ ...S.sectionSub, marginTop: 8 }}>Business</div>
302
+ <div style={S.roleGrid}>
303
+ {ROLE_PRESETS.filter((p) => p.category === 'business').map((p) => (
304
+ <RoleBtn key={p.id} preset={p} active={role === p.id} onClick={() => setRole(p.id)} />
305
+ ))}
306
+ <button type="button" onClick={() => setRole('custom')}
307
+ style={{ ...S.roleBtn, ...(role === 'custom' ? S.roleBtnActive : {}) }}>
308
+ Custom
309
+ </button>
310
+ </div>
311
+ {role === 'custom' && (
312
+ <input style={{ ...S.input, marginTop: 6 }} placeholder="Custom role name..."
313
+ value={customRole} onChange={(e) => setCustomRole(e.target.value)} autoFocus />
314
+ )}
315
+ {selectedPreset && <div style={S.hint}>{selectedPreset.desc}</div>}
316
+ </Section>
317
+
318
+ {/* Directory */}
319
+ <Section label="Directory">
320
+ <div style={S.chipRow}>
321
+ <Chip label="project root" active={!workingDir} onClick={() => setWorkingDir('')} />
322
+ {workspaces.map((ws) => (
323
+ <Chip key={ws.path} label={ws.path} active={workingDir === ws.path}
324
+ onClick={() => setWorkingDir(ws.path)} />
325
+ ))}
326
+ <button type="button" onClick={() => setShowDirPicker(true)} style={S.browseBtn}>Browse...</button>
327
+ </div>
328
+ {showDirPicker && (
329
+ <DirPicker initial={workingDir} onSelect={(p) => setWorkingDir(p)} onClose={() => setShowDirPicker(false)} />
330
+ )}
331
+ </Section>
332
+
333
+ {/* Permissions */}
334
+ <Section label="Permissions">
335
+ <div style={{ display: 'flex', gap: 6 }}>
336
+ {PERMISSION_LEVELS.map((perm) => (
337
+ <button key={perm.id} type="button" onClick={() => setPermission(perm.id)}
338
+ style={{ ...S.permBtn, ...(permission === perm.id ? S.permBtnActive : {}) }}>
339
+ <span style={S.permIcon}>{perm.icon}</span>
340
+ <div>
341
+ <div style={{ fontSize: 11, fontWeight: 600, color: permission === perm.id ? 'var(--text-bright)' : 'var(--text-primary)' }}>{perm.label}</div>
342
+ <div style={{ fontSize: 9, color: 'var(--text-dim)' }}>{perm.desc}</div>
375
343
  </div>
344
+ </button>
345
+ ))}
346
+ </div>
347
+ </Section>
348
+
349
+ {/* Effort */}
350
+ <Section label="Effort">
351
+ <div style={S.chipRow}>
352
+ {[
353
+ { id: 'low', label: 'Low', desc: 'Quick tasks' },
354
+ { id: 'medium', label: 'Medium', desc: 'Standard' },
355
+ { id: 'high', label: 'High', desc: 'Comprehensive' },
356
+ { id: 'max', label: 'Max', desc: 'Deep reasoning' },
357
+ ].map((e) => (
358
+ <button key={e.id} type="button" onClick={() => setEffort(e.id)}
359
+ title={e.desc}
360
+ style={{
361
+ ...S.chip, flex: 1, textAlign: 'center', padding: '5px 4px',
362
+ ...(effort === e.id ? { borderColor: 'var(--accent)', color: 'var(--text-bright)', background: 'rgba(51, 175, 188, 0.08)' } : {}),
363
+ }}>
364
+ {e.label}
365
+ </button>
366
+ ))}
367
+ </div>
368
+ </Section>
369
+
370
+ {/* Integrations */}
371
+ {installedIntegrations.length > 0 && (
372
+ <Section label={`Integrations (${selectedIntegrations.length})`}>
373
+ <div style={S.itemList}>
374
+ {installedIntegrations.map((item) => {
375
+ const active = selectedIntegrations.includes(item.id);
376
+ const ready = item.configured;
377
+ return (
378
+ <ItemBtn key={item.id} name={item.name} active={active}
379
+ sub={ready ? 'connected' : 'needs setup'} subColor={ready ? 'var(--green)' : 'var(--amber)'}
380
+ disabled={!ready} onClick={() => ready && toggleIntegration(item.id)} />
381
+ );
382
+ })}
383
+ </div>
384
+ </Section>
385
+ )}
386
+
387
+ {/* Skills */}
388
+ {installedSkills.length > 0 && (
389
+ <Section label={`Skills (${selectedSkills.length})`}>
390
+ <div style={S.itemList}>
391
+ {installedSkills.map((skill) => {
392
+ const active = selectedSkills.includes(skill.id);
393
+ return (
394
+ <ItemBtn key={skill.id} name={skill.name} active={active}
395
+ sub={skill.author || 'local'} onClick={() => toggleSkill(skill.id)} />
396
+ );
397
+ })}
398
+ </div>
399
+ </Section>
400
+ )}
401
+
402
+ {/* Schedule */}
403
+ <Section label="Schedule">
404
+ <button type="button" onClick={() => setScheduleEnabled(!scheduleEnabled)}
405
+ style={{ ...S.toggleBtn, borderColor: scheduleEnabled ? 'var(--accent)' : 'var(--border)' }}>
406
+ <span style={{ ...S.checkbox, background: scheduleEnabled ? 'var(--accent)' : 'transparent',
407
+ borderColor: scheduleEnabled ? 'var(--accent)' : 'var(--border)' }}>
408
+ {scheduleEnabled ? '\u2713' : ''}
409
+ </span>
410
+ <span style={{ fontSize: 11, color: 'var(--text-primary)' }}>Recurring schedule</span>
411
+ </button>
412
+ {scheduleEnabled && (
413
+ <div style={{ marginTop: 8, display: 'flex', flexDirection: 'column', gap: 6 }}>
414
+ <input style={S.input} placeholder={`${role || 'agent'}-daily`}
415
+ value={scheduleName} onChange={(e) => setScheduleName(e.target.value)} />
416
+ <div style={S.chipRow}>
417
+ {CRON_PRESETS.map((c) => (
418
+ <Chip key={c.value} label={c.label} active={scheduleCron === c.value}
419
+ onClick={() => setScheduleCron(c.value)} />
420
+ ))}
376
421
  </div>
377
- {active && (
378
- <span style={{ fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>{'\u2713'}</span>
379
- )}
422
+ </div>
423
+ )}
424
+ </Section>
425
+
426
+ {/* API Key — enables fast plan chat */}
427
+ <Section label="Anthropic API Key">
428
+ {hasApiKey ? (
429
+ <div style={{ display: 'flex', alignItems: 'center', justifyContent: 'space-between' }}>
430
+ <span style={{ fontSize: 11, color: 'var(--green)' }}>Connected — fast plan chat enabled</span>
431
+ <button type="button" onClick={async () => {
432
+ await fetch('/api/credentials/anthropic-api', { method: 'DELETE' });
433
+ setHasApiKey(false);
434
+ }} style={{ ...S.cancelBtn, fontSize: 9 }}>remove</button>
435
+ </div>
436
+ ) : showApiKeyInput ? (
437
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
438
+ <div style={{ fontSize: 10, color: 'var(--text-dim)', lineHeight: 1.5 }}>
439
+ Enables instant plan chat responses. Get a key from console.anthropic.com
440
+ </div>
441
+ <div style={{ display: 'flex', gap: 4 }}>
442
+ <input type="text" autoComplete="off" data-lpignore="true" data-1p-ignore style={{ ...S.input, flex: 1, WebkitTextSecurity: 'disc' }} placeholder="sk-ant-..."
443
+ value={apiKeyValue} onChange={(e) => setApiKeyValue(e.target.value)}
444
+ onKeyDown={(e) => e.key === 'Enter' && apiKeyValue && (async () => {
445
+ setApiKeySaving(true);
446
+ await fetch('/api/credentials/anthropic-api', {
447
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
448
+ body: JSON.stringify({ key: apiKeyValue }),
449
+ });
450
+ setHasApiKey(true); setShowApiKeyInput(false); setApiKeyValue('');
451
+ setApiKeySaving(false);
452
+ })()} />
453
+ <button type="button" disabled={!apiKeyValue || apiKeySaving} onClick={async () => {
454
+ setApiKeySaving(true);
455
+ await fetch('/api/credentials/anthropic-api', {
456
+ method: 'POST', headers: { 'Content-Type': 'application/json' },
457
+ body: JSON.stringify({ key: apiKeyValue }),
458
+ });
459
+ setHasApiKey(true); setShowApiKeyInput(false); setApiKeyValue('');
460
+ setApiKeySaving(false);
461
+ }} style={{ ...S.saveKeyBtn, opacity: !apiKeyValue ? 0.4 : 1 }}>
462
+ {apiKeySaving ? '...' : 'Save'}
463
+ </button>
464
+ </div>
465
+ <button type="button" onClick={() => setShowApiKeyInput(false)} style={S.cancelBtn}>cancel</button>
466
+ </div>
467
+ ) : (
468
+ <button type="button" onClick={() => setShowApiKeyInput(true)} style={{
469
+ ...S.chip, width: '100%', textAlign: 'center', padding: '6px',
470
+ color: 'var(--accent)', borderColor: 'var(--accent)',
471
+ }}>
472
+ Add API key for instant responses
380
473
  </button>
381
- );
382
- })}
474
+ )}
475
+ </Section>
476
+
477
+ {/* Advanced */}
478
+ <button type="button" onClick={() => setShowAdvanced(!showAdvanced)} style={S.advToggle}>
479
+ {showAdvanced ? '- hide advanced' : '+ advanced'}
480
+ </button>
481
+ {showAdvanced && (
482
+ <>
483
+ <Section label="Provider">
484
+ {providerList.map((p) => {
485
+ const ready = isProviderReady(p);
486
+ const status = getProviderStatus(p);
487
+ const isSelected = provider === p.id;
488
+ const isConnecting = connectingProvider === p.id;
489
+ return (
490
+ <div key={p.id} style={{ marginBottom: 3 }}>
491
+ <button type="button" onClick={() => handleProviderClick(p)}
492
+ style={{ ...S.providerBtn, borderColor: isSelected ? 'var(--accent)' : 'var(--border)', opacity: ready || isConnecting ? 1 : 0.5 }}>
493
+ <span style={{ fontSize: 11, fontWeight: 600, color: isSelected ? 'var(--text-bright)' : 'var(--text-primary)' }}>{p.name}</span>
494
+ <span style={{ fontSize: 10, color: ready ? 'var(--green)' : 'var(--text-dim)', marginLeft: 'auto' }}>{ready ? (isSelected ? 'active' : 'ready') : status}</span>
495
+ </button>
496
+ {isConnecting && (
497
+ <div style={S.connectBox}>
498
+ {!p.installed && <div><code style={S.code}>{p.installCommand}</code></div>}
499
+ {p.installed && p.authType === 'api-key' && !p.hasKey && (
500
+ <div style={{ display: 'flex', gap: 4 }}>
501
+ <input type="password" style={{ ...S.input, flex: 1 }} placeholder={`${p.envKey || 'API key'}...`}
502
+ value={apiKeyInput} onChange={(e) => setApiKeyInput(e.target.value)}
503
+ onKeyDown={(e) => e.key === 'Enter' && handleSaveKey()} />
504
+ <button type="button" onClick={handleSaveKey} style={S.saveKeyBtn}>{keySaving ? '...' : 'Save'}</button>
505
+ </div>
506
+ )}
507
+ <button type="button" onClick={() => setConnectingProvider(null)} style={S.cancelBtn}>cancel</button>
508
+ </div>
509
+ )}
510
+ </div>
511
+ );
512
+ })}
513
+ </Section>
514
+ <Section label="Model">
515
+ {(() => {
516
+ const models = providerList.find((p) => p.id === provider)?.models || [];
517
+ if (!models.length) return <div style={S.hint}>Select a provider first</div>;
518
+ return (
519
+ <select style={S.input} value={model} onChange={(e) => setModel(e.target.value)}>
520
+ <option value="auto">Auto (recommended)</option>
521
+ {models.map((m) => <option key={m.id} value={m.id}>{m.name} ({m.tier})</option>)}
522
+ </select>
523
+ );
524
+ })()}
525
+ </Section>
526
+ <Section label="File Scope">
527
+ <input style={S.input} placeholder="e.g. src/api/**, src/lib/**"
528
+ value={role === 'custom' ? scope : effectiveScope}
529
+ onChange={(e) => { if (role === 'custom') setScope(e.target.value); }}
530
+ readOnly={role !== 'custom'} />
531
+ <div style={S.hint}>{role === 'custom' ? 'Comma-separated glob patterns' : 'Auto-set by role'}</div>
532
+ </Section>
533
+ </>
534
+ )}
383
535
  </div>
384
- {selectedIntegrations.length > 0 && (
385
- <div style={styles.hint}>
386
- {selectedIntegrations.length} integration{selectedIntegrations.length !== 1 ? 's' : ''} will provide MCP tools to this agent
536
+ </div>
537
+
538
+ {/* RIGHT Prompt + Plan chat */}
539
+ <div style={S.right}>
540
+ {!planMode ? (
541
+ /* Prompt-only mode */
542
+ <div style={S.promptPanel}>
543
+ <div style={S.promptHeader}>
544
+ <span style={S.promptLabel}>{isPlanner ? 'WHAT TO PLAN' : 'TASK PROMPT'}</span>
545
+ <button type="button" onClick={() => setPlanMode(true)} style={S.planBtn}>
546
+ Plan with AI
547
+ </button>
548
+ </div>
549
+ <textarea
550
+ style={S.promptArea}
551
+ placeholder={isPlanner
552
+ ? 'What should this agent research or plan?\n\nBe specific about scope, constraints, and expected output...'
553
+ : 'Describe the task in detail.\n\nThe more context you provide here, the fewer iterations the agent will need. Include:\n- What to build or change\n- Key requirements and constraints\n- Expected behavior or output\n- Any files or areas to focus on'}
554
+ value={prompt}
555
+ onChange={(e) => setPrompt(e.target.value)}
556
+ />
387
557
  </div>
388
- )}
389
- </>
390
- )}
391
-
392
- {/* Skills picker */}
393
- {installedSkills.length > 0 && (
394
- <>
395
- <div style={styles.label}>SKILLS</div>
396
- <div style={styles.skillsGrid}>
397
- {installedSkills.map((skill) => {
398
- const active = selectedSkills.includes(skill.id);
399
- return (
400
- <button
401
- key={skill.id}
402
- type="button"
403
- onClick={() => toggleSkill(skill.id)}
404
- style={{
405
- ...styles.skillBtn,
406
- borderColor: active ? 'var(--accent)' : 'var(--border)',
407
- background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
408
- }}
409
- >
410
- <span style={{
411
- ...styles.skillIcon,
412
- background: active ? 'var(--accent)' : 'var(--bg-active)',
413
- color: active ? 'var(--bg-base)' : 'var(--text-dim)',
414
- }}>
415
- {skill.icon || skill.name.charAt(0)}
416
- </span>
417
- <div style={{ flex: 1, minWidth: 0 }}>
418
- <div style={{
419
- fontSize: 11, fontWeight: 600,
420
- color: active ? 'var(--text-bright)' : 'var(--text-primary)',
421
- }}>
422
- {skill.name}
558
+ ) : (
559
+ /* Plan chat mode */
560
+ <div style={S.chatPanel}>
561
+ <div style={S.chatHeader}>
562
+ <span style={S.chatTitle}>Plan with AI</span>
563
+ <div style={{ display: 'flex', gap: 8 }}>
564
+ {planMessages.length > 0 && !planLoading && (
565
+ <button type="button" onClick={applyPlanToPrompt} style={S.usePlanBtn}>
566
+ Generate Prompt
567
+ </button>
568
+ )}
569
+ <button type="button" onClick={() => setPlanMode(false)} style={S.closePlanBtn}>
570
+ Back to Prompt
571
+ </button>
572
+ </div>
573
+ </div>
574
+ <div style={S.chatMessages}>
575
+ {planMessages.length === 0 && (
576
+ <div style={S.chatEmpty}>
577
+ <div style={{ fontSize: 14, marginBottom: 8 }}>Discuss your idea before spawning</div>
578
+ <div style={{ fontSize: 12, color: 'var(--text-dim)', lineHeight: 1.6 }}>
579
+ Describe what you want this agent to accomplish. AI will help you
580
+ refine the plan, identify edge cases, and craft a solid prompt.
581
+ {role && <><br />Role: <strong>{role === 'custom' ? customRole : role}</strong></>}
423
582
  </div>
424
- <div style={{ fontSize: 9, color: 'var(--text-muted)' }}>
425
- {skill.author || 'local'}
583
+ </div>
584
+ )}
585
+ {planMessages.map((msg, i) => (
586
+ <div key={i} style={{
587
+ ...S.chatBubble,
588
+ ...(msg.from === 'user' ? S.chatUser : S.chatAI),
589
+ }}>
590
+ <div style={S.chatFrom}>{msg.from === 'user' ? 'You' : 'AI'}</div>
591
+ <div style={S.chatText}>
592
+ {msg.from === 'ai' ? <FormattedText text={msg.text} /> : msg.text}
426
593
  </div>
427
594
  </div>
428
- {active && (
429
- <span style={{ fontSize: 10, color: 'var(--accent)', flexShrink: 0 }}>{'\u2713'}</span>
430
- )}
595
+ ))}
596
+ {planLoading && (hasApiKey
597
+ ? <div style={{ alignSelf: 'flex-start', padding: '10px 16px', borderRadius: 10, background: 'var(--bg-surface)', border: '1px solid var(--border)', borderBottomLeftRadius: 2, fontSize: 11, color: 'var(--text-dim)' }}>Responding...</div>
598
+ : <PlanningIndicator />
599
+ )}
600
+ <div ref={chatEndRef} />
601
+ </div>
602
+ <div style={S.chatInputBar}>
603
+ <input
604
+ style={S.chatInput}
605
+ placeholder="Describe your idea, ask questions, refine the plan..."
606
+ value={planInput}
607
+ onChange={(e) => setPlanInput(e.target.value)}
608
+ onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) { e.preventDefault(); handlePlanSend(); } }}
609
+ autoFocus
610
+ />
611
+ <button type="button" onClick={handlePlanSend}
612
+ disabled={planLoading || !planInput.trim()}
613
+ style={{ ...S.sendBtn, opacity: planLoading || !planInput.trim() ? 0.3 : 1 }}>
614
+ Send
431
615
  </button>
432
- );
433
- })}
434
- </div>
435
- {selectedSkills.length > 0 && (
436
- <div style={styles.hint}>
437
- {selectedSkills.length} skill{selectedSkills.length !== 1 ? 's' : ''} will be injected into this agent's context
616
+ </div>
438
617
  </div>
439
618
  )}
440
- </>
441
- )}
442
-
443
- {/* Advanced toggle */}
444
- <button
445
- type="button"
446
- onClick={() => setShowAdvanced(!showAdvanced)}
447
- style={styles.advancedToggle}
448
- >
449
- {showAdvanced ? '- hide advanced' : '+ advanced options'}
450
- </button>
451
-
452
- {showAdvanced && (
453
- <>
454
- {/* Provider selector with connection flow */}
455
- <div style={styles.label}>PROVIDER</div>
456
- {providerList.map((p) => {
457
- const ready = isProviderReady(p);
458
- const status = getProviderStatus(p);
459
- const isSelected = provider === p.id;
460
- const isConnecting = connectingProvider === p.id;
461
-
462
- return (
463
- <div key={p.id} style={{ marginBottom: 2 }}>
464
- <button
465
- type="button"
466
- onClick={() => handleProviderClick(p)}
467
- style={{
468
- ...styles.providerBtn,
469
- borderColor: isSelected ? 'var(--accent)' : 'var(--border)',
470
- opacity: ready || isConnecting ? 1 : 0.6,
471
- }}
472
- >
473
- <div style={{ flex: 1 }}>
474
- <span style={{
475
- fontSize: 12, fontWeight: 600,
476
- color: isSelected ? 'var(--text-bright)' : 'var(--text-primary)',
477
- }}>
478
- {p.name}
479
- </span>
480
- <span style={styles.providerModels}>
481
- {p.models.map((m) => m.name).join(', ')}
482
- </span>
483
- </div>
484
- <span style={{
485
- fontSize: 10,
486
- color: ready ? 'var(--green)' : status === 'not installed' ? 'var(--text-muted)' : 'var(--amber)',
487
- }}>
488
- {ready ? (isSelected ? 'active' : 'ready') : status}
489
- </span>
490
- </button>
619
+ </div>
620
+ </div>
621
+ </div>
622
+ </div>
623
+ );
624
+ }
491
625
 
492
- {/* Connection flow inline expand */}
493
- {isConnecting && (
494
- <div style={styles.connectBox}>
495
- {!p.installed && (
496
- <div>
497
- <div style={styles.connectLabel}>Install first:</div>
498
- <code style={styles.connectCode}>{p.installCommand}</code>
499
- <div style={styles.connectHint}>Run this in your terminal, then click the provider again</div>
500
- </div>
501
- )}
502
- {p.installed && p.authType === 'api-key' && !p.hasKey && (
503
- <div>
504
- <div style={styles.connectLabel}>
505
- API Key {p.envKey ? `(${p.envKey})` : ''}
506
- </div>
507
- <div style={{ display: 'flex', gap: 4 }}>
508
- <input
509
- type="password"
510
- style={styles.input}
511
- placeholder="sk-..."
512
- value={apiKeyInput}
513
- onChange={(e) => setApiKeyInput(e.target.value)}
514
- onKeyDown={(e) => e.key === 'Enter' && (e.preventDefault(), handleSaveKey())}
515
- />
516
- <button
517
- type="button"
518
- onClick={handleSaveKey}
519
- disabled={keySaving || !apiKeyInput.trim()}
520
- style={styles.connectSaveBtn}
521
- >
522
- {keySaving ? '...' : 'Save'}
523
- </button>
524
- </div>
525
- <div style={styles.connectHint}>
526
- Encrypted locally. Never sent to GROOVE servers.
527
- </div>
528
- </div>
529
- )}
530
- {p.installed && p.authType === 'subscription' && (
531
- <div>
532
- <div style={styles.connectLabel}>Subscription auth</div>
533
- <div style={styles.connectHint}>
534
- {p.name} uses your existing subscription. Make sure you're logged in via the CLI.
535
- </div>
536
- </div>
537
- )}
538
- {p.installed && p.authType === 'local' && (
539
- <div>
540
- <div style={styles.connectLabel}>Local model</div>
541
- <div style={styles.connectHint}>
542
- Make sure {p.name} is running locally. No API key needed.
543
- </div>
544
- </div>
545
- )}
546
- <button
547
- type="button"
548
- onClick={() => setConnectingProvider(null)}
549
- style={styles.connectCancel}
550
- >
551
- cancel
552
- </button>
553
- </div>
554
- )}
555
- </div>
556
- );
557
- })}
558
-
559
- {/* Model selector */}
560
- {(() => {
561
- const currentProvider = providerList.find((p) => p.id === provider);
562
- const models = currentProvider?.models || [];
563
- if (models.length === 0) return null;
564
- return (
565
- <>
566
- <div style={styles.label}>MODEL</div>
567
- <select
568
- style={styles.input}
569
- value={model}
570
- onChange={(e) => setModel(e.target.value)}
571
- >
572
- <option value="auto">Auto (recommended)</option>
573
- {models.map((m) => (
574
- <option key={m.id} value={m.id}>
575
- {m.name} ({m.tier})
576
- </option>
577
- ))}
578
- </select>
579
- </>
580
- );
581
- })()}
582
-
583
- {/* Scope */}
584
- <div style={styles.label}>FILE SCOPE</div>
585
- <input
586
- style={styles.input}
587
- placeholder="e.g. src/api/**, src/lib/**"
588
- value={role === 'custom' ? scope : effectiveScope}
589
- onChange={(e) => { if (role === 'custom') setScope(e.target.value); }}
590
- readOnly={role !== 'custom'}
591
- />
592
- <div style={styles.hint}>
593
- {role === 'custom'
594
- ? 'Comma-separated glob patterns'
595
- : 'Auto-set by role preset'}
626
+ // --- Small reusable components ---
627
+
628
+ // Auto-escalating indicator: shows simple "Responding" for 3s, then switches to planning phases
629
+ function AutoEscalateIndicator() {
630
+ const [escalated, setEscalated] = useState(false);
631
+ useEffect(() => {
632
+ const timer = setTimeout(() => setEscalated(true), 3000);
633
+ return () => clearTimeout(timer);
634
+ }, []);
635
+ if (!escalated) {
636
+ return (
637
+ <div style={{
638
+ alignSelf: 'flex-start', padding: '10px 16px', borderRadius: 10,
639
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
640
+ borderBottomLeftRadius: 2, fontSize: 11, color: 'var(--text-dim)',
641
+ }}>
642
+ Responding...
643
+ </div>
644
+ );
645
+ }
646
+ return <PlanningIndicator />;
647
+ }
648
+
649
+ const PLAN_PHASES = [
650
+ 'Analyzing request',
651
+ 'Evaluating approach',
652
+ 'Considering scope',
653
+ 'Identifying constraints',
654
+ 'Crafting plan',
655
+ ];
656
+
657
+ function PlanningIndicator() {
658
+ const [phase, setPhase] = useState(0);
659
+ const [dots, setDots] = useState(1);
660
+
661
+ useEffect(() => {
662
+ const phaseTimer = setInterval(() => setPhase((p) => (p + 1) % PLAN_PHASES.length), 3000);
663
+ const dotTimer = setInterval(() => setDots((d) => (d % 3) + 1), 500);
664
+ return () => { clearInterval(phaseTimer); clearInterval(dotTimer); };
665
+ }, []);
666
+
667
+ return (
668
+ <div style={{
669
+ alignSelf: 'flex-start', padding: '14px 20px', borderRadius: 10,
670
+ background: 'var(--bg-surface)', border: '1px solid var(--border)',
671
+ borderBottomLeftRadius: 2, maxWidth: '80%',
672
+ }}>
673
+ <div style={{
674
+ fontSize: 9, fontWeight: 700, textTransform: 'uppercase',
675
+ letterSpacing: 1.2, color: 'var(--accent)', marginBottom: 10,
676
+ }}>
677
+ Planning
678
+ </div>
679
+ <div style={{ display: 'flex', flexDirection: 'column', gap: 6 }}>
680
+ {PLAN_PHASES.map((label, i) => {
681
+ const active = i === phase;
682
+ const done = i < phase;
683
+ return (
684
+ <div key={i} style={{
685
+ display: 'flex', alignItems: 'center', gap: 8,
686
+ fontSize: 11, color: active ? 'var(--text-bright)' : done ? 'var(--text-dim)' : 'var(--text-muted)',
687
+ transition: 'color 0.3s',
688
+ }}>
689
+ <span style={{
690
+ width: 6, height: 6, borderRadius: '50%', flexShrink: 0,
691
+ background: active ? 'var(--accent)' : done ? 'var(--text-dim)' : 'var(--border)',
692
+ transition: 'background 0.3s',
693
+ ...(active ? { boxShadow: '0 0 6px var(--accent)' } : {}),
694
+ }} />
695
+ {label}{active ? '.'.repeat(dots) : ''}
596
696
  </div>
597
- </>
598
- )}
599
-
600
- {error && <div style={styles.error}>{error}</div>}
601
-
602
- <button
603
- type="submit"
604
- disabled={submitting}
605
- style={{
606
- ...styles.submitBtn,
607
- opacity: submitting ? 0.5 : 1,
608
- }}
609
- >
610
- {submitting ? 'spawning...' : isPlanner ? 'Start Planning' : 'Spawn Agent'}
611
- </button>
612
- </form>
697
+ );
698
+ })}
699
+ </div>
613
700
  </div>
614
701
  );
615
702
  }
616
703
 
617
- const styles = {
618
- title: {
619
- fontSize: 11, fontWeight: 600, color: 'var(--text-dim)',
620
- textTransform: 'uppercase', letterSpacing: 1.5, marginBottom: 14,
704
+ function Section({ label, children }) {
705
+ return (
706
+ <div style={{ marginBottom: 14 }}>
707
+ <div style={S.sectionLabel}>{label}</div>
708
+ {children}
709
+ </div>
710
+ );
711
+ }
712
+
713
+ function RoleBtn({ preset, active, onClick }) {
714
+ return (
715
+ <button type="button" onClick={onClick} title={preset.desc}
716
+ style={{ ...S.roleBtn, ...(active ? S.roleBtnActive : {}) }}>
717
+ {preset.label}
718
+ </button>
719
+ );
720
+ }
721
+
722
+ function Chip({ label, active, onClick }) {
723
+ return (
724
+ <button type="button" onClick={onClick}
725
+ style={{ ...S.chip, ...(active ? { borderColor: 'var(--accent)', color: 'var(--text-bright)' } : {}) }}>
726
+ {label}
727
+ </button>
728
+ );
729
+ }
730
+
731
+ function ItemBtn({ name, active, sub, subColor, disabled, onClick }) {
732
+ return (
733
+ <button type="button" onClick={onClick}
734
+ style={{
735
+ ...S.itemBtn, borderColor: active ? 'var(--accent)' : 'var(--border)',
736
+ background: active ? 'rgba(51, 175, 188, 0.08)' : 'var(--bg-surface)',
737
+ opacity: disabled ? 0.5 : 1, cursor: disabled ? 'not-allowed' : 'pointer',
738
+ }}>
739
+ <span style={{ ...S.itemIcon, background: active ? 'var(--accent)' : 'var(--bg-active)', color: active ? 'var(--bg-base)' : 'var(--text-dim)' }}>
740
+ {name.charAt(0)}
741
+ </span>
742
+ <div style={{ flex: 1, minWidth: 0 }}>
743
+ <div style={{ fontSize: 11, fontWeight: 600, color: active ? 'var(--text-bright)' : 'var(--text-primary)' }}>{name}</div>
744
+ {sub && <div style={{ fontSize: 9, color: subColor || 'var(--text-dim)' }}>{sub}</div>}
745
+ </div>
746
+ {active && <span style={{ fontSize: 10, color: 'var(--accent)' }}>{'\u2713'}</span>}
747
+ </button>
748
+ );
749
+ }
750
+
751
+ // --- Styles ---
752
+
753
+ const S = {
754
+ // Overlay
755
+ overlay: {
756
+ position: 'fixed', inset: 0, zIndex: 900,
757
+ background: 'rgba(0, 0, 0, 0.7)',
758
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
759
+ backdropFilter: 'blur(4px)',
760
+ },
761
+ container: {
762
+ width: '92vw', height: '88vh', maxWidth: 1200,
763
+ background: 'var(--bg-base)', border: '1px solid var(--border)',
764
+ borderRadius: 10, display: 'flex', flexDirection: 'column',
765
+ overflow: 'hidden',
766
+ },
767
+
768
+ // Header
769
+ header: {
770
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
771
+ padding: '12px 20px', borderBottom: '1px solid var(--border)',
772
+ background: 'var(--bg-chrome)', flexShrink: 0,
773
+ },
774
+ headerTitle: {
775
+ fontSize: 15, fontWeight: 700, color: 'var(--text-bright)', letterSpacing: 0.3,
776
+ },
777
+ headerRight: {
778
+ display: 'flex', alignItems: 'center', gap: 12,
779
+ },
780
+ headerError: {
781
+ fontSize: 11, color: 'var(--red)', maxWidth: 300, overflow: 'hidden',
782
+ textOverflow: 'ellipsis', whiteSpace: 'nowrap',
783
+ },
784
+ spawnBtn: {
785
+ padding: '8px 24px', background: 'var(--accent)', color: 'var(--bg-base)',
786
+ border: 'none', borderRadius: 6, fontSize: 13, fontWeight: 700,
787
+ cursor: 'pointer', fontFamily: 'var(--font)', letterSpacing: 0.3,
788
+ },
789
+ closeBtn: {
790
+ background: 'none', border: 'none', color: 'var(--text-muted)',
791
+ fontSize: 22, cursor: 'pointer', padding: '0 4px', fontFamily: 'var(--font)',
792
+ },
793
+
794
+ // Body
795
+ body: {
796
+ flex: 1, display: 'flex', overflow: 'hidden', minHeight: 0,
797
+ },
798
+
799
+ // Left panel
800
+ left: {
801
+ width: 340, flexShrink: 0, borderRight: '1px solid var(--border)',
802
+ background: 'var(--bg-chrome)', display: 'flex', flexDirection: 'column',
803
+ },
804
+ leftScroll: {
805
+ flex: 1, overflowY: 'auto', padding: '16px 18px',
806
+ },
807
+
808
+ // Right panel
809
+ right: {
810
+ flex: 1, display: 'flex', flexDirection: 'column', overflow: 'hidden', minHeight: 0,
811
+ },
812
+
813
+ // Prompt mode
814
+ promptPanel: {
815
+ flex: 1, display: 'flex', flexDirection: 'column', padding: 20,
621
816
  },
622
- label: {
623
- fontSize: 11, color: 'var(--text-dim)',
624
- marginBottom: 4, marginTop: 12,
625
- textTransform: 'uppercase', letterSpacing: 1.5, fontWeight: 600,
817
+ promptHeader: {
818
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
819
+ marginBottom: 12,
820
+ },
821
+ promptLabel: {
822
+ fontSize: 11, fontWeight: 700, color: 'var(--text-dim)',
823
+ textTransform: 'uppercase', letterSpacing: 1.5,
824
+ },
825
+ planBtn: {
826
+ padding: '6px 16px', background: 'transparent',
827
+ border: '1px solid var(--accent)', borderRadius: 4,
828
+ color: 'var(--accent)', fontSize: 12, fontWeight: 600,
829
+ cursor: 'pointer', fontFamily: 'var(--font)',
830
+ },
831
+ promptArea: {
832
+ flex: 1, width: '100%', background: 'var(--bg-surface)',
833
+ border: '1px solid var(--border)', borderRadius: 6,
834
+ padding: '14px 16px', color: 'var(--text-primary)',
835
+ fontSize: 13, lineHeight: 1.7, outline: 'none',
836
+ fontFamily: 'var(--font)', resize: 'none',
837
+ },
838
+
839
+ // Chat mode
840
+ chatPanel: {
841
+ flex: 1, display: 'flex', flexDirection: 'column',
842
+ overflow: 'hidden', minHeight: 0,
843
+ },
844
+ chatHeader: {
845
+ display: 'flex', alignItems: 'center', justifyContent: 'space-between',
846
+ padding: '10px 20px', borderBottom: '1px solid var(--border)',
847
+ background: 'var(--bg-chrome)', flexShrink: 0,
848
+ },
849
+ chatTitle: {
850
+ fontSize: 12, fontWeight: 700, color: 'var(--accent)',
851
+ textTransform: 'uppercase', letterSpacing: 1,
852
+ },
853
+ usePlanBtn: {
854
+ padding: '4px 12px', background: 'var(--accent)', color: 'var(--bg-base)',
855
+ border: 'none', borderRadius: 4, fontSize: 11, fontWeight: 600,
856
+ cursor: 'pointer', fontFamily: 'var(--font)',
857
+ },
858
+ closePlanBtn: {
859
+ padding: '4px 12px', background: 'transparent', color: 'var(--text-muted)',
860
+ border: '1px solid var(--border)', borderRadius: 4,
861
+ fontSize: 11, cursor: 'pointer', fontFamily: 'var(--font)',
862
+ },
863
+ chatMessages: {
864
+ flex: 1, overflowY: 'auto', padding: '20px 24px',
865
+ display: 'flex', flexDirection: 'column', gap: 16,
866
+ },
867
+ chatEmpty: {
868
+ textAlign: 'center', padding: '60px 40px',
869
+ color: 'var(--text-muted)', fontFamily: 'var(--font)',
870
+ },
871
+ chatBubble: {
872
+ maxWidth: '80%', padding: '10px 16px', borderRadius: 10,
873
+ fontSize: 13, lineHeight: 1.6,
874
+ },
875
+ chatUser: {
876
+ alignSelf: 'flex-end', background: 'var(--accent)', color: 'var(--bg-base)',
877
+ borderBottomRightRadius: 2,
878
+ },
879
+ chatAI: {
880
+ alignSelf: 'flex-start', background: 'var(--bg-surface)',
881
+ border: '1px solid var(--border)', color: 'var(--text-primary)',
882
+ borderBottomLeftRadius: 2,
883
+ },
884
+ chatFrom: {
885
+ fontSize: 9, fontWeight: 700, textTransform: 'uppercase',
886
+ letterSpacing: 0.8, marginBottom: 4, opacity: 0.6,
887
+ },
888
+ chatText: {
889
+ whiteSpace: 'pre-wrap', wordBreak: 'break-word',
890
+ },
891
+ chatInputBar: {
892
+ display: 'flex', gap: 8, padding: '12px 20px',
893
+ borderTop: '1px solid var(--border)', background: 'var(--bg-chrome)',
894
+ flexShrink: 0,
895
+ },
896
+ chatInput: {
897
+ flex: 1, background: 'var(--bg-surface)', border: '1px solid var(--border)',
898
+ borderRadius: 6, padding: '10px 14px', color: 'var(--text-primary)',
899
+ fontSize: 13, outline: 'none', fontFamily: 'var(--font)',
900
+ },
901
+ sendBtn: {
902
+ padding: '10px 20px', background: 'var(--accent)', color: 'var(--bg-base)',
903
+ border: 'none', borderRadius: 6, fontSize: 13, fontWeight: 600,
904
+ cursor: 'pointer', fontFamily: 'var(--font)',
905
+ },
906
+
907
+ // Shared form styles
908
+ sectionLabel: {
909
+ fontSize: 10, fontWeight: 700, color: 'var(--text-dim)',
910
+ textTransform: 'uppercase', letterSpacing: 1.2, marginBottom: 6,
911
+ },
912
+ sectionSub: {
913
+ fontSize: 9, fontWeight: 600, color: 'var(--text-muted)',
914
+ textTransform: 'uppercase', letterSpacing: 1, marginBottom: 4,
626
915
  },
627
916
  roleGrid: {
628
- display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 4,
917
+ display: 'grid', gridTemplateColumns: '1fr 1fr 1fr', gap: 4,
629
918
  },
630
919
  roleBtn: {
631
920
  background: 'var(--bg-surface)', border: '1px solid var(--border)',
632
- borderRadius: 2, padding: '6px 4px',
921
+ borderRadius: 4, padding: '6px 4px',
633
922
  color: 'var(--text-primary)', fontSize: 11, cursor: 'pointer',
634
- fontFamily: 'var(--font)',
635
- transition: 'color 0.1s, border-color 0.1s',
923
+ fontFamily: 'var(--font)', transition: 'all 0.1s',
636
924
  },
637
925
  roleBtnActive: {
638
- borderColor: 'var(--accent)',
639
- color: 'var(--text-bright)',
926
+ borderColor: 'var(--accent)', color: 'var(--text-bright)',
927
+ background: 'rgba(51, 175, 188, 0.08)',
640
928
  },
641
- roleDesc: {
642
- fontSize: 10, color: 'var(--text-dim)', marginTop: 4, fontStyle: 'italic',
929
+ chipRow: {
930
+ display: 'flex', flexWrap: 'wrap', gap: 4,
643
931
  },
644
- permGrid: {
645
- display: 'flex', flexDirection: 'column', gap: 4,
646
- },
647
- permBtn: {
648
- display: 'flex', alignItems: 'center', gap: 10,
649
- padding: '8px 10px', width: '100%',
932
+ chip: {
650
933
  background: 'var(--bg-surface)', border: '1px solid var(--border)',
651
- borderRadius: 2, cursor: 'pointer', textAlign: 'left',
934
+ borderRadius: 3, padding: '3px 8px',
935
+ color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
652
936
  fontFamily: 'var(--font)',
653
937
  },
654
- permBtnActive: {
655
- borderColor: 'var(--accent)',
938
+ browseBtn: {
939
+ background: 'var(--bg-surface)', border: '1px solid var(--accent)',
940
+ borderRadius: 3, padding: '3px 10px',
941
+ color: 'var(--accent)', fontSize: 10, fontWeight: 600,
942
+ cursor: 'pointer', fontFamily: 'var(--font)',
943
+ },
944
+ permBtn: {
945
+ flex: 1, display: 'flex', alignItems: 'center', gap: 8,
946
+ padding: '8px 10px', background: 'var(--bg-surface)',
947
+ border: '1px solid var(--border)', borderRadius: 4,
948
+ cursor: 'pointer', textAlign: 'left', fontFamily: 'var(--font)',
656
949
  },
950
+ permBtnActive: { borderColor: 'var(--accent)' },
657
951
  permIcon: {
658
952
  fontSize: 14, fontWeight: 700, color: 'var(--accent)',
659
953
  width: 18, textAlign: 'center', flexShrink: 0,
660
954
  },
661
- permLabel: {
662
- fontSize: 11, color: 'var(--text-bright)', fontWeight: 600,
663
- },
664
- permDesc: {
665
- fontSize: 10, color: 'var(--text-dim)',
955
+ itemList: { display: 'flex', flexDirection: 'column', gap: 3 },
956
+ itemBtn: {
957
+ display: 'flex', alignItems: 'center', gap: 8,
958
+ padding: '5px 8px', width: '100%',
959
+ border: '1px solid var(--border)', borderRadius: 3,
960
+ cursor: 'pointer', textAlign: 'left', fontFamily: 'var(--font)',
961
+ transition: 'border-color 0.1s',
666
962
  },
667
- skillsGrid: {
668
- display: 'flex', flexDirection: 'column', gap: 3,
963
+ itemIcon: {
964
+ width: 20, height: 20, borderRadius: 4,
965
+ display: 'flex', alignItems: 'center', justifyContent: 'center',
966
+ fontSize: 9, fontWeight: 700, flexShrink: 0,
669
967
  },
670
- skillBtn: {
671
- display: 'flex', alignItems: 'center', gap: 8,
672
- padding: '6px 8px', width: '100%',
673
- border: '1px solid var(--border)',
674
- borderRadius: 2, cursor: 'pointer', textAlign: 'left',
675
- fontFamily: 'var(--font)',
676
- transition: 'border-color 0.1s, background 0.1s',
968
+ toggleBtn: {
969
+ display: 'flex', alignItems: 'center', gap: 8, width: '100%',
970
+ padding: '6px 8px', background: 'var(--bg-surface)',
971
+ border: '1px solid var(--border)', borderRadius: 3,
972
+ cursor: 'pointer', fontFamily: 'var(--font)',
677
973
  },
678
- skillIcon: {
679
- width: 22, height: 22, borderRadius: 4,
974
+ checkbox: {
975
+ width: 14, height: 14, borderRadius: 3, flexShrink: 0,
976
+ border: '2px solid var(--border)',
680
977
  display: 'flex', alignItems: 'center', justifyContent: 'center',
681
- fontSize: 10, fontWeight: 700, flexShrink: 0,
978
+ color: 'var(--bg-base)', fontSize: 9, fontWeight: 700,
682
979
  },
683
- advancedToggle: {
980
+ advToggle: {
684
981
  background: 'none', border: 'none', color: 'var(--text-dim)',
685
982
  fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
686
- padding: '8px 0', marginTop: 8,
983
+ padding: '6px 0', marginBottom: 4,
687
984
  },
688
985
  providerBtn: {
689
986
  width: '100%', display: 'flex', alignItems: 'center', gap: 8,
690
- padding: '8px 10px',
691
- background: 'var(--bg-surface)', border: '1px solid var(--border)',
692
- borderRadius: 2, cursor: 'pointer', textAlign: 'left',
693
- fontFamily: 'var(--font)',
694
- },
695
- providerModels: {
696
- fontSize: 10, color: 'var(--text-dim)', marginLeft: 6,
987
+ padding: '6px 10px', background: 'var(--bg-surface)',
988
+ border: '1px solid var(--border)', borderRadius: 3,
989
+ cursor: 'pointer', fontFamily: 'var(--font)',
697
990
  },
698
991
  connectBox: {
699
- padding: '8px 10px', margin: '2px 0 4px',
700
- background: 'var(--bg-base)', border: '1px solid var(--border)',
701
- borderRadius: 2,
702
- },
703
- connectLabel: {
704
- fontSize: 11, color: 'var(--text-primary)', fontWeight: 600, marginBottom: 4,
992
+ padding: '6px 10px', margin: '2px 0 4px',
993
+ background: 'var(--bg-base)', border: '1px solid var(--border)', borderRadius: 3,
705
994
  },
706
- connectCode: {
707
- display: 'block', padding: '6px 8px',
995
+ code: {
996
+ display: 'block', padding: '4px 8px',
708
997
  background: 'var(--bg-surface)', border: '1px solid var(--border)', borderRadius: 2,
709
998
  fontSize: 11, color: 'var(--accent)', wordBreak: 'break-all',
710
999
  },
711
- connectHint: {
712
- fontSize: 10, color: 'var(--text-dim)', marginTop: 4,
1000
+ saveKeyBtn: {
1001
+ padding: '6px 12px', background: 'transparent',
1002
+ border: '1px solid var(--accent)', borderRadius: 3,
1003
+ color: 'var(--accent)', fontSize: 11, fontWeight: 600,
1004
+ cursor: 'pointer', fontFamily: 'var(--font)',
713
1005
  },
714
- connectSaveBtn: {
715
- padding: '6px 12px', flexShrink: 0,
716
- background: 'transparent', border: '1px solid var(--accent)',
717
- borderRadius: 2, color: 'var(--accent)', fontSize: 11, fontWeight: 600,
718
- fontFamily: 'var(--font)', cursor: 'pointer',
719
- },
720
- connectCancel: {
1006
+ cancelBtn: {
721
1007
  background: 'none', border: 'none', color: 'var(--text-dim)',
722
1008
  fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
723
1009
  padding: '4px 0', marginTop: 4,
724
1010
  },
725
1011
  input: {
726
1012
  width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
727
- borderRadius: 2, padding: '6px 8px',
1013
+ borderRadius: 3, padding: '6px 8px',
728
1014
  color: 'var(--text-primary)', fontSize: 12, outline: 'none',
729
1015
  fontFamily: 'var(--font)',
730
1016
  },
731
- textarea: {
732
- width: '100%', background: 'var(--bg-surface)', border: '1px solid var(--border)',
733
- borderRadius: 2, padding: '6px 8px',
734
- color: 'var(--text-primary)', fontSize: 12, outline: 'none',
735
- fontFamily: 'var(--font)', resize: 'vertical',
736
- },
737
1017
  hint: {
738
1018
  fontSize: 10, color: 'var(--text-dim)', marginTop: 3,
739
1019
  },
740
- wsRow: {
741
- display: 'flex', flexWrap: 'wrap', gap: 4, marginTop: 6,
742
- },
743
- wsBtn: {
744
- background: 'var(--bg-surface)', border: '1px solid var(--border)',
745
- borderRadius: 2, padding: '3px 8px',
746
- color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
747
- fontFamily: 'var(--font)',
748
- transition: 'color 0.1s, border-color 0.1s',
749
- },
750
- browseBtn: {
751
- background: 'var(--bg-surface)', border: '1px solid var(--accent)',
752
- borderRadius: 2, padding: '3px 10px',
753
- color: 'var(--accent)', fontSize: 10, fontWeight: 600, cursor: 'pointer',
754
- fontFamily: 'var(--font)',
755
- },
756
- error: {
757
- color: 'var(--red)', fontSize: 11, marginTop: 8,
758
- },
759
- submitBtn: {
760
- width: '100%', marginTop: 14, padding: '8px',
761
- background: 'transparent', border: '1px solid var(--accent)',
762
- borderRadius: 2,
763
- color: 'var(--accent)', fontSize: 12, fontWeight: 600,
764
- fontFamily: 'var(--font)',
765
- cursor: 'pointer',
766
- },
767
1020
  };