groove-dev 0.27.87 → 0.27.88
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CLAUDE.md +3 -2
- 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 +115 -7
- package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
- package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
- package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
- package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -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-file-tree.jsx +254 -0
- package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
- package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
- package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
- package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
- package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
- package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
- package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
- package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
- 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 +115 -7
- package/packages/daemon/src/conversations.js +29 -3
- package/packages/daemon/src/providers/codex.js +28 -10
- package/packages/daemon/src/registry.js +30 -0
- package/packages/daemon/src/validate.js +23 -0
- package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
- package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -0
- package/packages/gui/dist/index.html +2 -2
- package/packages/gui/package.json +1 -1
- package/packages/gui/src/components/agents/agent-file-tree.jsx +254 -0
- package/packages/gui/src/components/agents/code-review.jsx +177 -0
- package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
- package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
- package/packages/gui/src/components/chat/chat-input.jsx +117 -40
- package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
- package/packages/gui/src/components/chat/chat-view.jsx +26 -1
- package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
- package/packages/gui/src/components/chat/model-picker.jsx +5 -0
- package/packages/gui/src/stores/groove.js +132 -1
- package/packages/gui/src/views/agents.jsx +22 -3
- package/test/doomsday-clock/index.html +55 -0
- package/test/doomsday-clock/script.js +66 -0
- package/test/doomsday-clock/style.css +315 -0
- package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
- package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
- package/packages/gui/dist/assets/index-C5e7KVGN.js +0 -8637
|
@@ -0,0 +1,377 @@
|
|
|
1
|
+
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
|
+
import { useState, useRef, useCallback, useEffect } from 'react';
|
|
3
|
+
import { useGrooveStore } from '../../stores/groove';
|
|
4
|
+
import { cn } from '../../lib/cn';
|
|
5
|
+
import { AgentFileTree } from './agent-file-tree';
|
|
6
|
+
import { AgentChat } from './agent-chat';
|
|
7
|
+
import { AgentFeed } from './agent-feed';
|
|
8
|
+
import { DiffViewer } from './diff-viewer';
|
|
9
|
+
import { CodeReview } from './code-review';
|
|
10
|
+
import { CodeEditor } from '../editor/code-editor';
|
|
11
|
+
import { Badge } from '../ui/badge';
|
|
12
|
+
import { Tooltip } from '../ui/tooltip';
|
|
13
|
+
import { ScrollArea } from '../ui/scroll-area';
|
|
14
|
+
import { roleColor } from '../../lib/status';
|
|
15
|
+
import { fmtNum } from '../../lib/format';
|
|
16
|
+
import {
|
|
17
|
+
X, Code2, MessageSquare, Activity, FileCode, GitCompareArrows,
|
|
18
|
+
ClipboardCheck, AlertTriangle, RefreshCw,
|
|
19
|
+
} from 'lucide-react';
|
|
20
|
+
|
|
21
|
+
const STATUS_VARIANT = {
|
|
22
|
+
running: 'success', starting: 'warning', stopped: 'default',
|
|
23
|
+
crashed: 'danger', completed: 'accent', killed: 'default', rotating: 'purple',
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
const TREE_DEFAULT = 220;
|
|
27
|
+
const TREE_MIN = 140;
|
|
28
|
+
const TREE_MAX = 360;
|
|
29
|
+
const RIGHT_DEFAULT = 340;
|
|
30
|
+
const RIGHT_MIN = 260;
|
|
31
|
+
const RIGHT_MAX = 520;
|
|
32
|
+
|
|
33
|
+
function AgentRail({ agents, activeId, onSelect }) {
|
|
34
|
+
return (
|
|
35
|
+
<div className="flex flex-col items-center gap-1 py-2 w-12 bg-surface-1 border-r border-border flex-shrink-0">
|
|
36
|
+
{agents.map((agent) => {
|
|
37
|
+
const colors = roleColor(agent.role);
|
|
38
|
+
const isActive = agent.id === activeId;
|
|
39
|
+
const isRunning = agent.status === 'running' || agent.status === 'starting';
|
|
40
|
+
const initial = (agent.role || '?')[0].toUpperCase();
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<Tooltip key={agent.id} content={`${agent.name} — ${agent.status}`} side="right">
|
|
44
|
+
<button
|
|
45
|
+
onClick={() => onSelect(agent.id)}
|
|
46
|
+
className={cn(
|
|
47
|
+
'relative w-9 h-9 rounded-lg flex items-center justify-center',
|
|
48
|
+
'text-xs font-bold font-sans cursor-pointer transition-all',
|
|
49
|
+
isActive
|
|
50
|
+
? 'ring-1.5 ring-accent bg-accent/12'
|
|
51
|
+
: 'hover:bg-surface-3',
|
|
52
|
+
)}
|
|
53
|
+
style={{ color: colors.text, background: isActive ? colors.bg : undefined }}
|
|
54
|
+
>
|
|
55
|
+
{initial}
|
|
56
|
+
<span
|
|
57
|
+
className={cn(
|
|
58
|
+
'absolute bottom-0.5 right-0.5 w-2 h-2 rounded-full border border-surface-1',
|
|
59
|
+
isRunning ? 'bg-success animate-pulse' :
|
|
60
|
+
agent.status === 'completed' ? 'bg-accent' :
|
|
61
|
+
agent.status === 'crashed' ? 'bg-danger' : 'bg-text-4',
|
|
62
|
+
)}
|
|
63
|
+
/>
|
|
64
|
+
</button>
|
|
65
|
+
</Tooltip>
|
|
66
|
+
);
|
|
67
|
+
})}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function TabBar({ tabs, activeFile, files, onSelect, onClose, diffMode, onToggleDiff, workspaceSnapshots }) {
|
|
73
|
+
const hasSnapshot = activeFile && workspaceSnapshots[activeFile];
|
|
74
|
+
|
|
75
|
+
return (
|
|
76
|
+
<div className="flex items-stretch h-9 bg-surface-3 border-b border-border-subtle flex-shrink-0">
|
|
77
|
+
<div className="flex items-stretch flex-1 min-w-0 overflow-x-auto scrollbar-none">
|
|
78
|
+
{tabs.map((path) => {
|
|
79
|
+
const isActive = path === activeFile;
|
|
80
|
+
const file = files[path];
|
|
81
|
+
const isDirty = file && file.content !== file.originalContent;
|
|
82
|
+
const name = path.split('/').pop();
|
|
83
|
+
|
|
84
|
+
return (
|
|
85
|
+
<div
|
|
86
|
+
key={path}
|
|
87
|
+
className={cn(
|
|
88
|
+
'flex items-center gap-1.5 px-3 text-xs font-sans cursor-pointer select-none',
|
|
89
|
+
'border-r border-white/5 transition-colors duration-75 flex-shrink-0',
|
|
90
|
+
isActive
|
|
91
|
+
? 'bg-surface-0 text-text-0 border-b border-b-accent'
|
|
92
|
+
: 'bg-surface-3 text-text-4 hover:text-text-1 hover:bg-surface-4 border-b border-b-transparent',
|
|
93
|
+
)}
|
|
94
|
+
onClick={() => onSelect(path)}
|
|
95
|
+
>
|
|
96
|
+
<span className="truncate max-w-[120px]">{name}</span>
|
|
97
|
+
{isDirty && <span className="w-1.5 h-1.5 rounded-full bg-warning flex-shrink-0" />}
|
|
98
|
+
<button
|
|
99
|
+
onClick={(e) => { e.stopPropagation(); onClose(path); }}
|
|
100
|
+
className="p-0.5 rounded hover:bg-surface-5 text-text-4 hover:text-text-1 transition-colors cursor-pointer ml-0.5"
|
|
101
|
+
>
|
|
102
|
+
<X size={12} />
|
|
103
|
+
</button>
|
|
104
|
+
</div>
|
|
105
|
+
);
|
|
106
|
+
})}
|
|
107
|
+
</div>
|
|
108
|
+
{hasSnapshot && (
|
|
109
|
+
<div className="flex items-center gap-0.5 px-2 border-l border-border-subtle flex-shrink-0">
|
|
110
|
+
<button
|
|
111
|
+
onClick={() => onToggleDiff(false)}
|
|
112
|
+
className={cn(
|
|
113
|
+
'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
114
|
+
!diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
115
|
+
)}
|
|
116
|
+
>
|
|
117
|
+
<FileCode size={11} /> Code
|
|
118
|
+
</button>
|
|
119
|
+
<button
|
|
120
|
+
onClick={() => onToggleDiff(true)}
|
|
121
|
+
className={cn(
|
|
122
|
+
'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
123
|
+
diffMode ? 'bg-surface-4 text-text-0 font-medium' : 'text-text-3 hover:text-text-1',
|
|
124
|
+
)}
|
|
125
|
+
>
|
|
126
|
+
<GitCompareArrows size={11} /> Diff
|
|
127
|
+
</button>
|
|
128
|
+
</div>
|
|
129
|
+
)}
|
|
130
|
+
</div>
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export function WorkspaceMode() {
|
|
135
|
+
const agents = useGrooveStore((s) => s.agents);
|
|
136
|
+
const activeTeamId = useGrooveStore((s) => s.activeTeamId);
|
|
137
|
+
const workspaceAgentId = useGrooveStore((s) => s.workspaceAgentId);
|
|
138
|
+
const setWorkspaceAgent = useGrooveStore((s) => s.setWorkspaceAgent);
|
|
139
|
+
const workspaceReviewMode = useGrooveStore((s) => s.workspaceReviewMode);
|
|
140
|
+
const toggleReviewMode = useGrooveStore((s) => s.toggleReviewMode);
|
|
141
|
+
const workspaceSnapshots = useGrooveStore((s) => s.workspaceSnapshots);
|
|
142
|
+
|
|
143
|
+
const editorFiles = useGrooveStore((s) => s.editorFiles);
|
|
144
|
+
const editorActiveFile = useGrooveStore((s) => s.editorActiveFile);
|
|
145
|
+
const editorOpenTabs = useGrooveStore((s) => s.editorOpenTabs);
|
|
146
|
+
const editorChangedFiles = useGrooveStore((s) => s.editorChangedFiles);
|
|
147
|
+
const setActiveFile = useGrooveStore((s) => s.setActiveFile);
|
|
148
|
+
const closeFile = useGrooveStore((s) => s.closeFile);
|
|
149
|
+
const updateFileContent = useGrooveStore((s) => s.updateFileContent);
|
|
150
|
+
const saveFile = useGrooveStore((s) => s.saveFile);
|
|
151
|
+
const reloadFile = useGrooveStore((s) => s.reloadFile);
|
|
152
|
+
const dismissFileChange = useGrooveStore((s) => s.dismissFileChange);
|
|
153
|
+
|
|
154
|
+
const teamAgents = agents.filter((a) => a.teamId === activeTeamId);
|
|
155
|
+
const agent = teamAgents.find((a) => a.id === workspaceAgentId) || teamAgents[0];
|
|
156
|
+
|
|
157
|
+
const [treeWidth, setTreeWidth] = useState(TREE_DEFAULT);
|
|
158
|
+
const [rightWidth, setRightWidth] = useState(RIGHT_DEFAULT);
|
|
159
|
+
const [diffMode, setDiffMode] = useState(false);
|
|
160
|
+
const [rightTab, setRightTab] = useState('chat');
|
|
161
|
+
|
|
162
|
+
const treeDragging = useRef(false);
|
|
163
|
+
const rightDragging = useRef(false);
|
|
164
|
+
const startX = useRef(0);
|
|
165
|
+
const startW = useRef(0);
|
|
166
|
+
|
|
167
|
+
useEffect(() => {
|
|
168
|
+
setDiffMode(false);
|
|
169
|
+
}, [editorActiveFile]);
|
|
170
|
+
|
|
171
|
+
const onTreeMouseDown = useCallback((e) => {
|
|
172
|
+
e.preventDefault();
|
|
173
|
+
treeDragging.current = true;
|
|
174
|
+
startX.current = e.clientX;
|
|
175
|
+
startW.current = treeWidth;
|
|
176
|
+
function onMove(e) {
|
|
177
|
+
if (!treeDragging.current) return;
|
|
178
|
+
setTreeWidth(Math.min(Math.max(startW.current + e.clientX - startX.current, TREE_MIN), TREE_MAX));
|
|
179
|
+
}
|
|
180
|
+
function onUp() {
|
|
181
|
+
treeDragging.current = false;
|
|
182
|
+
document.removeEventListener('mousemove', onMove);
|
|
183
|
+
document.removeEventListener('mouseup', onUp);
|
|
184
|
+
}
|
|
185
|
+
document.addEventListener('mousemove', onMove);
|
|
186
|
+
document.addEventListener('mouseup', onUp);
|
|
187
|
+
}, [treeWidth]);
|
|
188
|
+
|
|
189
|
+
const onRightMouseDown = useCallback((e) => {
|
|
190
|
+
e.preventDefault();
|
|
191
|
+
rightDragging.current = true;
|
|
192
|
+
startX.current = e.clientX;
|
|
193
|
+
startW.current = rightWidth;
|
|
194
|
+
function onMove(e) {
|
|
195
|
+
if (!rightDragging.current) return;
|
|
196
|
+
setRightWidth(Math.min(Math.max(startW.current - (e.clientX - startX.current), RIGHT_MIN), RIGHT_MAX));
|
|
197
|
+
}
|
|
198
|
+
function onUp() {
|
|
199
|
+
rightDragging.current = false;
|
|
200
|
+
document.removeEventListener('mousemove', onMove);
|
|
201
|
+
document.removeEventListener('mouseup', onUp);
|
|
202
|
+
}
|
|
203
|
+
document.addEventListener('mousemove', onMove);
|
|
204
|
+
document.addEventListener('mouseup', onUp);
|
|
205
|
+
}, [rightWidth]);
|
|
206
|
+
|
|
207
|
+
if (!agent) {
|
|
208
|
+
return (
|
|
209
|
+
<div className="flex items-center justify-center h-full text-text-4 text-xs font-sans">
|
|
210
|
+
No agents in this team
|
|
211
|
+
</div>
|
|
212
|
+
);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
const isAlive = agent.status === 'running' || agent.status === 'starting';
|
|
216
|
+
const ctxPct = Math.round((agent.contextUsage || 0) * 100);
|
|
217
|
+
const file = editorActiveFile ? editorFiles[editorActiveFile] : null;
|
|
218
|
+
const hasExternalChange = editorActiveFile && editorChangedFiles[editorActiveFile];
|
|
219
|
+
|
|
220
|
+
return (
|
|
221
|
+
<div className="flex h-full bg-surface-0">
|
|
222
|
+
{/* Left Rail — Agent Switcher */}
|
|
223
|
+
<AgentRail agents={teamAgents} activeId={agent.id} onSelect={setWorkspaceAgent} />
|
|
224
|
+
|
|
225
|
+
{/* Center Panel — File Tree + Editor */}
|
|
226
|
+
<div className="flex flex-1 min-w-0">
|
|
227
|
+
{/* File Tree Sidebar */}
|
|
228
|
+
<div className="flex-shrink-0 bg-surface-1 border-r border-border relative" style={{ width: treeWidth }}>
|
|
229
|
+
<AgentFileTree agentId={agent.id} />
|
|
230
|
+
<div
|
|
231
|
+
className="absolute top-0 right-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
232
|
+
onMouseDown={onTreeMouseDown}
|
|
233
|
+
onDoubleClick={() => setTreeWidth(TREE_DEFAULT)}
|
|
234
|
+
/>
|
|
235
|
+
</div>
|
|
236
|
+
|
|
237
|
+
{/* Editor Area */}
|
|
238
|
+
<div className="flex-1 flex flex-col min-w-0">
|
|
239
|
+
{workspaceReviewMode ? (
|
|
240
|
+
<CodeReview agentId={agent.id} />
|
|
241
|
+
) : (
|
|
242
|
+
<>
|
|
243
|
+
<TabBar
|
|
244
|
+
tabs={editorOpenTabs}
|
|
245
|
+
activeFile={editorActiveFile}
|
|
246
|
+
files={editorFiles}
|
|
247
|
+
onSelect={setActiveFile}
|
|
248
|
+
onClose={closeFile}
|
|
249
|
+
diffMode={diffMode}
|
|
250
|
+
onToggleDiff={setDiffMode}
|
|
251
|
+
workspaceSnapshots={workspaceSnapshots}
|
|
252
|
+
/>
|
|
253
|
+
|
|
254
|
+
<div className="flex-1 relative min-h-0">
|
|
255
|
+
{hasExternalChange && (
|
|
256
|
+
<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">
|
|
257
|
+
<AlertTriangle size={14} className="text-warning" />
|
|
258
|
+
<span className="text-xs text-warning font-sans flex-1">File modified externally</span>
|
|
259
|
+
<button
|
|
260
|
+
onClick={() => reloadFile(editorActiveFile)}
|
|
261
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-text-1 hover:bg-surface-4 rounded cursor-pointer"
|
|
262
|
+
>
|
|
263
|
+
<RefreshCw size={12} /> Reload
|
|
264
|
+
</button>
|
|
265
|
+
<button
|
|
266
|
+
onClick={() => dismissFileChange(editorActiveFile)}
|
|
267
|
+
className="flex items-center gap-1 px-2 py-1 text-xs text-text-3 hover:bg-surface-4 rounded cursor-pointer"
|
|
268
|
+
>
|
|
269
|
+
<X size={12} /> Dismiss
|
|
270
|
+
</button>
|
|
271
|
+
</div>
|
|
272
|
+
)}
|
|
273
|
+
|
|
274
|
+
{!editorActiveFile && (
|
|
275
|
+
<div className="w-full h-full flex items-center justify-center text-text-4 font-sans">
|
|
276
|
+
<div className="text-center space-y-2">
|
|
277
|
+
<Code2 size={32} className="mx-auto" />
|
|
278
|
+
<p className="text-sm">Open a file from the tree</p>
|
|
279
|
+
<p className="text-2xs text-text-4">Files scoped to {agent.name}</p>
|
|
280
|
+
</div>
|
|
281
|
+
</div>
|
|
282
|
+
)}
|
|
283
|
+
|
|
284
|
+
{editorActiveFile && diffMode && (
|
|
285
|
+
<DiffViewer filePath={editorActiveFile} />
|
|
286
|
+
)}
|
|
287
|
+
|
|
288
|
+
{editorActiveFile && !diffMode && file && (
|
|
289
|
+
<CodeEditor
|
|
290
|
+
content={file.content}
|
|
291
|
+
language={file.language}
|
|
292
|
+
onChange={(content) => updateFileContent(editorActiveFile, content)}
|
|
293
|
+
onSave={() => saveFile(editorActiveFile)}
|
|
294
|
+
/>
|
|
295
|
+
)}
|
|
296
|
+
</div>
|
|
297
|
+
</>
|
|
298
|
+
)}
|
|
299
|
+
</div>
|
|
300
|
+
</div>
|
|
301
|
+
|
|
302
|
+
{/* Right Panel — Chat + Activity */}
|
|
303
|
+
<div className="flex flex-col bg-surface-1 border-l border-border relative" style={{ width: rightWidth }}>
|
|
304
|
+
{/* Resize handle */}
|
|
305
|
+
<div
|
|
306
|
+
className="absolute top-0 left-0 bottom-0 w-1 cursor-col-resize hover:bg-accent/30 transition-colors z-10"
|
|
307
|
+
onMouseDown={onRightMouseDown}
|
|
308
|
+
onDoubleClick={() => setRightWidth(RIGHT_DEFAULT)}
|
|
309
|
+
/>
|
|
310
|
+
|
|
311
|
+
{/* Header */}
|
|
312
|
+
<div className="flex items-center gap-2 px-4 py-2.5 border-b border-border flex-shrink-0">
|
|
313
|
+
<div className="flex-1 min-w-0">
|
|
314
|
+
<div className="flex items-center gap-2">
|
|
315
|
+
<span className="text-sm font-bold text-text-0 font-sans truncate">{agent.name}</span>
|
|
316
|
+
<Badge variant={STATUS_VARIANT[agent.status]} dot={isAlive ? 'pulse' : undefined}>
|
|
317
|
+
{agent.status}
|
|
318
|
+
</Badge>
|
|
319
|
+
</div>
|
|
320
|
+
<div className="flex items-center gap-3 mt-0.5">
|
|
321
|
+
<span className="text-2xs text-text-3 font-sans">
|
|
322
|
+
{fmtNum(agent.tokensUsed || 0)} tokens
|
|
323
|
+
</span>
|
|
324
|
+
<span className="text-2xs text-text-3 font-sans">
|
|
325
|
+
ctx {ctxPct}%
|
|
326
|
+
</span>
|
|
327
|
+
</div>
|
|
328
|
+
</div>
|
|
329
|
+
<button
|
|
330
|
+
onClick={toggleReviewMode}
|
|
331
|
+
className={cn(
|
|
332
|
+
'flex items-center gap-1 px-2 py-1 text-xs font-sans rounded cursor-pointer transition-colors',
|
|
333
|
+
workspaceReviewMode
|
|
334
|
+
? 'bg-accent/15 text-accent'
|
|
335
|
+
: 'text-text-3 hover:text-text-1 hover:bg-surface-3',
|
|
336
|
+
)}
|
|
337
|
+
title="Review Changes"
|
|
338
|
+
>
|
|
339
|
+
<ClipboardCheck size={13} />
|
|
340
|
+
</button>
|
|
341
|
+
</div>
|
|
342
|
+
|
|
343
|
+
{/* Tab switcher */}
|
|
344
|
+
<div className="flex items-center gap-0 border-b border-border-subtle flex-shrink-0">
|
|
345
|
+
<button
|
|
346
|
+
onClick={() => setRightTab('chat')}
|
|
347
|
+
className={cn(
|
|
348
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs font-sans cursor-pointer transition-colors',
|
|
349
|
+
rightTab === 'chat'
|
|
350
|
+
? 'text-text-0 border-b border-b-accent font-medium'
|
|
351
|
+
: 'text-text-3 hover:text-text-1',
|
|
352
|
+
)}
|
|
353
|
+
>
|
|
354
|
+
<MessageSquare size={12} /> Chat
|
|
355
|
+
</button>
|
|
356
|
+
<button
|
|
357
|
+
onClick={() => setRightTab('activity')}
|
|
358
|
+
className={cn(
|
|
359
|
+
'flex items-center gap-1.5 px-3 py-2 text-xs font-sans cursor-pointer transition-colors',
|
|
360
|
+
rightTab === 'activity'
|
|
361
|
+
? 'text-text-0 border-b border-b-accent font-medium'
|
|
362
|
+
: 'text-text-3 hover:text-text-1',
|
|
363
|
+
)}
|
|
364
|
+
>
|
|
365
|
+
<Activity size={12} /> Activity
|
|
366
|
+
</button>
|
|
367
|
+
</div>
|
|
368
|
+
|
|
369
|
+
{/* Content */}
|
|
370
|
+
<div className="flex-1 min-h-0">
|
|
371
|
+
{rightTab === 'chat' && <AgentChat agent={agent} />}
|
|
372
|
+
{rightTab === 'activity' && <AgentFeed agent={agent} />}
|
|
373
|
+
</div>
|
|
374
|
+
</div>
|
|
375
|
+
</div>
|
|
376
|
+
);
|
|
377
|
+
}
|
|
@@ -1,10 +1,23 @@
|
|
|
1
1
|
// FSL-1.1-Apache-2.0 — see LICENSE
|
|
2
2
|
import { useState, useRef, useEffect, useCallback } from 'react';
|
|
3
|
-
import { Send, Loader2, Square, Paperclip, Image as ImageIcon } from 'lucide-react';
|
|
3
|
+
import { Send, Loader2, Square, Paperclip, Image as ImageIcon, Zap, Bot } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { formatModelName } from './model-picker';
|
|
6
6
|
|
|
7
|
-
|
|
7
|
+
const EFFORT_OPTIONS = [
|
|
8
|
+
{ value: 'none', label: 'None' },
|
|
9
|
+
{ value: 'low', label: 'Low' },
|
|
10
|
+
{ value: 'medium', label: 'Med' },
|
|
11
|
+
{ value: 'high', label: 'High' },
|
|
12
|
+
{ value: 'xhigh', label: 'XHigh' },
|
|
13
|
+
];
|
|
14
|
+
|
|
15
|
+
const VERBOSITY_OPTIONS = [
|
|
16
|
+
{ value: 'low', label: 'Concise' },
|
|
17
|
+
{ value: 'medium', label: 'Normal' },
|
|
18
|
+
];
|
|
19
|
+
|
|
20
|
+
export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, replyContext, onClearReply, role, isCodex, reasoningEffort, onReasoningEffortChange, verbosity, onVerbosityChange, mode, onModeChange }) {
|
|
8
21
|
const [input, setInput] = useState('');
|
|
9
22
|
const textareaRef = useRef(null);
|
|
10
23
|
const fileInputRef = useRef(null);
|
|
@@ -13,7 +26,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
13
26
|
const el = textareaRef.current;
|
|
14
27
|
if (!el) return;
|
|
15
28
|
el.style.height = 'auto';
|
|
16
|
-
el.style.height = Math.min(el.scrollHeight,
|
|
29
|
+
el.style.height = Math.min(el.scrollHeight, 400) + 'px';
|
|
17
30
|
}, []);
|
|
18
31
|
|
|
19
32
|
useEffect(() => {
|
|
@@ -51,6 +64,7 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
51
64
|
|
|
52
65
|
const isActive = streaming || sending;
|
|
53
66
|
const canSend = input.trim() && !sending && !disabled;
|
|
67
|
+
const currentMode = mode || 'api';
|
|
54
68
|
|
|
55
69
|
const placeholder = disabled
|
|
56
70
|
? 'Select a model to start chatting...'
|
|
@@ -72,21 +86,26 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
72
86
|
</div>
|
|
73
87
|
)}
|
|
74
88
|
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
89
|
+
<textarea
|
|
90
|
+
ref={textareaRef}
|
|
91
|
+
value={input}
|
|
92
|
+
onChange={(e) => setInput(e.target.value)}
|
|
93
|
+
onKeyDown={onKeyDown}
|
|
94
|
+
placeholder={placeholder}
|
|
95
|
+
disabled={disabled}
|
|
96
|
+
rows={1}
|
|
97
|
+
className={cn(
|
|
98
|
+
'w-full resize-y rounded-xl px-4 py-2.5 text-sm',
|
|
99
|
+
'bg-surface-0 border text-text-0 font-sans',
|
|
100
|
+
'placeholder:text-text-4',
|
|
101
|
+
'focus:outline-none focus:ring-1',
|
|
102
|
+
'min-h-[40px]',
|
|
103
|
+
'border-border focus:ring-accent/40',
|
|
104
|
+
'disabled:opacity-50 disabled:cursor-not-allowed',
|
|
105
|
+
)}
|
|
106
|
+
/>
|
|
88
107
|
|
|
89
|
-
<div className="flex items-
|
|
108
|
+
<div className="flex items-center gap-2 mt-2">
|
|
90
109
|
<input
|
|
91
110
|
ref={fileInputRef}
|
|
92
111
|
type="file"
|
|
@@ -98,35 +117,93 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
98
117
|
<button
|
|
99
118
|
onClick={() => fileInputRef.current?.click()}
|
|
100
119
|
disabled={disabled}
|
|
101
|
-
className="w-
|
|
120
|
+
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 disabled:opacity-30 disabled:cursor-not-allowed flex-shrink-0"
|
|
102
121
|
title="Attach file"
|
|
103
122
|
>
|
|
104
|
-
<Paperclip size={
|
|
123
|
+
<Paperclip size={14} />
|
|
105
124
|
</button>
|
|
106
125
|
|
|
107
|
-
<
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
126
|
+
<div className="flex items-center h-7 rounded-lg bg-surface-3 border border-border-subtle p-0.5">
|
|
127
|
+
<button
|
|
128
|
+
onClick={() => onModeChange?.('api')}
|
|
129
|
+
className={cn(
|
|
130
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
131
|
+
currentMode === 'api' ? 'bg-accent/15 text-accent border border-accent/25' : 'text-text-3 hover:text-text-1',
|
|
132
|
+
)}
|
|
133
|
+
title="Lightweight — fast and cheap, no tools"
|
|
134
|
+
>
|
|
135
|
+
<Zap size={11} /> Chat
|
|
136
|
+
</button>
|
|
137
|
+
<button
|
|
138
|
+
onClick={() => onModeChange?.('agent')}
|
|
139
|
+
className={cn(
|
|
140
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
141
|
+
currentMode === 'agent' ? 'bg-purple/15 text-purple border border-purple/25' : 'text-text-3 hover:text-text-1',
|
|
142
|
+
)}
|
|
143
|
+
title="Full agent — tools, files, session resume"
|
|
144
|
+
>
|
|
145
|
+
<Bot size={11} /> Agent
|
|
146
|
+
</button>
|
|
147
|
+
</div>
|
|
148
|
+
|
|
149
|
+
{currentModel && (
|
|
150
|
+
<div className={cn(
|
|
151
|
+
'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-mono border',
|
|
152
|
+
isImageModel
|
|
153
|
+
? 'bg-purple/8 border-purple/20 text-purple'
|
|
154
|
+
: 'bg-surface-3 border-border-subtle text-text-3',
|
|
155
|
+
)}>
|
|
156
|
+
{isImageModel && <ImageIcon size={9} />}
|
|
157
|
+
<span className="max-w-[80px] truncate">{formatModelName(currentModel)}</span>
|
|
158
|
+
</div>
|
|
159
|
+
)}
|
|
160
|
+
|
|
161
|
+
{isCodex && (
|
|
162
|
+
<>
|
|
163
|
+
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
164
|
+
{EFFORT_OPTIONS.map((opt) => (
|
|
165
|
+
<button
|
|
166
|
+
key={opt.value}
|
|
167
|
+
onClick={() => onReasoningEffortChange?.(opt.value)}
|
|
168
|
+
className={cn(
|
|
169
|
+
'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
170
|
+
reasoningEffort === opt.value
|
|
171
|
+
? 'bg-accent/15 text-accent'
|
|
172
|
+
: 'text-text-4 hover:text-text-1',
|
|
173
|
+
)}
|
|
174
|
+
title={`Reasoning: ${opt.label}`}
|
|
175
|
+
>
|
|
176
|
+
{opt.label}
|
|
177
|
+
</button>
|
|
178
|
+
))}
|
|
179
|
+
</div>
|
|
180
|
+
|
|
181
|
+
<div className="flex items-center h-6 rounded-md bg-surface-3 border border-border-subtle p-0.5">
|
|
182
|
+
{VERBOSITY_OPTIONS.map((opt) => (
|
|
183
|
+
<button
|
|
184
|
+
key={opt.value}
|
|
185
|
+
onClick={() => onVerbosityChange?.(opt.value)}
|
|
186
|
+
className={cn(
|
|
187
|
+
'h-5 px-1.5 rounded text-2xs font-semibold font-sans transition-colors cursor-pointer',
|
|
188
|
+
verbosity === opt.value
|
|
189
|
+
? 'bg-accent/15 text-accent'
|
|
190
|
+
: 'text-text-4 hover:text-text-1',
|
|
191
|
+
)}
|
|
192
|
+
title={`Verbosity: ${opt.label}`}
|
|
193
|
+
>
|
|
194
|
+
{opt.label}
|
|
195
|
+
</button>
|
|
196
|
+
))}
|
|
197
|
+
</div>
|
|
198
|
+
</>
|
|
199
|
+
)}
|
|
200
|
+
|
|
201
|
+
<div className="flex-1" />
|
|
125
202
|
|
|
126
203
|
{isActive ? (
|
|
127
204
|
<button
|
|
128
205
|
onClick={onStop}
|
|
129
|
-
className="w-
|
|
206
|
+
className="w-8 h-8 flex items-center justify-center rounded-lg bg-danger/80 text-white hover:bg-danger transition-all cursor-pointer shadow-lg shadow-danger/20 flex-shrink-0"
|
|
130
207
|
title="Stop generation"
|
|
131
208
|
>
|
|
132
209
|
<Square size={14} fill="currentColor" />
|
|
@@ -136,14 +213,14 @@ export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImag
|
|
|
136
213
|
onClick={handleSend}
|
|
137
214
|
disabled={!canSend}
|
|
138
215
|
className={cn(
|
|
139
|
-
'w-
|
|
216
|
+
'w-8 h-8 flex items-center justify-center rounded-lg transition-all cursor-pointer flex-shrink-0',
|
|
140
217
|
'disabled:opacity-20 disabled:cursor-not-allowed',
|
|
141
218
|
canSend
|
|
142
219
|
? 'bg-accent/15 text-accent hover:bg-accent/25 border border-accent/25'
|
|
143
220
|
: 'bg-surface-4 text-text-4',
|
|
144
221
|
)}
|
|
145
222
|
>
|
|
146
|
-
{sending ? <Loader2 size={
|
|
223
|
+
{sending ? <Loader2 size={14} className="animate-spin" /> : <Send size={14} />}
|
|
147
224
|
</button>
|
|
148
225
|
)}
|
|
149
226
|
</div>
|
|
@@ -3,7 +3,6 @@ import { useRef, useEffect, useState, useCallback } from 'react';
|
|
|
3
3
|
import { Copy, Check, ArrowRight, Download, Maximize2, X, Image as ImageIcon, RefreshCw } from 'lucide-react';
|
|
4
4
|
import { cn } from '../../lib/cn';
|
|
5
5
|
import { timeAgo } from '../../lib/format';
|
|
6
|
-
import { Avatar } from '../ui/avatar';
|
|
7
6
|
import { ThinkingIndicator } from '../ui/thinking-indicator';
|
|
8
7
|
|
|
9
8
|
const API_STATUS_MESSAGES = [
|
|
@@ -249,7 +248,7 @@ function UserMessage({ msg }) {
|
|
|
249
248
|
return (
|
|
250
249
|
<div className="flex justify-end">
|
|
251
250
|
<div className="max-w-[85%]">
|
|
252
|
-
<div className="px-3.5 py-2.5 rounded-2xl rounded-br-md bg-
|
|
251
|
+
<div className="px-3.5 py-2.5 rounded-2xl rounded-br-md bg-info/10 border border-info/15">
|
|
253
252
|
<p className="text-sm text-text-0 font-sans whitespace-pre-wrap break-words leading-relaxed">{msg.text}</p>
|
|
254
253
|
</div>
|
|
255
254
|
<div className="text-2xs text-text-4 font-sans mt-1 text-right">{timeAgo(msg.timestamp)}</div>
|
|
@@ -260,20 +259,18 @@ function UserMessage({ msg }) {
|
|
|
260
259
|
|
|
261
260
|
function AssistantMessage({ msg, model, role }) {
|
|
262
261
|
const cleanText = stripEmojis(msg.text);
|
|
263
|
-
const displayName =
|
|
264
|
-
|
|
262
|
+
const displayName = role
|
|
263
|
+
? `${model || 'Assistant'} ${role.charAt(0).toUpperCase() + role.slice(1)}`
|
|
264
|
+
: (model || 'Assistant');
|
|
265
265
|
return (
|
|
266
|
-
<div className="
|
|
267
|
-
<
|
|
268
|
-
<div className="
|
|
269
|
-
<div className="text-
|
|
270
|
-
|
|
271
|
-
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
272
|
-
<RenderedMarkdown text={cleanText} />
|
|
273
|
-
</div>
|
|
266
|
+
<div className="max-w-[85%]">
|
|
267
|
+
<div className="text-2xs text-text-3 font-sans mb-1 font-medium">{displayName}</div>
|
|
268
|
+
<div className="border-l-2 border-accent/40 pl-3.5 py-1">
|
|
269
|
+
<div className="text-sm text-text-1 font-sans whitespace-pre-wrap break-words leading-relaxed">
|
|
270
|
+
<RenderedMarkdown text={cleanText} />
|
|
274
271
|
</div>
|
|
275
|
-
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
276
272
|
</div>
|
|
273
|
+
<div className="text-2xs text-text-4 font-sans mt-1">{timeAgo(msg.timestamp)}</div>
|
|
277
274
|
</div>
|
|
278
275
|
);
|
|
279
276
|
}
|