groove-dev 0.27.91 → 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.
- package/node_modules/@groove-dev/cli/package.json +1 -1
- package/node_modules/@groove-dev/daemon/package.json +1 -1
- package/node_modules/@groove-dev/daemon/src/api.js +228 -3
- package/node_modules/@groove-dev/daemon/src/introducer.js +42 -0
- package/node_modules/@groove-dev/daemon/src/process.js +5 -1
- package/node_modules/@groove-dev/daemon/src/providers/base.js +4 -0
- package/node_modules/@groove-dev/daemon/src/providers/claude-code.js +8 -0
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +33 -4
- package/node_modules/@groove-dev/daemon/src/providers/gemini.js +14 -1
- package/node_modules/@groove-dev/daemon/src/providers/grok.js +8 -1
- package/node_modules/@groove-dev/daemon/src/providers/local.js +8 -1
- package/node_modules/@groove-dev/daemon/src/tunnel-manager.js +74 -5
- package/node_modules/@groove-dev/daemon/src/validate.js +22 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/node_modules/@groove-dev/gui/dist/index.html +2 -2
- package/node_modules/@groove-dev/gui/package.json +1 -1
- package/node_modules/@groove-dev/gui/src/components/agents/agent-chat.jsx +26 -44
- package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/node_modules/@groove-dev/gui/src/components/chat/chat-header.jsx +3 -30
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +163 -153
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +15 -5
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +26 -17
- package/node_modules/@groove-dev/gui/src/components/editor/code-editor.jsx +29 -23
- package/node_modules/@groove-dev/gui/src/components/settings/quick-connect.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/node_modules/@groove-dev/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/node_modules/@groove-dev/gui/src/components/ui/slider.jsx +50 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +145 -9
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +707 -14
- package/package.json +1 -1
- package/packages/cli/package.json +1 -1
- package/packages/daemon/package.json +1 -1
- package/packages/daemon/src/api.js +228 -3
- package/packages/daemon/src/introducer.js +42 -0
- package/packages/daemon/src/process.js +5 -1
- package/packages/daemon/src/providers/base.js +4 -0
- package/packages/daemon/src/providers/claude-code.js +8 -0
- package/packages/daemon/src/providers/codex.js +33 -4
- package/packages/daemon/src/providers/gemini.js +14 -1
- package/packages/daemon/src/providers/grok.js +8 -1
- package/packages/daemon/src/providers/local.js +8 -1
- package/packages/daemon/src/tunnel-manager.js +74 -5
- package/packages/daemon/src/validate.js +22 -1
- package/packages/gui/dist/assets/index-Bo6AeNmM.css +1 -0
- package/packages/gui/dist/assets/index-DWv32qyJ.js +8653 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-chat.jsx +26 -44
- package/packages/gui/src/components/agents/agent-file-tree.jsx +29 -28
- package/packages/gui/src/components/agents/workspace-mode.jsx +53 -143
- package/packages/gui/src/components/chat/chat-header.jsx +3 -30
- package/packages/gui/src/components/chat/chat-input.jsx +163 -153
- package/packages/gui/src/components/chat/chat-view.jsx +15 -5
- package/packages/gui/src/components/chat/conversation-list.jsx +26 -17
- package/packages/gui/src/components/editor/code-editor.jsx +29 -23
- package/packages/gui/src/components/settings/quick-connect.jsx +5 -1
- package/packages/gui/src/components/settings/remote-server-card.jsx +9 -5
- package/packages/gui/src/components/settings/ssh-wizard.jsx +5 -1
- package/packages/gui/src/components/ui/slider.jsx +50 -0
- package/packages/gui/src/stores/groove.js +145 -9
- package/packages/gui/src/views/agents.jsx +707 -14
- package/workspace.png +0 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-MLIZRMj1.js +0 -8642
- package/packages/gui/dist/assets/index-D4vJ_1ET.css +0 -1
- package/packages/gui/dist/assets/index-MLIZRMj1.js +0 -8642
|
@@ -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-
|
|
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-
|
|
14
|
+
<link rel="stylesheet" crossorigin href="/assets/index-Bo6AeNmM.css">
|
|
15
15
|
</head>
|
|
16
16
|
<body>
|
|
17
17
|
<div id="root"></div>
|
|
@@ -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
|
|
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-
|
|
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-
|
|
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
|
|
64
|
-
<
|
|
65
|
-
<div className="
|
|
66
|
-
<div className="text-
|
|
67
|
-
|
|
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(
|
|
85
|
+
function TypingIndicator() {
|
|
90
86
|
return (
|
|
91
|
-
<div className="
|
|
92
|
-
<div className="
|
|
93
|
-
|
|
94
|
-
<
|
|
95
|
-
|
|
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-
|
|
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-
|
|
195
|
-
|
|
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-
|
|
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={
|
|
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-
|
|
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-[
|
|
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-
|
|
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={
|
|
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-
|
|
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={
|
|
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,
|
|
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
|
|
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
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
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
|
-
|
|
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
|
-
{
|
|
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
|
-
<
|
|
237
|
-
|
|
232
|
+
<FileEdit size={10} />
|
|
233
|
+
Agent Files
|
|
238
234
|
</div>
|
|
239
|
-
{
|
|
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
|
-
|
|
252
|
-
|
|
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,
|
|
19
|
-
ClipboardCheck,
|
|
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-
|
|
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
|
-
|
|
110
|
-
|
|
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={
|
|
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
|
-
|
|
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
|
-
<
|
|
130
|
+
<ClipboardCheck size={12} />
|
|
119
131
|
</button>
|
|
132
|
+
</Tooltip>
|
|
133
|
+
<Tooltip content="Back to Team View" side="bottom">
|
|
120
134
|
<button
|
|
121
|
-
onClick={
|
|
122
|
-
className=
|
|
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
|
-
<
|
|
138
|
+
<Users size={12} />
|
|
139
|
+
<span className="text-2xs">Team</span>
|
|
128
140
|
</button>
|
|
129
|
-
</
|
|
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-[#
|
|
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-
|
|
260
|
-
<
|
|
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="
|
|
255
|
+
className="text-2xs text-accent hover:text-accent/80 font-sans cursor-pointer"
|
|
265
256
|
>
|
|
266
|
-
|
|
257
|
+
Reload
|
|
267
258
|
</button>
|
|
268
259
|
<button
|
|
269
260
|
onClick={() => dismissFileChange(editorActiveFile)}
|
|
270
|
-
className="
|
|
261
|
+
className="p-0.5 text-text-4 hover:text-text-1 cursor-pointer"
|
|
271
262
|
>
|
|
272
|
-
<X size={
|
|
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-[#
|
|
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,
|
|
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,
|
|
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
|
|
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)}
|