repoburg 1.3.196 → 1.3.197
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/agent-pane/backend/data.db-shm +0 -0
- package/agent-pane/backend/data.db-wal +0 -0
- package/agent-pane/backend/dist/index.js +13399 -10390
- package/agent-pane/backend/package-lock.json +162 -92
- package/agent-pane/backend/scripts/backfill-from-jsonl.ts +206 -0
- package/agent-pane/backend/scripts/import-session-from-jsonl.ts +264 -0
- package/agent-pane/backend/src/claude/claude-message-mapper.test.ts +116 -8
- package/agent-pane/backend/src/claude/claude-message-mapper.ts +114 -9
- package/agent-pane/backend/src/claude/claude-provider.ts +918 -84
- package/agent-pane/backend/src/claude/claude-types.ts +16 -4
- package/agent-pane/backend/src/codex/app-server-client.ts +22 -0
- package/agent-pane/backend/src/db/index.ts +102 -0
- package/agent-pane/backend/src/db/schema.ts +4 -0
- package/agent-pane/backend/src/index.ts +3 -1
- package/agent-pane/backend/src/integration/helpers/test-app.ts +2 -0
- package/agent-pane/backend/src/integration/session-operations.test.ts +18 -11
- package/agent-pane/backend/src/integration/threads.test.ts +309 -0
- package/agent-pane/backend/src/providers/runtime-provider.ts +19 -1
- package/agent-pane/backend/src/routes/providers.ts +11 -0
- package/agent-pane/backend/src/routes/sessions.test.ts +143 -9
- package/agent-pane/backend/src/routes/sessions.ts +133 -28
- package/agent-pane/backend/src/routes/threads.ts +308 -0
- package/agent-pane/backend/src/routes/turns.ts +32 -9
- package/agent-pane/frontend/dist/assets/index-BYo-lnxm.js +362 -0
- package/agent-pane/frontend/dist/assets/index-Dwwhx7YN.css +1 -0
- package/agent-pane/frontend/dist/index.html +2 -2
- package/agent-pane/frontend/package-lock.json +51 -9
- package/agent-pane/frontend/src/__tests__/ThreadsPanel.test.tsx +192 -0
- package/agent-pane/frontend/src/api/providers.ts +6 -1
- package/agent-pane/frontend/src/api/sessions.test.ts +4 -0
- package/agent-pane/frontend/src/api/sessions.ts +13 -5
- package/agent-pane/frontend/src/api/websocket.ts +65 -1
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.test.tsx +4 -4
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +13 -4
- package/agent-pane/frontend/src/components/CommandInputBar.test.tsx +2 -2
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +292 -29
- package/agent-pane/frontend/src/components/CommandPalette.test.tsx +0 -4
- package/agent-pane/frontend/src/components/CommandPalette.tsx +1 -17
- package/agent-pane/frontend/src/components/CommentableText.tsx +291 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +9 -2
- package/agent-pane/frontend/src/components/RightPanel.test.tsx +5 -5
- package/agent-pane/frontend/src/components/RightPanel.tsx +20 -14
- package/agent-pane/frontend/src/components/SessionTabs.tsx +7 -2
- package/agent-pane/frontend/src/components/StatusBar.tsx +68 -1
- package/agent-pane/frontend/src/components/TurnContent.test.tsx +29 -18
- package/agent-pane/frontend/src/components/TurnContent.tsx +199 -28
- package/agent-pane/frontend/src/components/input/SlashPopup.tsx +24 -5
- package/agent-pane/frontend/src/components/panels/TasksPanel.test.tsx +97 -0
- package/agent-pane/frontend/src/components/panels/TasksPanel.tsx +208 -0
- package/agent-pane/frontend/src/components/panels/ThreadsPanel.tsx +350 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +47 -25
- package/agent-pane/frontend/src/hooks/useSessionStatus.test.ts +2 -0
- package/agent-pane/frontend/src/store/sessionSlice.ts +29 -6
- package/agent-pane/frontend/src/store/store.test.ts +5 -5
- package/agent-pane/frontend/src/store/store.ts +4 -1
- package/agent-pane/frontend/src/store/threadSlice.ts +136 -0
- package/agent-pane/frontend/src/store/wsSlice.ts +72 -3
- package/agent-pane/frontend/src/styles/app.css +150 -19
- package/agent-pane/frontend/src/test/fixtures.ts +4 -0
- package/agent-pane/frontend/src/types/index.ts +5 -2
- package/agent-pane/frontend/src/utils/duration.test.ts +4 -0
- package/agent-pane/frontend/vite.config.ts +2 -2
- package/agent-pane/shared/provider-types.ts +14 -0
- package/agent-pane/shared/types.ts +60 -0
- package/package.json +1 -1
- package/agent-pane/frontend/dist/assets/index-Bg0PF2aV.css +0 -1
- package/agent-pane/frontend/dist/assets/index-BlybeX5e.js +0 -281
- package/agent-pane/frontend/src/components/panels/LogsPanel.test.tsx +0 -58
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +0 -25
- package/agent-pane/frontend/src/components/panels/MetricsPanel.test.tsx +0 -71
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +0 -114
|
@@ -1,12 +1,70 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
3
|
import { memo, useState, useEffect } from 'react';
|
|
4
|
-
import {
|
|
5
|
-
|
|
4
|
+
import {
|
|
5
|
+
Layers, ChevronRight, Brain, Terminal, SatelliteDish, Bot, Cog,
|
|
6
|
+
FilePlus, Plug, Search, ListTodo, Minimize2, CircleAlert, Check, Square, FileText, X,
|
|
7
|
+
} from 'lucide-react';
|
|
8
|
+
import type { Turn, Item, DiscussionThread, Attachment } from '../types/index.ts';
|
|
9
|
+
import { runtimeIcon } from '../types/index.ts';
|
|
6
10
|
import CollapsibleToolCalls, { isToolItem } from './CollapsibleToolCalls.tsx';
|
|
11
|
+
import CommentableText from './CommentableText.tsx';
|
|
7
12
|
|
|
8
13
|
marked.setOptions({ breaks: true, gfm: true });
|
|
9
14
|
|
|
15
|
+
/** Compact inline attachment pills shown below the user prompt in the render area. */
|
|
16
|
+
function AttachmentPills({ attachments }: { attachments: Attachment[] }) {
|
|
17
|
+
const [previewSrc, setPreviewSrc] = useState<string | null>(null);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
if (!previewSrc) return;
|
|
21
|
+
function onKey(e: KeyboardEvent) {
|
|
22
|
+
if (e.key === 'Escape') setPreviewSrc(null);
|
|
23
|
+
}
|
|
24
|
+
window.addEventListener('keydown', onKey);
|
|
25
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
26
|
+
}, [previewSrc]);
|
|
27
|
+
|
|
28
|
+
if (!Array.isArray(attachments) || attachments.length === 0) return null;
|
|
29
|
+
|
|
30
|
+
return (
|
|
31
|
+
<>
|
|
32
|
+
<div className="attachment-pills">
|
|
33
|
+
{attachments.map((att, i) => {
|
|
34
|
+
const isImage = att.media_type.startsWith('image/');
|
|
35
|
+
const src = `data:${att.media_type};base64,${att.data}`;
|
|
36
|
+
return (
|
|
37
|
+
<span
|
|
38
|
+
key={i}
|
|
39
|
+
className="attachment-pill"
|
|
40
|
+
title={att.name}
|
|
41
|
+
onClick={isImage ? () => setPreviewSrc(src) : undefined}
|
|
42
|
+
style={isImage ? { cursor: 'pointer' } : undefined}
|
|
43
|
+
>
|
|
44
|
+
{isImage ? (
|
|
45
|
+
<img src={src} alt={att.name} className="attachment-pill-thumb" />
|
|
46
|
+
) : (
|
|
47
|
+
<FileText size={12} className="attachment-pill-icon" />
|
|
48
|
+
)}
|
|
49
|
+
<span className="attachment-pill-name">{att.name}</span>
|
|
50
|
+
</span>
|
|
51
|
+
);
|
|
52
|
+
})}
|
|
53
|
+
</div>
|
|
54
|
+
{previewSrc && (
|
|
55
|
+
<div className="image-preview-modal" onClick={() => setPreviewSrc(null)}>
|
|
56
|
+
<div className="image-preview-content" onClick={(e) => e.stopPropagation()}>
|
|
57
|
+
<img src={previewSrc} alt="preview" />
|
|
58
|
+
<button className="image-preview-close" onClick={() => setPreviewSrc(null)}>
|
|
59
|
+
<X size={20} />
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
)}
|
|
64
|
+
</>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
10
68
|
function renderMarkdown(content: string): string {
|
|
11
69
|
const raw = marked.parse(content) as string;
|
|
12
70
|
return DOMPurify.sanitize(raw);
|
|
@@ -28,20 +86,28 @@ function cleanCommand(cmd: string): string {
|
|
|
28
86
|
const MAX_OUTPUT = 10_000;
|
|
29
87
|
|
|
30
88
|
/** Render a single item as a turn card. */
|
|
31
|
-
function ItemCard({ item }: { item: Item }) {
|
|
89
|
+
function ItemCard({ item, runtimeType, turn, threads }: { item: Item; runtimeType?: string; turn: Turn; threads: DiscussionThread[] }) {
|
|
32
90
|
const isRunning = item.status === 'in_progress';
|
|
33
91
|
const [showFullOutput, setShowFullOutput] = useState(false);
|
|
34
92
|
|
|
35
93
|
switch (item.type) {
|
|
36
94
|
case 'agent_message': {
|
|
95
|
+
const { iconClass, iconText } = runtimeIcon(runtimeType ?? 'codex');
|
|
37
96
|
return (
|
|
38
97
|
<div className="render-content">
|
|
39
98
|
<div className="turn-header">
|
|
40
|
-
<span className=
|
|
99
|
+
<span className={`turn-header-icon ${iconClass}`}>{iconText}</span>
|
|
41
100
|
<span className="turn-header-title">Agent</span>
|
|
42
101
|
{isRunning && <span className="turn-header-meta">writing…</span>}
|
|
43
102
|
</div>
|
|
44
|
-
<
|
|
103
|
+
<CommentableText
|
|
104
|
+
itemId={item.id}
|
|
105
|
+
turnId={turn.id}
|
|
106
|
+
sessionId={turn.session_id}
|
|
107
|
+
content={itemText(item) || '…'}
|
|
108
|
+
threads={threads}
|
|
109
|
+
runtimeType={runtimeType}
|
|
110
|
+
/>
|
|
45
111
|
</div>
|
|
46
112
|
);
|
|
47
113
|
}
|
|
@@ -51,7 +117,7 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
51
117
|
<div className="render-content">
|
|
52
118
|
<div className="turn-header">
|
|
53
119
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
54
|
-
|
|
120
|
+
<Brain size={14} />
|
|
55
121
|
</span>
|
|
56
122
|
<span className="turn-header-title">Reasoning</span>
|
|
57
123
|
</div>
|
|
@@ -73,7 +139,7 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
73
139
|
<div className="render-content">
|
|
74
140
|
<div className="turn-header">
|
|
75
141
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
76
|
-
|
|
142
|
+
<Terminal size={14} />
|
|
77
143
|
</span>
|
|
78
144
|
<span className="turn-header-title">Command</span>
|
|
79
145
|
{summary && <span className="turn-header-meta">{summary}</span>}
|
|
@@ -98,13 +164,14 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
98
164
|
}
|
|
99
165
|
|
|
100
166
|
case 'file_change': {
|
|
101
|
-
const
|
|
167
|
+
const rawChanges = item.data?.changes;
|
|
168
|
+
const changes = Array.isArray(rawChanges) ? (rawChanges as Array<{ path: string; kind: string }>) : [];
|
|
102
169
|
const summary = `${changes.length} file${changes.length !== 1 ? 's' : ''}`;
|
|
103
170
|
return (
|
|
104
171
|
<div className="render-content">
|
|
105
172
|
<div className="turn-header">
|
|
106
173
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
107
|
-
|
|
174
|
+
<FilePlus size={14} />
|
|
108
175
|
</span>
|
|
109
176
|
<span className="turn-header-title">File Changes</span>
|
|
110
177
|
<span className="turn-header-meta">{summary}</span>
|
|
@@ -128,6 +195,24 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
128
195
|
case 'mcp_tool_call': {
|
|
129
196
|
const data = item.data || {};
|
|
130
197
|
const tool = (data.tool as string) || 'mcp';
|
|
198
|
+
|
|
199
|
+
// Compact status card for Agent calls (non-clickable — subagent view removed)
|
|
200
|
+
if (tool === 'Agent') {
|
|
201
|
+
const args = data.arguments as Record<string, unknown> | null;
|
|
202
|
+
const desc = (args?.description as string) || (args?.prompt as string) || 'Subagent';
|
|
203
|
+
const isFailed = item.status === 'failed';
|
|
204
|
+
const summary = isRunning ? 'running…' : isFailed ? 'failed' : 'done';
|
|
205
|
+
return (
|
|
206
|
+
<div className="agent-call-card">
|
|
207
|
+
<span className="agent-call-icon"><Bot size={14} /></span>
|
|
208
|
+
<span className="agent-call-desc">{desc}</span>
|
|
209
|
+
<span className={`agent-call-status ${isRunning ? 'running' : isFailed ? 'failed' : 'done'}`}>
|
|
210
|
+
{summary}
|
|
211
|
+
</span>
|
|
212
|
+
</div>
|
|
213
|
+
);
|
|
214
|
+
}
|
|
215
|
+
|
|
131
216
|
const server = (data.server as string) || '';
|
|
132
217
|
const args = data.arguments as Record<string, unknown> | null;
|
|
133
218
|
const result = data.result as {
|
|
@@ -142,7 +227,7 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
142
227
|
<div className="render-content">
|
|
143
228
|
<div className="turn-header">
|
|
144
229
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
145
|
-
|
|
230
|
+
<Plug size={14} />
|
|
146
231
|
</span>
|
|
147
232
|
<span className="turn-header-title">MCP: {tool}</span>
|
|
148
233
|
{server && <span className="turn-header-meta">{server}</span>}
|
|
@@ -191,7 +276,7 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
191
276
|
<div className="render-content">
|
|
192
277
|
<div className="turn-header">
|
|
193
278
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
194
|
-
|
|
279
|
+
<Search size={14} />
|
|
195
280
|
</span>
|
|
196
281
|
<span className="turn-header-title">Web Search</span>
|
|
197
282
|
</div>
|
|
@@ -206,14 +291,14 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
206
291
|
<div className="render-content">
|
|
207
292
|
<div className="turn-header">
|
|
208
293
|
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
209
|
-
|
|
294
|
+
<ListTodo size={14} />
|
|
210
295
|
</span>
|
|
211
296
|
<span className="turn-header-title">Todo List</span>
|
|
212
297
|
</div>
|
|
213
298
|
<ul className="todo-list">
|
|
214
299
|
{items.map((t, i) => (
|
|
215
300
|
<li key={i} className={t.completed ? 'todo-done' : 'todo-pending'}>
|
|
216
|
-
{t.completed ?
|
|
301
|
+
{t.completed ? <Check size={14} /> : <Square size={14} />} {t.text}
|
|
217
302
|
</li>
|
|
218
303
|
))}
|
|
219
304
|
</ul>
|
|
@@ -221,13 +306,73 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
221
306
|
);
|
|
222
307
|
}
|
|
223
308
|
|
|
309
|
+
case 'background_task': {
|
|
310
|
+
const data = item.data || {};
|
|
311
|
+
const taskType = (data.task_type as string) || 'unknown';
|
|
312
|
+
const subagentType = (data.subagent_type as string) || null;
|
|
313
|
+
const desc = (data.description as string) || '';
|
|
314
|
+
const taskStatus = (data.status as string) || 'running';
|
|
315
|
+
const lastTool = (data.last_tool_name as string) || null;
|
|
316
|
+
const tokenCount = (data.token_count as number) || null;
|
|
317
|
+
const isSubagent = taskType === 'local_agent';
|
|
318
|
+
const isMonitor = taskType === 'local_bash';
|
|
319
|
+
const TaskIcon = isSubagent ? Bot : isMonitor ? SatelliteDish : Cog;
|
|
320
|
+
const typeLabel = isSubagent ? `Subagent${subagentType ? `: ${subagentType}` : ''}` : isMonitor ? 'Monitor' : 'Background Task';
|
|
321
|
+
const statusLabel = taskStatus === 'running' ? 'running…' : taskStatus === 'completed' ? 'done' : taskStatus === 'failed' ? 'failed' : taskStatus === 'stopped' ? 'stopped' : taskStatus;
|
|
322
|
+
const isFailed = item.status === 'failed' || taskStatus === 'failed';
|
|
323
|
+
return (
|
|
324
|
+
<div className="render-content">
|
|
325
|
+
<div className="turn-header">
|
|
326
|
+
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
327
|
+
<TaskIcon size={14} />
|
|
328
|
+
</span>
|
|
329
|
+
<span className="turn-header-title">{typeLabel}</span>
|
|
330
|
+
<span className="turn-header-meta" style={isFailed ? { color: 'var(--red)' } : undefined}>
|
|
331
|
+
{statusLabel}
|
|
332
|
+
</span>
|
|
333
|
+
</div>
|
|
334
|
+
{desc && <p className="user-message" style={{ fontSize: '13px' }}>{desc}</p>}
|
|
335
|
+
{lastTool && isRunning && (
|
|
336
|
+
<div className="cmd-output" style={{ color: 'var(--text-3)', fontSize: '12px' }}>
|
|
337
|
+
last tool: {lastTool}
|
|
338
|
+
{tokenCount != null && ` · ${tokenCount.toLocaleString()} tokens`}
|
|
339
|
+
</div>
|
|
340
|
+
)}
|
|
341
|
+
<div className="cmd-output" style={{ color: 'var(--text-muted)', fontSize: '11px' }}>
|
|
342
|
+
Output available in Tasks panel — click the status bar indicator or the Tasks tab.
|
|
343
|
+
</div>
|
|
344
|
+
</div>
|
|
345
|
+
);
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
case 'context_compaction': {
|
|
349
|
+
const trigger = (item.data?.trigger as string) ?? 'auto';
|
|
350
|
+
const pre = (item.data?.pre_tokens as number) ?? 0;
|
|
351
|
+
const post = (item.data?.post_tokens as number) ?? 0;
|
|
352
|
+
const reduction = pre > 0 ? Math.round((1 - post / pre) * 100) : 0;
|
|
353
|
+
const label = trigger === 'manual' ? 'Context compacted (/compact)' : 'Context auto-compacted';
|
|
354
|
+
return (
|
|
355
|
+
<div className="render-content">
|
|
356
|
+
<div className="turn-header">
|
|
357
|
+
<span className="turn-header-icon" style={{ background: 'var(--surface-2)', color: 'var(--text-3)' }}>
|
|
358
|
+
<Minimize2 size={14} />
|
|
359
|
+
</span>
|
|
360
|
+
<span className="turn-header-title">{label}</span>
|
|
361
|
+
</div>
|
|
362
|
+
<div className="cmd-output" style={{ color: 'var(--text-3)' }}>
|
|
363
|
+
{pre.toLocaleString()} → {post.toLocaleString()} tokens ({reduction}% reduction)
|
|
364
|
+
</div>
|
|
365
|
+
</div>
|
|
366
|
+
);
|
|
367
|
+
}
|
|
368
|
+
|
|
224
369
|
case 'error': {
|
|
225
370
|
const msg = (item.data?.message as string) || 'Unknown error';
|
|
226
371
|
return (
|
|
227
372
|
<div className="render-content">
|
|
228
373
|
<div className="turn-header">
|
|
229
374
|
<span className="turn-header-icon" style={{ background: 'var(--red)', color: '#fff' }}>
|
|
230
|
-
|
|
375
|
+
<CircleAlert size={14} />
|
|
231
376
|
</span>
|
|
232
377
|
<span className="turn-header-title">Error</span>
|
|
233
378
|
</div>
|
|
@@ -248,21 +393,28 @@ export { MemoItemCard };
|
|
|
248
393
|
|
|
249
394
|
interface Props {
|
|
250
395
|
turn: Turn;
|
|
396
|
+
runtimeType?: string;
|
|
397
|
+
threads?: DiscussionThread[];
|
|
251
398
|
}
|
|
252
399
|
|
|
253
|
-
function TurnContentBase({ turn }: Props) {
|
|
400
|
+
function TurnContentBase({ turn, runtimeType, threads = [] }: Props) {
|
|
254
401
|
const [intermediateExpanded, setIntermediateExpanded] = useState(false);
|
|
255
402
|
|
|
256
|
-
// Auto-expand Level 1 when turn is running/pending, auto-collapse when completed
|
|
403
|
+
// Auto-expand Level 1 when turn is running/pending, auto-collapse when completed.
|
|
404
|
+
// Guard: don't auto-collapse if there are in_progress background_task items —
|
|
405
|
+
// Monitor tasks may still be running after the turn's result/success arrives.
|
|
257
406
|
useEffect(() => {
|
|
407
|
+
const hasRunningBgTask = turn.items.some(
|
|
408
|
+
(item) => item.type === 'background_task' && item.status === 'in_progress',
|
|
409
|
+
);
|
|
258
410
|
if (turn.status === 'running' || turn.status === 'pending') {
|
|
259
411
|
setIntermediateExpanded(true);
|
|
260
|
-
} else if (turn.status === 'completed') {
|
|
412
|
+
} else if (turn.status === 'completed' && !hasRunningBgTask) {
|
|
261
413
|
setIntermediateExpanded(false);
|
|
262
414
|
}
|
|
263
|
-
}, [turn.status]);
|
|
415
|
+
}, [turn.status, turn.items]);
|
|
264
416
|
|
|
265
|
-
// Find the last agent_message item — that is the final response
|
|
417
|
+
// Find the last agent_message item FROM THE MAIN AGENT — that is the final response.
|
|
266
418
|
let lastAgentIdx = -1;
|
|
267
419
|
for (let i = turn.items.length - 1; i >= 0; i--) {
|
|
268
420
|
if (turn.items[i].type === 'agent_message') {
|
|
@@ -275,6 +427,18 @@ function TurnContentBase({ turn }: Props) {
|
|
|
275
427
|
// Intermediate = everything before final agent_message (or ALL items if running)
|
|
276
428
|
const intermediateItems = hasFinalResponse ? turn.items.slice(0, lastAgentIdx) : turn.items;
|
|
277
429
|
|
|
430
|
+
// Filter out empty reasoning items (text is "" — signed-empty thinking blocks from Claude SDK).
|
|
431
|
+
// These inflate the intermediate step count and render as empty cards.
|
|
432
|
+
const visibleIntermediate = intermediateItems.filter(
|
|
433
|
+
(item) => !(item.type === 'reasoning' && (!item.data?.text || item.data.text === '')),
|
|
434
|
+
);
|
|
435
|
+
|
|
436
|
+
// Filter main flow: remove background_task items (they belong in Tasks panel).
|
|
437
|
+
const mainIntermediate = visibleIntermediate.filter((item) => {
|
|
438
|
+
if (item.type === 'background_task') return false;
|
|
439
|
+
return true;
|
|
440
|
+
});
|
|
441
|
+
|
|
278
442
|
// Group intermediate items into segments by agent_message boundaries
|
|
279
443
|
// Each segment: one agent_message + following tool/non-tool items until next agent_message
|
|
280
444
|
interface Segment {
|
|
@@ -284,7 +448,7 @@ function TurnContentBase({ turn }: Props) {
|
|
|
284
448
|
}
|
|
285
449
|
const segments: Segment[] = [];
|
|
286
450
|
let currentSeg: Segment | null = null;
|
|
287
|
-
for (const item of
|
|
451
|
+
for (const item of mainIntermediate) {
|
|
288
452
|
if (item.type === 'agent_message') {
|
|
289
453
|
if (currentSeg) segments.push(currentSeg);
|
|
290
454
|
currentSeg = { agentMessage: item, toolItems: [], otherItems: [] };
|
|
@@ -296,7 +460,11 @@ function TurnContentBase({ turn }: Props) {
|
|
|
296
460
|
}
|
|
297
461
|
if (currentSeg) segments.push(currentSeg);
|
|
298
462
|
|
|
299
|
-
|
|
463
|
+
// Count only tool items for the collapsed header (reasoning is not a "step")
|
|
464
|
+
const toolItemCount = mainIntermediate.filter((item) => isToolItem(item)).length;
|
|
465
|
+
// Show intermediate section when there are tool calls or when turn is running/pending
|
|
466
|
+
// (so the user sees thinking activity). For completed turns with only reasoning, hide it.
|
|
467
|
+
const showIntermediate = toolItemCount > 0 || (mainIntermediate.length > 0 && (turn.status === 'running' || turn.status === 'pending'));
|
|
300
468
|
|
|
301
469
|
return (
|
|
302
470
|
<div className="turn-group">
|
|
@@ -309,17 +477,20 @@ function TurnContentBase({ turn }: Props) {
|
|
|
309
477
|
<span className="turn-header-title">You</span>
|
|
310
478
|
</div>
|
|
311
479
|
<p className="user-message">{turn.user_prompt}</p>
|
|
480
|
+
{Array.isArray(turn.attachments) && turn.attachments.length > 0 && <AttachmentPills attachments={turn.attachments} />}
|
|
312
481
|
</div>
|
|
313
482
|
|
|
314
483
|
{/* Level 1: Intermediate steps — collapsible (auto-expand while running, auto-collapse on complete) */}
|
|
315
|
-
{
|
|
484
|
+
{showIntermediate && (
|
|
316
485
|
<div className="tool-calls">
|
|
317
486
|
<div className="tool-calls-header" onClick={() => setIntermediateExpanded((e) => !e)}>
|
|
318
487
|
<span className="tool-calls-icon">
|
|
319
488
|
<Layers size={12} />
|
|
320
489
|
</span>
|
|
321
490
|
<span className="tool-calls-summary">
|
|
322
|
-
{
|
|
491
|
+
{toolItemCount > 0
|
|
492
|
+
? `${toolItemCount} tool call${toolItemCount > 1 ? 's' : ''}`
|
|
493
|
+
: 'thinking…'}
|
|
323
494
|
</span>
|
|
324
495
|
<ChevronRight
|
|
325
496
|
size={14}
|
|
@@ -332,13 +503,13 @@ function TurnContentBase({ turn }: Props) {
|
|
|
332
503
|
{segments.map((seg, i) => (
|
|
333
504
|
<div key={i} className="intermediate-segment">
|
|
334
505
|
{/* Agent message for this segment */}
|
|
335
|
-
{seg.agentMessage && <MemoItemCard item={seg.agentMessage} />}
|
|
506
|
+
{seg.agentMessage && <MemoItemCard item={seg.agentMessage} runtimeType={runtimeType} turn={turn} threads={threads} />}
|
|
336
507
|
{/* Non-tool items inline (reasoning, etc.) */}
|
|
337
508
|
{seg.otherItems.map((item) => (
|
|
338
|
-
<MemoItemCard key={item.id} item={item} />
|
|
509
|
+
<MemoItemCard key={item.id} item={item} runtimeType={runtimeType} turn={turn} threads={threads} />
|
|
339
510
|
))}
|
|
340
511
|
{/* Level 2: Tool calls for this segment — collapsed by default */}
|
|
341
|
-
{seg.toolItems.length > 0 && <CollapsibleToolCalls items={seg.toolItems} />}
|
|
512
|
+
{seg.toolItems.length > 0 && <CollapsibleToolCalls items={seg.toolItems} runtimeType={runtimeType} turn={turn} threads={threads} />}
|
|
342
513
|
</div>
|
|
343
514
|
))}
|
|
344
515
|
</div>
|
|
@@ -347,10 +518,10 @@ function TurnContentBase({ turn }: Props) {
|
|
|
347
518
|
)}
|
|
348
519
|
|
|
349
520
|
{/* Final agent message — always visible */}
|
|
350
|
-
{hasFinalResponse && <MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} />}
|
|
521
|
+
{hasFinalResponse && <MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} runtimeType={runtimeType} turn={turn} threads={threads} />}
|
|
351
522
|
|
|
352
523
|
{/* Items after final agent message (rare — errors, etc.) */}
|
|
353
|
-
{hasFinalResponse && turn.items.slice(lastAgentIdx + 1).map((item) => <MemoItemCard key={item.id} item={item} />)}
|
|
524
|
+
{hasFinalResponse && turn.items.slice(lastAgentIdx + 1).map((item) => <MemoItemCard key={item.id} item={item} runtimeType={runtimeType} turn={turn} threads={threads} />)}
|
|
354
525
|
|
|
355
526
|
{/* Pending state (no items at all) */}
|
|
356
527
|
{turn.items.length === 0 && !turn.error && (turn.status === 'pending' || turn.status === 'running') && (
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ProviderCapabilities } from '../../types/index.ts';
|
|
1
|
+
import type { ProviderCapabilities, ProviderSlashCommand } from '../../types/index.ts';
|
|
2
2
|
|
|
3
3
|
/** A slash command definition. */
|
|
4
4
|
export interface SlashCommand {
|
|
@@ -9,10 +9,13 @@ export interface SlashCommand {
|
|
|
9
9
|
capability: keyof ProviderCapabilities | null;
|
|
10
10
|
/** Sub-popup type to open when selected. */
|
|
11
11
|
subPopup?: 'sandbox' | 'websearch' | 'network';
|
|
12
|
+
/** When true, selecting inserts `/<id> ` as text (provider-native commands
|
|
13
|
+
* like /compact, /clear that are handled by the runtime, not the UI). */
|
|
14
|
+
insertAsText?: boolean;
|
|
12
15
|
}
|
|
13
16
|
|
|
14
|
-
/** All
|
|
15
|
-
const
|
|
17
|
+
/** All built-in slash commands. */
|
|
18
|
+
const BUILTIN_COMMANDS: SlashCommand[] = [
|
|
16
19
|
{ id: 'model', label: '/model', hint: 'Change model', capability: 'dynamicModels' },
|
|
17
20
|
{ id: 'sandbox', label: '/sandbox', hint: 'Sandbox mode', capability: 'sandbox', subPopup: 'sandbox' },
|
|
18
21
|
{ id: 'websearch', label: '/websearch', hint: 'Web search mode', capability: 'webSearch', subPopup: 'websearch' },
|
|
@@ -22,12 +25,28 @@ const ALL_COMMANDS: SlashCommand[] = [
|
|
|
22
25
|
{ id: 'mcp', label: '/mcp', hint: 'Browse MCP servers', capability: 'mcpServers' },
|
|
23
26
|
];
|
|
24
27
|
|
|
25
|
-
/**
|
|
28
|
+
/** Map provider-native commands (fetched dynamically from the runtime) to SlashCommand. */
|
|
29
|
+
function mapProviderCommands(commands: ProviderSlashCommand[]): SlashCommand[] {
|
|
30
|
+
return commands.map((c) => ({
|
|
31
|
+
id: c.name,
|
|
32
|
+
label: `/${c.name}`,
|
|
33
|
+
hint: c.description,
|
|
34
|
+
capability: null,
|
|
35
|
+
insertAsText: true,
|
|
36
|
+
}));
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Filter built-in and provider commands by capability and text filter. */
|
|
26
40
|
export function filterCommands(
|
|
27
41
|
filter: string,
|
|
28
42
|
capabilities: ProviderCapabilities,
|
|
43
|
+
providerCommands: ProviderSlashCommand[] = [],
|
|
29
44
|
): SlashCommand[] {
|
|
30
|
-
|
|
45
|
+
const all = [...BUILTIN_COMMANDS, ...mapProviderCommands(providerCommands)];
|
|
46
|
+
const seen = new Set<string>();
|
|
47
|
+
return all.filter((cmd) => {
|
|
48
|
+
if (seen.has(cmd.label)) return false;
|
|
49
|
+
seen.add(cmd.label);
|
|
31
50
|
if (cmd.capability && !capabilities[cmd.capability]) return false;
|
|
32
51
|
if (filter && !cmd.label.toLowerCase().includes(filter.toLowerCase())) return false;
|
|
33
52
|
return true;
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
|
2
|
+
import { render, screen, cleanup } from '@testing-library/react';
|
|
3
|
+
import TasksPanel from './TasksPanel.tsx';
|
|
4
|
+
import { resetStore } from '../../test/component-helpers.ts';
|
|
5
|
+
import { makeDetail, makeTurn, makeItem } from '../../test/fixtures.ts';
|
|
6
|
+
|
|
7
|
+
describe('TasksPanel', () => {
|
|
8
|
+
beforeEach(() => {
|
|
9
|
+
resetStore();
|
|
10
|
+
});
|
|
11
|
+
afterEach(() => {
|
|
12
|
+
cleanup();
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
it('shows empty state when no background tasks', () => {
|
|
16
|
+
const detail = makeDetail('s1', []);
|
|
17
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
18
|
+
render(<TasksPanel />);
|
|
19
|
+
expect(screen.getByText('No background tasks yet')).toBeInTheDocument();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('renders a Monitor task card with description', () => {
|
|
23
|
+
const item = makeItem('i1', 't1', 'background_task');
|
|
24
|
+
item.data = {
|
|
25
|
+
task_type: 'local_bash',
|
|
26
|
+
description: 'watch echo hello',
|
|
27
|
+
status: 'running',
|
|
28
|
+
output: 'hello\n',
|
|
29
|
+
};
|
|
30
|
+
item.status = 'in_progress';
|
|
31
|
+
const turn = makeTurn('t1', 's1', { items: [item] });
|
|
32
|
+
const detail = makeDetail('s1', [turn]);
|
|
33
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
34
|
+
render(<TasksPanel />);
|
|
35
|
+
expect(screen.getByText('Monitor')).toBeInTheDocument();
|
|
36
|
+
expect(screen.getByText('watch echo hello')).toBeInTheDocument();
|
|
37
|
+
// Output is in a <code> element inside .task-output
|
|
38
|
+
const outputEl = document.querySelector('.task-output code');
|
|
39
|
+
expect(outputEl?.textContent).toContain('hello');
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
it('does not show Subagent (local_agent) tasks in Tasks panel', () => {
|
|
43
|
+
const item = makeItem('i2', 't1', 'background_task');
|
|
44
|
+
item.data = {
|
|
45
|
+
task_type: 'local_agent',
|
|
46
|
+
subagent_type: 'explore-codebase',
|
|
47
|
+
description: 'explore the codebase',
|
|
48
|
+
status: 'completed',
|
|
49
|
+
};
|
|
50
|
+
item.status = 'completed';
|
|
51
|
+
const turn = makeTurn('t1', 's1', { items: [item] });
|
|
52
|
+
const detail = makeDetail('s1', [turn]);
|
|
53
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
54
|
+
render(<TasksPanel />);
|
|
55
|
+
// local_agent items are filtered out — they appear in subagent tabs, not Tasks panel
|
|
56
|
+
expect(screen.getByText('No background tasks yet')).toBeInTheDocument();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('sorts running tasks before completed', () => {
|
|
60
|
+
const runningItem = makeItem('i1', 't2', 'background_task');
|
|
61
|
+
runningItem.data = { task_type: 'local_bash', description: 'running task', status: 'running' };
|
|
62
|
+
runningItem.status = 'in_progress';
|
|
63
|
+
|
|
64
|
+
const doneItem = makeItem('i2', 't1', 'background_task');
|
|
65
|
+
doneItem.data = { task_type: 'local_bash', description: 'done task', status: 'completed' };
|
|
66
|
+
doneItem.status = 'completed';
|
|
67
|
+
|
|
68
|
+
const turn1 = makeTurn('t1', 's1', { items: [doneItem] });
|
|
69
|
+
const turn2 = makeTurn('t2', 's1', { items: [runningItem] });
|
|
70
|
+
const detail = makeDetail('s1', [turn1, turn2]);
|
|
71
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
72
|
+
render(<TasksPanel />);
|
|
73
|
+
|
|
74
|
+
const cards = document.querySelectorAll('.task-card');
|
|
75
|
+
expect(cards.length).toBe(2);
|
|
76
|
+
// Running task should be first
|
|
77
|
+
expect(cards[0].textContent).toContain('running task');
|
|
78
|
+
expect(cards[1].textContent).toContain('done task');
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('shows task count in section title (local_agent excluded)', () => {
|
|
82
|
+
const item1 = makeItem('i1', 't1', 'background_task');
|
|
83
|
+
item1.data = { task_type: 'local_bash', description: 'task 1', status: 'completed' };
|
|
84
|
+
item1.status = 'completed';
|
|
85
|
+
|
|
86
|
+
const item2 = makeItem('i2', 't1', 'background_task');
|
|
87
|
+
item2.data = { task_type: 'local_agent', description: 'task 2', status: 'completed' };
|
|
88
|
+
item2.status = 'completed';
|
|
89
|
+
|
|
90
|
+
const turn = makeTurn('t1', 's1', { items: [item1, item2] });
|
|
91
|
+
const detail = makeDetail('s1', [turn]);
|
|
92
|
+
resetStore({ activeSessionId: 's1', sessionDetails: { s1: detail } });
|
|
93
|
+
render(<TasksPanel />);
|
|
94
|
+
// Only local_bash counted — local_agent filtered out
|
|
95
|
+
expect(screen.getByText(/Background Tasks \(1\)/)).toBeInTheDocument();
|
|
96
|
+
});
|
|
97
|
+
});
|