repoburg 1.3.186 → 1.3.188
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/src/codex/codex-provider.ts +105 -2
- package/agent-pane/backend/src/db/index.ts +24 -0
- package/agent-pane/backend/src/db/schema.ts +11 -0
- package/agent-pane/backend/src/index.ts +2 -0
- package/agent-pane/backend/src/routes/sessions.ts +37 -0
- package/agent-pane/backend/src/routes/workspaces.ts +110 -0
- package/agent-pane/frontend/src/api/client.ts +12 -0
- package/agent-pane/frontend/src/api/sessions.ts +17 -1
- package/agent-pane/frontend/src/api/websocket.ts +11 -1
- package/agent-pane/frontend/src/api/workspaces.ts +24 -0
- package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +50 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +11 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +61 -48
- package/agent-pane/frontend/src/components/SessionTabs.tsx +6 -3
- package/agent-pane/frontend/src/components/StatusBar.tsx +13 -5
- package/agent-pane/frontend/src/components/Toast.tsx +60 -14
- package/agent-pane/frontend/src/components/TurnContent.tsx +104 -12
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +325 -35
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +6 -2
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +15 -2
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +18 -57
- package/agent-pane/frontend/src/store/store.ts +159 -16
- package/agent-pane/frontend/src/styles/app.css +79 -2
- package/agent-pane/frontend/src/types/index.ts +46 -8
- package/package.json +1 -1
|
@@ -1,14 +1,30 @@
|
|
|
1
1
|
import { useStore } from '../store/store.ts';
|
|
2
2
|
import TurnContent from './TurnContent.tsx';
|
|
3
3
|
import type { Item } from '../types/index.ts';
|
|
4
|
-
import { memo } from 'react';
|
|
4
|
+
import { memo, useEffect, useRef } from 'react';
|
|
5
5
|
|
|
6
6
|
function RenderAreaBase() {
|
|
7
|
-
const { activeSessionId, sessionDetails, activeTurnIndex, activeRightTab, changesScope, submittingPrompt } = useStore();
|
|
7
|
+
const { activeSessionId, sessionDetails, activeTurnIndex, activeRightTab, changesScope, submittingPrompt, loading, selectedChangePath, setSelectedChangePath } = useStore();
|
|
8
8
|
|
|
9
9
|
const detail = activeSessionId ? sessionDetails[activeSessionId] : null;
|
|
10
10
|
const turns = detail?.turns ?? [];
|
|
11
|
-
const
|
|
11
|
+
const renderScrollRef = useRef<HTMLDivElement>(null);
|
|
12
|
+
|
|
13
|
+
// F4: Scroll to selected change path when it changes
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!selectedChangePath || activeRightTab !== 'changes') return;
|
|
16
|
+
const timer = setTimeout(() => {
|
|
17
|
+
const sanitized = selectedChangePath.replace(/[^a-zA-Z0-9]/g, '-');
|
|
18
|
+
const el = document.getElementById(`diff-${sanitized}`);
|
|
19
|
+
if (el) {
|
|
20
|
+
el.scrollIntoView({ behavior: 'smooth', block: 'start' });
|
|
21
|
+
el.classList.add('diff-highlight');
|
|
22
|
+
setTimeout(() => el.classList.remove('diff-highlight'), 1500);
|
|
23
|
+
}
|
|
24
|
+
setSelectedChangePath(null);
|
|
25
|
+
}, 50);
|
|
26
|
+
return () => clearTimeout(timer);
|
|
27
|
+
}, [selectedChangePath, activeRightTab, setSelectedChangePath]);
|
|
12
28
|
|
|
13
29
|
// When Changes tab is active, render file changes from items
|
|
14
30
|
if (activeRightTab === 'changes') {
|
|
@@ -20,7 +36,6 @@ function RenderAreaBase() {
|
|
|
20
36
|
);
|
|
21
37
|
}
|
|
22
38
|
|
|
23
|
-
// Collect all file_change items across turns
|
|
24
39
|
const allChanges: Array<{ path: string; kind: string; turnIdx: number }> = [];
|
|
25
40
|
turns.forEach((t, tIdx) => {
|
|
26
41
|
t.items.forEach((item: Item) => {
|
|
@@ -33,16 +48,11 @@ function RenderAreaBase() {
|
|
|
33
48
|
|
|
34
49
|
let entries = allChanges;
|
|
35
50
|
if (changesScope === 'turngroup') {
|
|
36
|
-
let start = activeTurnIndex;
|
|
37
|
-
while (start > 0 && turns[start] && false) start--; // no user/agent distinction in real turns
|
|
38
51
|
entries = allChanges.filter((c) => c.turnIdx === activeTurnIndex);
|
|
39
52
|
}
|
|
40
53
|
|
|
41
|
-
// Deduplicate by path (keep last)
|
|
42
54
|
const seen = new Map<string, { path: string; kind: string }>();
|
|
43
|
-
for (const c of entries) {
|
|
44
|
-
seen.set(c.path, { path: c.path, kind: c.kind });
|
|
45
|
-
}
|
|
55
|
+
for (const c of entries) seen.set(c.path, { path: c.path, kind: c.kind });
|
|
46
56
|
const deduped = Array.from(seen.values());
|
|
47
57
|
|
|
48
58
|
if (deduped.length === 0) {
|
|
@@ -56,7 +66,7 @@ function RenderAreaBase() {
|
|
|
56
66
|
}
|
|
57
67
|
|
|
58
68
|
return (
|
|
59
|
-
<div className="render-scroll">
|
|
69
|
+
<div className="render-scroll" ref={renderScrollRef}>
|
|
60
70
|
<div className="render-content">
|
|
61
71
|
<div style={{ marginBottom: '20px' }}>
|
|
62
72
|
<span style={{ fontSize: '16px', fontWeight: 600, color: 'var(--text)' }}>
|
|
@@ -72,10 +82,11 @@ function RenderAreaBase() {
|
|
|
72
82
|
const statusLabel = c.kind === 'add' ? 'Added' : c.kind === 'delete' ? 'Deleted' : 'Modified';
|
|
73
83
|
const statusClass = c.kind === 'add' ? 'fc-add' : c.kind === 'delete' ? 'fc-del' : 'fc-mod';
|
|
74
84
|
const statusChar = c.kind === 'add' ? 'A' : c.kind === 'delete' ? 'D' : 'M';
|
|
85
|
+
const sanitizedId = c.path.replace(/[^a-zA-Z0-9]/g, '-');
|
|
75
86
|
|
|
76
87
|
return (
|
|
77
88
|
<div key={c.path}>
|
|
78
|
-
<div className="diff-file-header">
|
|
89
|
+
<div className="diff-file-header" id={`diff-${sanitizedId}`}>
|
|
79
90
|
<span className={`diff-file-status ${statusClass}`}>{statusChar}</span>
|
|
80
91
|
<span className="diff-file-name">
|
|
81
92
|
{dir && <span style={{ color: 'var(--text-muted)' }}>{dir}/</span>}
|
|
@@ -96,12 +107,35 @@ function RenderAreaBase() {
|
|
|
96
107
|
);
|
|
97
108
|
}
|
|
98
109
|
|
|
99
|
-
//
|
|
100
|
-
if (!
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
110
|
+
// F2: Skeleton loading when fetching session detail for the first time
|
|
111
|
+
if (loading && !detail) {
|
|
112
|
+
return (
|
|
113
|
+
<div className="render-scroll">
|
|
114
|
+
<div className="skeleton-bars">
|
|
115
|
+
<div className="skeleton-bar" />
|
|
116
|
+
<div className="skeleton-bar" />
|
|
117
|
+
<div className="skeleton-bar" />
|
|
118
|
+
<div className="skeleton-bar" />
|
|
119
|
+
<div className="skeleton-bar" />
|
|
120
|
+
</div>
|
|
121
|
+
</div>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
if (!detail) {
|
|
126
|
+
return (
|
|
127
|
+
<div className="render-scroll">
|
|
128
|
+
<div className="empty-state">No session selected</div>
|
|
129
|
+
</div>
|
|
130
|
+
);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
// Render ALL turns, scrollable
|
|
134
|
+
return (
|
|
135
|
+
<div className="render-scroll" ref={renderScrollRef}>
|
|
136
|
+
{/* Submitting prompt — shown at top while waiting for API response */}
|
|
137
|
+
{submittingPrompt && (
|
|
138
|
+
<div className="turn-block">
|
|
105
139
|
<div className="turn-group">
|
|
106
140
|
<div className="render-content">
|
|
107
141
|
<div className="turn-header">
|
|
@@ -115,40 +149,19 @@ function RenderAreaBase() {
|
|
|
115
149
|
</div>
|
|
116
150
|
</div>
|
|
117
151
|
</div>
|
|
118
|
-
)
|
|
119
|
-
}
|
|
152
|
+
)}
|
|
120
153
|
|
|
121
|
-
|
|
122
|
-
<div className="render-scroll">
|
|
154
|
+
{turns.length === 0 && !submittingPrompt && (
|
|
123
155
|
<div className="empty-state">
|
|
124
|
-
|
|
156
|
+
No turns yet. Send a message below to start.
|
|
125
157
|
</div>
|
|
126
|
-
|
|
127
|
-
);
|
|
128
|
-
}
|
|
158
|
+
)}
|
|
129
159
|
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
<span className="banner-icon">!</span>
|
|
136
|
-
<span className="banner-text">{turn.error}</span>
|
|
137
|
-
</div>
|
|
138
|
-
);
|
|
139
|
-
} else if (turn.status === 'aborted') {
|
|
140
|
-
banner = (
|
|
141
|
-
<div className="aborted-banner">
|
|
142
|
-
<span className="banner-icon">■</span>
|
|
143
|
-
<span className="banner-text">Turn was cancelled by user.</span>
|
|
144
|
-
</div>
|
|
145
|
-
);
|
|
146
|
-
}
|
|
147
|
-
|
|
148
|
-
return (
|
|
149
|
-
<div className="render-scroll">
|
|
150
|
-
{banner}
|
|
151
|
-
<TurnContent turn={turn} />
|
|
160
|
+
{turns.map((turn, idx) => (
|
|
161
|
+
<div key={turn.id} id={`turn-${idx}`} className="turn-block">
|
|
162
|
+
<TurnContent turn={turn} />
|
|
163
|
+
</div>
|
|
164
|
+
))}
|
|
152
165
|
</div>
|
|
153
166
|
);
|
|
154
167
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { useStore } from '../store/store.ts';
|
|
2
|
-
import { statusToDot } from '../types/index.ts';
|
|
2
|
+
import { statusToDot, runtimeIcon } from '../types/index.ts';
|
|
3
3
|
import StatusDot from './StatusDot.tsx';
|
|
4
|
+
import RuntimeIcon from './RuntimeIcon.tsx';
|
|
4
5
|
import { X, Plus } from 'lucide-react';
|
|
5
6
|
|
|
6
7
|
export default function SessionTabs() {
|
|
@@ -11,16 +12,18 @@ export default function SessionTabs() {
|
|
|
11
12
|
{openTabs.map((sid) => {
|
|
12
13
|
const s = sessions.find((x) => x.id === sid);
|
|
13
14
|
if (!s) return null;
|
|
14
|
-
const dot = statusToDot(s.status, s.is_running === 1);
|
|
15
|
+
const dot = statusToDot(s.status, s.is_running === 1, s.has_unreviewed === 1, s.lifecycle);
|
|
15
16
|
const isActive = activeSessionId === sid;
|
|
17
|
+
const ri = runtimeIcon(s.runtime_type);
|
|
16
18
|
return (
|
|
17
19
|
<div
|
|
18
20
|
key={sid}
|
|
19
21
|
className={`ide-tab ${isActive ? 'active' : ''}`}
|
|
20
22
|
onClick={() => selectSession(sid)}
|
|
21
23
|
>
|
|
24
|
+
<RuntimeIcon iconClass={ri.iconClass} iconText={ri.iconText} size={14} />
|
|
22
25
|
{s.title || sid.slice(0, 8)}
|
|
23
|
-
{
|
|
26
|
+
{dot && <StatusDot status={dot} />}
|
|
24
27
|
<span
|
|
25
28
|
className="ide-tab-close"
|
|
26
29
|
onClick={(e) => {
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { useStore } from '../store/store.ts';
|
|
2
|
-
import { statusToDot, statusToLabel } from '../types/index.ts';
|
|
2
|
+
import { statusToDot, statusToLabel, formatDuration } from '../types/index.ts';
|
|
3
3
|
import StatusDot from './StatusDot.tsx';
|
|
4
4
|
import { Zap, Command } from 'lucide-react';
|
|
5
5
|
|
|
@@ -24,9 +24,17 @@ export default function StatusBar() {
|
|
|
24
24
|
}
|
|
25
25
|
|
|
26
26
|
const isRunning = session.is_running === 1;
|
|
27
|
-
const dot = statusToDot(session.status, isRunning);
|
|
28
|
-
const label = statusToLabel(session.status, isRunning);
|
|
29
|
-
|
|
27
|
+
const dot = statusToDot(session.status, isRunning, session.has_unreviewed === 1, session.lifecycle);
|
|
28
|
+
const label = statusToLabel(session.status, isRunning, session.has_unreviewed === 1, session.lifecycle);
|
|
29
|
+
|
|
30
|
+
// Calculate duration
|
|
31
|
+
let duration = '—';
|
|
32
|
+
if (detail && detail.turns.length > 0) {
|
|
33
|
+
const firstTurn = detail.turns[0];
|
|
34
|
+
const lastTurn = detail.turns[detail.turns.length - 1];
|
|
35
|
+
const endISO = isRunning ? null : lastTurn.updated_at;
|
|
36
|
+
duration = formatDuration(firstTurn.created_at, endISO);
|
|
37
|
+
}
|
|
30
38
|
|
|
31
39
|
return (
|
|
32
40
|
<div className="ide-statusbar">
|
|
@@ -52,7 +60,7 @@ export default function StatusBar() {
|
|
|
52
60
|
<StatusDot status={dot} /> {label}
|
|
53
61
|
</div>
|
|
54
62
|
<div className="sb-divider">|</div>
|
|
55
|
-
<div className="sb-item">{
|
|
63
|
+
<div className="sb-item">{duration}</div>
|
|
56
64
|
<div className="sb-divider">|</div>
|
|
57
65
|
<div className="sb-item"><Command size={14} /> K</div>
|
|
58
66
|
</div>
|
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
1
2
|
import { useStore } from '../store/store.ts';
|
|
2
|
-
import { X, CheckCircle, AlertCircle, Info, AlertTriangle } from 'lucide-react';
|
|
3
|
+
import { X, CheckCircle, AlertCircle, Info, AlertTriangle, Undo2 } from 'lucide-react';
|
|
3
4
|
import type { Toast as ToastType } from '../store/store.ts';
|
|
4
5
|
|
|
5
6
|
const ICONS: Record<ToastType['type'], typeof CheckCircle> = {
|
|
@@ -9,26 +10,71 @@ const ICONS: Record<ToastType['type'], typeof CheckCircle> = {
|
|
|
9
10
|
warn: AlertTriangle,
|
|
10
11
|
};
|
|
11
12
|
|
|
13
|
+
const AUTO_DISMISS_MS = 4000;
|
|
14
|
+
|
|
15
|
+
function ToastItem({ toast }: { toast: ToastType }) {
|
|
16
|
+
const removeToast = useStore((s) => s.removeToast);
|
|
17
|
+
const Icon = ICONS[toast.type];
|
|
18
|
+
const timerRef = useRef<ReturnType<typeof setTimeout> | null>(null);
|
|
19
|
+
|
|
20
|
+
// Manage auto-dismiss with hover-to-pause
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
function startTimer() {
|
|
23
|
+
timerRef.current = setTimeout(() => removeToast(toast.id), AUTO_DISMISS_MS);
|
|
24
|
+
}
|
|
25
|
+
startTimer();
|
|
26
|
+
return () => {
|
|
27
|
+
if (timerRef.current) clearTimeout(timerRef.current);
|
|
28
|
+
};
|
|
29
|
+
}, [toast.id, removeToast]);
|
|
30
|
+
|
|
31
|
+
function handleMouseEnter() {
|
|
32
|
+
if (timerRef.current) {
|
|
33
|
+
clearTimeout(timerRef.current);
|
|
34
|
+
timerRef.current = null;
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function handleMouseLeave() {
|
|
39
|
+
timerRef.current = setTimeout(() => removeToast(toast.id), 2000);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
return (
|
|
43
|
+
<div
|
|
44
|
+
className={`toast toast-${toast.type}`}
|
|
45
|
+
onMouseEnter={handleMouseEnter}
|
|
46
|
+
onMouseLeave={handleMouseLeave}
|
|
47
|
+
>
|
|
48
|
+
<Icon size={16} />
|
|
49
|
+
<span className="toast-message">{toast.message}</span>
|
|
50
|
+
{toast.undoAction && (
|
|
51
|
+
<button
|
|
52
|
+
className="toast-undo"
|
|
53
|
+
onClick={() => {
|
|
54
|
+
toast.undoAction?.();
|
|
55
|
+
removeToast(toast.id);
|
|
56
|
+
}}
|
|
57
|
+
>
|
|
58
|
+
<Undo2 size={12} /> {toast.undoLabel || 'Undo'}
|
|
59
|
+
</button>
|
|
60
|
+
)}
|
|
61
|
+
<button className="toast-close" onClick={() => removeToast(toast.id)}>
|
|
62
|
+
<X size={14} />
|
|
63
|
+
</button>
|
|
64
|
+
</div>
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
|
|
12
68
|
export default function Toast() {
|
|
13
69
|
const toasts = useStore((s) => s.toasts);
|
|
14
|
-
const removeToast = useStore((s) => s.removeToast);
|
|
15
70
|
|
|
16
71
|
if (toasts.length === 0) return null;
|
|
17
72
|
|
|
18
73
|
return (
|
|
19
74
|
<div className="toast-container">
|
|
20
|
-
{toasts.map((toast) =>
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
<div key={toast.id} className={`toast toast-${toast.type}`}>
|
|
24
|
-
<Icon size={16} />
|
|
25
|
-
<span className="toast-message">{toast.message}</span>
|
|
26
|
-
<button className="toast-close" onClick={() => removeToast(toast.id)}>
|
|
27
|
-
<X size={14} />
|
|
28
|
-
</button>
|
|
29
|
-
</div>
|
|
30
|
-
);
|
|
31
|
-
})}
|
|
75
|
+
{toasts.map((toast) => (
|
|
76
|
+
<ToastItem key={toast.id} toast={toast} />
|
|
77
|
+
))}
|
|
32
78
|
</div>
|
|
33
79
|
);
|
|
34
80
|
}
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
import { marked } from 'marked';
|
|
2
2
|
import DOMPurify from 'dompurify';
|
|
3
|
-
import { memo } from 'react';
|
|
3
|
+
import { memo, useState, useEffect } from 'react';
|
|
4
|
+
import { Layers, ChevronRight } from 'lucide-react';
|
|
4
5
|
import type { Turn, Item } from '../types/index.ts';
|
|
6
|
+
import CollapsibleToolCalls, { isToolItem } from './CollapsibleToolCalls.tsx';
|
|
5
7
|
|
|
6
8
|
marked.setOptions({ breaks: true, gfm: true });
|
|
7
9
|
|
|
@@ -157,15 +159,65 @@ function ItemCard({ item }: { item: Item }) {
|
|
|
157
159
|
}
|
|
158
160
|
|
|
159
161
|
const MemoItemCard = memo(ItemCard);
|
|
162
|
+
export { MemoItemCard };
|
|
160
163
|
|
|
161
164
|
interface Props {
|
|
162
165
|
turn: Turn;
|
|
163
166
|
}
|
|
164
167
|
|
|
165
168
|
function TurnContentBase({ turn }: Props) {
|
|
169
|
+
const [intermediateExpanded, setIntermediateExpanded] = useState(false);
|
|
170
|
+
|
|
171
|
+
// Auto-expand Level 1 when turn is running/pending, auto-collapse when completed
|
|
172
|
+
useEffect(() => {
|
|
173
|
+
if (turn.status === 'running' || turn.status === 'pending') {
|
|
174
|
+
setIntermediateExpanded(true);
|
|
175
|
+
} else if (turn.status === 'completed') {
|
|
176
|
+
setIntermediateExpanded(false);
|
|
177
|
+
}
|
|
178
|
+
}, [turn.status]);
|
|
179
|
+
|
|
180
|
+
// Find the last agent_message item — that is the final response
|
|
181
|
+
let lastAgentIdx = -1;
|
|
182
|
+
for (let i = turn.items.length - 1; i >= 0; i--) {
|
|
183
|
+
if (turn.items[i].type === 'agent_message') {
|
|
184
|
+
lastAgentIdx = i;
|
|
185
|
+
break;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const hasFinalResponse = lastAgentIdx >= 0;
|
|
190
|
+
// Intermediate = everything before final agent_message (or ALL items if running)
|
|
191
|
+
const intermediateItems = hasFinalResponse
|
|
192
|
+
? turn.items.slice(0, lastAgentIdx)
|
|
193
|
+
: turn.items;
|
|
194
|
+
|
|
195
|
+
// Group intermediate items into segments by agent_message boundaries
|
|
196
|
+
// Each segment: one agent_message + following tool/non-tool items until next agent_message
|
|
197
|
+
interface Segment {
|
|
198
|
+
agentMessage: Item | null;
|
|
199
|
+
toolItems: Item[];
|
|
200
|
+
otherItems: Item[];
|
|
201
|
+
}
|
|
202
|
+
const segments: Segment[] = [];
|
|
203
|
+
let currentSeg: Segment | null = null;
|
|
204
|
+
for (const item of intermediateItems) {
|
|
205
|
+
if (item.type === 'agent_message') {
|
|
206
|
+
if (currentSeg) segments.push(currentSeg);
|
|
207
|
+
currentSeg = { agentMessage: item, toolItems: [], otherItems: [] };
|
|
208
|
+
} else {
|
|
209
|
+
if (!currentSeg) currentSeg = { agentMessage: null, toolItems: [], otherItems: [] };
|
|
210
|
+
if (isToolItem(item)) currentSeg.toolItems.push(item);
|
|
211
|
+
else currentSeg.otherItems.push(item);
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
if (currentSeg) segments.push(currentSeg);
|
|
215
|
+
|
|
216
|
+
const intermediateCount = intermediateItems.length;
|
|
217
|
+
|
|
166
218
|
return (
|
|
167
219
|
<div className="turn-group">
|
|
168
|
-
{/* User prompt */}
|
|
220
|
+
{/* User prompt — always visible at top */}
|
|
169
221
|
<div className="render-content">
|
|
170
222
|
<div className="turn-header">
|
|
171
223
|
<span className="turn-header-icon" style={{ background: 'var(--amber)', color: '#fff' }}>U</span>
|
|
@@ -174,19 +226,52 @@ function TurnContentBase({ turn }: Props) {
|
|
|
174
226
|
<p className="user-message">{turn.user_prompt}</p>
|
|
175
227
|
</div>
|
|
176
228
|
|
|
177
|
-
{/*
|
|
178
|
-
{
|
|
179
|
-
<
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
229
|
+
{/* Level 1: Intermediate steps — collapsible (auto-expand while running, auto-collapse on complete) */}
|
|
230
|
+
{intermediateCount > 0 && (
|
|
231
|
+
<div className="tool-calls">
|
|
232
|
+
<div className="tool-calls-header" onClick={() => setIntermediateExpanded((e) => !e)}>
|
|
233
|
+
<span className="tool-calls-icon">
|
|
234
|
+
<Layers size={12} />
|
|
235
|
+
</span>
|
|
236
|
+
<span className="tool-calls-summary">
|
|
237
|
+
{intermediateCount} intermediate step{intermediateCount > 1 ? 's' : ''}
|
|
238
|
+
</span>
|
|
239
|
+
<ChevronRight
|
|
240
|
+
size={14}
|
|
241
|
+
className="tool-calls-chevron"
|
|
242
|
+
style={{ transform: intermediateExpanded ? 'rotate(90deg)' : 'none' }}
|
|
243
|
+
/>
|
|
244
|
+
</div>
|
|
245
|
+
{intermediateExpanded && (
|
|
246
|
+
<div className="tool-calls-body">
|
|
247
|
+
{segments.map((seg, i) => (
|
|
248
|
+
<div key={i} className="intermediate-segment">
|
|
249
|
+
{/* Agent message for this segment */}
|
|
250
|
+
{seg.agentMessage && <MemoItemCard item={seg.agentMessage} />}
|
|
251
|
+
{/* Non-tool items inline (reasoning, etc.) */}
|
|
252
|
+
{seg.otherItems.map((item) => (
|
|
253
|
+
<MemoItemCard key={item.id} item={item} />
|
|
254
|
+
))}
|
|
255
|
+
{/* Level 2: Tool calls for this segment — collapsed by default */}
|
|
256
|
+
{seg.toolItems.length > 0 && <CollapsibleToolCalls items={seg.toolItems} />}
|
|
257
|
+
</div>
|
|
258
|
+
))}
|
|
259
|
+
</div>
|
|
260
|
+
)}
|
|
186
261
|
</div>
|
|
187
262
|
)}
|
|
188
263
|
|
|
189
|
-
{/*
|
|
264
|
+
{/* Final agent message — always visible */}
|
|
265
|
+
{hasFinalResponse && (
|
|
266
|
+
<MemoItemCard key={turn.items[lastAgentIdx].id} item={turn.items[lastAgentIdx]} />
|
|
267
|
+
)}
|
|
268
|
+
|
|
269
|
+
{/* Items after final agent message (rare — errors, etc.) */}
|
|
270
|
+
{hasFinalResponse && turn.items.slice(lastAgentIdx + 1).map((item) => (
|
|
271
|
+
<MemoItemCard key={item.id} item={item} />
|
|
272
|
+
))}
|
|
273
|
+
|
|
274
|
+
{/* Pending state (no items at all) */}
|
|
190
275
|
{turn.items.length === 0 && !turn.error && (turn.status === 'pending' || turn.status === 'running') && (
|
|
191
276
|
<div className="render-content">
|
|
192
277
|
<p style={{ color: 'var(--text-muted)', fontStyle: 'italic' }}>
|
|
@@ -194,6 +279,13 @@ function TurnContentBase({ turn }: Props) {
|
|
|
194
279
|
</p>
|
|
195
280
|
</div>
|
|
196
281
|
)}
|
|
282
|
+
|
|
283
|
+
{/* Turn error (if any) */}
|
|
284
|
+
{turn.error && (
|
|
285
|
+
<div className="render-content">
|
|
286
|
+
<pre className="cmd-output" style={{ color: 'var(--red)' }}><code>{turn.error}</code></pre>
|
|
287
|
+
</div>
|
|
288
|
+
)}
|
|
197
289
|
</div>
|
|
198
290
|
);
|
|
199
291
|
}
|