groove-dev 0.17.8 → 0.18.2

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