groove-dev 0.27.74 → 0.27.75
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/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +256 -4
- package/node_modules/@groove-dev/daemon/src/conversations.js +16 -0
- package/node_modules/@groove-dev/daemon/src/index.js +41 -1
- package/node_modules/@groove-dev/daemon/src/preview.js +18 -2
- package/node_modules/@groove-dev/daemon/src/process.js +6 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +38 -0
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +156 -0
- package/node_modules/@groove-dev/daemon/src/providers/index.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/nano-banana.js +103 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CAT9SCJi.js +8620 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-CVzz6zyb.css +1 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/app.css +29 -0
- package/node_modules/@groove-dev/gui/src/app.jsx +2 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +16 -5
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +40 -7
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +149 -31
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -2
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +105 -52
- package/node_modules/@groove-dev/gui/src/components/layout/activity-bar.jsx +5 -2
- package/node_modules/@groove-dev/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +81 -0
- package/node_modules/@groove-dev/gui/src/components/preview/preview-workspace.jsx +263 -0
- package/node_modules/@groove-dev/gui/src/components/preview/screenshot-overlay.jsx +203 -0
- package/node_modules/@groove-dev/gui/src/components/ui/toast.jsx +6 -2
- package/node_modules/@groove-dev/gui/src/stores/groove.js +149 -9
- package/node_modules/@groove-dev/gui/src/views/preview.jsx +6 -0
- package/node_modules/@groove-dev/gui/src/views/settings.jsx +199 -114
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +256 -4
- package/packages/daemon/src/conversations.js +16 -0
- package/packages/daemon/src/index.js +41 -1
- package/packages/daemon/src/preview.js +18 -2
- package/packages/daemon/src/process.js +6 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/codex.js +38 -0
- package/packages/daemon/src/providers/grok.js +156 -0
- package/packages/daemon/src/providers/index.js +5 -1
- package/packages/daemon/src/providers/nano-banana.js +103 -0
- package/packages/gui/dist/assets/index-CAT9SCJi.js +8620 -0
- package/packages/gui/dist/assets/index-CVzz6zyb.css +1 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/app.css +29 -0
- package/packages/gui/src/app.jsx +2 -0
- package/packages/gui/src/components/chat/chat-header.jsx +16 -5
- package/packages/gui/src/components/chat/chat-input.jsx +40 -7
- package/packages/gui/src/components/chat/chat-messages.jsx +149 -31
- package/packages/gui/src/components/chat/chat-view.jsx +26 -2
- package/packages/gui/src/components/chat/model-picker.jsx +105 -52
- package/packages/gui/src/components/layout/activity-bar.jsx +5 -2
- package/packages/gui/src/components/layout/welcome-splash.jsx +215 -88
- package/packages/gui/src/components/preview/preview-toolbar.jsx +81 -0
- package/packages/gui/src/components/preview/preview-workspace.jsx +263 -0
- package/packages/gui/src/components/preview/screenshot-overlay.jsx +203 -0
- package/packages/gui/src/components/ui/toast.jsx +6 -2
- package/packages/gui/src/stores/groove.js +149 -9
- package/packages/gui/src/views/preview.jsx +6 -0
- package/packages/gui/src/views/settings.jsx +199 -114
- package/node_modules/@groove-dev/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/node_modules/@groove-dev/gui/dist/assets/index-QR7lyguO.css +0 -1
- package/packages/gui/dist/assets/index-DFP3r2yE.js +0 -8615
- package/packages/gui/dist/assets/index-QR7lyguO.css +0 -1
|
@@ -56,8 +56,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
56
56
|
selectedPeerId: null,
|
|
57
57
|
},
|
|
58
58
|
|
|
59
|
+
// ── Preview ───────────────────────────────────────────────
|
|
60
|
+
previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false },
|
|
61
|
+
previewChat: [],
|
|
62
|
+
previewIterating: false,
|
|
63
|
+
|
|
59
64
|
// ── Navigation ────────────────────────────────────────────
|
|
60
|
-
activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings'
|
|
65
|
+
activeView: 'agents', // 'agents' | 'editor' | 'dashboard' | 'marketplace' | 'teams' | 'settings' | 'preview'
|
|
61
66
|
detailPanel: null, // null | { type: 'agent', agentId } | { type: 'spawn' } | { type: 'journalist' }
|
|
62
67
|
teamDetailPanels: {}, // { [teamId]: detailPanel } — persists panel state per team
|
|
63
68
|
commandPaletteOpen: false,
|
|
@@ -455,21 +460,30 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
455
460
|
'success',
|
|
456
461
|
'Project ready to preview',
|
|
457
462
|
msg.url,
|
|
458
|
-
{ label: '
|
|
463
|
+
{ label: 'Open Preview', onClick: () => get().openPreview(msg.url, msg.teamId, msg.kind) },
|
|
459
464
|
{ persistent: true },
|
|
460
465
|
);
|
|
461
466
|
break;
|
|
462
467
|
|
|
463
|
-
case 'preview:failed':
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
468
|
+
case 'preview:failed': {
|
|
469
|
+
const failKind = msg.kind || '';
|
|
470
|
+
if (failKind !== 'no_preview' && failKind !== 'cli' && failKind !== 'none') {
|
|
471
|
+
get().addToast(
|
|
472
|
+
'warning',
|
|
473
|
+
'Preview could not launch',
|
|
474
|
+
msg.reason ? String(msg.reason).slice(0, 200) : 'Unknown error',
|
|
475
|
+
);
|
|
476
|
+
}
|
|
469
477
|
break;
|
|
478
|
+
}
|
|
470
479
|
|
|
471
|
-
case 'preview:stopped':
|
|
480
|
+
case 'preview:stopped': {
|
|
481
|
+
const ps = get().previewState;
|
|
482
|
+
if (ps.teamId && ps.teamId === msg.teamId) {
|
|
483
|
+
set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false });
|
|
484
|
+
}
|
|
472
485
|
break;
|
|
486
|
+
}
|
|
473
487
|
|
|
474
488
|
case 'agent:stalled': {
|
|
475
489
|
const name = msg.agentName || msg.agentId;
|
|
@@ -899,6 +913,55 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
899
913
|
break;
|
|
900
914
|
}
|
|
901
915
|
|
|
916
|
+
case 'conversation:image': {
|
|
917
|
+
const { conversationId, prompt, url, b64_json, mimeType, model: imgModel, provider: imgProvider } = msg.data || msg;
|
|
918
|
+
if (!conversationId) break;
|
|
919
|
+
const imageUrl = url || (b64_json ? `data:${mimeType || 'image/png'};base64,${b64_json}` : null);
|
|
920
|
+
set((s) => {
|
|
921
|
+
const msgs = { ...s.conversationMessages };
|
|
922
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
923
|
+
const arr = [...msgs[conversationId]];
|
|
924
|
+
const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading' && m.prompt === prompt);
|
|
925
|
+
if (loadingIdx >= 0) {
|
|
926
|
+
arr[loadingIdx] = { from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() };
|
|
927
|
+
} else {
|
|
928
|
+
arr.push({ from: 'assistant', type: 'image', imageUrl, prompt, model: imgModel, provider: imgProvider, timestamp: Date.now() });
|
|
929
|
+
}
|
|
930
|
+
msgs[conversationId] = arr.slice(-200);
|
|
931
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
932
|
+
const isActive = s.streamingConversationId === conversationId;
|
|
933
|
+
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
934
|
+
});
|
|
935
|
+
break;
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
case 'conversation:image-progress': {
|
|
939
|
+
const { conversationId, status, prompt: imgPrompt, error: imgError } = msg.data || msg;
|
|
940
|
+
if (!conversationId) break;
|
|
941
|
+
if (status === 'generating') {
|
|
942
|
+
set((s) => {
|
|
943
|
+
const msgs = { ...s.conversationMessages };
|
|
944
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
945
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'assistant', type: 'image-loading', prompt: imgPrompt, timestamp: Date.now() }];
|
|
946
|
+
return { conversationMessages: msgs, streamingConversationId: conversationId };
|
|
947
|
+
});
|
|
948
|
+
} else if (status === 'error') {
|
|
949
|
+
set((s) => {
|
|
950
|
+
const msgs = { ...s.conversationMessages };
|
|
951
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
952
|
+
const arr = [...msgs[conversationId]];
|
|
953
|
+
const loadingIdx = arr.findLastIndex((m) => m.type === 'image-loading');
|
|
954
|
+
if (loadingIdx >= 0) arr.splice(loadingIdx, 1);
|
|
955
|
+
arr.push({ from: 'system', text: `Image generation failed: ${imgError || 'Unknown error'}`, timestamp: Date.now() });
|
|
956
|
+
msgs[conversationId] = arr;
|
|
957
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
958
|
+
const isActive = s.streamingConversationId === conversationId;
|
|
959
|
+
return { conversationMessages: msgs, sendingMessage: isActive ? false : s.sendingMessage, streamingConversationId: isActive ? null : s.streamingConversationId };
|
|
960
|
+
});
|
|
961
|
+
}
|
|
962
|
+
break;
|
|
963
|
+
}
|
|
964
|
+
|
|
902
965
|
case 'conversation:error': {
|
|
903
966
|
const { conversationId, error } = msg.data || msg;
|
|
904
967
|
if (conversationId) {
|
|
@@ -1107,6 +1170,50 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1107
1170
|
persistJSON('groove:expandedNodes', expanded);
|
|
1108
1171
|
},
|
|
1109
1172
|
|
|
1173
|
+
// ── Preview ──────────────────────────────────────────────
|
|
1174
|
+
|
|
1175
|
+
openPreview(url, teamId, kind) {
|
|
1176
|
+
set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], activeView: 'preview' });
|
|
1177
|
+
},
|
|
1178
|
+
closePreview() {
|
|
1179
|
+
const { previewState } = get();
|
|
1180
|
+
if (previewState.teamId) {
|
|
1181
|
+
api.delete(`/preview/${previewState.teamId}`).catch(() => {});
|
|
1182
|
+
}
|
|
1183
|
+
set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false, activeView: 'agents' });
|
|
1184
|
+
},
|
|
1185
|
+
setPreviewDevice(size) {
|
|
1186
|
+
set((s) => ({ previewState: { ...s.previewState, deviceSize: size } }));
|
|
1187
|
+
},
|
|
1188
|
+
toggleScreenshotMode() {
|
|
1189
|
+
set((s) => ({ previewState: { ...s.previewState, screenshotMode: !s.previewState.screenshotMode } }));
|
|
1190
|
+
},
|
|
1191
|
+
async iteratePreview(message, screenshotBase64) {
|
|
1192
|
+
const { previewState } = get();
|
|
1193
|
+
if (!previewState.teamId) return;
|
|
1194
|
+
|
|
1195
|
+
const userMsg = { role: 'user', content: message, screenshot: screenshotBase64 || null, timestamp: Date.now() };
|
|
1196
|
+
set((s) => ({ previewChat: [...s.previewChat, userMsg], previewIterating: true }));
|
|
1197
|
+
|
|
1198
|
+
try {
|
|
1199
|
+
const body = { message };
|
|
1200
|
+
if (screenshotBase64) body.screenshot = screenshotBase64;
|
|
1201
|
+
const res = await api.post(`/preview/${previewState.teamId}/iterate`, body);
|
|
1202
|
+
const assistantMsg = { role: 'assistant', content: res.response || res.message || 'Changes routed to planner.', timestamp: Date.now() };
|
|
1203
|
+
set((s) => ({ previewChat: [...s.previewChat, assistantMsg], previewIterating: false }));
|
|
1204
|
+
} catch (err) {
|
|
1205
|
+
const errMsg = { role: 'assistant', content: `Failed to iterate: ${err.message}`, timestamp: Date.now() };
|
|
1206
|
+
set((s) => ({ previewChat: [...s.previewChat, errMsg], previewIterating: false }));
|
|
1207
|
+
}
|
|
1208
|
+
},
|
|
1209
|
+
addPreviewChatMessage(role, content, screenshot) {
|
|
1210
|
+
const msg = { role, content, screenshot: screenshot || null, timestamp: Date.now() };
|
|
1211
|
+
set((s) => ({ previewChat: [...s.previewChat, msg] }));
|
|
1212
|
+
},
|
|
1213
|
+
clearPreviewChat() {
|
|
1214
|
+
set({ previewChat: [] });
|
|
1215
|
+
},
|
|
1216
|
+
|
|
1110
1217
|
// ── Toasts ────────────────────────────────────────────────
|
|
1111
1218
|
|
|
1112
1219
|
addToast(type, message, detail, action, options = {}) {
|
|
@@ -1488,6 +1595,13 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1488
1595
|
get().fetchTreeDir('');
|
|
1489
1596
|
},
|
|
1490
1597
|
|
|
1598
|
+
async removeRecentProject(path) {
|
|
1599
|
+
try {
|
|
1600
|
+
await api.delete('/projects/recent', { path });
|
|
1601
|
+
} catch {}
|
|
1602
|
+
get().fetchProjectDir();
|
|
1603
|
+
},
|
|
1604
|
+
|
|
1491
1605
|
toggleProjectPicker() {
|
|
1492
1606
|
set((s) => ({ showProjectPicker: !s.showProjectPicker }));
|
|
1493
1607
|
},
|
|
@@ -1977,6 +2091,32 @@ export const useGrooveStore = create((set, get) => ({
|
|
|
1977
2091
|
}
|
|
1978
2092
|
},
|
|
1979
2093
|
|
|
2094
|
+
async sendImageMessage(conversationId, prompt, { model, size, quality } = {}) {
|
|
2095
|
+
const conv = get().conversations.find((c) => c.id === conversationId);
|
|
2096
|
+
if (!conv) return;
|
|
2097
|
+
|
|
2098
|
+
set((s) => {
|
|
2099
|
+
const msgs = { ...s.conversationMessages };
|
|
2100
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2101
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'user', text: prompt, timestamp: Date.now() }];
|
|
2102
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
2103
|
+
return { conversationMessages: msgs, sendingMessage: true, streamingConversationId: conversationId };
|
|
2104
|
+
});
|
|
2105
|
+
|
|
2106
|
+
try {
|
|
2107
|
+
await api.post(`/conversations/${encodeURIComponent(conversationId)}/generate-image`, { prompt, model, size, quality });
|
|
2108
|
+
} catch (err) {
|
|
2109
|
+
set((s) => {
|
|
2110
|
+
const msgs = { ...s.conversationMessages };
|
|
2111
|
+
if (!msgs[conversationId]) msgs[conversationId] = [];
|
|
2112
|
+
msgs[conversationId] = [...msgs[conversationId], { from: 'system', text: `Image failed: ${err.message}`, timestamp: Date.now() }];
|
|
2113
|
+
persistJSON('groove:conversationMessages', msgs);
|
|
2114
|
+
return { conversationMessages: msgs, sendingMessage: false, streamingConversationId: null };
|
|
2115
|
+
});
|
|
2116
|
+
get().addToast('error', 'Image generation failed', err.message);
|
|
2117
|
+
}
|
|
2118
|
+
},
|
|
2119
|
+
|
|
1980
2120
|
// ── Editor ────────────────────────────────────────────────
|
|
1981
2121
|
|
|
1982
2122
|
async openFile(path) {
|
|
@@ -46,6 +46,7 @@ const KEY_PLACEHOLDERS = {
|
|
|
46
46
|
'claude-code': 'sk-ant-...',
|
|
47
47
|
codex: 'Paste from platform.openai.com',
|
|
48
48
|
gemini: 'Paste from aistudio.google.com',
|
|
49
|
+
grok: 'xai-...',
|
|
49
50
|
};
|
|
50
51
|
|
|
51
52
|
function ProviderCard({ provider, onKeyChange }) {
|
|
@@ -58,6 +59,7 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
58
59
|
const [customPathOpen, setCustomPathOpen] = useState(false);
|
|
59
60
|
const [customPath, setCustomPath] = useState('');
|
|
60
61
|
const [savingPath, setSavingPath] = useState(false);
|
|
62
|
+
const [loginPending, setLoginPending] = useState(false);
|
|
61
63
|
const addToast = useGrooveStore((s) => s.addToast);
|
|
62
64
|
const installProgress = useGrooveStore((s) => s.providerInstallProgress[provider.id]);
|
|
63
65
|
const loginProvider = useGrooveStore((s) => s.loginProvider);
|
|
@@ -97,8 +99,11 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
97
99
|
|
|
98
100
|
async function handleLogin(body) {
|
|
99
101
|
try {
|
|
102
|
+
setLoginPending(true);
|
|
100
103
|
await loginProvider(provider.id, body);
|
|
101
|
-
} catch {
|
|
104
|
+
} catch {
|
|
105
|
+
setLoginPending(false);
|
|
106
|
+
}
|
|
102
107
|
}
|
|
103
108
|
|
|
104
109
|
async function handleSavePath() {
|
|
@@ -310,54 +315,154 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
310
315
|
|
|
311
316
|
{/* Installed but needs auth */}
|
|
312
317
|
{provider.installed && !isReady && !settingKey && !isInstalling && (
|
|
313
|
-
<div className="flex flex-col gap-
|
|
314
|
-
{
|
|
318
|
+
<div className="flex flex-col gap-3 flex-1">
|
|
319
|
+
{/* ── Claude Code auth ── */}
|
|
320
|
+
{provider.id === 'claude-code' && !loginPending && (
|
|
315
321
|
<>
|
|
316
|
-
<
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
322
|
+
<div className="space-y-1.5">
|
|
323
|
+
<p className="text-xs text-text-1 font-sans font-medium">Sign in with your Claude account</p>
|
|
324
|
+
<p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your existing Anthropic account or Claude subscription.</p>
|
|
325
|
+
</div>
|
|
326
|
+
<Button variant="primary" size="sm" onClick={() => handleLogin()} className="w-full h-9 text-xs gap-1.5">
|
|
327
|
+
<ExternalLink size={12} /> Sign In
|
|
328
|
+
</Button>
|
|
329
|
+
<button
|
|
330
|
+
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
331
|
+
className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
|
|
321
332
|
>
|
|
322
|
-
|
|
333
|
+
I have an API key instead
|
|
334
|
+
</button>
|
|
335
|
+
</>
|
|
336
|
+
)}
|
|
337
|
+
|
|
338
|
+
{/* ── Codex auth ── */}
|
|
339
|
+
{provider.id === 'codex' && !loginPending && (
|
|
340
|
+
<>
|
|
341
|
+
<div className="space-y-1.5">
|
|
342
|
+
<p className="text-xs text-text-1 font-sans font-medium">Sign in with your ChatGPT account</p>
|
|
343
|
+
<p className="text-2xs text-text-3 font-sans">A browser window will open where you can sign in with your ChatGPT Plus or Teams subscription.</p>
|
|
344
|
+
</div>
|
|
345
|
+
<Button variant="primary" size="sm" onClick={() => handleLogin({ method: 'chatgpt-plus' })} className="w-full h-9 text-xs gap-1.5">
|
|
346
|
+
<ExternalLink size={12} /> Sign In
|
|
323
347
|
</Button>
|
|
324
|
-
<
|
|
325
|
-
variant="secondary"
|
|
326
|
-
size="sm"
|
|
348
|
+
<button
|
|
327
349
|
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
328
|
-
className="
|
|
350
|
+
className="text-2xs text-text-4 hover:text-accent cursor-pointer font-sans text-center"
|
|
329
351
|
>
|
|
330
|
-
|
|
352
|
+
I have an API key instead
|
|
353
|
+
</button>
|
|
354
|
+
</>
|
|
355
|
+
)}
|
|
356
|
+
|
|
357
|
+
{/* ── Gemini auth ── */}
|
|
358
|
+
{provider.id === 'gemini' && (
|
|
359
|
+
<>
|
|
360
|
+
<div className="space-y-2">
|
|
361
|
+
<p className="text-xs text-text-1 font-sans font-medium">Add your Gemini API key</p>
|
|
362
|
+
<div className="space-y-1.5">
|
|
363
|
+
<div className="flex items-start gap-2">
|
|
364
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
|
|
365
|
+
<p className="text-2xs text-text-2 font-sans">
|
|
366
|
+
Go to <button onClick={() => window.open('https://aistudio.google.com/apikey', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">aistudio.google.com</button> and sign in with Google
|
|
367
|
+
</p>
|
|
368
|
+
</div>
|
|
369
|
+
<div className="flex items-start gap-2">
|
|
370
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
|
|
371
|
+
<p className="text-2xs text-text-2 font-sans">Click "Create API Key" and copy it</p>
|
|
372
|
+
</div>
|
|
373
|
+
<div className="flex items-start gap-2">
|
|
374
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
|
|
375
|
+
<p className="text-2xs text-text-2 font-sans">Paste it below</p>
|
|
376
|
+
</div>
|
|
377
|
+
</div>
|
|
378
|
+
</div>
|
|
379
|
+
<div className="relative">
|
|
380
|
+
<input
|
|
381
|
+
value={keyInput}
|
|
382
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
383
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
384
|
+
type={showKey ? 'text' : 'password'}
|
|
385
|
+
placeholder="AIza..."
|
|
386
|
+
className="w-full h-9 px-3 pr-9 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
387
|
+
autoFocus
|
|
388
|
+
/>
|
|
389
|
+
<button onClick={() => setShowKey(!showKey)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
390
|
+
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
391
|
+
</button>
|
|
392
|
+
</div>
|
|
393
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="w-full h-8 text-xs">
|
|
394
|
+
Save Key
|
|
331
395
|
</Button>
|
|
332
396
|
</>
|
|
333
|
-
)
|
|
397
|
+
)}
|
|
398
|
+
|
|
399
|
+
{/* ── Grok (xAI) auth ── */}
|
|
400
|
+
{provider.id === 'grok' && (
|
|
334
401
|
<>
|
|
402
|
+
<div className="space-y-2">
|
|
403
|
+
<p className="text-xs text-text-1 font-sans font-medium">Add your xAI API key</p>
|
|
404
|
+
<div className="space-y-1.5">
|
|
405
|
+
<div className="flex items-start gap-2">
|
|
406
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">1</span>
|
|
407
|
+
<p className="text-2xs text-text-2 font-sans">
|
|
408
|
+
Go to <button onClick={() => window.open('https://console.x.ai', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.x.ai</button> and sign in
|
|
409
|
+
</p>
|
|
410
|
+
</div>
|
|
411
|
+
<div className="flex items-start gap-2">
|
|
412
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">2</span>
|
|
413
|
+
<p className="text-2xs text-text-2 font-sans">Create an API key and copy it</p>
|
|
414
|
+
</div>
|
|
415
|
+
<div className="flex items-start gap-2">
|
|
416
|
+
<span className="text-2xs font-bold text-accent font-mono mt-0.5">3</span>
|
|
417
|
+
<p className="text-2xs text-text-2 font-sans">Paste it below</p>
|
|
418
|
+
</div>
|
|
419
|
+
</div>
|
|
420
|
+
</div>
|
|
421
|
+
<div className="relative">
|
|
422
|
+
<input
|
|
423
|
+
value={keyInput}
|
|
424
|
+
onChange={(e) => setKeyInput(e.target.value)}
|
|
425
|
+
onKeyDown={(e) => e.key === 'Enter' && handleSetKey()}
|
|
426
|
+
type={showKey ? 'text' : 'password'}
|
|
427
|
+
placeholder="xai-..."
|
|
428
|
+
className="w-full h-9 px-3 pr-9 text-xs bg-surface-0 border border-border rounded-md text-text-0 font-mono placeholder:text-text-4 focus:outline-none focus:ring-1 focus:ring-accent"
|
|
429
|
+
autoFocus
|
|
430
|
+
/>
|
|
431
|
+
<button onClick={() => setShowKey(!showKey)} className="absolute right-2.5 top-1/2 -translate-y-1/2 text-text-4 hover:text-text-2 cursor-pointer">
|
|
432
|
+
{showKey ? <EyeOff size={12} /> : <Eye size={12} />}
|
|
433
|
+
</button>
|
|
434
|
+
</div>
|
|
435
|
+
<Button variant="primary" size="sm" onClick={handleSetKey} disabled={!keyInput.trim()} className="w-full h-8 text-xs">
|
|
436
|
+
Save Key
|
|
437
|
+
</Button>
|
|
438
|
+
</>
|
|
439
|
+
)}
|
|
440
|
+
|
|
441
|
+
{/* ── Any provider: login pending state ── */}
|
|
442
|
+
{(provider.id === 'claude-code' || provider.id === 'codex') && loginPending && (
|
|
443
|
+
<div className="flex flex-col gap-3">
|
|
444
|
+
<div className="flex items-center gap-2 p-3 bg-accent/5 border border-accent/15 rounded-md">
|
|
445
|
+
<Loader2 size={14} className="text-accent animate-spin" />
|
|
446
|
+
<div>
|
|
447
|
+
<p className="text-xs text-accent font-sans font-medium">Check your browser</p>
|
|
448
|
+
<p className="text-2xs text-text-3 font-sans">Complete the sign-in in the browser window that opened.</p>
|
|
449
|
+
</div>
|
|
450
|
+
</div>
|
|
335
451
|
<Button
|
|
336
452
|
variant="primary"
|
|
337
453
|
size="sm"
|
|
338
|
-
onClick={() => {
|
|
339
|
-
className="w-full h-8 text-
|
|
454
|
+
onClick={() => { setLoginPending(false); if (onKeyChange) onKeyChange(); }}
|
|
455
|
+
className="w-full h-8 text-xs gap-1.5"
|
|
340
456
|
>
|
|
341
|
-
<
|
|
457
|
+
<Check size={12} /> I've signed in
|
|
342
458
|
</Button>
|
|
343
|
-
<
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
onClick={() => handleLogin({ method: 'chatgpt-plus' })}
|
|
347
|
-
className="w-full h-8 text-2xs gap-1.5"
|
|
459
|
+
<button
|
|
460
|
+
onClick={() => setLoginPending(false)}
|
|
461
|
+
className="text-2xs text-text-4 hover:text-text-2 cursor-pointer font-sans text-center"
|
|
348
462
|
>
|
|
349
|
-
|
|
350
|
-
</
|
|
351
|
-
|
|
352
|
-
) : (
|
|
353
|
-
<Button
|
|
354
|
-
variant="primary"
|
|
355
|
-
size="sm"
|
|
356
|
-
onClick={() => { setSettingKey(true); setShowKey(false); setKeyInput(''); }}
|
|
357
|
-
className="w-full h-8 text-2xs gap-1.5"
|
|
358
|
-
>
|
|
359
|
-
<Key size={11} /> Add API Key
|
|
360
|
-
</Button>
|
|
463
|
+
Cancel
|
|
464
|
+
</button>
|
|
465
|
+
</div>
|
|
361
466
|
)}
|
|
362
467
|
</div>
|
|
363
468
|
)}
|
|
@@ -390,6 +495,26 @@ function ProviderCard({ provider, onKeyChange }) {
|
|
|
390
495
|
<label className="text-2xs font-semibold text-text-2 font-sans mb-1.5 block">
|
|
391
496
|
{provider.hasKey ? 'Update API Key' : `${provider.name} API Key`}
|
|
392
497
|
</label>
|
|
498
|
+
{!provider.hasKey && provider.id === 'claude-code' && (
|
|
499
|
+
<p className="text-2xs text-text-3 font-sans mb-1.5">
|
|
500
|
+
Get yours at <button onClick={() => window.open('https://console.anthropic.com/settings/keys', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.anthropic.com</button>
|
|
501
|
+
</p>
|
|
502
|
+
)}
|
|
503
|
+
{!provider.hasKey && provider.id === 'codex' && (
|
|
504
|
+
<p className="text-2xs text-text-3 font-sans mb-1.5">
|
|
505
|
+
Get yours at <button onClick={() => window.open('https://platform.openai.com/api-keys', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">platform.openai.com</button>
|
|
506
|
+
</p>
|
|
507
|
+
)}
|
|
508
|
+
{!provider.hasKey && provider.id === 'gemini' && (
|
|
509
|
+
<p className="text-2xs text-text-3 font-sans mb-1.5">
|
|
510
|
+
Get yours at <button onClick={() => window.open('https://aistudio.google.com/apikey', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">aistudio.google.com</button>
|
|
511
|
+
</p>
|
|
512
|
+
)}
|
|
513
|
+
{!provider.hasKey && provider.id === 'grok' && (
|
|
514
|
+
<p className="text-2xs text-text-3 font-sans mb-1.5">
|
|
515
|
+
Get yours at <button onClick={() => window.open('https://console.x.ai', '_blank')} className="text-accent hover:underline cursor-pointer font-sans">console.x.ai</button>
|
|
516
|
+
</p>
|
|
517
|
+
)}
|
|
393
518
|
<div className="relative">
|
|
394
519
|
<input
|
|
395
520
|
value={keyInput}
|
|
@@ -1445,85 +1570,45 @@ export default function SettingsView() {
|
|
|
1445
1570
|
</div>
|
|
1446
1571
|
</ConfigCard>
|
|
1447
1572
|
|
|
1448
|
-
<ConfigCard icon={
|
|
1449
|
-
<div className="
|
|
1450
|
-
|
|
1451
|
-
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
1468
|
-
|
|
1469
|
-
|
|
1470
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
1475
|
-
|
|
1476
|
-
|
|
1477
|
-
|
|
1478
|
-
|
|
1479
|
-
|
|
1480
|
-
|
|
1481
|
-
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
1486
|
-
|
|
1487
|
-
</ConfigCard>
|
|
1488
|
-
|
|
1489
|
-
<ConfigCard icon={Users} label="Max Agents" description="Concurrent agent limit. 0 = unlimited.">
|
|
1490
|
-
<div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
|
|
1491
|
-
{[0, 4, 8, 12, 20].map((n) => {
|
|
1492
|
-
const isActive = (config.maxAgents || 0) === n;
|
|
1493
|
-
return (
|
|
1494
|
-
<button
|
|
1495
|
-
key={n}
|
|
1496
|
-
onClick={() => updateConfig('maxAgents', n)}
|
|
1497
|
-
className={cn(
|
|
1498
|
-
'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
1499
|
-
isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
1500
|
-
)}
|
|
1501
|
-
>
|
|
1502
|
-
{n === 0 ? '\u221E' : n}
|
|
1503
|
-
</button>
|
|
1504
|
-
);
|
|
1505
|
-
})}
|
|
1506
|
-
</div>
|
|
1507
|
-
</ConfigCard>
|
|
1508
|
-
|
|
1509
|
-
<ConfigCard icon={Newspaper} label="Journalist Interval" description="Seconds between synthesis cycles.">
|
|
1510
|
-
<div className="flex bg-surface-0 rounded-md p-0.5 border border-border-subtle">
|
|
1511
|
-
{[60, 120, 300, 600].map((n) => {
|
|
1512
|
-
const isActive = (config.journalistInterval || 120) === n;
|
|
1513
|
-
const label = n < 60 ? `${n}s` : `${n / 60}m`;
|
|
1514
|
-
return (
|
|
1515
|
-
<button
|
|
1516
|
-
key={n}
|
|
1517
|
-
onClick={() => updateConfig('journalistInterval', n)}
|
|
1518
|
-
className={cn(
|
|
1519
|
-
'flex-1 px-2 py-1.5 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
|
|
1520
|
-
isActive ? 'bg-accent/15 text-accent shadow-sm' : 'text-text-3 hover:text-text-1',
|
|
1521
|
-
)}
|
|
1522
|
-
>
|
|
1523
|
-
{label}
|
|
1524
|
-
</button>
|
|
1525
|
-
);
|
|
1526
|
-
})}
|
|
1573
|
+
<ConfigCard icon={MessageSquare} label="Default Chat Model" description="Provider and model for new chat conversations.">
|
|
1574
|
+
<div className="space-y-2">
|
|
1575
|
+
<select
|
|
1576
|
+
value={config.defaultChatProvider || config.defaultProvider || 'claude-code'}
|
|
1577
|
+
onChange={(e) => {
|
|
1578
|
+
updateConfig('defaultChatProvider', e.target.value);
|
|
1579
|
+
const prov = providers.find((p) => p.id === e.target.value);
|
|
1580
|
+
const chatModels = (prov?.models || []).filter((m) => {
|
|
1581
|
+
const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
|
|
1582
|
+
return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
|
|
1583
|
+
});
|
|
1584
|
+
if (chatModels.length > 0) {
|
|
1585
|
+
const first = typeof chatModels[0] === 'string' ? chatModels[0] : chatModels[0].id;
|
|
1586
|
+
updateConfig('defaultChatModel', first);
|
|
1587
|
+
}
|
|
1588
|
+
}}
|
|
1589
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer"
|
|
1590
|
+
>
|
|
1591
|
+
{visibleProviders.filter((p) => p.installed && (p.authType === 'local' || p.authType === 'subscription' || p.hasKey)).map((p) => (
|
|
1592
|
+
<option key={p.id} value={p.id}>{p.name}</option>
|
|
1593
|
+
))}
|
|
1594
|
+
</select>
|
|
1595
|
+
<select
|
|
1596
|
+
value={config.defaultChatModel || ''}
|
|
1597
|
+
onChange={(e) => updateConfig('defaultChatModel', e.target.value || null)}
|
|
1598
|
+
className="w-full h-8 px-2.5 text-xs bg-surface-0 border border-border-subtle rounded-md text-text-0 font-mono focus:outline-none focus:ring-1 focus:ring-accent cursor-pointer"
|
|
1599
|
+
>
|
|
1600
|
+
<option value="">Auto (Sonnet)</option>
|
|
1601
|
+
{(providers.find((p) => p.id === (config.defaultChatProvider || config.defaultProvider || 'claude-code'))?.models || [])
|
|
1602
|
+
.filter((m) => {
|
|
1603
|
+
const id = (typeof m === 'string' ? m : m.id || '').toLowerCase();
|
|
1604
|
+
return !id.includes('dall-e') && !id.includes('imagen') && !id.includes('image');
|
|
1605
|
+
})
|
|
1606
|
+
.map((m) => {
|
|
1607
|
+
const id = typeof m === 'string' ? m : m.id;
|
|
1608
|
+
const name = typeof m === 'string' ? m : m.name || m.id;
|
|
1609
|
+
return <option key={id} value={id}>{name}</option>;
|
|
1610
|
+
})}
|
|
1611
|
+
</select>
|
|
1527
1612
|
</div>
|
|
1528
1613
|
</ConfigCard>
|
|
1529
1614
|
|