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.
- package/node_modules/@groove-dev/cli/package.json +4 -3
- package/node_modules/@groove-dev/daemon/google-oauth.json +5 -0
- package/node_modules/@groove-dev/daemon/integrations-registry.json +0 -40
- package/node_modules/@groove-dev/daemon/package.json +4 -3
- package/node_modules/@groove-dev/daemon/src/api.js +212 -21
- package/node_modules/@groove-dev/daemon/src/index.js +68 -1
- package/node_modules/@groove-dev/daemon/src/integrations.js +59 -20
- package/node_modules/@groove-dev/daemon/src/process.js +83 -11
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +4 -0
- package/node_modules/@groove-dev/daemon/src/registry.js +1 -1
- package/node_modules/@groove-dev/gui/.groove/audit.log +1 -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/credentials.json +6 -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-x5suAiK7.js +182 -0
- package/node_modules/@groove-dev/gui/dist/index.html +1 -1
- package/node_modules/@groove-dev/gui/package.json +5 -4
- package/node_modules/@groove-dev/gui/src/App.jsx +149 -76
- package/node_modules/@groove-dev/gui/src/components/AgentActions.jsx +130 -1
- package/node_modules/@groove-dev/gui/src/components/AgentChat.jsx +47 -7
- package/node_modules/@groove-dev/gui/src/components/AgentNode.jsx +13 -83
- package/node_modules/@groove-dev/gui/src/components/SpawnPanel.jsx +918 -580
- package/node_modules/@groove-dev/gui/src/stores/groove.js +31 -2
- package/node_modules/@groove-dev/gui/src/views/AgentTree.jsx +133 -67
- package/node_modules/@groove-dev/gui/src/views/FileEditor.jsx +85 -1
- package/node_modules/@groove-dev/gui/src/views/IntegrationsStore.jsx +121 -44
- package/package.json +1 -2
- package/packages/cli/package.json +4 -3
- package/packages/daemon/integrations-registry.json +0 -40
- package/packages/daemon/package.json +4 -3
- package/packages/daemon/src/api.js +212 -21
- package/packages/daemon/src/index.js +68 -1
- package/packages/daemon/src/integrations.js +59 -20
- package/packages/daemon/src/process.js +83 -11
- package/packages/daemon/src/providers/claude-code.js +4 -0
- package/packages/daemon/src/registry.js +1 -1
- package/packages/gui/dist/assets/index-x5suAiK7.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/package.json +5 -4
- package/packages/gui/src/App.jsx +149 -76
- package/packages/gui/src/components/AgentActions.jsx +130 -1
- package/packages/gui/src/components/AgentChat.jsx +47 -7
- package/packages/gui/src/components/AgentNode.jsx +13 -83
- package/packages/gui/src/components/SpawnPanel.jsx +918 -580
- package/packages/gui/src/stores/groove.js +31 -2
- package/packages/gui/src/views/AgentTree.jsx +133 -67
- package/packages/gui/src/views/FileEditor.jsx +85 -1
- package/packages/gui/src/views/IntegrationsStore.jsx +121 -44
- 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-D5dtDQf0.js +0 -156
- package/packages/gui/dist/assets/index-D5dtDQf0.js +0 -156
|
@@ -1,12 +1,54 @@
|
|
|
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
|
+
// 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)' }}>×</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
|
|
28
|
-
{ id: 'full', label: 'Full Send', desc: 'No reviews,
|
|
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
|
|
100
|
-
|
|
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
|
-
|
|
126
|
-
setProvider(p.id);
|
|
127
|
-
setModel('auto');
|
|
128
|
-
setConnectingProvider(null);
|
|
129
|
-
return;
|
|
179
|
+
setProvider(p.id); setModel('auto'); setConnectingProvider(null); return;
|
|
130
180
|
}
|
|
131
|
-
|
|
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
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
241
|
+
setPlanMessages((prev) => [...prev, { from: 'ai', text: 'Failed to reach AI. Write your prompt directly.' }]);
|
|
152
242
|
}
|
|
153
|
-
|
|
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
|
-
|
|
167
|
-
:
|
|
168
|
-
|
|
169
|
-
|
|
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
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
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
|
-
|
|
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={
|
|
208
|
-
<div style={
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
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
|
-
|
|
217
|
-
|
|
218
|
-
|
|
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
|
-
{
|
|
330
|
+
{submitting ? 'Spawning...'
|
|
331
|
+
: scheduleEnabled ? 'Spawn + Schedule'
|
|
332
|
+
: isPlanner ? 'Start Planning' : 'Spawn Agent'}
|
|
226
333
|
</button>
|
|
227
|
-
|
|
228
|
-
|
|
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}>×</button>
|
|
335
|
+
</div>
|
|
238
336
|
</div>
|
|
239
337
|
|
|
240
|
-
{
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
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
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
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
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
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
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
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
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
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
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
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
|
-
|
|
378
|
-
|
|
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
|
-
|
|
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
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
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
|
-
|
|
425
|
-
|
|
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
|
-
|
|
429
|
-
|
|
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
|
-
|
|
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
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
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
|
-
|
|
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
|
-
<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
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
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
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
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
|
-
|
|
623
|
-
fontSize:
|
|
624
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
645
|
-
display: 'flex',
|
|
1014
|
+
chipRow: {
|
|
1015
|
+
display: 'flex', flexWrap: 'wrap', gap: 4,
|
|
646
1016
|
},
|
|
647
|
-
|
|
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:
|
|
1019
|
+
borderRadius: 3, padding: '3px 8px',
|
|
1020
|
+
color: 'var(--text-dim)', fontSize: 10, cursor: 'pointer',
|
|
652
1021
|
fontFamily: 'var(--font)',
|
|
653
1022
|
},
|
|
654
|
-
|
|
655
|
-
|
|
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
|
-
|
|
662
|
-
|
|
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
|
-
|
|
665
|
-
|
|
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
|
-
|
|
668
|
-
display: 'flex',
|
|
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
|
-
|
|
671
|
-
|
|
672
|
-
|
|
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
|
-
|
|
1063
|
+
color: 'var(--bg-base)', fontSize: 9, fontWeight: 700,
|
|
682
1064
|
},
|
|
683
|
-
|
|
1065
|
+
advToggle: {
|
|
684
1066
|
background: 'none', border: 'none', color: 'var(--text-dim)',
|
|
685
1067
|
fontSize: 10, cursor: 'pointer', fontFamily: 'var(--font)',
|
|
686
|
-
padding: '
|
|
1068
|
+
padding: '6px 0', marginBottom: 4,
|
|
687
1069
|
},
|
|
688
1070
|
providerBtn: {
|
|
689
1071
|
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,
|
|
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: '
|
|
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
|
-
|
|
704
|
-
|
|
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
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
};
|