repoburg 1.3.183 → 1.3.186
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/package-lock.json +2359 -0
- package/agent-pane/backend/package.json +29 -0
- package/agent-pane/backend/src/codex/codex-provider.ts +260 -0
- package/agent-pane/backend/src/codex/item-mapper.ts +59 -0
- package/agent-pane/backend/src/db/index.ts +67 -0
- package/agent-pane/backend/src/db/schema.ts +65 -0
- package/agent-pane/backend/src/index.ts +55 -0
- package/agent-pane/backend/src/routes/sessions.ts +97 -0
- package/agent-pane/backend/src/routes/turns.ts +60 -0
- package/agent-pane/backend/src/ws/index.ts +43 -0
- package/agent-pane/backend/tsconfig.json +23 -0
- package/agent-pane/frontend/index.html +18 -0
- package/agent-pane/frontend/package-lock.json +1948 -0
- package/agent-pane/frontend/package.json +28 -0
- package/agent-pane/frontend/src/App.tsx +44 -0
- package/agent-pane/frontend/src/AppInitializer.tsx +46 -0
- package/agent-pane/frontend/src/api/client.ts +29 -0
- package/agent-pane/frontend/src/api/sessions.ts +31 -0
- package/agent-pane/frontend/src/api/websocket.ts +139 -0
- package/agent-pane/frontend/src/components/CommandInputBar.tsx +42 -0
- package/agent-pane/frontend/src/components/CommandPalette.tsx +196 -0
- package/agent-pane/frontend/src/components/DiffViewer.tsx +27 -0
- package/agent-pane/frontend/src/components/IdeWorkspace.tsx +25 -0
- package/agent-pane/frontend/src/components/NewSessionDialog.tsx +84 -0
- package/agent-pane/frontend/src/components/RenderArea.tsx +157 -0
- package/agent-pane/frontend/src/components/RightPanel.tsx +40 -0
- package/agent-pane/frontend/src/components/RuntimeIcon.tsx +16 -0
- package/agent-pane/frontend/src/components/SessionTabs.tsx +41 -0
- package/agent-pane/frontend/src/components/StatusBar.tsx +60 -0
- package/agent-pane/frontend/src/components/StatusDot.tsx +13 -0
- package/agent-pane/frontend/src/components/Toast.tsx +34 -0
- package/agent-pane/frontend/src/components/TurnContent.tsx +202 -0
- package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +60 -0
- package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +77 -0
- package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +25 -0
- package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +69 -0
- package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +84 -0
- package/agent-pane/frontend/src/main.tsx +10 -0
- package/agent-pane/frontend/src/store/store.ts +492 -0
- package/agent-pane/frontend/src/styles/app.css +288 -0
- package/agent-pane/frontend/src/styles/tokens.css +62 -0
- package/agent-pane/frontend/src/types/index.ts +115 -0
- package/agent-pane/frontend/tsconfig.json +22 -0
- package/agent-pane/frontend/vite.config.ts +16 -0
- package/backend/dist/src/notifications/notifications.service.js +1 -1
- package/backend/dist/src/notifications/notifications.service.js.map +1 -1
- package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
- package/package.json +2 -1
|
@@ -0,0 +1,492 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import type { Session, SessionDetail, Turn, Item, LogEntry, RightTab, ChangesScope, TokenUsage } from '../types/index.ts';
|
|
3
|
+
import {
|
|
4
|
+
fetchSessions,
|
|
5
|
+
fetchSessionDetail,
|
|
6
|
+
createSession as apiCreateSession,
|
|
7
|
+
deleteSession as apiDeleteSession,
|
|
8
|
+
submitPrompt as apiSubmitPrompt,
|
|
9
|
+
cancelTurn as apiCancelTurn,
|
|
10
|
+
} from '../api/sessions.ts';
|
|
11
|
+
|
|
12
|
+
export interface CreateSessionBody {
|
|
13
|
+
title?: string;
|
|
14
|
+
runtime_type?: string;
|
|
15
|
+
model?: string;
|
|
16
|
+
cwd?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface Toast {
|
|
20
|
+
id: string;
|
|
21
|
+
type: 'success' | 'error' | 'info' | 'warn';
|
|
22
|
+
message: string;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
interface StoreState {
|
|
26
|
+
// Data
|
|
27
|
+
sessions: Session[];
|
|
28
|
+
sessionDetails: Record<string, SessionDetail>;
|
|
29
|
+
activeSessionId: string | null;
|
|
30
|
+
openTabs: string[];
|
|
31
|
+
activeTurnIndex: number;
|
|
32
|
+
activeRightTab: RightTab;
|
|
33
|
+
changesScope: ChangesScope;
|
|
34
|
+
eventLog: LogEntry[];
|
|
35
|
+
loading: boolean;
|
|
36
|
+
error: string | null;
|
|
37
|
+
showNewSessionDialog: boolean;
|
|
38
|
+
wsConnected: boolean;
|
|
39
|
+
toasts: Toast[];
|
|
40
|
+
showCommandPalette: boolean;
|
|
41
|
+
submittingPrompt: string | null;
|
|
42
|
+
|
|
43
|
+
// Actions — REST
|
|
44
|
+
init: () => Promise<void>;
|
|
45
|
+
selectSession: (id: string) => Promise<void>;
|
|
46
|
+
closeTab: (id: string) => void;
|
|
47
|
+
setActiveTurnIndex: (idx: number) => void;
|
|
48
|
+
switchRightTab: (tab: RightTab) => void;
|
|
49
|
+
setChangesScope: (scope: ChangesScope) => void;
|
|
50
|
+
createSession: (body: CreateSessionBody) => Promise<void>;
|
|
51
|
+
deleteSession: (id: string) => Promise<void>;
|
|
52
|
+
submitPrompt: (prompt: string) => Promise<void>;
|
|
53
|
+
cancelTurn: () => Promise<void>;
|
|
54
|
+
refreshActiveSession: () => Promise<void>;
|
|
55
|
+
setShowNewSessionDialog: (show: boolean) => void;
|
|
56
|
+
setWsConnected: (connected: boolean) => void;
|
|
57
|
+
addLogEntry: (level: LogEntry['level'], message: string) => void;
|
|
58
|
+
addToast: (type: Toast['type'], message: string) => void;
|
|
59
|
+
removeToast: (id: string) => void;
|
|
60
|
+
setShowCommandPalette: (show: boolean) => void;
|
|
61
|
+
|
|
62
|
+
// Actions — WebSocket event handlers
|
|
63
|
+
onTurnStarted: (sessionId: string, turnId: string) => void;
|
|
64
|
+
onItemStarted: (sessionId: string, turnId: string, item: Item) => void;
|
|
65
|
+
onItemUpdated: (sessionId: string, turnId: string, item: Item) => void;
|
|
66
|
+
onItemCompleted: (sessionId: string, turnId: string, item: Item) => void;
|
|
67
|
+
onTurnCompleted: (sessionId: string, turnId: string, usage: TokenUsage) => void;
|
|
68
|
+
onTurnFailed: (sessionId: string, turnId: string, error: string) => void;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
export const useStore = create<StoreState>((set, get) => ({
|
|
72
|
+
sessions: [],
|
|
73
|
+
sessionDetails: {},
|
|
74
|
+
activeSessionId: null,
|
|
75
|
+
openTabs: [],
|
|
76
|
+
activeTurnIndex: 0,
|
|
77
|
+
activeRightTab: 'turns',
|
|
78
|
+
changesScope: 'session',
|
|
79
|
+
eventLog: [],
|
|
80
|
+
loading: false,
|
|
81
|
+
error: null,
|
|
82
|
+
showNewSessionDialog: false,
|
|
83
|
+
wsConnected: false,
|
|
84
|
+
toasts: [],
|
|
85
|
+
showCommandPalette: false,
|
|
86
|
+
submittingPrompt: null,
|
|
87
|
+
|
|
88
|
+
// === REST Actions ===
|
|
89
|
+
|
|
90
|
+
init: async () => {
|
|
91
|
+
set({ loading: true, error: null });
|
|
92
|
+
try {
|
|
93
|
+
const sessions = await fetchSessions();
|
|
94
|
+
set({ sessions, loading: false });
|
|
95
|
+
if (sessions.length > 0 && !get().activeSessionId) {
|
|
96
|
+
await get().selectSession(sessions[0].id);
|
|
97
|
+
}
|
|
98
|
+
} catch (err) {
|
|
99
|
+
set({ loading: false, error: (err as Error).message });
|
|
100
|
+
}
|
|
101
|
+
},
|
|
102
|
+
|
|
103
|
+
selectSession: async (id) => {
|
|
104
|
+
// Check cache first
|
|
105
|
+
if (get().sessionDetails[id]) {
|
|
106
|
+
const detail = get().sessionDetails[id];
|
|
107
|
+
set({
|
|
108
|
+
activeSessionId: id,
|
|
109
|
+
activeTurnIndex: detail.turns.length > 0 ? detail.turns.length - 1 : 0,
|
|
110
|
+
activeRightTab: 'turns',
|
|
111
|
+
openTabs: get().openTabs.includes(id) ? get().openTabs : [...get().openTabs, id],
|
|
112
|
+
});
|
|
113
|
+
return;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
set({ loading: true, error: null });
|
|
117
|
+
try {
|
|
118
|
+
const detail = await fetchSessionDetail(id);
|
|
119
|
+
set((state) => ({
|
|
120
|
+
sessionDetails: { ...state.sessionDetails, [id]: detail },
|
|
121
|
+
activeSessionId: id,
|
|
122
|
+
activeTurnIndex: detail.turns.length > 0 ? detail.turns.length - 1 : 0,
|
|
123
|
+
activeRightTab: 'turns',
|
|
124
|
+
openTabs: state.openTabs.includes(id) ? state.openTabs : [...state.openTabs, id],
|
|
125
|
+
loading: false,
|
|
126
|
+
}));
|
|
127
|
+
} catch (err) {
|
|
128
|
+
set({ loading: false, error: (err as Error).message });
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
closeTab: (id) => {
|
|
133
|
+
set((state) => {
|
|
134
|
+
const newTabs = state.openTabs.filter((t) => t !== id);
|
|
135
|
+
if (state.activeSessionId === id && newTabs.length > 0) {
|
|
136
|
+
const newActive = newTabs[newTabs.length - 1];
|
|
137
|
+
const detail = state.sessionDetails[newActive];
|
|
138
|
+
return {
|
|
139
|
+
openTabs: newTabs,
|
|
140
|
+
activeSessionId: newActive,
|
|
141
|
+
activeTurnIndex: detail && detail.turns.length > 0 ? detail.turns.length - 1 : 0,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
return {
|
|
145
|
+
openTabs: newTabs,
|
|
146
|
+
activeSessionId: state.activeSessionId === id ? null : state.activeSessionId,
|
|
147
|
+
};
|
|
148
|
+
});
|
|
149
|
+
},
|
|
150
|
+
|
|
151
|
+
setActiveTurnIndex: (idx) => set({ activeTurnIndex: idx }),
|
|
152
|
+
switchRightTab: (tab) => set({ activeRightTab: tab }),
|
|
153
|
+
setChangesScope: (scope) => set({ changesScope: scope }),
|
|
154
|
+
setShowNewSessionDialog: (show) => set({ showNewSessionDialog: show }),
|
|
155
|
+
setShowCommandPalette: (show) => set({ showCommandPalette: show }),
|
|
156
|
+
setWsConnected: (connected) => set({ wsConnected: connected }),
|
|
157
|
+
|
|
158
|
+
addToast: (type, message) => {
|
|
159
|
+
const id = Math.random().toString(36).slice(2, 9);
|
|
160
|
+
set((state) => ({ toasts: [...state.toasts, { id, type, message }] }));
|
|
161
|
+
// Auto-dismiss after 4s
|
|
162
|
+
setTimeout(() => {
|
|
163
|
+
set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) }));
|
|
164
|
+
}, 4000);
|
|
165
|
+
},
|
|
166
|
+
|
|
167
|
+
removeToast: (id) => {
|
|
168
|
+
set((state) => ({ toasts: state.toasts.filter((t) => t.id !== id) }));
|
|
169
|
+
},
|
|
170
|
+
|
|
171
|
+
addLogEntry: (level, message) => {
|
|
172
|
+
const time = new Date().toLocaleTimeString('en-GB', { hour12: false });
|
|
173
|
+
set((state) => ({
|
|
174
|
+
eventLog: [...state.eventLog, { time, level, message }],
|
|
175
|
+
}));
|
|
176
|
+
},
|
|
177
|
+
|
|
178
|
+
createSession: async (body) => {
|
|
179
|
+
set({ error: null });
|
|
180
|
+
try {
|
|
181
|
+
const session = await apiCreateSession(body);
|
|
182
|
+
set((state) => ({
|
|
183
|
+
sessions: [session, ...state.sessions],
|
|
184
|
+
showNewSessionDialog: false,
|
|
185
|
+
}));
|
|
186
|
+
await get().selectSession(session.id);
|
|
187
|
+
get().addLogEntry('INFO', `Session created — ${session.id.slice(0, 8)}`);
|
|
188
|
+
get().addToast('success', `Session created — ${session.title || session.id.slice(0, 8)}`);
|
|
189
|
+
} catch (err) {
|
|
190
|
+
set({ error: (err as Error).message });
|
|
191
|
+
get().addToast('error', `Failed to create session — ${(err as Error).message}`);
|
|
192
|
+
}
|
|
193
|
+
},
|
|
194
|
+
|
|
195
|
+
deleteSession: async (id) => {
|
|
196
|
+
set({ error: null });
|
|
197
|
+
try {
|
|
198
|
+
await apiDeleteSession(id);
|
|
199
|
+
set((state) => {
|
|
200
|
+
const newDetails = { ...state.sessionDetails };
|
|
201
|
+
delete newDetails[id];
|
|
202
|
+
const newTabs = state.openTabs.filter((t) => t !== id);
|
|
203
|
+
let newActive = state.activeSessionId;
|
|
204
|
+
let newTurnIndex = state.activeTurnIndex;
|
|
205
|
+
if (state.activeSessionId === id) {
|
|
206
|
+
newActive = newTabs.length > 0 ? newTabs[newTabs.length - 1] : null;
|
|
207
|
+
const detail = newActive ? newDetails[newActive] : null;
|
|
208
|
+
newTurnIndex = detail && detail.turns.length > 0 ? detail.turns.length - 1 : 0;
|
|
209
|
+
}
|
|
210
|
+
return {
|
|
211
|
+
sessions: state.sessions.filter((s) => s.id !== id),
|
|
212
|
+
sessionDetails: newDetails,
|
|
213
|
+
openTabs: newTabs,
|
|
214
|
+
activeSessionId: newActive,
|
|
215
|
+
activeTurnIndex: newTurnIndex,
|
|
216
|
+
};
|
|
217
|
+
});
|
|
218
|
+
get().addLogEntry('INFO', `Session deleted — ${id.slice(0, 8)}`);
|
|
219
|
+
get().addToast('info', 'Session deleted');
|
|
220
|
+
} catch (err) {
|
|
221
|
+
set({ error: (err as Error).message });
|
|
222
|
+
get().addToast('error', `Failed to delete session — ${(err as Error).message}`);
|
|
223
|
+
}
|
|
224
|
+
},
|
|
225
|
+
|
|
226
|
+
submitPrompt: async (prompt) => {
|
|
227
|
+
const sessionId = get().activeSessionId;
|
|
228
|
+
if (!sessionId) return;
|
|
229
|
+
|
|
230
|
+
set({ error: null, submittingPrompt: prompt });
|
|
231
|
+
|
|
232
|
+
// Mark session as running immediately (visual feedback)
|
|
233
|
+
set((state) => {
|
|
234
|
+
const detail = state.sessionDetails[sessionId];
|
|
235
|
+
if (!detail) return {};
|
|
236
|
+
return {
|
|
237
|
+
sessionDetails: {
|
|
238
|
+
...state.sessionDetails,
|
|
239
|
+
[sessionId]: { ...detail, is_running: 1, status: 'running' },
|
|
240
|
+
},
|
|
241
|
+
sessions: state.sessions.map((s) =>
|
|
242
|
+
s.id === sessionId ? { ...s, is_running: 1, status: 'running' } : s,
|
|
243
|
+
),
|
|
244
|
+
};
|
|
245
|
+
});
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const turn = await apiSubmitPrompt(sessionId, prompt);
|
|
249
|
+
get().addLogEntry('INFO', `Turn submitted — "${prompt.slice(0, 40)}"`);
|
|
250
|
+
|
|
251
|
+
// Add the real turn to session detail (now with real ID for WS events)
|
|
252
|
+
set((state) => {
|
|
253
|
+
const detail = state.sessionDetails[sessionId];
|
|
254
|
+
if (!detail) return { submittingPrompt: null };
|
|
255
|
+
const newTurn: Turn = { ...turn, items: [] };
|
|
256
|
+
return {
|
|
257
|
+
submittingPrompt: null,
|
|
258
|
+
sessionDetails: {
|
|
259
|
+
...state.sessionDetails,
|
|
260
|
+
[sessionId]: {
|
|
261
|
+
...detail,
|
|
262
|
+
turns: [...detail.turns, newTurn],
|
|
263
|
+
},
|
|
264
|
+
},
|
|
265
|
+
activeTurnIndex: detail.turns.length, // index of new turn
|
|
266
|
+
};
|
|
267
|
+
});
|
|
268
|
+
} catch (err) {
|
|
269
|
+
set({ submittingPrompt: null, error: (err as Error).message });
|
|
270
|
+
get().addLogEntry('ERROR', `Turn failed — ${(err as Error).message}`);
|
|
271
|
+
get().addToast('error', `Failed to send prompt — ${(err as Error).message}`);
|
|
272
|
+
|
|
273
|
+
// Revert running state
|
|
274
|
+
set((state) => ({
|
|
275
|
+
sessionDetails: state.sessionDetails[sessionId]
|
|
276
|
+
? { ...state.sessionDetails, [sessionId]: { ...state.sessionDetails[sessionId], is_running: 0, status: 'active' } }
|
|
277
|
+
: state.sessionDetails,
|
|
278
|
+
sessions: state.sessions.map((s) =>
|
|
279
|
+
s.id === sessionId ? { ...s, is_running: 0, status: 'active' } : s,
|
|
280
|
+
),
|
|
281
|
+
}));
|
|
282
|
+
}
|
|
283
|
+
},
|
|
284
|
+
|
|
285
|
+
cancelTurn: async () => {
|
|
286
|
+
const sessionId = get().activeSessionId;
|
|
287
|
+
if (!sessionId) return;
|
|
288
|
+
|
|
289
|
+
try {
|
|
290
|
+
await apiCancelTurn(sessionId);
|
|
291
|
+
get().addLogEntry('INFO', 'Turn cancelled');
|
|
292
|
+
} catch (err) {
|
|
293
|
+
set({ error: (err as Error).message });
|
|
294
|
+
}
|
|
295
|
+
},
|
|
296
|
+
|
|
297
|
+
refreshActiveSession: async () => {
|
|
298
|
+
const sessionId = get().activeSessionId;
|
|
299
|
+
if (!sessionId) return;
|
|
300
|
+
|
|
301
|
+
try {
|
|
302
|
+
const detail = await fetchSessionDetail(sessionId);
|
|
303
|
+
set((state) => ({
|
|
304
|
+
sessionDetails: { ...state.sessionDetails, [sessionId]: detail },
|
|
305
|
+
activeTurnIndex: detail.turns.length > 0 ? Math.min(state.activeTurnIndex, detail.turns.length - 1) : 0,
|
|
306
|
+
sessions: state.sessions.map((s) =>
|
|
307
|
+
s.id === sessionId ? { ...s, is_running: detail.is_running, status: detail.status } : s,
|
|
308
|
+
),
|
|
309
|
+
}));
|
|
310
|
+
} catch (err) {
|
|
311
|
+
set({ error: (err as Error).message });
|
|
312
|
+
}
|
|
313
|
+
},
|
|
314
|
+
|
|
315
|
+
// === WebSocket Event Handlers ===
|
|
316
|
+
|
|
317
|
+
onTurnStarted: (sessionId, turnId) => {
|
|
318
|
+
get().addLogEntry('DEBUG', `turn:started — ${turnId.slice(0, 8)}`);
|
|
319
|
+
|
|
320
|
+
set((state) => {
|
|
321
|
+
const detail = state.sessionDetails[sessionId];
|
|
322
|
+
if (!detail) return {};
|
|
323
|
+
|
|
324
|
+
return {
|
|
325
|
+
sessionDetails: {
|
|
326
|
+
...state.sessionDetails,
|
|
327
|
+
[sessionId]: {
|
|
328
|
+
...detail,
|
|
329
|
+
is_running: 1,
|
|
330
|
+
status: 'running',
|
|
331
|
+
turns: detail.turns.map((t) =>
|
|
332
|
+
t.id === turnId ? { ...t, status: 'running' } : t,
|
|
333
|
+
),
|
|
334
|
+
},
|
|
335
|
+
},
|
|
336
|
+
sessions: state.sessions.map((s) =>
|
|
337
|
+
s.id === sessionId ? { ...s, is_running: 1, status: 'running' } : s,
|
|
338
|
+
),
|
|
339
|
+
// If this is the active session, switch to the running turn
|
|
340
|
+
activeTurnIndex: state.activeSessionId === sessionId
|
|
341
|
+
? detail.turns.findIndex((t) => t.id === turnId)
|
|
342
|
+
: state.activeTurnIndex,
|
|
343
|
+
};
|
|
344
|
+
});
|
|
345
|
+
},
|
|
346
|
+
|
|
347
|
+
onItemStarted: (sessionId, turnId, item) => {
|
|
348
|
+
get().addLogEntry('DEBUG', `item:started — ${item.type}`);
|
|
349
|
+
|
|
350
|
+
set((state) => {
|
|
351
|
+
const detail = state.sessionDetails[sessionId];
|
|
352
|
+
if (!detail) return {};
|
|
353
|
+
|
|
354
|
+
return {
|
|
355
|
+
sessionDetails: {
|
|
356
|
+
...state.sessionDetails,
|
|
357
|
+
[sessionId]: upsertItem(detail, turnId, item),
|
|
358
|
+
},
|
|
359
|
+
};
|
|
360
|
+
});
|
|
361
|
+
},
|
|
362
|
+
|
|
363
|
+
onItemUpdated: (sessionId, turnId, item) => {
|
|
364
|
+
// No log for item:updated — too noisy (fires frequently during streaming)
|
|
365
|
+
|
|
366
|
+
set((state) => {
|
|
367
|
+
const detail = state.sessionDetails[sessionId];
|
|
368
|
+
if (!detail) return {};
|
|
369
|
+
|
|
370
|
+
return {
|
|
371
|
+
sessionDetails: {
|
|
372
|
+
...state.sessionDetails,
|
|
373
|
+
[sessionId]: upsertItem(detail, turnId, item),
|
|
374
|
+
},
|
|
375
|
+
};
|
|
376
|
+
});
|
|
377
|
+
},
|
|
378
|
+
|
|
379
|
+
onItemCompleted: (sessionId, turnId, item) => {
|
|
380
|
+
get().addLogEntry('DEBUG', `item:completed — ${item.type}`);
|
|
381
|
+
|
|
382
|
+
set((state) => {
|
|
383
|
+
const detail = state.sessionDetails[sessionId];
|
|
384
|
+
if (!detail) return {};
|
|
385
|
+
|
|
386
|
+
const updatedDetail = upsertItem(detail, turnId, item);
|
|
387
|
+
|
|
388
|
+
// Update turn-level fields from item
|
|
389
|
+
let turns = updatedDetail.turns;
|
|
390
|
+
if (item.type === 'agent_message' && item.data?.text) {
|
|
391
|
+
turns = turns.map((t) =>
|
|
392
|
+
t.id === turnId ? { ...t, agent_response: item.data!.text as string } : t,
|
|
393
|
+
);
|
|
394
|
+
}
|
|
395
|
+
if (item.type === 'reasoning' && item.data?.text) {
|
|
396
|
+
turns = turns.map((t) => {
|
|
397
|
+
if (t.id !== turnId) return t;
|
|
398
|
+
const thoughts = t.thoughts ? t.thoughts + '\n' + (item.data!.text as string) : (item.data!.text as string);
|
|
399
|
+
return { ...t, thoughts };
|
|
400
|
+
});
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
return {
|
|
404
|
+
sessionDetails: {
|
|
405
|
+
...state.sessionDetails,
|
|
406
|
+
[sessionId]: { ...updatedDetail, turns },
|
|
407
|
+
},
|
|
408
|
+
};
|
|
409
|
+
});
|
|
410
|
+
},
|
|
411
|
+
|
|
412
|
+
onTurnCompleted: (sessionId, turnId, usage) => {
|
|
413
|
+
get().addLogEntry('INFO', `turn:completed — ${usage.input_tokens} in / ${usage.output_tokens} out`);
|
|
414
|
+
|
|
415
|
+
set((state) => {
|
|
416
|
+
const detail = state.sessionDetails[sessionId];
|
|
417
|
+
if (!detail) return {};
|
|
418
|
+
|
|
419
|
+
return {
|
|
420
|
+
sessionDetails: {
|
|
421
|
+
...state.sessionDetails,
|
|
422
|
+
[sessionId]: {
|
|
423
|
+
...detail,
|
|
424
|
+
is_running: 0,
|
|
425
|
+
status: 'active',
|
|
426
|
+
turns: detail.turns.map((t) =>
|
|
427
|
+
t.id === turnId ? { ...t, status: 'completed', token_usage: usage } : t,
|
|
428
|
+
),
|
|
429
|
+
},
|
|
430
|
+
},
|
|
431
|
+
sessions: state.sessions.map((s) =>
|
|
432
|
+
s.id === sessionId ? { ...s, is_running: 0, status: 'active' } : s,
|
|
433
|
+
),
|
|
434
|
+
};
|
|
435
|
+
});
|
|
436
|
+
},
|
|
437
|
+
|
|
438
|
+
onTurnFailed: (sessionId, turnId, error) => {
|
|
439
|
+
get().addLogEntry('ERROR', `turn:failed — ${error}`);
|
|
440
|
+
get().addToast('error', `Turn failed — ${error}`);
|
|
441
|
+
|
|
442
|
+
set((state) => {
|
|
443
|
+
const detail = state.sessionDetails[sessionId];
|
|
444
|
+
if (!detail) return {};
|
|
445
|
+
|
|
446
|
+
const isAbort = error.includes('cancelled') || error.includes('aborted');
|
|
447
|
+
const status = isAbort ? 'aborted' : 'failed';
|
|
448
|
+
|
|
449
|
+
return {
|
|
450
|
+
sessionDetails: {
|
|
451
|
+
...state.sessionDetails,
|
|
452
|
+
[sessionId]: {
|
|
453
|
+
...detail,
|
|
454
|
+
is_running: 0,
|
|
455
|
+
status: isAbort ? 'active' : 'failed',
|
|
456
|
+
turns: detail.turns.map((t) =>
|
|
457
|
+
t.id === turnId ? { ...t, status, error } : t,
|
|
458
|
+
),
|
|
459
|
+
},
|
|
460
|
+
},
|
|
461
|
+
sessions: state.sessions.map((s) =>
|
|
462
|
+
s.id === sessionId ? { ...s, is_running: 0, status: isAbort ? 'active' : 'failed' } : s,
|
|
463
|
+
),
|
|
464
|
+
};
|
|
465
|
+
});
|
|
466
|
+
},
|
|
467
|
+
}));
|
|
468
|
+
|
|
469
|
+
/** Insert or replace an item in the correct turn within a SessionDetail. */
|
|
470
|
+
function upsertItem(detail: SessionDetail, turnId: string, item: Item): SessionDetail {
|
|
471
|
+
return {
|
|
472
|
+
...detail,
|
|
473
|
+
turns: detail.turns.map((turn) => {
|
|
474
|
+
if (turn.id !== turnId) return turn;
|
|
475
|
+
|
|
476
|
+
// Find existing item by external_item_id (SDK uses same ID across started/updated/completed)
|
|
477
|
+
const existingIdx = turn.items.findIndex(
|
|
478
|
+
(i) => i.external_item_id === item.external_item_id,
|
|
479
|
+
);
|
|
480
|
+
|
|
481
|
+
if (existingIdx >= 0) {
|
|
482
|
+
// Replace existing item
|
|
483
|
+
const newItems = [...turn.items];
|
|
484
|
+
newItems[existingIdx] = { ...item, id: newItems[existingIdx].id };
|
|
485
|
+
return { ...turn, items: newItems };
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
// Insert new item
|
|
489
|
+
return { ...turn, items: [...turn.items, item] };
|
|
490
|
+
}),
|
|
491
|
+
};
|
|
492
|
+
}
|