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.
- package/CLAUDE.md +0 -7
- package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
- package/node_modules/@groove-dev/daemon/integrations-registry.json +10 -48
- package/node_modules/@groove-dev/daemon/src/api.js +103 -12
- package/node_modules/@groove-dev/daemon/src/integrations.js +94 -16
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
- package/node_modules/@groove-dev/gui/.groove/codebase-index.json +64 -0
- package/node_modules/@groove-dev/gui/.groove/config.json +10 -0
- package/node_modules/@groove-dev/gui/.groove/coordination.md +5 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.host +1 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.pid +1 -0
- package/node_modules/@groove-dev/gui/.groove/daemon.port +1 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.key +3 -0
- package/node_modules/@groove-dev/gui/.groove/federation/identity.pub +3 -0
- package/node_modules/@groove-dev/gui/.groove/integrations/package.json +6 -0
- package/node_modules/@groove-dev/gui/.groove/state.json +3 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DXkccbmd.js +182 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/src/App.jsx +27 -4
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +1 -1
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +839 -586
- package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +157 -42
- package/package.json +1 -2
- package/packages/daemon/integrations-registry.json +10 -48
- package/packages/daemon/src/api.js +103 -12
- package/packages/daemon/src/integrations.js +94 -16
- package/packages/daemon/src/providers/claude-code.js +4 -0
- package/packages/gui/dist/assets/index-DXkccbmd.js +182 -0
- package/packages/gui/dist/index.html +1 -1
- package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js +68 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_autocomplete.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js +1420 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_commands.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js +17 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-css.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js +22 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-html.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js +34 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-javascript.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js +101 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-json.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js +2534 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-markdown.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js +789 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_lang-python.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js +115 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_language.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js +1136 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_search.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js +63 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_state.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js +179 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_theme-one-dark.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js +104 -0
- package/packages/gui/node_modules/.vite/deps/@codemirror_view.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js +46 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-fit.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js +121 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_addon-web-links.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js +9237 -0
- package/packages/gui/node_modules/.vite/deps/@xterm_xterm.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js +9934 -0
- package/packages/gui/node_modules/.vite/deps/@xyflow_react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/_metadata.json +184 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js +5169 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3EE34IFC.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js +2000 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3IB5EUP7.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js +1115 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3LBP22MX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js +701 -0
- package/packages/gui/node_modules/.vite/deps/chunk-3Q7HT7ZF.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js +1776 -0
- package/packages/gui/node_modules/.vite/deps/chunk-44CLUOQE.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js +280 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5RZAEUNX.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js +30 -0
- package/packages/gui/node_modules/.vite/deps/chunk-5WRI5ZAA.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js +1004 -0
- package/packages/gui/node_modules/.vite/deps/chunk-7FYDPZIO.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js +292 -0
- package/packages/gui/node_modules/.vite/deps/chunk-BX6POZPY.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js +1062 -0
- package/packages/gui/node_modules/.vite/deps/chunk-HVFOBSCQ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js +10985 -0
- package/packages/gui/node_modules/.vite/deps/chunk-RE2FU7ZU.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js +3459 -0
- package/packages/gui/node_modules/.vite/deps/chunk-YYJMNVCJ.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/package.json +3 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react-dom.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js +20217 -0
- package/packages/gui/node_modules/.vite/deps/react-dom_client.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react.js +5 -0
- package/packages/gui/node_modules/.vite/deps/react.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js +278 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-dev-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js +6 -0
- package/packages/gui/node_modules/.vite/deps/react_jsx-runtime.js.map +7 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js +56 -0
- package/packages/gui/node_modules/.vite/deps/zustand.js.map +7 -0
- package/packages/gui/src/App.jsx +27 -4
- package/packages/gui/src/components/AgentChat.jsx +1 -1
- package/packages/gui/src/components/SpawnPanel.jsx +839 -586
- package/packages/gui/src/views/FileEditor.jsx +85 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +157 -42
- package/docs/FILE-EDITOR-PLAN.md +0 -253
- package/docs/GUI_DESIGN_SPEC.md +0 -402
- package/docs/SKILLS-API-SPEC.md +0 -277
- package/node_modules/@groove-dev/gui/dist/assets/index-CsymvgNh.js +0 -156
- package/packages/gui/dist/assets/index-CsymvgNh.js +0 -156
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
// GROOVE GUI — Spawn Panel (
|
|
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
|
|
28
|
-
{ id: 'full', label: 'Full Send', desc: 'No reviews,
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
126
|
-
setProvider(p.id);
|
|
127
|
-
setModel('auto');
|
|
128
|
-
setConnectingProvider(null);
|
|
129
|
-
return;
|
|
131
|
+
setProvider(p.id); setModel('auto'); setConnectingProvider(null); return;
|
|
130
132
|
}
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
193
|
+
setPlanMessages((prev) => [...prev, { from: 'ai', text: 'Failed to reach AI. Write your prompt directly.' }]);
|
|
152
194
|
}
|
|
153
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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={
|
|
208
|
-
<div style={
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
321
|
-
|
|
322
|
-
|
|
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
|
-
|
|
329
|
-
|
|
330
|
-
|
|
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}>×</button>
|
|
286
|
+
</div>
|
|
335
287
|
</div>
|
|
336
288
|
|
|
337
|
-
{/*
|
|
338
|
-
{
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
<div style={
|
|
342
|
-
{
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
385
|
-
|
|
386
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
444
|
-
|
|
445
|
-
|
|
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
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
623
|
-
|
|
624
|
-
marginBottom:
|
|
625
|
-
|
|
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:
|
|
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
|
-
|
|
926
|
+
borderColor: 'var(--accent)', color: 'var(--text-bright)',
|
|
927
|
+
background: 'rgba(51, 175, 188, 0.08)',
|
|
640
928
|
},
|
|
641
|
-
|
|
642
|
-
|
|
929
|
+
chipRow: {
|
|
930
|
+
display: 'flex', flexWrap: 'wrap', gap: 4,
|
|
643
931
|
},
|
|
644
|
-
|
|
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:
|
|
934
|
+
borderRadius: 3, padding: '3px 8px',
|
|
935
|
+
color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
|
|
652
936
|
fontFamily: 'var(--font)',
|
|
653
937
|
},
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
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
|
-
|
|
668
|
-
|
|
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
|
-
|
|
671
|
-
display: 'flex', alignItems: 'center', gap: 8,
|
|
672
|
-
padding: '6px 8px',
|
|
673
|
-
border: '1px solid var(--border)',
|
|
674
|
-
|
|
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
|
-
|
|
679
|
-
width:
|
|
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
|
-
|
|
978
|
+
color: 'var(--bg-base)', fontSize: 9, fontWeight: 700,
|
|
682
979
|
},
|
|
683
|
-
|
|
980
|
+
advToggle: {
|
|
684
981
|
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
685
982
|
fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
686
|
-
padding: '
|
|
983
|
+
padding: '6px 0', marginBottom: 4,
|
|
687
984
|
},
|
|
688
985
|
providerBtn: {
|
|
689
986
|
width: '100%', display: 'flex', alignItems: 'center', gap: 8,
|
|
690
|
-
padding: '
|
|
691
|
-
|
|
692
|
-
|
|
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: '
|
|
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
|
-
|
|
707
|
-
display: 'block', padding: '
|
|
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
|
-
|
|
712
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
};
|