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.
Files changed (53) hide show
  1. package/CLAUDE.md +3 -2
  2. package/node_modules/@groove-dev/cli/package.json +1 -1
  3. package/node_modules/@groove-dev/daemon/package.json +1 -1
  4. package/node_modules/@groove-dev/daemon/src/api.js +115 -7
  5. package/node_modules/@groove-dev/daemon/src/conversations.js +29 -3
  6. package/node_modules/@groove-dev/daemon/src/providers/codex.js +28 -10
  7. package/node_modules/@groove-dev/daemon/src/registry.js +30 -0
  8. package/node_modules/@groove-dev/daemon/src/validate.js +23 -0
  9. package/node_modules/@groove-dev/gui/dist/assets/index-BSqk8cbI.css +1 -0
  10. package/node_modules/@groove-dev/gui/dist/assets/index-B_igwWvq.js +8642 -0
  11. package/node_modules/@groove-dev/gui/dist/index.html +2 -2
  12. package/node_modules/@groove-dev/gui/package.json +1 -1
  13. package/node_modules/@groove-dev/gui/src/components/agents/agent-file-tree.jsx +254 -0
  14. package/node_modules/@groove-dev/gui/src/components/agents/code-review.jsx +177 -0
  15. package/node_modules/@groove-dev/gui/src/components/agents/diff-viewer.jsx +148 -0
  16. package/node_modules/@groove-dev/gui/src/components/agents/workspace-mode.jsx +377 -0
  17. package/node_modules/@groove-dev/gui/src/components/chat/chat-input.jsx +117 -40
  18. package/node_modules/@groove-dev/gui/src/components/chat/chat-messages.jsx +10 -13
  19. package/node_modules/@groove-dev/gui/src/components/chat/chat-view.jsx +26 -1
  20. package/node_modules/@groove-dev/gui/src/components/chat/conversation-list.jsx +14 -14
  21. package/node_modules/@groove-dev/gui/src/components/chat/model-picker.jsx +5 -0
  22. package/node_modules/@groove-dev/gui/src/stores/groove.js +132 -1
  23. package/node_modules/@groove-dev/gui/src/views/agents.jsx +22 -3
  24. package/package.json +1 -1
  25. package/packages/cli/package.json +1 -1
  26. package/packages/daemon/package.json +1 -1
  27. package/packages/daemon/src/api.js +115 -7
  28. package/packages/daemon/src/conversations.js +29 -3
  29. package/packages/daemon/src/providers/codex.js +28 -10
  30. package/packages/daemon/src/registry.js +30 -0
  31. package/packages/daemon/src/validate.js +23 -0
  32. package/packages/gui/dist/assets/index-BSqk8cbI.css +1 -0
  33. package/packages/gui/dist/assets/index-B_igwWvq.js +8642 -0
  34. package/packages/gui/dist/index.html +2 -2
  35. package/packages/gui/package.json +1 -1
  36. package/packages/gui/src/components/agents/agent-file-tree.jsx +254 -0
  37. package/packages/gui/src/components/agents/code-review.jsx +177 -0
  38. package/packages/gui/src/components/agents/diff-viewer.jsx +148 -0
  39. package/packages/gui/src/components/agents/workspace-mode.jsx +377 -0
  40. package/packages/gui/src/components/chat/chat-input.jsx +117 -40
  41. package/packages/gui/src/components/chat/chat-messages.jsx +10 -13
  42. package/packages/gui/src/components/chat/chat-view.jsx +26 -1
  43. package/packages/gui/src/components/chat/conversation-list.jsx +14 -14
  44. package/packages/gui/src/components/chat/model-picker.jsx +5 -0
  45. package/packages/gui/src/stores/groove.js +132 -1
  46. package/packages/gui/src/views/agents.jsx +22 -3
  47. package/test/doomsday-clock/index.html +55 -0
  48. package/test/doomsday-clock/script.js +66 -0
  49. package/test/doomsday-clock/style.css +315 -0
  50. package/node_modules/@groove-dev/gui/dist/assets/index-BCQY8ojz.css +0 -1
  51. package/node_modules/@groove-dev/gui/dist/assets/index-C5e7KVGN.js +0 -8637
  52. package/packages/gui/dist/assets/index-BCQY8ojz.css +0 -1
  53. 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
- export function ChatInput({ onSend, onStop, sending, streaming, disabled, isImageModel, currentModel, replyContext, onClearReply, role }) {
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, 200) + 'px';
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
- {currentModel && (
76
- <div className="flex items-center gap-2 mb-2">
77
- <div className={cn(
78
- 'flex items-center gap-1 h-6 px-2 rounded-md text-2xs font-mono border',
79
- isImageModel
80
- ? 'bg-purple/8 border-purple/20 text-purple'
81
- : 'bg-surface-3 border-border-subtle text-text-3',
82
- )}>
83
- {isImageModel && <ImageIcon size={9} />}
84
- <span className="max-w-[80px] truncate">{formatModelName(currentModel)}</span>
85
- </div>
86
- </div>
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-end gap-2">
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-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 disabled:opacity-30 disabled:cursor-not-allowed flex-shrink-0"
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={16} />
123
+ <Paperclip size={14} />
105
124
  </button>
106
125
 
107
- <textarea
108
- ref={textareaRef}
109
- value={input}
110
- onChange={(e) => setInput(e.target.value)}
111
- onKeyDown={onKeyDown}
112
- placeholder={placeholder}
113
- disabled={disabled}
114
- rows={1}
115
- className={cn(
116
- 'flex-1 resize-y rounded-xl px-4 py-2.5 text-sm',
117
- 'bg-surface-0 border text-text-0 font-sans',
118
- 'placeholder:text-text-4',
119
- 'focus:outline-none focus:ring-1',
120
- 'min-h-[40px]',
121
- 'border-border focus:ring-accent/40',
122
- 'disabled:opacity-50 disabled:cursor-not-allowed',
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-10 h-10 flex items-center justify-center rounded-xl bg-danger/80 text-white hover:bg-danger transition-all cursor-pointer shadow-lg shadow-danger/20 flex-shrink-0"
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-10 h-10 flex items-center justify-center rounded-xl transition-all cursor-pointer flex-shrink-0',
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={16} className="animate-spin" /> : <Send size={16} />}
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-accent/10 border border-accent/15">
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 = model || 'Assistant';
264
- const avatarRole = role || 'chat';
262
+ const displayName = role
263
+ ? `${model || 'Assistant'} ${role.charAt(0).toUpperCase() + role.slice(1)}`
264
+ : (model || 'Assistant');
265
265
  return (
266
- <div className="flex gap-2.5">
267
- <Avatar name={displayName} role={avatarRole} size="sm" className="mt-1 flex-shrink-0" />
268
- <div className="max-w-[85%]">
269
- <div className="text-2xs text-text-3 font-sans mb-1 font-medium">{displayName}</div>
270
- <div className="border-l-2 border-accent/40 pl-3.5 py-1">
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
  }