groove-dev 0.27.89 → 0.27.92

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 (74) hide show
  1. package/moe-training/client/parsers/claude-code.js +0 -2
  2. package/moe-training/client/session-attestation.js +2 -1
  3. package/moe-training/client/trajectory-capture.js +6 -0
  4. package/moe-training/test/client/parsers/claude-code.test.js +2 -2
  5. package/node_modules/@groove-dev/cli/package.json +1 -1
  6. package/node_modules/@groove-dev/daemon/package.json +1 -1
  7. package/node_modules/@groove-dev/daemon/src/api.js +244 -12
  8. package/node_modules/@groove-dev/daemon/src/conversations.js +32 -6
  9. package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
  10. package/node_modules/@groove-dev/daemon/src/process.js +5 -1
  11. package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
  12. package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +9 -1
  13. package/node_modules/@groove-dev/daemon/src/providers/codex.js +34 -5
  14. package/node_modules/@groove-dev/daemon/src/providers/gemini.js +15 -2
  15. package/node_modules/@groove-dev/daemon/src/providers/grok.js +10 -3
  16. package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
  17. package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
  18. package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
  19. package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  20. package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  21. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  22. package/node_modules/@groove-dev/gui/package.json +1 -1
  23. package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
  24. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
  25. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
  26. package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
  27. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
  28. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +16 -8
  29. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
  30. package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
  31. package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
  32. package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
  33. package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
  34. package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
  35. package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
  36. package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
  37. package/package.json +1 -1
  38. package/packages/cli/package.json +1 -1
  39. package/packages/daemon/package.json +1 -1
  40. package/packages/daemon/src/api.js +244 -12
  41. package/packages/daemon/src/conversations.js +32 -6
  42. package/packages/daemon/src/introducer.js +42 -0
  43. package/packages/daemon/src/process.js +5 -1
  44. package/packages/daemon/src/providers/base.js +4 -0
  45. package/packages/daemon/src/providers/claude-code.js +9 -1
  46. package/packages/daemon/src/providers/codex.js +34 -5
  47. package/packages/daemon/src/providers/gemini.js +15 -2
  48. package/packages/daemon/src/providers/grok.js +10 -3
  49. package/packages/daemon/src/providers/local.js +8 -1
  50. package/packages/daemon/src/tunnel-manager.js +74 -5
  51. package/packages/daemon/src/validate.js +22 -1
  52. package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
  53. package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
  54. package/packages/gui/dist/index.html +2 -2
  55. package/packages/gui/package.json +1 -1
  56. package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
  57. package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
  58. package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
  59. package/packages/gui/src/components/chat/chat-header.jsx +3 -30
  60. package/packages/gui/src/components/chat/chat-input.jsx +163 -153
  61. package/packages/gui/src/components/chat/chat-view.jsx +16 -8
  62. package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
  63. package/packages/gui/src/components/editor/code-editor.jsx +29 -23
  64. package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
  65. package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
  66. package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
  67. package/packages/gui/src/components/ui/slider.jsx +50 -0
  68. package/packages/gui/src/stores/groove.js +145 -9
  69. package/packages/gui/src/views/agents.jsx +707 -14
  70. package/workspace.png +0 -0
  71. package/node_modules/@groove-dev/gui/dist/assets/index-BKD8JAsV.js +0 -8642
  72. package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
  73. package/packages/gui/dist/assets/index-BKD8JAsV.js +0 -8642
  74. package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
@@ -6,12 +6,12 @@
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
7
  <link rel="icon" type="image/png" href="/favicon.png" />
8
8
  <title>Groove GUI</title>
9
- <script type="module" crossorigin src="/assets/index-BKD8JAsV.js"></script>
9
+ <script type="module" crossorigin src="/assets/index-DWv32qyJ.js"></script>
10
10
  <link rel="modulepreload" crossorigin href="/assets/vendor-C0HXlhrU.js">
11
11
  <link rel="modulepreload" crossorigin href="/assets/reactflow-BQPfi37R.js">
12
12
  <link rel="modulepreload" crossorigin href="/assets/codemirror-BBL3i_JW.js">
13
13
  <link rel="modulepreload" crossorigin href="/assets/xterm--7_ns2zW.js">
14
- <link rel="stylesheet" crossorigin href="/assets/index-D4vJ_1ET.css">
14
+ <link rel="stylesheet" crossorigin href="/assets/index-Bo6AeNmM.css">
15
15
  </head>
16
16
  <body>
17
17
  <div id="root"></div>
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@groove-dev/gui",
3
- "version": "0.27.89",
3
+ "version": "0.27.92",
4
4
  "description": "GROOVE GUI — visual agent control plane",
5
5
  "license": "FSL-1.1-Apache-2.0",
6
6
  "type": "module",
@@ -3,7 +3,6 @@ import { useState, useRef, useEffect } from 'react';
3
3
  import { Send, Loader2, MessageSquare, HelpCircle, ArrowRight, Paperclip, Square } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { cn } from '../../lib/cn';
6
- import { Avatar } from '../ui/avatar';
7
6
  import { ThinkingIndicator } from '../ui/thinking-indicator';
8
7
  import { timeAgo } from '../../lib/format';
9
8
 
@@ -45,14 +44,14 @@ function UserMessage({ msg }) {
45
44
  </div>
46
45
  )}
47
46
  <div className={cn(
48
- 'px-3.5 py-2.5 rounded-2xl rounded-br-md',
47
+ 'px-3 py-2 rounded-xl rounded-br-sm',
49
48
  msg.isQuery ? 'bg-info/10 border border-info/15' : 'bg-accent/10 border border-accent/15',
50
49
  )}>
51
- <p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">
50
+ <p className="text-xs text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">
52
51
  <FormattedText text={msg.text} />
53
52
  </p>
54
53
  </div>
55
- <div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
54
+ <div className="text-2xs text-text-4 font-sans mt-0.5 text-right">{timeAgo(msg.timestamp)}</div>
56
55
  </div>
57
56
  </div>
58
57
  );
@@ -60,17 +59,14 @@ function UserMessage({ msg }) {
60
59
 
61
60
  function AgentMessage({ msg, agent }) {
62
61
  return (
63
- <div className="flex gap-2.5">
64
- <Avatar name={agent?.name} role={agent?.role} size="sm" className="mt-1 flex-shrink-0" />
65
- <div className="max-w-[85%]">
66
- <div className="text-2xs text-text-3 font-sans mb-1 font-medium">{agent?.name}</div>
67
- <div className="px-3.5 py-2.5 rounded-2xl rounded-bl-md bg-surface-4 border border-border-subtle">
68
- <div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
69
- <FormattedText text={msg.text} />
70
- </div>
62
+ <div>
63
+ <div className="text-2xs text-text-3 font-sans mb-0.5 font-medium">{agent?.name}</div>
64
+ <div className="border-l-2 border-accent/40 pl-3 py-0.5">
65
+ <div className="text-xs text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
66
+ <FormattedText text={msg.text} />
71
67
  </div>
72
- <div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
73
68
  </div>
69
+ <div className="text-2xs text-text-4 font-sans mt-0.5">{timeAgo(msg.timestamp)}</div>
74
70
  </div>
75
71
  );
76
72
  }
@@ -86,16 +82,13 @@ function SystemMessage({ msg }) {
86
82
  );
87
83
  }
88
84
 
89
- function TypingIndicator({ name }) {
85
+ function TypingIndicator() {
90
86
  return (
91
- <div className="flex gap-2.5">
92
- <div className="w-6 h-6" />
93
- <div className="px-3.5 py-2.5 rounded-2xl rounded-bl-md bg-surface-4 border border-border-subtle">
94
- <div className="flex items-center gap-1">
95
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '0ms' }} />
96
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '150ms' }} />
97
- <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '300ms' }} />
98
- </div>
87
+ <div className="border-l-2 border-accent/40 pl-3 py-2">
88
+ <div className="flex items-center gap-1">
89
+ <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '0ms' }} />
90
+ <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '150ms' }} />
91
+ <span className="w-1.5 h-1.5 rounded-full bg-text-3 animate-pulse" style={{ animationDelay: '300ms' }} />
99
92
  </div>
100
93
  </div>
101
94
  );
@@ -166,7 +159,7 @@ export function AgentChat({ agent }) {
166
159
  return (
167
160
  <div className="flex flex-col h-full">
168
161
  {/* Messages */}
169
- <div ref={scrollRef} className="flex-1 overflow-y-auto px-4 py-4 space-y-4">
162
+ <div ref={scrollRef} className="flex-1 overflow-y-auto px-3 py-3 space-y-3">
170
163
  {messages.length === 0 && (
171
164
  <div className="flex flex-col items-center justify-center h-full text-center py-12">
172
165
  <div className="w-12 h-12 rounded-full bg-accent/8 flex items-center justify-center mb-4">
@@ -191,19 +184,8 @@ export function AgentChat({ agent }) {
191
184
  </div>
192
185
 
193
186
  {/* ── Input area ──────────────────────────────────── */}
194
- <div className="border-t border-border-subtle px-4 py-3 bg-surface-1">
195
- {/* Mode indicator */}
196
- <div className="flex items-center gap-2 mb-2">
197
- <span className="text-2xs font-semibold font-sans px-2 py-0.5 rounded-full bg-accent/12 text-accent">
198
- {isAlive ? 'Instruct' : 'Continue'}
199
- </span>
200
- <span className="text-2xs text-text-4 font-sans">
201
- {isAlive ? 'Message goes directly to this agent' : 'Resumes with full context'}
202
- </span>
203
- </div>
204
-
205
- <div className="flex items-end gap-2">
206
- {/* File import */}
187
+ <div className="border-t border-border-subtle px-3 py-2 bg-surface-1">
188
+ <div className="flex items-end gap-1.5">
207
189
  <input
208
190
  ref={fileInputRef}
209
191
  type="file"
@@ -214,10 +196,10 @@ export function AgentChat({ agent }) {
214
196
  />
215
197
  <button
216
198
  onClick={() => fileInputRef.current?.click()}
217
- className="w-10 h-10 flex items-center justify-center rounded-xl text-text-4 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer"
199
+ className="w-8 h-8 flex items-center justify-center rounded-lg text-text-4 hover:text-text-1 hover:bg-surface-3 transition-colors cursor-pointer flex-shrink-0"
218
200
  title="Attach file"
219
201
  >
220
- <Paperclip size={16} />
202
+ <Paperclip size={14} />
221
203
  </button>
222
204
  <textarea
223
205
  ref={inputRef}
@@ -227,11 +209,11 @@ export function AgentChat({ agent }) {
227
209
  placeholder={isAlive ? 'Instruct this agent...' : 'Continue conversation...'}
228
210
  rows={1}
229
211
  className={cn(
230
- 'flex-1 resize-y rounded-xl px-4 py-2.5 text-sm',
212
+ 'flex-1 resize-none rounded-lg px-3 py-1.5 text-xs',
231
213
  'bg-surface-0 border text-text-0 font-sans',
232
214
  'placeholder:text-text-4',
233
215
  'focus:outline-none focus:ring-1',
234
- 'min-h-[40px]',
216
+ 'min-h-[32px] max-h-[120px]',
235
217
  'border-border focus:ring-accent/40',
236
218
  )}
237
219
  />
@@ -239,23 +221,23 @@ export function AgentChat({ agent }) {
239
221
  <button
240
222
  onClick={() => useGrooveStore.getState().stopAgent(agent.id)}
241
223
  title="Stop agent"
242
- className="w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer bg-danger/80 text-white hover:bg-danger shadow-lg shadow-danger/20"
224
+ className="w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer bg-danger/80 text-white hover:bg-danger flex-shrink-0"
243
225
  >
244
- <Square size={14} fill="currentColor" />
226
+ <Square size={12} fill="currentColor" />
245
227
  </button>
246
228
  )}
247
229
  <button
248
230
  onClick={handleSend}
249
231
  disabled={!input.trim() || sending}
250
232
  className={cn(
251
- 'w-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer',
233
+ 'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
252
234
  'disabled:opacity-20 disabled:cursor-not-allowed',
253
235
  input.trim()
254
236
  ? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
255
237
  : 'bg-surface-4 text-text-4',
256
238
  )}
257
239
  >
258
- {sending ? <Loader2 size={16} className="animate-spin" /> : <Send size={16} />}
240
+ {sending ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
259
241
  </button>
260
242
  </div>
261
243
  </div>
@@ -3,7 +3,7 @@ import { useState, useEffect, useRef, useCallback } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { api } from '../../lib/api';
6
- import { ChevronRight, ChevronDown, File, Folder, FolderOpen, Clock, FilePlus, FolderPlus, RefreshCw, ChevronsDownUp } from 'lucide-react';
6
+ import { ChevronRight, ChevronDown, File, Folder, FolderOpen, FileEdit, Eye, FilePlus, FolderPlus, RefreshCw, ChevronsDownUp } from 'lucide-react';
7
7
  import { ScrollArea } from '../ui/scroll-area';
8
8
 
9
9
  const FILE_COLORS = {
@@ -78,7 +78,6 @@ function TreeEntry({ entry, depth, onOpen, expandedDirs, onToggleDir }) {
78
78
 
79
79
  export function AgentFileTree({ agentId }) {
80
80
  const agents = useGrooveStore((s) => s.agents);
81
- const activityLog = useGrooveStore((s) => s.activityLog);
82
81
  const openFile = useGrooveStore((s) => s.openFile);
83
82
  const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
84
83
  const createFile = useGrooveStore((s) => s.createFile);
@@ -86,29 +85,26 @@ export function AgentFileTree({ agentId }) {
86
85
 
87
86
  const agent = agents.find((a) => a.id === agentId);
88
87
  const scope = agent?.scope || [];
89
- const log = activityLog[agentId] || [];
88
+ const isRunning = agent?.status === 'running' || agent?.status === 'starting';
90
89
 
91
90
  const [treeData, setTreeData] = useState([]);
92
91
  const [expandedDirs, setExpandedDirs] = useState(new Set());
93
92
  const [loading, setLoading] = useState(true);
93
+ const [touchedFiles, setTouchedFiles] = useState([]);
94
94
  const fetchedRef = useRef(new Set());
95
95
 
96
- const recentFiles = (() => {
97
- const seen = new Set();
98
- const files = [];
99
- for (let i = log.length - 1; i >= 0; i--) {
100
- const t = (log[i].text || '').toLowerCase();
101
- if (!(t.includes('writ') || t.includes('edit') || t.includes('creat') || t.includes('read'))) continue;
102
- const match = log[i].text.match(/(?:Write|Edit|Create|Read|wrote|editing|writing|reading)\S*\s+([\w./-]+\.[\w]+)/i);
103
- if (!match) continue;
104
- const path = match[1];
105
- if (seen.has(path) || path.startsWith('.') || path.includes('node_modules')) continue;
106
- seen.add(path);
107
- files.push(path);
108
- if (files.length >= 10) break;
96
+ useEffect(() => {
97
+ let cancelled = false;
98
+ async function poll() {
99
+ try {
100
+ const data = await api.get(`/agents/${encodeURIComponent(agentId)}/files-touched`);
101
+ if (!cancelled && data.files) setTouchedFiles(data.files);
102
+ } catch { /* agent may not exist yet */ }
109
103
  }
110
- return files;
111
- })();
104
+ poll();
105
+ const interval = isRunning ? setInterval(poll, 5000) : null;
106
+ return () => { cancelled = true; if (interval) clearInterval(interval); };
107
+ }, [agentId, isRunning]);
112
108
 
113
109
  const fetchDir = useCallback(async (dirPath) => {
114
110
  if (fetchedRef.current.has(dirPath)) return;
@@ -230,26 +226,31 @@ export function AgentFileTree({ agentId }) {
230
226
  </div>
231
227
  <ScrollArea className="flex-1 min-h-0">
232
228
  <div className="py-2">
233
- {recentFiles.length > 0 && (
229
+ {touchedFiles.length > 0 && (
234
230
  <div className="mb-3">
235
231
  <div className="flex items-center gap-1.5 px-3 py-1.5 text-2xs font-semibold text-text-3 uppercase tracking-wider">
236
- <Clock size={10} />
237
- Recently Touched
232
+ <FileEdit size={10} />
233
+ Agent Files
238
234
  </div>
239
- {recentFiles.map((path) => {
240
- const name = path.split('/').pop();
235
+ {touchedFiles.slice(0, 15).map((f) => {
236
+ const name = f.path.split('/').pop();
237
+ const hasWrites = f.writes > 0;
241
238
  return (
242
239
  <button
243
- key={path}
244
- onClick={() => openFile(path)}
240
+ key={f.path}
241
+ onClick={() => openFile(f.path)}
245
242
  className={cn(
246
243
  'w-full flex items-center gap-1.5 px-3 py-1 text-xs font-sans cursor-pointer',
247
244
  'hover:bg-surface-4/50 transition-colors text-left',
248
- editorActiveFile === path && 'bg-accent/8 text-accent',
245
+ editorActiveFile === f.path && 'bg-accent/8 text-accent',
249
246
  )}
250
247
  >
251
- <File size={12} className={cn(getFileColor(name), 'flex-shrink-0')} />
252
- <span className="truncate text-text-1">{name}</span>
248
+ {hasWrites
249
+ ? <FileEdit size={12} className="text-warning flex-shrink-0" />
250
+ : <Eye size={12} className="text-info flex-shrink-0" />
251
+ }
252
+ <span className="truncate text-text-1 flex-1">{name}</span>
253
+ {hasWrites && <span className="text-2xs text-warning/60 flex-shrink-0">{f.writes}w</span>}
253
254
  </button>
254
255
  );
255
256
  })}
@@ -3,33 +3,20 @@ import { useState, useRef, useCallback, useEffect } from 'react';
3
3
  import { useGrooveStore } from '../../stores/groove';
4
4
  import { cn } from '../../lib/cn';
5
5
  import { AgentFileTree } from './agent-file-tree';
6
- import { AgentChat } from './agent-chat';
7
- import { AgentFeed } from './agent-feed';
8
6
  import { DiffViewer } from './diff-viewer';
9
7
  import { CodeReview } from './code-review';
10
8
  import { CodeEditor } from '../editor/code-editor';
11
- import { Badge } from '../ui/badge';
12
9
  import { Tooltip } from '../ui/tooltip';
13
- import { ScrollArea } from '../ui/scroll-area';
14
10
  import { roleColor } from '../../lib/status';
15
- import { fmtNum } from '../../lib/format';
16
11
  import { MediaViewer, isMediaFile } from '../editor/media-viewer';
17
12
  import {
18
- X, Code2, MessageSquare, Activity, FileCode, GitCompareArrows,
19
- ClipboardCheck, AlertTriangle, RefreshCw, Users,
13
+ X, Code2, FileCode, GitCompareArrows,
14
+ ClipboardCheck, Users,
20
15
  } from 'lucide-react';
21
16
 
22
- const STATUS_VARIANT = {
23
- running: 'success', starting: 'warning', stopped: 'default',
24
- crashed: 'danger', completed: 'accent', killed: 'default', rotating: 'purple',
25
- };
26
-
27
17
  const TREE_DEFAULT = 220;
28
18
  const TREE_MIN = 140;
29
19
  const TREE_MAX = 360;
30
- const RIGHT_DEFAULT = 340;
31
- const RIGHT_MIN = 260;
32
- const RIGHT_MAX = 520;
33
20
 
34
21
  function AgentRail({ agents, activeId, onSelect }) {
35
22
  return (
@@ -70,11 +57,11 @@ function AgentRail({ agents, activeId, onSelect }) {
70
57
  );
71
58
  }
72
59
 
73
- function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots }) {
60
+ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots, onBackToTeam, onToggleReview, reviewMode }) {
74
61
  const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
75
62
 
76
63
  return (
77
- <div className="flex items-stretch h-8 bg-surface-2 border-b border-border-subtle flex-shrink-0">
64
+ <div className="flex items-stretch h-8 bg-[#1a1e25] border-b border-[#1e2229] flex-shrink-0">
78
65
  <div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
79
66
  {tabs.map((path) => {
80
67
  const isActive = path === activeFile;
@@ -106,28 +93,53 @@ function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggle
106
93
  );
107
94
  })}
108
95
  </div>
109
- {hasSnapshot && (
110
- <div className="flex items-center gap-0.5 px-2 border-l border-border-subtle flex-shrink-0">
96
+ <div className="flex items-center gap-0.5 px-2 border-l border-border-subtle flex-shrink-0">
97
+ {hasSnapshot && (
98
+ <>
99
+ <button
100
+ onClick={() => onToggleDiff(false)}
101
+ className={cn(
102
+ 'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
103
+ !diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
104
+ )}
105
+ >
106
+ <FileCode size={11} /> Code
107
+ </button>
108
+ <button
109
+ onClick={() => onToggleDiff(true)}
110
+ className={cn(
111
+ 'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
112
+ diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
113
+ )}
114
+ >
115
+ <GitCompareArrows size={11} /> Diff
116
+ </button>
117
+ <div className="w-px h-4 bg-border-subtle mx-1" />
118
+ </>
119
+ )}
120
+ <Tooltip content="Review Changes" side="bottom">
111
121
  <button
112
- onClick={() => onToggleDiff(false)}
122
+ onClick={onToggleReview}
113
123
  className={cn(
114
124
  'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
115
- !diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
125
+ reviewMode
126
+ ? 'bg-accent/15 text-accent'
127
+ : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
116
128
  )}
117
129
  >
118
- <FileCode size={11} /> Code
130
+ <ClipboardCheck size={12} />
119
131
  </button>
132
+ </Tooltip>
133
+ <Tooltip content="Back to Team View" side="bottom">
120
134
  <button
121
- onClick={() => onToggleDiff(true)}
122
- className={cn(
123
- 'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
124
- diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
125
- )}
135
+ onClick={onBackToTeam}
136
+ className="flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors text-text-3 hover:text-text-1 hover:bg-surface-3"
126
137
  >
127
- <GitCompareArrows size={11} /> Diff
138
+ <Users size={12} />
139
+ <span className="text-2xs">Team</span>
128
140
  </button>
129
- </div>
130
- )}
141
+ </Tooltip>
142
+ </div>
131
143
  </div>
132
144
  );
133
145
  }
@@ -157,12 +169,9 @@ export function WorkspaceMode() {
157
169
  const agent = teamAgents.find((a) => a.id === workspaceAgentId) || teamAgents[0];
158
170
 
159
171
  const [treeWidth, setTreeWidth] = useState(TREE_DEFAULT);
160
- const [rightWidth, setRightWidth] = useState(RIGHT_DEFAULT);
161
172
  const [diffMode, setDiffMode] = useState(false);
162
- const [rightTab, setRightTab] = useState('chat');
163
173
 
164
174
  const treeDragging = useRef(false);
165
- const rightDragging = useRef(false);
166
175
  const startX = useRef(0);
167
176
  const startW = useRef(0);
168
177
 
@@ -188,24 +197,6 @@ export function WorkspaceMode() {
188
197
  document.addEventListener('mouseup', onUp);
189
198
  }, [treeWidth]);
190
199
 
191
- const onRightMouseDown = useCallback((e) => {
192
- e.preventDefault();
193
- rightDragging.current = true;
194
- startX.current = e.clientX;
195
- startW.current = rightWidth;
196
- function onMove(e) {
197
- if (!rightDragging.current) return;
198
- setRightWidth(Math.min(Math.max(startW.current - (e.clientX - startX.current), RIGHT_MIN), RIGHT_MAX));
199
- }
200
- function onUp() {
201
- rightDragging.current = false;
202
- document.removeEventListener('mousemove', onMove);
203
- document.removeEventListener('mouseup', onUp);
204
- }
205
- document.addEventListener('mousemove', onMove);
206
- document.addEventListener('mouseup', onUp);
207
- }, [rightWidth]);
208
-
209
200
  if (!agent) {
210
201
  return (
211
202
  <div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
@@ -214,8 +205,6 @@ export function WorkspaceMode() {
214
205
  );
215
206
  }
216
207
 
217
- const isAlive = agent.status === 'running' || agent.status === 'starting';
218
- const ctxPct = Math.round((agent.contextUsage || 0) * 100);
219
208
  const file = editorActiveFile ? editorFiles[editorActiveFile] : null;
220
209
  const hasExternalChange = editorActiveFile && editorChangedFiles[editorActiveFile];
221
210
  const isMedia = editorActiveFile && isMediaFile(editorActiveFile);
@@ -238,7 +227,7 @@ export function WorkspaceMode() {
238
227
  </div>
239
228
 
240
229
  {/* Editor Area */}
241
- <div className="flex-1 flex flex-col min-w-0 bg-[#1a1e25]">
230
+ <div className="flex-1 flex flex-col min-w-0 bg-[#13161b]">
242
231
  {workspaceReviewMode ? (
243
232
  <CodeReview agentId={agent.id} />
244
233
  ) : (
@@ -252,30 +241,32 @@ export function WorkspaceMode() {
252
241
  diffMode={diffMode}
253
242
  onToggleDiff={setDiffMode}
254
243
  workspaceSnapshots={workspaceSnapshots}
244
+ onBackToTeam={() => setWorkspaceMode(false)}
245
+ onToggleReview={toggleReviewMode}
246
+ reviewMode={workspaceReviewMode}
255
247
  />
256
248
 
257
249
  <div className="flex-1 relative min-h-0">
258
250
  {hasExternalChange && (
259
- <div className="absolute top-0 left-0 right-0 z-10 flex items-center gap-2 px-4 py-2 bg-warning/10 border-b border-warning/20">
260
- <AlertTriangle size={14} className="text-warning" />
261
- <span className="text-xs text-warning font-sans flex-1">File modified externally</span>
251
+ <div className="absolute top-1 right-3 z-10 flex items-center gap-1.5 px-2 py-1 rounded-md bg-surface-2/90 border border-border-subtle backdrop-blur-sm">
252
+ <span className="text-2xs text-text-3 font-sans">Modified</span>
262
253
  <button
263
254
  onClick={() => reloadFile(editorActiveFile)}
264
- className="flex items-center gap-1 px-2 py-1 text-xs text-text-1 hover:bg-surface-4 rounded cursor-pointer"
255
+ className="text-2xs text-accent hover:text-accent/80 font-sans cursor-pointer"
265
256
  >
266
- <RefreshCw size={12} /> Reload
257
+ Reload
267
258
  </button>
268
259
  <button
269
260
  onClick={() => dismissFileChange(editorActiveFile)}
270
- className="flex items-center gap-1 px-2 py-1 text-xs text-text-3 hover:bg-surface-4 rounded cursor-pointer"
261
+ className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"
271
262
  >
272
- <X size={12} /> Dismiss
263
+ <X size={10} />
273
264
  </button>
274
265
  </div>
275
266
  )}
276
267
 
277
268
  {!editorActiveFile && (
278
- <div className="w-full h-full flex items-center justify-center text-text-4 font-sans bg-[#1a1e25]">
269
+ <div className="w-full h-full flex items-center justify-center text-text-4 font-sans bg-[#13161b]">
279
270
  <div className="text-center space-y-2">
280
271
  <Code2 size={32} className="mx-auto" />
281
272
  <p className="text-sm">Open a file from the tree</p>
@@ -305,87 +296,6 @@ export function WorkspaceMode() {
305
296
  )}
306
297
  </div>
307
298
  </div>
308
-
309
- {/* Right Panel — Chat + Activity */}
310
- <div className="flex flex-col bg-surface-1 border-l border-border relative" style={{ width: rightWidth }}>
311
- {/* Resize handle */}
312
- <div
313
- className="absolute top-0 left-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
314
- onMouseDown={onRightMouseDown}
315
- onDoubleClick={() => setRightWidth(RIGHT_DEFAULT)}
316
- />
317
-
318
- {/* Header */}
319
- <div className="flex items-center gap-2 px-4 py-2.5 border-b border-border flex-shrink-0">
320
- <div className="flex-1 min-w-0">
321
- <div className="flex items-center gap-2">
322
- <span className="text-sm font-bold text-text-0 font-sans truncate">{agent.name}</span>
323
- <Badge variant={STATUS_VARIANT[agent.status]} dot={isAlive ? 'pulse' : undefined}>
324
- {agent.status}
325
- </Badge>
326
- </div>
327
- <div className="flex items-center gap-3 mt-0.5">
328
- <span className="text-2xs text-text-3 font-sans">
329
- {fmtNum(agent.tokensUsed || 0)} tokens
330
- </span>
331
- <span className="text-2xs text-text-3 font-sans">
332
- ctx {ctxPct}%
333
- </span>
334
- </div>
335
- </div>
336
- <button
337
- onClick={toggleReviewMode}
338
- className={cn(
339
- 'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
340
- workspaceReviewMode
341
- ? 'bg-accent/15 text-accent'
342
- : 'text-text-3 hover:text-text-1 hover:bg-surface-3',
343
- )}
344
- title="Review Changes"
345
- >
346
- <ClipboardCheck size={13} />
347
- </button>
348
- <button
349
- onClick={() => setWorkspaceMode(false)}
350
- className="flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors text-text-3 hover:text-text-1 hover:bg-surface-3"
351
- title="Back to agent tree"
352
- >
353
- <Users size={13} />
354
- </button>
355
- </div>
356
-
357
- {/* Tab switcher */}
358
- <div className="flex items-center gap-0 border-b border-border-subtle flex-shrink-0">
359
- <button
360
- onClick={() => setRightTab('chat')}
361
- className={cn(
362
- 'flex items-center gap-1.5 px-3 py-2 text-xs font-sans cursor-pointer transition-colors',
363
- rightTab === 'chat'
364
- ? 'text-text-0 border-b border-b-accent font-medium'
365
- : 'text-text-3 hover:text-text-1',
366
- )}
367
- >
368
- <MessageSquare size={12} /> Chat
369
- </button>
370
- <button
371
- onClick={() => setRightTab('activity')}
372
- className={cn(
373
- 'flex items-center gap-1.5 px-3 py-2 text-xs font-sans cursor-pointer transition-colors',
374
- rightTab === 'activity'
375
- ? 'text-text-0 border-b border-b-accent font-medium'
376
- : 'text-text-3 hover:text-text-1',
377
- )}
378
- >
379
- <Activity size={12} /> Activity
380
- </button>
381
- </div>
382
-
383
- {/* Content */}
384
- <div className="flex-1 min-h-0">
385
- {rightTab === 'chat' && <AgentChat agent={agent} />}
386
- {rightTab === 'activity' && <AgentFeed agent={agent} />}
387
- </div>
388
- </div>
389
299
  </div>
390
300
  );
391
301
  }
@@ -1,10 +1,9 @@
1
1
  // FSL-1.1-Apache-2.0 — see LICENSE
2
2
  import { useState, useRef, useEffect } from 'react';
3
- import { Pencil, Pin, PinOff, Trash2, Hash, MoreHorizontal, Zap, Bot, ChevronDown, X } from 'lucide-react';
3
+ import { Pencil, Pin, PinOff, Trash2, Hash, MoreHorizontal, ChevronDown, X } from 'lucide-react';
4
4
  import { useGrooveStore } from '../../stores/groove';
5
5
  import { fmtNum } from '../../lib/format';
6
6
  import { ModelPicker, formatModelName } from './model-picker';
7
- import { Tooltip } from '../ui/tooltip';
8
7
  import { cn } from '../../lib/cn';
9
8
  import { roleColor } from '../../lib/status';
10
9
 
@@ -25,7 +24,7 @@ const CHAT_ROLES = [
25
24
  { id: 'support', label: 'Support', desc: 'Customer support, FAQs' },
26
25
  ];
27
26
 
28
- export function ChatHeader({ conversation, model, onModelChange, onModeChange, role, onRoleChange }) {
27
+ export function ChatHeader({ conversation, model, onModelChange, role, onRoleChange, sidebarCollapsed }) {
29
28
  const renameConversation = useGrooveStore((s) => s.renameConversation);
30
29
  const pinConversation = useGrooveStore((s) => s.pinConversation);
31
30
  const deleteConversation = useGrooveStore((s) => s.deleteConversation);
@@ -77,10 +76,9 @@ export function ChatHeader({ conversation, model, onModelChange, onModeChange, r
77
76
 
78
77
  const agent = useGrooveStore((s) => s.agents.find((a) => a.id === conversation.agentId));
79
78
  const tokens = agent?.tokensUsed || 0;
80
- const mode = conversation.mode || 'api';
81
79
 
82
80
  return (
83
- <div className="h-12 flex items-center gap-3 px-4 border-b border-border-subtle bg-surface-0/80 flex-shrink-0">
81
+ <div className={cn("h-12 flex items-center gap-3 border-b border-border-subtle bg-surface-0/80 flex-shrink-0", sidebarCollapsed ? 'pl-10 pr-4' : 'px-4')}>
84
82
  <Hash size={14} className="text-text-4 flex-shrink-0" />
85
83
 
86
84
  {editing ? (
@@ -105,31 +103,6 @@ export function ChatHeader({ conversation, model, onModelChange, onModeChange, r
105
103
  <div className="w-px h-5 bg-border-subtle" />
106
104
 
107
105
  <div className="flex items-center gap-2 flex-shrink-0">
108
- <div className="flex items-center h-7 rounded-lg bg-surface-3 border border-border-subtle p-0.5">
109
- <Tooltip content="Lightweight — fast and cheap, no tools" side="bottom">
110
- <button
111
- onClick={() => onModeChange?.('api')}
112
- className={cn(
113
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
114
- mode === 'api' ? 'bg-accent/15 text-accent border border-accent/25' : 'text-text-3 hover:text-text-1',
115
- )}
116
- >
117
- <Zap size={11} /> Chat
118
- </button>
119
- </Tooltip>
120
- <Tooltip content="Full agent — tools, files, session resume" side="bottom">
121
- <button
122
- onClick={() => onModeChange?.('agent')}
123
- className={cn(
124
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
125
- mode === 'agent' ? 'bg-purple/15 text-purple border border-purple/25' : 'text-text-3 hover:text-text-1',
126
- )}
127
- >
128
- <Bot size={11} /> Agent
129
- </button>
130
- </Tooltip>
131
- </div>
132
-
133
106
  <div ref={roleMenuRef} className="relative">
134
107
  <button
135
108
  onClick={() => setRoleMenuOpen(!roleMenuOpen)}