groove-dev 0.27.140 → 0.27.141

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. package/node_modules/@groove-dev/cli/package.json +1 -1
  2. package/node_modules/@groove-dev/daemon/integrations-registry.json +12 -44
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +82 -16
  5. package/node_modules/@groove-dev/daemon/src/integrations.js +10 -0
  6. package/node_modules/@groove-dev/daemon/src/journalist.js +169 -0
  7. package/node_modules/@groove-dev/daemon/src/keeper.js +3 -3
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +11 -0
  9. package/node_modules/@groove-dev/daemon/src/process.js +76 -0
  10. package/node_modules/@groove-dev/daemon/src/validate.js +8 -0
  11. package/node_modules/@groove-dev/gui/dist/assets/index-A4e1gIDh.css +1 -0
  12. package/node_modules/@groove-dev/gui/dist/assets/index-P1hsM27-.js +8696 -0
  13. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  14. package/node_modules/@groove-dev/gui/package.json +1 -1
  15. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +3 -3
  16. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +7 -2
  17. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +5 -4
  18. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +160 -12
  19. package/node_modules/@groove-dev/gui/src/components/editor/ai-panel.jsx +77 -6
  20. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +2 -49
  21. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +15 -4
  22. package/node_modules/@groove-dev/gui/src/components/keeper/global-modals.jsx +10 -10
  23. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +152 -3
  24. package/node_modules/@groove-dev/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  25. package/node_modules/@groove-dev/gui/src/stores/groove.js +110 -32
  26. package/node_modules/@groove-dev/gui/src/views/agents.jsx +114 -56
  27. package/node_modules/@groove-dev/gui/src/views/memory.jsx +9 -9
  28. package/node_modules/@groove-dev/gui/src/views/model-lab.jsx +1 -6
  29. package/node_modules/@groove-dev/gui/src/views/models.jsx +658 -565
  30. package/package.json +1 -1
  31. package/packages/cli/package.json +1 -1
  32. package/packages/daemon/integrations-registry.json +12 -44
  33. package/packages/daemon/package.json +1 -1
  34. package/packages/daemon/src/api.js +82 -16
  35. package/packages/daemon/src/integrations.js +10 -0
  36. package/packages/daemon/src/journalist.js +169 -0
  37. package/packages/daemon/src/keeper.js +3 -3
  38. package/packages/daemon/src/model-lab.js +11 -0
  39. package/packages/daemon/src/process.js +76 -0
  40. package/packages/daemon/src/validate.js +8 -0
  41. package/packages/gui/dist/assets/index-A4e1gIDh.css +1 -0
  42. package/packages/gui/dist/assets/index-P1hsM27-.js +8696 -0
  43. package/packages/gui/dist/index.html +2 -2
  44. package/packages/gui/package.json +1 -1
  45. package/packages/gui/src/components/agents/agent-chat.jsx +3 -3
  46. package/packages/gui/src/components/agents/agent-file-tree.jsx +7 -2
  47. package/packages/gui/src/components/agents/code-review.jsx +5 -4
  48. package/packages/gui/src/components/agents/workspace-mode.jsx +160 -12
  49. package/packages/gui/src/components/editor/ai-panel.jsx +77 -6
  50. package/packages/gui/src/components/editor/file-tree.jsx +2 -49
  51. package/packages/gui/src/components/editor/terminal.jsx +15 -4
  52. package/packages/gui/src/components/keeper/global-modals.jsx +10 -10
  53. package/packages/gui/src/components/layout/terminal-panel.jsx +152 -3
  54. package/packages/gui/src/components/marketplace/integration-wizard.jsx +223 -18
  55. package/packages/gui/src/stores/groove.js +110 -32
  56. package/packages/gui/src/views/agents.jsx +114 -56
  57. package/packages/gui/src/views/memory.jsx +9 -9
  58. package/packages/gui/src/views/model-lab.jsx +1 -6
  59. package/packages/gui/src/views/models.jsx +658 -565
  60. package/plan_files/keeper-manual.md +53 -42
  61. package/node_modules/@groove-dev/gui/dist/assets/index-BV9CAiw1.css +0 -1
  62. package/node_modules/@groove-dev/gui/dist/assets/index-DK6UIz0n.js +0 -8698
  63. package/packages/gui/dist/assets/index-BV9CAiw1.css +0 -1
  64. package/packages/gui/dist/assets/index-DK6UIz0n.js +0 -8698
@@ -213,6 +213,7 @@ export const useGrooveStore = create((set, get) => ({
213
213
 
214
214
  // ── Editor (Cursor-style) ────────────────────────────────
215
215
  editorSelectedAgent: null,
216
+ editorPendingSnippet: null,
216
217
  editorViewMode: 'code',
217
218
  editorAiPanelOpen: false,
218
219
  editorAiPanelWidth: Number(localStorage.getItem('groove:editorAiPanelWidth')) || 360,
@@ -665,6 +666,7 @@ export const useGrooveStore = create((set, get) => ({
665
666
  case 'lab:runtime:added':
666
667
  case 'lab:runtime:updated':
667
668
  case 'lab:runtime:removed':
669
+ case 'llama:server:stopped':
668
670
  get().fetchLabRuntimes();
669
671
  break;
670
672
 
@@ -1320,7 +1322,7 @@ export const useGrooveStore = create((set, get) => ({
1320
1322
  },
1321
1323
 
1322
1324
  async _handleKeeperCommand(agentId, message, command) {
1323
- const rest = message.replace(/\[\w+[-\w]*\]/i, '').trim();
1325
+ const rest = message.replace(/\[\w+[-\w]*\]|\b(?:save|append|update|delete|view|doc|link|read|instruct)\b/i, '').trim();
1324
1326
  const tags = (rest.match(/#[\w/.-]+/g) || []).map(t => t.replace(/^#/, ''));
1325
1327
 
1326
1328
  const addSystemMsg = (text) => {
@@ -1335,23 +1337,21 @@ export const useGrooveStore = create((set, get) => ({
1335
1337
  }
1336
1338
 
1337
1339
  case 'save': {
1338
- if (tags.length === 0) { addSystemMsg('Usage: [save] #tag your message here'); return true; }
1340
+ if (tags.length === 0) { addSystemMsg('Usage: save #tag your message here'); return true; }
1339
1341
  const content = rest.replace(/#[\w/.-]+/g, '').trim();
1340
- if (!content) { addSystemMsg('Usage: [save] #tag your message here'); return true; }
1341
- get().addChatMessage(agentId, 'user', message, false);
1342
+ if (!content) { addSystemMsg('Usage: save #tag your message here'); return true; }
1342
1343
  await get().saveKeeperItem(tags[0], content);
1343
1344
  addSystemMsg(`Saved to #${tags[0]}`);
1344
- return true;
1345
+ return { passthrough: content };
1345
1346
  }
1346
1347
 
1347
1348
  case 'append': {
1348
- if (tags.length === 0) { addSystemMsg('Usage: [append] #tag content to add'); return true; }
1349
+ if (tags.length === 0) { addSystemMsg('Usage: append #tag content to add'); return true; }
1349
1350
  const content = rest.replace(/#[\w/.-]+/g, '').trim();
1350
- if (!content) { addSystemMsg('Usage: [append] #tag content to add'); return true; }
1351
- get().addChatMessage(agentId, 'user', message, false);
1351
+ if (!content) { addSystemMsg('Usage: append #tag content to add'); return true; }
1352
1352
  await get().appendKeeperItem(tags[0], content);
1353
1353
  addSystemMsg(`Appended to #${tags[0]}`);
1354
- return true;
1354
+ return { passthrough: content };
1355
1355
  }
1356
1356
 
1357
1357
  case 'update': {
@@ -2031,7 +2031,6 @@ export const useGrooveStore = create((set, get) => ({
2031
2031
  ...(tlc?.reasoningEffort != null && { teamReasoningEffort: tlc.reasoningEffort }),
2032
2032
  ...(tlc?.temperature != null && { teamTemperature: tlc.temperature }),
2033
2033
  ...(tlc?.verbosity != null && { teamVerbosity: tlc.verbosity }),
2034
- ...(tlc?.mode && { mode: tlc.mode }),
2035
2034
  };
2036
2035
  const result = await api.post('/recommended-team/launch', body);
2037
2036
  const totalOk = (result.launched || 0) + (result.reused || 0);
@@ -2685,10 +2684,13 @@ export const useGrooveStore = create((set, get) => ({
2685
2684
 
2686
2685
  async instructAgent(id, message) {
2687
2686
  // ── Keeper command interception ─────────────────────────
2688
- const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]/i);
2687
+ const keeperCmd = message.match(/\[(save|append|update|delete|view|doc|link|read|instruct)\]|\b(save|append|update|delete|view|doc|link|read)\b(?=\s+#[\w/.-])/i);
2689
2688
  if (keeperCmd) {
2690
- const handled = await get()._handleKeeperCommand(id, message, keeperCmd[1].toLowerCase());
2691
- if (handled) return { status: 'keeper_handled' };
2689
+ const handled = await get()._handleKeeperCommand(id, message, (keeperCmd[1] || keeperCmd[2]).toLowerCase());
2690
+ if (handled === true) return { status: 'keeper_handled' };
2691
+ if (handled?.passthrough) {
2692
+ message = handled.passthrough;
2693
+ }
2692
2694
  }
2693
2695
 
2694
2696
  get().addChatMessage(id, 'user', message, false);
@@ -3226,10 +3228,27 @@ export const useGrooveStore = create((set, get) => ({
3226
3228
  set({ editorQuickSearchOpen: open });
3227
3229
  },
3228
3230
 
3231
+ attachSnippet(snippet) {
3232
+ set({ editorPendingSnippet: snippet });
3233
+ if (!get().editorAiPanelOpen) {
3234
+ set({ editorAiPanelOpen: true });
3235
+ }
3236
+ },
3237
+
3238
+ clearSnippet() {
3239
+ set({ editorPendingSnippet: null });
3240
+ },
3241
+
3229
3242
  async sendCodeToAgent(agentId, instruction, filePath, lineStart, lineEnd, selectedCode) {
3230
3243
  if (!agentId) return;
3231
- const message = `Instruction: ${instruction}\nFile: ${filePath}\nLines ${lineStart}-${lineEnd}:\n\`\`\`\n${selectedCode}\n\`\`\``;
3232
- await get().instructAgent(agentId, message);
3244
+ get().attachSnippet({
3245
+ type: 'code',
3246
+ instruction,
3247
+ filePath,
3248
+ lineStart,
3249
+ lineEnd,
3250
+ code: selectedCode,
3251
+ });
3233
3252
  },
3234
3253
 
3235
3254
  async fetchGitStatus() {
@@ -3844,11 +3863,17 @@ export const useGrooveStore = create((set, get) => ({
3844
3863
  const payload = line.slice(6);
3845
3864
  if (payload === '[DONE]') continue;
3846
3865
  try {
3847
- const chunk = JSON.parse(payload);
3848
- if (chunk.type === 'reasoning' && chunk.content) {
3866
+ const parsed = JSON.parse(payload);
3867
+
3868
+ // Support both raw OpenAI format (piped) and legacy wrapper format
3869
+ const delta = parsed.choices?.[0]?.delta;
3870
+ const reasoningText = delta?.reasoning_content || (parsed.type === 'reasoning' ? parsed.content : null);
3871
+ const contentText = delta?.content || (parsed.type === 'token' ? parsed.content : null);
3872
+
3873
+ if (reasoningText) {
3849
3874
  if (!firstTokenTime) firstTokenTime = performance.now();
3850
3875
  tokenCount++;
3851
- fullReasoning += chunk.content;
3876
+ fullReasoning += reasoningText;
3852
3877
  set((s) => {
3853
3878
  const sessions = s.labSessions.map((sess) => {
3854
3879
  if (sess.id !== sessionId) return sess;
@@ -3859,10 +3884,10 @@ export const useGrooveStore = create((set, get) => ({
3859
3884
  return { labSessions: sessions };
3860
3885
  });
3861
3886
  }
3862
- if (chunk.type === 'token' && chunk.content) {
3887
+ if (contentText) {
3863
3888
  if (!firstTokenTime) firstTokenTime = performance.now();
3864
3889
  tokenCount++;
3865
- fullContent += chunk.content;
3890
+ fullContent += contentText;
3866
3891
  set((s) => {
3867
3892
  const sessions = s.labSessions.map((sess) => {
3868
3893
  if (sess.id !== sessionId) return sess;
@@ -3873,11 +3898,13 @@ export const useGrooveStore = create((set, get) => ({
3873
3898
  return { labSessions: sessions };
3874
3899
  });
3875
3900
  }
3876
- if (chunk.type === 'done' && chunk.metrics) {
3901
+
3902
+ // Handle done event (legacy wrapper) or finish_reason (raw OpenAI)
3903
+ if (parsed.type === 'done' && parsed.metrics) {
3877
3904
  const elapsed = performance.now() - startTime;
3878
3905
  const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3879
3906
  const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3880
- const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...chunk.metrics };
3907
+ const msgMetrics = { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed, ...parsed.metrics };
3881
3908
 
3882
3909
  set((s) => {
3883
3910
  const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
@@ -3891,15 +3918,15 @@ export const useGrooveStore = create((set, get) => ({
3891
3918
  labSessions: sessions,
3892
3919
  labMetrics: {
3893
3920
  ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist,
3894
- memory: chunk.metrics.memoryUsage || s.labMetrics.memory,
3895
- totalTokens: s.labMetrics.totalTokens + (chunk.metrics.totalTokens || tokenCount),
3896
- generationTime: chunk.metrics.generationTime || elapsed,
3921
+ memory: parsed.metrics.memoryUsage || s.labMetrics.memory,
3922
+ totalTokens: s.labMetrics.totalTokens + (parsed.metrics.totalTokens || tokenCount),
3923
+ generationTime: parsed.metrics.generationTime || elapsed,
3897
3924
  },
3898
3925
  };
3899
3926
  });
3900
3927
  }
3901
- if (chunk.type === 'error') {
3902
- throw new Error(chunk.error || 'Inference error');
3928
+ if (parsed.type === 'error') {
3929
+ throw new Error(parsed.error || 'Inference error');
3903
3930
  }
3904
3931
  } catch (e) {
3905
3932
  if (e.message && e.message !== 'Inference error' && !e.message.startsWith('HTTP ')) continue;
@@ -3908,14 +3935,24 @@ export const useGrooveStore = create((set, get) => ({
3908
3935
  }
3909
3936
  }
3910
3937
 
3911
- // Final metrics if no done event came
3912
- if (!get().labSessions.find((s) => s.id === sessionId)?.messages.slice(-1)[0]?.metrics) {
3913
- const elapsed = performance.now() - startTime;
3914
- const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3915
- const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3938
+ // Compute final metrics from client-side timing
3939
+ const elapsed = performance.now() - startTime;
3940
+ const ttft = firstTokenTime ? firstTokenTime - startTime : null;
3941
+ const tps = tokenCount > 0 && elapsed > 0 ? (tokenCount / (elapsed / 1000)) : null;
3942
+ if (tokenCount > 0) {
3916
3943
  set((s) => {
3917
3944
  const tpsHist = [...s.labMetrics.tokensPerSecHistory, tps].slice(-10);
3945
+ const sessions = s.labSessions.map((sess) => {
3946
+ if (sess.id !== sessionId) return sess;
3947
+ const msgs = [...sess.messages];
3948
+ const last = msgs[msgs.length - 1];
3949
+ if (!last?.metrics) {
3950
+ msgs[msgs.length - 1] = { ...last, metrics: { ttft, tokensPerSec: tps, tokens: tokenCount, generationTime: elapsed } };
3951
+ }
3952
+ return { ...sess, messages: msgs };
3953
+ });
3918
3954
  return {
3955
+ labSessions: sessions,
3919
3956
  labMetrics: { ...s.labMetrics, ttft, tokensPerSec: tps, tokensPerSecHistory: tpsHist, totalTokens: s.labMetrics.totalTokens + tokenCount, generationTime: elapsed },
3920
3957
  };
3921
3958
  });
@@ -4040,4 +4077,45 @@ export const useGrooveStore = create((set, get) => ({
4040
4077
  return false;
4041
4078
  }
4042
4079
  },
4080
+
4081
+ // ── Integration Agent Install ────────────────────────────
4082
+
4083
+ async installViaExistingAgent(integration, agentId) {
4084
+ const message = buildIntegrationPrompt(integration);
4085
+ await get().instructAgent(agentId, message);
4086
+ get().setActiveView('agents');
4087
+ get().selectAgent(agentId);
4088
+ },
4089
+
4090
+ async spawnIntegrationTeam(integration) {
4091
+ const team = await get().createTeam(integration.name);
4092
+ const prompt = buildIntegrationPrompt(integration);
4093
+ const agent = await get().spawnAgent({ role: 'planner', prompt, teamId: team.id });
4094
+ get().setActiveView('agents');
4095
+ get().selectAgent(agent.id);
4096
+ return agent;
4097
+ },
4043
4098
  }));
4099
+
4100
+ function buildIntegrationPrompt(integration) {
4101
+ const lines = [
4102
+ `Set up the "${integration.name}" integration for this project.`,
4103
+ '',
4104
+ ];
4105
+ if (integration.description) lines.push(`**Description:** ${integration.description}`);
4106
+ if (integration.npmPackage) lines.push(`**npm package:** ${integration.npmPackage}`);
4107
+ if (integration.authType) lines.push(`**Auth type:** ${integration.authType}`);
4108
+ if (integration.envKeys?.length) {
4109
+ lines.push('', '**Environment keys required:**');
4110
+ for (const k of integration.envKeys) {
4111
+ lines.push(`- \`${k.key}\` — ${k.label}${k.required ? ' (required)' : ''}`);
4112
+ }
4113
+ }
4114
+ if (integration.setupSteps?.length) {
4115
+ lines.push('', '**Setup steps:**');
4116
+ integration.setupSteps.forEach((step, i) => lines.push(`${i + 1}. ${step}`));
4117
+ }
4118
+ if (integration.setupUrl) lines.push(``, `**Setup URL:** ${integration.setupUrl}`);
4119
+ if (integration.agentInstructions) lines.push('', `**Agent instructions:** ${integration.agentInstructions}`);
4120
+ return lines.join('\n');
4121
+ }
@@ -10,7 +10,7 @@ import { RootNode } from '../components/agents/root-node';
10
10
  import { cn } from '../lib/cn';
11
11
  import { Button } from '../components/ui/button';
12
12
  import { Badge } from '../components/ui/badge';
13
- import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, Box, HardDrive, LayoutGrid } from 'lucide-react';
13
+ import { Plus, Users, UserPlus, Zap, X, Check, Rocket, Server, Monitor, Code2, TestTube, Shield, Pencil, Copy, Trash2, ChevronDown, ChevronLeft, ChevronRight, FolderOpen, Eye, Settings2, Search, GripVertical, Cloud, FileText, Database, Megaphone, Calculator, UserCheck, Headphones, BarChart3, Pen, Presentation, Globe, MessageCircle, Save, Layers, LayoutGrid, Activity, Gauge, Cpu } from 'lucide-react';
14
14
  import { PreviewWorkspace } from '../components/preview/preview-workspace';
15
15
  import { WorkspaceMode } from '../components/agents/workspace-mode';
16
16
  import { ContextMenu, ContextMenuTrigger, ContextMenuContent, ContextMenuItem, ContextMenuSeparator } from '../components/ui/context-menu';
@@ -1258,7 +1258,7 @@ function RecommendedTeamCard() {
1258
1258
  const [tsModel, setTsModel] = useState(teamLaunchConfig?.model || '');
1259
1259
  const [tsReasoning, setTsReasoning] = useState(teamLaunchConfig?.reasoningEffort ?? 50);
1260
1260
  const [tsTemp, setTsTemp] = useState(teamLaunchConfig?.temperature ?? 0.5);
1261
- const [tsMode, setTsMode] = useState(teamLaunchConfig?.mode || 'sandbox');
1261
+ const [expandedAgent, setExpandedAgent] = useState(null);
1262
1262
 
1263
1263
  useEffect(() => {
1264
1264
  fetchProviders().then((list) => {
@@ -1283,6 +1283,15 @@ function RecommendedTeamCard() {
1283
1283
  setEditedAgents(next);
1284
1284
  }
1285
1285
 
1286
+ function handleAgentField(i, updates) {
1287
+ if (typeof updates === 'string') {
1288
+ const [field, value] = [updates, arguments[2]];
1289
+ setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, [field]: value } : a));
1290
+ } else {
1291
+ setEditedAgents((prev) => (prev ?? agentEdits).map((a, idx) => idx === i ? { ...a, ...updates } : a));
1292
+ }
1293
+ }
1294
+
1286
1295
  function handleTsProviderChange(id) {
1287
1296
  setTsProvider(id);
1288
1297
  const p = providers.find((x) => x.id === id);
@@ -1298,7 +1307,6 @@ function RecommendedTeamCard() {
1298
1307
  ...(tsProvider && { provider: tsProvider, model: tsModel }),
1299
1308
  reasoningEffort: tsReasoning,
1300
1309
  ...(showTemp && { temperature: tsTemp }),
1301
- mode: tsMode,
1302
1310
  },
1303
1311
  });
1304
1312
  try {
@@ -1376,41 +1384,6 @@ function RecommendedTeamCard() {
1376
1384
  formatValue={(v) => v.toFixed(2)}
1377
1385
  />
1378
1386
  )}
1379
- {/* Build Mode */}
1380
- <div className="space-y-1">
1381
- <label className="text-2xs text-text-3 font-sans">Build Mode</label>
1382
- <div className="flex rounded-md bg-surface-4 border border-border-subtle p-0.5">
1383
- <button
1384
- onClick={() => setTsMode('sandbox')}
1385
- className={cn(
1386
- 'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
1387
- tsMode === 'sandbox'
1388
- ? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
1389
- : 'text-text-3 hover:text-text-1',
1390
- )}
1391
- >
1392
- <Box size={11} />
1393
- Sandbox
1394
- </button>
1395
- <button
1396
- onClick={() => setTsMode('production')}
1397
- className={cn(
1398
- 'flex-1 flex items-center justify-center gap-1.5 rounded px-2 py-1.5 text-xs font-sans transition-all cursor-pointer',
1399
- tsMode === 'production'
1400
- ? 'bg-surface-2 text-text-0 font-semibold shadow-sm'
1401
- : 'text-text-3 hover:text-text-1',
1402
- )}
1403
- >
1404
- <HardDrive size={11} />
1405
- Production
1406
- </button>
1407
- </div>
1408
- <p className="text-2xs text-text-4 font-sans">
1409
- {tsMode === 'sandbox'
1410
- ? 'Files live in a team directory, removable with the team'
1411
- : 'Files live in the project directory, persist forever'}
1412
- </p>
1413
- </div>
1414
1387
  </div>
1415
1388
  )}
1416
1389
  </div>
@@ -1419,31 +1392,116 @@ function RecommendedTeamCard() {
1419
1392
  {agentEdits.map((a, i) => {
1420
1393
  const Icon = ROLE_ICONS[a.role] || Code2;
1421
1394
  const nameValid = !a.name || NAME_RE.test(a.name);
1395
+ const isExpanded = expandedAgent === i;
1396
+ const agentProvider = providers.find((p) => p.id === (a.provider || tsProvider));
1397
+ const agentModels = (agentProvider?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
1422
1398
  return (
1423
- <div key={i} className="flex items-center gap-2 px-2.5 py-1.5 rounded-md bg-surface-4 border border-border-subtle">
1424
- <Icon size={12} className="text-text-2 shrink-0" />
1425
- <input
1426
- type="text"
1427
- value={a.name}
1428
- onChange={(e) => handleNameChange(i, e.target.value)}
1429
- placeholder={a.role}
1430
- className={cn(
1431
- 'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
1432
- !nameValid && 'text-red-400',
1399
+ <div key={i} className="rounded-md bg-surface-4 border border-border-subtle overflow-hidden">
1400
+ <div
1401
+ className="flex items-center gap-2 px-2.5 py-1.5 cursor-pointer hover:bg-surface-5/50 transition-colors"
1402
+ onClick={() => setExpandedAgent(isExpanded ? null : i)}
1403
+ >
1404
+ <Icon size={12} className="text-text-2 shrink-0" />
1405
+ <input
1406
+ type="text"
1407
+ value={a.name}
1408
+ onChange={(e) => handleNameChange(i, e.target.value)}
1409
+ onClick={(e) => e.stopPropagation()}
1410
+ placeholder={a.role}
1411
+ className={cn(
1412
+ 'flex-1 min-w-0 bg-transparent text-xs font-mono text-text-0 outline-none placeholder:text-text-4',
1413
+ !nameValid && 'text-red-400',
1414
+ )}
1415
+ maxLength={64}
1416
+ spellCheck={false}
1417
+ />
1418
+ {a.provider && a.provider !== tsProvider && (
1419
+ <span className="text-2xs text-accent font-mono shrink-0">{a.provider}</span>
1433
1420
  )}
1434
- maxLength={64}
1435
- spellCheck={false}
1436
- />
1437
- {a.scope?.length > 0 && (
1438
- <span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
1439
- {a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
1440
- </span>
1421
+ {a.scope?.length > 0 && (
1422
+ <span className="text-2xs text-text-4 font-mono shrink-0 truncate max-w-[120px]">
1423
+ {a.scope[0]}{a.scope.length > 1 ? ` +${a.scope.length - 1}` : ''}
1424
+ </span>
1425
+ )}
1426
+ <ChevronDown size={10} className={cn('text-text-4 shrink-0 transition-transform duration-200', !isExpanded && '-rotate-90')} />
1427
+ </div>
1428
+ {isExpanded && (
1429
+ <div className="px-2.5 pb-2.5 pt-1 space-y-2.5 border-t border-border-subtle">
1430
+ <div className="flex gap-2">
1431
+ <div className="flex-1 space-y-1">
1432
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Cpu size={10} />Provider</label>
1433
+ <Select value={a.provider || ''} onValueChange={(id) => {
1434
+ const p = providers.find((x) => x.id === id);
1435
+ const pModels = (p?.models || []).filter((m) => m.type !== 'image' && !m.disabled);
1436
+ handleAgentField(i, { provider: id, model: pModels[0]?.id || '' });
1437
+ }}>
1438
+ <SelectTrigger placeholder="Team default" className="bg-surface-3 h-7 text-xs" />
1439
+ <SelectContent>
1440
+ <SelectItem value="">Team default</SelectItem>
1441
+ {providers.map((p) => (
1442
+ <SelectItem key={p.id} value={p.id}>{p.displayName || p.name || p.id}</SelectItem>
1443
+ ))}
1444
+ </SelectContent>
1445
+ </Select>
1446
+ </div>
1447
+ <div className="flex-1 space-y-1">
1448
+ <label className="text-2xs text-text-3 font-sans">Model</label>
1449
+ <Select value={a.model || ''} onValueChange={(v) => handleAgentField(i, 'model', v)}>
1450
+ <SelectTrigger placeholder="Auto" className="bg-surface-3 h-7 text-xs" />
1451
+ <SelectContent>
1452
+ <SelectItem value="">Auto</SelectItem>
1453
+ {agentModels.map((m) => (
1454
+ <SelectItem key={m.id} value={m.id}>{m.name || m.id}</SelectItem>
1455
+ ))}
1456
+ </SelectContent>
1457
+ </Select>
1458
+ </div>
1459
+ </div>
1460
+ <div className="space-y-1">
1461
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Activity size={10} />Model Routing</label>
1462
+ <div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
1463
+ {[{ value: 'fixed', label: 'Fixed' }, { value: 'auto', label: 'Auto' }, { value: 'auto-floor', label: 'Auto + Floor' }].map((opt) => (
1464
+ <button
1465
+ key={opt.value}
1466
+ onClick={() => handleAgentField(i, 'routingMode', opt.value)}
1467
+ className={cn(
1468
+ 'flex-1 px-2 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1469
+ (a.routingMode || 'auto') === opt.value
1470
+ ? 'bg-accent/15 text-accent shadow-sm'
1471
+ : 'text-text-3 hover:text-text-1',
1472
+ )}
1473
+ >
1474
+ {opt.label}
1475
+ </button>
1476
+ ))}
1477
+ </div>
1478
+ </div>
1479
+ <div className="space-y-1">
1480
+ <label className="flex items-center gap-1 text-2xs text-text-3 font-sans"><Gauge size={10} />Effort Level</label>
1481
+ <div className="flex bg-surface-3 rounded-md p-0.5 border border-border-subtle">
1482
+ {[{ value: 'min', label: 'Min' }, { value: 'low', label: 'Low' }, { value: 'default', label: 'Default' }, { value: 'high', label: 'High' }, { value: 'max', label: 'Max' }].map((opt) => (
1483
+ <button
1484
+ key={opt.value}
1485
+ onClick={() => handleAgentField(i, 'effort', opt.value)}
1486
+ className={cn(
1487
+ 'flex-1 px-1.5 py-1 text-2xs font-semibold font-sans rounded transition-all cursor-pointer',
1488
+ (a.effort || 'default') === opt.value
1489
+ ? 'bg-accent/15 text-accent shadow-sm'
1490
+ : 'text-text-3 hover:text-text-1',
1491
+ )}
1492
+ >
1493
+ {opt.label}
1494
+ </button>
1495
+ ))}
1496
+ </div>
1497
+ </div>
1498
+ </div>
1441
1499
  )}
1442
1500
  </div>
1443
1501
  );
1444
1502
  })}
1445
1503
 
1446
- {recommendedTeam.projectDir && tsMode === 'sandbox' && (
1504
+ {recommendedTeam.projectDir && (
1447
1505
  <div className="flex items-center gap-1.5 text-2xs text-text-2 font-mono pt-0.5">
1448
1506
  <span className="text-text-4">Project:</span>
1449
1507
  <span className="text-accent">{recommendedTeam.projectDir}/</span>
@@ -7,15 +7,15 @@ import { Dialog, DialogContent } from '../components/ui/dialog';
7
7
  import { BookOpen, Plus, Search, Trash2, Pencil, ChevronRight, Hash, FolderOpen, Clock, Save, Link2, FileText, Sparkles, HelpCircle } from 'lucide-react';
8
8
 
9
9
  const COMMANDS = [
10
- { cmd: '[save]', args: '#tag', desc: 'Save the current message as a tagged memory' },
11
- { cmd: '[append]', args: '#tag', desc: 'Add to an existing memory without overwriting' },
12
- { cmd: '[update]', args: '#tag', desc: 'Open the editor to modify a memory in place' },
13
- { cmd: '[delete]', args: '#tag', desc: 'Remove a memory permanently' },
14
- { cmd: '[view]', args: '#tag', desc: 'Read a memory in the viewer' },
15
- { cmd: '[read]', args: '#tag1 #tag2 ...', desc: 'Send memory content to the agent — agent reads it, chat stays clean' },
16
- { cmd: '[doc]', args: '#tag', desc: 'AI synthesizes the full conversation into a document' },
17
- { cmd: '[link]', args: '#tag path/to/doc', desc: 'Link a memory to a NORTHSTAR or external document' },
18
- { cmd: '[instruct]', args: '', desc: 'Show this command reference' },
10
+ { cmd: 'save', args: '#tag', desc: 'Save the message and send it to the agent' },
11
+ { cmd: 'append', args: '#tag', desc: 'Add to an existing memory and send to agent' },
12
+ { cmd: 'update', args: '#tag', desc: 'Open the editor to modify a memory in place' },
13
+ { cmd: 'delete', args: '#tag', desc: 'Remove a memory permanently' },
14
+ { cmd: 'view', args: '#tag', desc: 'Read a memory in the viewer' },
15
+ { cmd: 'read', args: '#tag1 #tag2 ...', desc: 'Send memory content to the agent — chat stays clean' },
16
+ { cmd: 'doc', args: '#tag', desc: 'AI synthesizes the full conversation into a document' },
17
+ { cmd: 'link', args: '#tag path/to/doc', desc: 'Link a memory to a NORTHSTAR or external document' },
18
+ { cmd: '[instruct]', args: '', desc: 'Show this command reference' },
19
19
  ];
20
20
 
21
21
  function formatRelative(iso) {
@@ -171,17 +171,12 @@ export default function ModelLabView() {
171
171
  <PanelToggle collapsed={false} onClick={() => setLeftCollapsed(true)} side="left" />
172
172
  </div>
173
173
  <ScrollArea className="flex-1 min-h-0">
174
- <div className="px-4 pb-4 space-y-5">
174
+ <div className="px-4 pb-4 space-y-5 divide-y divide-border-subtle [&>*]:pt-5 [&>*:first-child]:pt-0">
175
175
  <LaunchModel />
176
- <div className="h-px bg-border-subtle" />
177
176
  <RuntimeConfig />
178
- <div className="h-px bg-border-subtle" />
179
177
  <ModelSelector />
180
- <div className="h-px bg-border-subtle" />
181
178
  <ParameterPanel />
182
- <div className="h-px bg-border-subtle" />
183
179
  <PresetManager />
184
- <div className="h-px bg-border-subtle" />
185
180
  <SystemPromptEditor />
186
181
  </div>
187
182
  </ScrollArea>