groove-dev 0.27.131 → 0.27.134

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 (70) hide show
  1. package/AGENT_ORCHESTRATION.md +375 -0
  2. package/moe-training/shared/envelope-schema.js +1 -1
  3. package/node_modules/@groove-dev/cli/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/package.json +1 -1
  5. package/node_modules/@groove-dev/daemon/src/index.js +3 -1
  6. package/node_modules/@groove-dev/daemon/src/introducer.js +48 -4
  7. package/node_modules/@groove-dev/daemon/src/llama-server.js +4 -4
  8. package/node_modules/@groove-dev/daemon/src/model-lab.js +8 -0
  9. package/node_modules/@groove-dev/daemon/src/preview.js +85 -58
  10. package/node_modules/@groove-dev/daemon/src/process.js +9 -0
  11. package/node_modules/@groove-dev/daemon/src/terminal-pty.js +24 -14
  12. package/node_modules/@groove-dev/daemon/src/validate.js +0 -4
  13. package/{packages/gui/dist/assets/codemirror-CFF1Lrnz.js → node_modules/@groove-dev/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
  14. package/node_modules/@groove-dev/gui/dist/assets/index-BgQL4bNl.css +1 -0
  15. package/{packages/gui/dist/assets/index-BiB9oY9U.js → node_modules/@groove-dev/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
  16. package/node_modules/@groove-dev/gui/dist/index.html +3 -3
  17. package/node_modules/@groove-dev/gui/package.json +1 -1
  18. package/node_modules/@groove-dev/gui/src/app.css +6 -6
  19. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +12 -1
  20. package/node_modules/@groove-dev/gui/src/components/agents/agent-feed.jsx +15 -5
  21. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +6 -6
  22. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +11 -9
  23. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +26 -3
  24. package/node_modules/@groove-dev/gui/src/components/editor/file-tree.jsx +6 -6
  25. package/node_modules/@groove-dev/gui/src/components/editor/terminal.jsx +20 -8
  26. package/node_modules/@groove-dev/gui/src/components/lab/chat-playground.jsx +10 -1
  27. package/node_modules/@groove-dev/gui/src/components/lab/lab-assistant.jsx +4 -4
  28. package/node_modules/@groove-dev/gui/src/components/lab/system-prompt-editor.jsx +17 -3
  29. package/node_modules/@groove-dev/gui/src/components/layout/terminal-panel.jsx +2 -4
  30. package/node_modules/@groove-dev/gui/src/components/preview/preview-toolbar.jsx +8 -6
  31. package/node_modules/@groove-dev/gui/src/stores/groove.js +82 -15
  32. package/node_modules/@groove-dev/gui/src/views/agents.jsx +82 -74
  33. package/node_modules/@groove-dev/gui/src/views/editor.jsx +11 -9
  34. package/node_modules/moe-training/shared/envelope-schema.js +1 -1
  35. package/package.json +1 -1
  36. package/packages/cli/package.json +1 -1
  37. package/packages/daemon/package.json +1 -1
  38. package/packages/daemon/src/index.js +3 -1
  39. package/packages/daemon/src/introducer.js +48 -4
  40. package/packages/daemon/src/llama-server.js +4 -4
  41. package/packages/daemon/src/model-lab.js +8 -0
  42. package/packages/daemon/src/preview.js +85 -58
  43. package/packages/daemon/src/process.js +9 -0
  44. package/packages/daemon/src/terminal-pty.js +24 -14
  45. package/packages/daemon/src/validate.js +0 -4
  46. package/{node_modules/@groove-dev/gui/dist/assets/codemirror-CFF1Lrnz.js → packages/gui/dist/assets/codemirror-DRQdprYi.js} +11 -11
  47. package/packages/gui/dist/assets/index-BgQL4bNl.css +1 -0
  48. package/{node_modules/@groove-dev/gui/dist/assets/index-BiB9oY9U.js → packages/gui/dist/assets/index-Dozp69tK.js} +1721 -1721
  49. package/packages/gui/dist/index.html +3 -3
  50. package/packages/gui/package.json +1 -1
  51. package/packages/gui/src/app.css +6 -6
  52. package/packages/gui/src/components/agents/agent-chat.jsx +12 -1
  53. package/packages/gui/src/components/agents/agent-feed.jsx +15 -5
  54. package/packages/gui/src/components/agents/agent-file-tree.jsx +6 -6
  55. package/packages/gui/src/components/agents/workspace-mode.jsx +11 -9
  56. package/packages/gui/src/components/editor/code-editor.jsx +26 -3
  57. package/packages/gui/src/components/editor/file-tree.jsx +6 -6
  58. package/packages/gui/src/components/editor/terminal.jsx +20 -8
  59. package/packages/gui/src/components/lab/chat-playground.jsx +10 -1
  60. package/packages/gui/src/components/lab/lab-assistant.jsx +4 -4
  61. package/packages/gui/src/components/lab/system-prompt-editor.jsx +17 -3
  62. package/packages/gui/src/components/layout/terminal-panel.jsx +2 -4
  63. package/packages/gui/src/components/preview/preview-toolbar.jsx +8 -6
  64. package/packages/gui/src/stores/groove.js +82 -15
  65. package/packages/gui/src/views/agents.jsx +82 -74
  66. package/packages/gui/src/views/editor.jsx +11 -9
  67. package/CENTRAL_COMMAND_REBUILD.md +0 -689
  68. package/MERKLE_TREE_ARCHITECTURE.md +0 -354
  69. package/node_modules/@groove-dev/gui/dist/assets/index-CeyDFVub.css +0 -1
  70. package/packages/gui/dist/assets/index-CeyDFVub.css +0 -1
@@ -70,6 +70,7 @@ export const useGrooveStore = create((set, get) => ({
70
70
  showPreviewInAgents: false,
71
71
  previewChat: [],
72
72
  previewIterating: false,
73
+ teamPreviews: {}, // teamId -> { url, kind, active } — survives team switches
73
74
 
74
75
  // ── Team Launch Config (set during planner spawn, cascades to team) ──
75
76
  teamLaunchConfig: null, // { provider, model, reasoningEffort, temperature, verbosity, mode }
@@ -224,9 +225,9 @@ export const useGrooveStore = create((set, get) => ({
224
225
  labLlamaInstalled: null,
225
226
  labLaunchPhase: null,
226
227
  labLaunchError: null,
227
- labAssistantAgentId: null,
228
+ labAssistantAgentId: localStorage.getItem('groove:labAssistantAgentId') || null,
228
229
  labAssistantMode: false,
229
- labAssistantBackend: null,
230
+ labAssistantBackend: localStorage.getItem('groove:labAssistantBackend') || null,
230
231
 
231
232
  // ── Onboarding ────────────────────────────────────────────
232
233
  onboardingComplete: localStorage.getItem('groove:onboardingComplete') === 'true',
@@ -553,15 +554,20 @@ export const useGrooveStore = create((set, get) => ({
553
554
  get().addToast('info', `QC agent ${msg.name} auto-spawned`, 'Auditing phase 1 work');
554
555
  break;
555
556
 
556
- case 'preview:ready':
557
+ case 'preview:ready': {
558
+ const proxyUrl = msg.teamId ? `/api/preview/${msg.teamId}/proxy/` : msg.url;
559
+ set((s) => ({
560
+ teamPreviews: { ...s.teamPreviews, [msg.teamId]: { url: proxyUrl, kind: msg.kind, active: true } },
561
+ }));
557
562
  get().addToast(
558
563
  'success',
559
564
  'Project ready to preview',
560
565
  msg.url,
561
- { label: 'Open Preview', onClick: () => get().openPreview(msg.url, msg.teamId, msg.kind) },
566
+ { label: 'Open Preview', onClick: () => get().openPreview(proxyUrl, msg.teamId, msg.kind) },
562
567
  { persistent: true },
563
568
  );
564
569
  break;
570
+ }
565
571
 
566
572
  case 'preview:failed': {
567
573
  const failKind = msg.kind || '';
@@ -576,9 +582,11 @@ export const useGrooveStore = create((set, get) => ({
576
582
  }
577
583
 
578
584
  case 'preview:stopped': {
579
- const ps = get().previewState;
580
- if (ps.teamId && ps.teamId === msg.teamId) {
581
- set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false });
585
+ const tp = get().teamPreviews[msg.teamId];
586
+ if (tp) {
587
+ set((s) => ({
588
+ teamPreviews: { ...s.teamPreviews, [msg.teamId]: { ...tp, active: false } },
589
+ }));
582
590
  }
583
591
  break;
584
592
  }
@@ -1235,11 +1243,15 @@ export const useGrooveStore = create((set, get) => ({
1235
1243
  },
1236
1244
 
1237
1245
  switchTeam(id) {
1238
- const { activeTeamId, detailPanel, teamDetailPanels } = get();
1246
+ const { activeTeamId, detailPanel, teamDetailPanels, teamPreviews } = get();
1239
1247
  const updated = { ...teamDetailPanels };
1240
1248
  if (activeTeamId) updated[activeTeamId] = detailPanel;
1241
1249
  const restored = updated[id] || null;
1242
- set({ activeTeamId: id, detailPanel: restored, teamDetailPanels: updated });
1250
+ const tp = teamPreviews[id];
1251
+ const previewUpdate = tp
1252
+ ? { previewState: { url: tp.url, teamId: id, kind: tp.kind, deviceSize: 'desktop', screenshotMode: false } }
1253
+ : {};
1254
+ set({ activeTeamId: id, detailPanel: restored, teamDetailPanels: updated, ...previewUpdate });
1243
1255
  localStorage.setItem('groove:activeTeamId', id);
1244
1256
  },
1245
1257
 
@@ -1421,24 +1433,60 @@ export const useGrooveStore = create((set, get) => ({
1421
1433
  const data = await api.get('/preview');
1422
1434
  const previews = data.previews || [];
1423
1435
  if (previews.length > 0) {
1424
- const p = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
1425
- set({
1426
- previewState: { url: `/api/preview/${p.teamId}/proxy/`, teamId: p.teamId, kind: p.kind, deviceSize: 'desktop', screenshotMode: false },
1436
+ const updates = {};
1437
+ for (const p of previews) {
1438
+ updates[p.teamId] = { url: `/api/preview/${p.teamId}/proxy/`, kind: p.kind, active: true };
1439
+ }
1440
+ const most = previews.sort((a, b) => (b.startedAt || 0) - (a.startedAt || 0))[0];
1441
+ set((s) => ({
1442
+ teamPreviews: { ...s.teamPreviews, ...updates },
1443
+ previewState: { url: `/api/preview/${most.teamId}/proxy/`, teamId: most.teamId, kind: most.kind, deviceSize: 'desktop', screenshotMode: false },
1427
1444
  showPreviewInAgents: true,
1428
- });
1445
+ }));
1429
1446
  }
1430
1447
  } catch {}
1431
1448
  },
1432
1449
 
1433
1450
  openPreview(url, teamId, kind) {
1434
- set({ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], showPreviewInAgents: true });
1451
+ set((s) => ({
1452
+ previewState: { url, teamId, kind, deviceSize: 'desktop', screenshotMode: false },
1453
+ teamPreviews: { ...s.teamPreviews, [teamId]: { url, kind, active: true } },
1454
+ previewChat: [],
1455
+ showPreviewInAgents: true,
1456
+ }));
1435
1457
  },
1436
1458
  closePreview() {
1459
+ set({ showPreviewInAgents: false });
1460
+ },
1461
+ stopPreview() {
1437
1462
  const { previewState } = get();
1438
1463
  if (previewState.teamId) {
1439
1464
  api.delete(`/preview/${previewState.teamId}`).catch(() => {});
1465
+ set((s) => ({
1466
+ teamPreviews: {
1467
+ ...s.teamPreviews,
1468
+ [previewState.teamId]: { ...s.teamPreviews[previewState.teamId], active: false },
1469
+ },
1470
+ showPreviewInAgents: false,
1471
+ }));
1472
+ }
1473
+ },
1474
+ async relaunchPreview(teamId) {
1475
+ try {
1476
+ const result = await api.post(`/preview/${teamId}/launch`);
1477
+ if (result.launched) {
1478
+ const proxyUrl = `/api/preview/${teamId}/proxy/`;
1479
+ set((s) => ({
1480
+ previewState: { url: proxyUrl, teamId, kind: result.kind, deviceSize: 'desktop', screenshotMode: false },
1481
+ teamPreviews: { ...s.teamPreviews, [teamId]: { url: proxyUrl, kind: result.kind, active: true } },
1482
+ showPreviewInAgents: true,
1483
+ }));
1484
+ } else {
1485
+ get().addToast('warning', 'Preview could not launch', result.reason ? String(result.reason).slice(0, 200) : 'Build or server failed');
1486
+ }
1487
+ } catch (err) {
1488
+ get().addToast('error', 'Failed to launch preview', err.message);
1440
1489
  }
1441
- set({ previewState: { url: null, teamId: null, kind: null, deviceSize: 'desktop', screenshotMode: false }, previewChat: [], previewIterating: false, showPreviewInAgents: false, activeView: 'agents' });
1442
1490
  },
1443
1491
  togglePreviewInAgents() {
1444
1492
  set((s) => ({ showPreviewInAgents: !s.showPreviewInAgents }));
@@ -3500,6 +3548,7 @@ export const useGrooveStore = create((set, get) => ({
3500
3548
  const decoder = new TextDecoder();
3501
3549
  let buffer = '';
3502
3550
  let fullContent = '';
3551
+ let fullReasoning = '';
3503
3552
 
3504
3553
  while (true) {
3505
3554
  const { done, value } = await reader.read();
@@ -3514,6 +3563,20 @@ export const useGrooveStore = create((set, get) => ({
3514
3563
  if (payload === '[DONE]') continue;
3515
3564
  try {
3516
3565
  const chunk = JSON.parse(payload);
3566
+ if (chunk.type === 'reasoning' && chunk.content) {
3567
+ if (!firstTokenTime) firstTokenTime = performance.now();
3568
+ tokenCount++;
3569
+ fullReasoning += chunk.content;
3570
+ set((s) => {
3571
+ const sessions = s.labSessions.map((sess) => {
3572
+ if (sess.id !== sessionId) return sess;
3573
+ const msgs = [...sess.messages];
3574
+ msgs[msgs.length - 1] = { ...msgs[msgs.length - 1], reasoning: fullReasoning };
3575
+ return { ...sess, messages: msgs };
3576
+ });
3577
+ return { labSessions: sessions };
3578
+ });
3579
+ }
3517
3580
  if (chunk.type === 'token' && chunk.content) {
3518
3581
  if (!firstTokenTime) firstTokenTime = performance.now();
3519
3582
  tokenCount++;
@@ -3640,6 +3703,8 @@ export const useGrooveStore = create((set, get) => ({
3640
3703
  }
3641
3704
  try {
3642
3705
  const data = await api.post('/lab/assistant', { backend });
3706
+ localStorage.setItem('groove:labAssistantAgentId', data.agentId);
3707
+ localStorage.setItem('groove:labAssistantBackend', backend);
3643
3708
  set({ labAssistantAgentId: data.agentId, labAssistantMode: true, labAssistantBackend: backend });
3644
3709
  get().addToast('info', `Lab Assistant started for ${backend}`);
3645
3710
  } catch (err) {
@@ -3654,6 +3719,8 @@ export const useGrooveStore = create((set, get) => ({
3654
3719
  clearLabAssistant() {
3655
3720
  const id = get().labAssistantAgentId;
3656
3721
  if (id) api.delete(`/agents/${encodeURIComponent(id)}`).catch(() => {});
3722
+ localStorage.removeItem('groove:labAssistantAgentId');
3723
+ localStorage.removeItem('groove:labAssistantBackend');
3657
3724
  set({ labAssistantAgentId: null, labAssistantMode: false, labAssistantBackend: null });
3658
3725
  },
3659
3726
 
@@ -10,12 +10,11 @@ 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, Archive, Box, HardDrive, LayoutGrid, ArrowUpCircle } 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, Box, HardDrive, LayoutGrid } 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';
17
17
  import { Dialog, DialogContent } from '../components/ui/dialog';
18
- import { TeamRemovalDialog } from '../components/teams/team-removal-dialog';
19
18
  import { Select, SelectTrigger, SelectContent, SelectItem } from '../components/ui/select';
20
19
  import { ScrollArea } from '../components/ui/scroll-area';
21
20
  import { Tooltip } from '../components/ui/tooltip';
@@ -97,11 +96,9 @@ export function TeamTabBar() {
97
96
  const agents = useGrooveStore((s) => s.agents);
98
97
  const switchTeam = useGrooveStore((s) => s.switchTeam);
99
98
  const createTeam = useGrooveStore((s) => s.createTeam);
100
- const archiveTeam = useGrooveStore((s) => s.archiveTeam);
101
99
  const deleteTeamPermanently = useGrooveStore((s) => s.deleteTeamPermanently);
102
100
  const renameTeam = useGrooveStore((s) => s.renameTeam);
103
101
  const cloneTeam = useGrooveStore((s) => s.cloneTeam);
104
- const promoteTeam = useGrooveStore((s) => s.promoteTeam);
105
102
  const reorderTeams = useGrooveStore((s) => s.reorderTeams);
106
103
  const addToast = useGrooveStore((s) => s.addToast);
107
104
 
@@ -109,7 +106,6 @@ export function TeamTabBar() {
109
106
  const [newName, setNewName] = useState('');
110
107
  const [renamingId, setRenamingId] = useState(null);
111
108
  const [renameValue, setRenameValue] = useState('');
112
- const [archiveConfirm, setArchiveConfirm] = useState(null);
113
109
  const submitting = useRef(false);
114
110
  const [dragId, setDragId] = useState(null);
115
111
  const [dragOverId, setDragOverId] = useState(null);
@@ -263,11 +259,6 @@ export function TeamTabBar() {
263
259
  <ContextMenuItem onSelect={() => cloneTeam(team.id)}>
264
260
  <Copy size={12} /> Clone
265
261
  </ContextMenuItem>
266
- {team.mode !== 'production' && (
267
- <ContextMenuItem onSelect={() => promoteTeam(team.id)}>
268
- <ArrowUpCircle size={12} /> Promote to Production
269
- </ContextMenuItem>
270
- )}
271
262
  <ContextMenuSeparator />
272
263
  <ContextMenuItem danger onSelect={() => {
273
264
  const teamAgents = agents.filter((a) => a.teamId === team.id);
@@ -275,9 +266,9 @@ export function TeamTabBar() {
275
266
  addToast('error', 'Stop running agents first');
276
267
  return;
277
268
  }
278
- setArchiveConfirm(team);
269
+ deleteTeamPermanently(team.id);
279
270
  }}>
280
- <Trash2 size={12} /> {team.isDefault ? 'Wipe' : 'Archive'}
271
+ <Trash2 size={12} /> Delete
281
272
  </ContextMenuItem>
282
273
  </ContextMenuContent>
283
274
  </ContextMenu>
@@ -325,14 +316,6 @@ export function TeamTabBar() {
325
316
  </button>
326
317
  )}
327
318
 
328
- <TeamRemovalDialog
329
- team={archiveConfirm}
330
- open={!!archiveConfirm}
331
- onOpenChange={(open) => !open && setArchiveConfirm(null)}
332
- onArchive={archiveTeam}
333
- onDeletePermanently={deleteTeamPermanently}
334
- mode={archiveConfirm?.mode || 'sandbox'}
335
- />
336
319
  </div>
337
320
  );
338
321
  }
@@ -1500,6 +1483,9 @@ export default function AgentsView() {
1500
1483
  const showPreviewInAgents = useGrooveStore((s) => s.showPreviewInAgents);
1501
1484
  const previewState = useGrooveStore((s) => s.previewState);
1502
1485
  const togglePreviewInAgents = useGrooveStore((s) => s.togglePreviewInAgents);
1486
+ const teamPreviews = useGrooveStore((s) => s.teamPreviews);
1487
+ const relaunchPreview = useGrooveStore((s) => s.relaunchPreview);
1488
+ const openPreview = useGrooveStore((s) => s.openPreview);
1503
1489
  const workspaceMode = useGrooveStore((s) => s.workspaceMode);
1504
1490
  const setWorkspaceMode = useGrooveStore((s) => s.setWorkspaceMode);
1505
1491
  const openTeamBuilder = useGrooveStore((s) => s.openTeamBuilder);
@@ -1569,7 +1555,7 @@ export default function AgentsView() {
1569
1555
  <EmptyState onPlanner={openPlannerConfig} onSpawn={() => openDetail({ type: 'spawn' })} onTeamBuilder={openTeamBuilder} />
1570
1556
  ) : workspaceMode ? (
1571
1557
  <WorkspaceMode />
1572
- ) : showPreviewInAgents && previewState.url && previewState.teamId === activeTeamId ? (
1558
+ ) : showPreviewInAgents && previewState.url && previewState.teamId === activeTeamId && teamPreviews[activeTeamId]?.active ? (
1573
1559
  <PreviewWorkspace embedded />
1574
1560
  ) : (
1575
1561
  <ReactFlowProvider key={activeTeamId}>
@@ -1579,59 +1565,81 @@ export default function AgentsView() {
1579
1565
  </div>
1580
1566
  {!workspaceMode && <RecommendedTeamCard />}
1581
1567
  {!isLoading && teamAgents.length > 0 && !workspaceMode && (
1582
- <button
1583
- onClick={() => openDetail({ type: 'spawn' })}
1584
- className="absolute bottom-4 left-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
1585
- >
1586
- <Plus size={14} />
1587
- Spawn
1588
- </button>
1589
- )}
1590
- {!isLoading && teamAgents.length > 0 && !workspaceMode && (
1591
- <button
1592
- onClick={() => {
1593
- const positions = loadPositions(activeTeamId);
1594
- const layout = {};
1595
- const roleCounts = new Map();
1596
- [...teamAgents].sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)).forEach((agent) => {
1597
- const key = agent.name || agent.id;
1598
- const pos = positions[key];
1599
- if (!pos) return;
1600
- const role = agent.role || 'agent';
1601
- const count = roleCounts.get(role) || 0;
1602
- roleCounts.set(role, count + 1);
1603
- const roleKey = count === 0 ? role : `${role}-${count}`;
1604
- layout[roleKey] = pos;
1605
- });
1606
- if (positions[ROOT_ID]) layout[ROOT_ID] = positions[ROOT_ID];
1607
- saveRoleLayout(layout);
1608
- addToast('success', 'Layout saved', 'Future spawns will use these positions');
1609
- }}
1610
- className="absolute bottom-4 left-28 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-accent/15 text-accent text-xs font-semibold font-sans hover:bg-accent/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
1611
- >
1612
- <LayoutGrid size={14} />
1613
- {Object.keys(loadRoleLayout()).length > 0 ? 'Update Layout' : 'Save Layout'}
1614
- </button>
1615
- )}
1616
- {!isLoading && teamAgents.length > 0 && !workspaceMode && (
1617
- <button
1618
- onClick={() => setWorkspaceMode(true)}
1619
- className={cn(
1620
- 'absolute bottom-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md text-xs font-semibold font-sans transition-colors cursor-pointer select-none shadow-lg shadow-black/10',
1621
- previewState.url && previewState.teamId === activeTeamId ? 'right-32' : 'right-4',
1622
- 'bg-accent/15 text-accent hover:bg-accent/25',
1623
- )}
1624
- >
1625
- <Code2 size={14} /> Workspace
1626
- </button>
1627
- )}
1628
- {!isLoading && teamAgents.length > 0 && !workspaceMode && previewState.url && previewState.teamId === activeTeamId && (
1629
- <button
1630
- onClick={togglePreviewInAgents}
1631
- className="absolute bottom-4 right-4 z-40 flex items-center gap-1.5 h-8 px-4 rounded-md bg-info/15 text-info text-xs font-semibold font-sans hover:bg-info/25 transition-colors cursor-pointer select-none shadow-lg shadow-black/10"
1632
- >
1633
- {showPreviewInAgents ? <><Users size={14} /> Team</> : <><Eye size={14} /> Preview</>}
1634
- </button>
1568
+ <div className="absolute bottom-3 left-3 z-40 flex items-center gap-1">
1569
+ <Tooltip content="Spawn agent" side="top">
1570
+ <button
1571
+ onClick={() => openDetail({ type: 'spawn' })}
1572
+ className="flex items-center justify-center w-7 h-7 rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
1573
+ >
1574
+ <Plus size={15} />
1575
+ </button>
1576
+ </Tooltip>
1577
+ <Tooltip content={Object.keys(loadRoleLayout()).length > 0 ? 'Update layout' : 'Save layout'} side="top">
1578
+ <button
1579
+ onClick={() => {
1580
+ const positions = loadPositions(activeTeamId);
1581
+ const layout = {};
1582
+ const roleCounts = new Map();
1583
+ [...teamAgents].sort((a, b) => (a.name || a.id).localeCompare(b.name || b.id)).forEach((agent) => {
1584
+ const key = agent.name || agent.id;
1585
+ const pos = positions[key];
1586
+ if (!pos) return;
1587
+ const role = agent.role || 'agent';
1588
+ const count = roleCounts.get(role) || 0;
1589
+ roleCounts.set(role, count + 1);
1590
+ const roleKey = count === 0 ? role : `${role}-${count}`;
1591
+ layout[roleKey] = pos;
1592
+ });
1593
+ if (positions[ROOT_ID]) layout[ROOT_ID] = positions[ROOT_ID];
1594
+ saveRoleLayout(layout);
1595
+ addToast('success', 'Layout saved', 'Future spawns will use these positions');
1596
+ }}
1597
+ className="flex items-center justify-center w-7 h-7 rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
1598
+ >
1599
+ <LayoutGrid size={15} />
1600
+ </button>
1601
+ </Tooltip>
1602
+ <Tooltip content="Workspace" side="top">
1603
+ <button
1604
+ onClick={() => setWorkspaceMode(true)}
1605
+ className="flex items-center justify-center w-7 h-7 rounded-md text-text-3 hover:text-accent hover:bg-accent/10 transition-colors cursor-pointer"
1606
+ >
1607
+ <Code2 size={15} />
1608
+ </button>
1609
+ </Tooltip>
1610
+ {(() => {
1611
+ const tp = teamPreviews[activeTeamId];
1612
+ if (!tp) return null;
1613
+ const isActive = tp.active && previewState.url && previewState.teamId === activeTeamId;
1614
+ const isShowing = isActive && showPreviewInAgents;
1615
+ const tooltip = isShowing ? 'Show team' : isActive ? 'Show preview' : 'Relaunch preview';
1616
+ return (
1617
+ <Tooltip content={tooltip} side="top">
1618
+ <button
1619
+ onClick={() => {
1620
+ if (isActive) {
1621
+ if (showPreviewInAgents) {
1622
+ togglePreviewInAgents();
1623
+ } else {
1624
+ openPreview(tp.url, activeTeamId, tp.kind);
1625
+ }
1626
+ } else {
1627
+ relaunchPreview(activeTeamId);
1628
+ }
1629
+ }}
1630
+ className={cn(
1631
+ 'flex items-center justify-center w-7 h-7 rounded-md transition-colors cursor-pointer',
1632
+ isActive
1633
+ ? 'text-text-3 hover:text-info hover:bg-info/10'
1634
+ : 'text-text-4 hover:text-warning hover:bg-warning/10',
1635
+ )}
1636
+ >
1637
+ {isShowing ? <Users size={15} /> : <Eye size={15} />}
1638
+ </button>
1639
+ </Tooltip>
1640
+ );
1641
+ })()}
1642
+ </div>
1635
1643
  )}
1636
1644
  <PlannerConfigDialog open={plannerConfigOpen} onOpenChange={setPlannerConfigOpen} onLaunch={handlePlannerLaunch} />
1637
1645
  <TeamBuilder />
@@ -117,17 +117,19 @@ export default function EditorView() {
117
117
  />
118
118
  </div>
119
119
 
120
+ {/* Sidebar expand rail */}
121
+ {sidebarCollapsed && (
122
+ <button
123
+ onClick={() => setSidebarCollapsed(false)}
124
+ className="flex-shrink-0 w-6 flex items-center justify-center border-r border-border bg-surface-2 text-text-4 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
125
+ title="Show sidebar"
126
+ >
127
+ <PanelLeftOpen size={14} />
128
+ </button>
129
+ )}
130
+
120
131
  {/* Editor area */}
121
132
  <div className="flex-1 flex flex-col min-w-0 bg-surface-1">
122
- {sidebarCollapsed && (
123
- <button
124
- onClick={() => setSidebarCollapsed(false)}
125
- className="absolute top-2 left-2 z-10 w-7 h-7 flex items-center justify-center rounded-md text-text-3 hover:text-text-0 hover:bg-surface-3 transition-colors cursor-pointer"
126
- title="Show sidebar"
127
- >
128
- <PanelLeftOpen size={15} />
129
- </button>
130
- )}
131
133
 
132
134
  {/* Tab bar */}
133
135
  <EditorTabs />
@@ -2,7 +2,7 @@
2
2
 
3
3
  import { SUPPORTED_PROVIDERS, MODEL_TIERS, TRAINING_EXCLUSION_REASONS } from './constants.js';
4
4
 
5
- export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination', 'edit', 'instruction', 'clarification', 'approval'];
5
+ export const STEP_TYPES = ['thought', 'action', 'observation', 'correction', 'resolution', 'error', 'coordination', 'edit', 'instruction', 'clarification', 'approval', 'delegate', 'yield'];
6
6
  const VALID_QUALITY_TIERS = ['TIER_A', 'TIER_B', 'TIER_C'];
7
7
  const VALID_FEEDBACK_SIGNALS = ['accepted', 'modified', 'rejected', 'iterated'];
8
8
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "groove-dev",
3
- "version": "0.27.131",
3
+ "version": "0.27.134",
4
4
  "description": "Open-source agent orchestration layer — the AI company OS. Local model agent engine (GGUF/Ollama/llama-server), HuggingFace model browser, MCP integrations (Slack, Gmail, Stripe, 15+), agent scheduling (cron), business roles (CMO, CFO, EA). GUI dashboard, multi-agent coordination, zero cold-start, infinite sessions. Works with Claude Code, Codex, Gemini CLI, Ollama, any local model.",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "author": "Groove Dev <hello@groovedev.ai> (https://groovedev.ai)",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/cli",
3
- "version": "0.27.131",
3
+ "version": "0.27.134",
4
4
  "description": "GROOVE CLI — manage AI coding agents from your terminal",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/daemon",
3
- "version": "0.27.131",
3
+ "version": "0.27.134",
4
4
  "description": "GROOVE daemon — agent orchestration engine",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -373,7 +373,9 @@ export class Daemon {
373
373
  if (msg.rows !== undefined && (typeof msg.rows !== 'number' || msg.rows < 1 || msg.rows > 200)) break;
374
374
  try {
375
375
  const id = this.terminalManager.spawn(ws, { cwd: msg.cwd, cols: msg.cols, rows: msg.rows });
376
- ws.send(JSON.stringify({ type: 'terminal:spawned', id }));
376
+ const spawned = { type: 'terminal:spawned', id };
377
+ if (msg.requestId) spawned.requestId = msg.requestId;
378
+ ws.send(JSON.stringify(spawned));
377
379
  } catch (err) {
378
380
  console.error('[terminal] spawn error:', err);
379
381
  ws.send(JSON.stringify({ type: 'terminal:error', message: err.message }));
@@ -1,8 +1,8 @@
1
1
  // GROOVE — Introduction Protocol
2
2
  // FSL-1.1-Apache-2.0 — see LICENSE
3
3
 
4
- import { writeFileSync, readFileSync, existsSync } from 'fs';
5
- import { resolve } from 'path';
4
+ import { writeFileSync, readFileSync, existsSync, readdirSync, statSync } from 'fs';
5
+ import { resolve, dirname, basename } from 'path';
6
6
  import { escapeMd } from './validate.js';
7
7
 
8
8
  const GROOVE_SECTION_START = '<!-- GROOVE:START -->';
@@ -28,7 +28,50 @@ export class Introducer {
28
28
  ];
29
29
 
30
30
  if (newAgent.workingDir) {
31
- lines.push(`Your working directory: \`${newAgent.workingDir}\` — you are spawned inside this subdirectory. Stay within it unless coordination requires otherwise.`);
31
+ lines.push(`Your working directory: \`${newAgent.workingDir}\` — this is the team orchestration directory (.groove/, coordination files). Do NOT create source code or project files here.`);
32
+
33
+ // Inject parent directory context so agents know the root layout
34
+ const parentDir = dirname(newAgent.workingDir);
35
+ const teamDirName = basename(newAgent.workingDir);
36
+ lines.push(`Your project root: \`${parentDir}\` — all source code, features, and builds go here (one level up from team dir).`);
37
+ lines.push('');
38
+ lines.push('## Project Root Structure');
39
+ lines.push('');
40
+ lines.push(`Team dir: \`${teamDirName}/\` (orchestration only — do NOT build here)`);
41
+ lines.push(`Project root: \`${parentDir}\``);
42
+ lines.push('');
43
+ try {
44
+ const entries = readdirSync(parentDir, { withFileTypes: true });
45
+ const dirs = [];
46
+ const files = [];
47
+ for (const entry of entries) {
48
+ if (entry.name.startsWith('.') || entry.name === 'node_modules') continue;
49
+ if (entry.name === teamDirName) continue;
50
+ if (entry.isDirectory()) {
51
+ dirs.push(entry.name + '/');
52
+ } else {
53
+ files.push(entry.name);
54
+ }
55
+ }
56
+ if (dirs.length > 0) {
57
+ lines.push('Directories:');
58
+ for (const d of dirs.slice(0, 30)) {
59
+ lines.push(` ${d}`);
60
+ }
61
+ if (dirs.length > 30) lines.push(` (+${dirs.length - 30} more)`);
62
+ }
63
+ if (files.length > 0) {
64
+ lines.push('Files:');
65
+ for (const f of files.slice(0, 20)) {
66
+ lines.push(` ${f}`);
67
+ }
68
+ if (files.length > 20) lines.push(` (+${files.length - 20} more)`);
69
+ }
70
+ lines.push('');
71
+ lines.push('When creating or modifying project files, use "../" paths relative to the team dir (e.g., "../demo/src/app.js"). The team directory is ephemeral and may be deleted — never put project work inside it.');
72
+ } catch {
73
+ // Parent dir not readable — skip
74
+ }
32
75
  }
33
76
 
34
77
  if (newAgent.scope && newAgent.scope.length > 0) {
@@ -185,7 +228,8 @@ export class Introducer {
185
228
  lines.push('');
186
229
  lines.push(`CRITICAL: NEVER delete files you did not create in this session. Do NOT remove files from other projects, previous work, or unrelated directories.`);
187
230
  if (newAgent.workingDir) {
188
- lines.push(`Your working directory is \`${newAgent.workingDir}\`. Stay inside it. Do NOT modify or delete files outside this directory.`);
231
+ const parentDir = dirname(newAgent.workingDir);
232
+ lines.push(`Your team directory is \`${newAgent.workingDir}\` (orchestration only). Build all project files in the project root: \`${parentDir}\`.`);
189
233
  }
190
234
  lines.push(`If you see files that seem unrelated to your task, leave them alone — they belong to another project or agent.`);
191
235
 
@@ -42,7 +42,7 @@ export class LlamaServerManager {
42
42
  const server = this.servers.get(modelPath);
43
43
  server.users++;
44
44
  server.lastUsed = Date.now();
45
- return `http://127.0.0.1:${server.port}/v1`;
45
+ return `http://localhost:${server.port}`;
46
46
  }
47
47
 
48
48
  // Check capacity
@@ -120,7 +120,7 @@ export class LlamaServerManager {
120
120
  data: { modelPath, port },
121
121
  });
122
122
 
123
- return `http://127.0.0.1:${port}/v1`;
123
+ return `http://localhost:${port}`;
124
124
  } catch (err) {
125
125
  // Server failed to start
126
126
  await this.stopServer(modelPath);
@@ -187,7 +187,7 @@ export class LlamaServerManager {
187
187
  const start = Date.now();
188
188
  while (Date.now() - start < HEALTH_TIMEOUT) {
189
189
  try {
190
- const res = await fetch(`http://127.0.0.1:${port}/health`, {
190
+ const res = await fetch(`http://localhost:${port}/health`, {
191
191
  signal: AbortSignal.timeout(2000),
192
192
  });
193
193
  if (res.ok) {
@@ -209,7 +209,7 @@ export class LlamaServerManager {
209
209
  if (!server) return { running: false };
210
210
 
211
211
  try {
212
- const res = await fetch(`http://127.0.0.1:${server.port}/health`, {
212
+ const res = await fetch(`http://localhost:${server.port}/health`, {
213
213
  signal: AbortSignal.timeout(3000),
214
214
  });
215
215
  const data = await res.json().catch(() => ({}));
@@ -274,6 +274,14 @@ export class ModelLab {
274
274
  try {
275
275
  const chunk = JSON.parse(payload);
276
276
  const delta = chunk.choices?.[0]?.delta;
277
+ if (delta?.reasoning_content) {
278
+ if (ttft === null) {
279
+ ttft = Date.now() - requestStart;
280
+ generationStart = Date.now();
281
+ }
282
+ completionTokens++;
283
+ yield { type: 'reasoning', content: delta.reasoning_content };
284
+ }
277
285
  if (delta?.content) {
278
286
  if (ttft === null) {
279
287
  ttft = Date.now() - requestStart;