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.
Files changed (48) hide show
  1. package/agent-pane/backend/package-lock.json +2359 -0
  2. package/agent-pane/backend/package.json +29 -0
  3. package/agent-pane/backend/src/codex/codex-provider.ts +260 -0
  4. package/agent-pane/backend/src/codex/item-mapper.ts +59 -0
  5. package/agent-pane/backend/src/db/index.ts +67 -0
  6. package/agent-pane/backend/src/db/schema.ts +65 -0
  7. package/agent-pane/backend/src/index.ts +55 -0
  8. package/agent-pane/backend/src/routes/sessions.ts +97 -0
  9. package/agent-pane/backend/src/routes/turns.ts +60 -0
  10. package/agent-pane/backend/src/ws/index.ts +43 -0
  11. package/agent-pane/backend/tsconfig.json +23 -0
  12. package/agent-pane/frontend/index.html +18 -0
  13. package/agent-pane/frontend/package-lock.json +1948 -0
  14. package/agent-pane/frontend/package.json +28 -0
  15. package/agent-pane/frontend/src/App.tsx +44 -0
  16. package/agent-pane/frontend/src/AppInitializer.tsx +46 -0
  17. package/agent-pane/frontend/src/api/client.ts +29 -0
  18. package/agent-pane/frontend/src/api/sessions.ts +31 -0
  19. package/agent-pane/frontend/src/api/websocket.ts +139 -0
  20. package/agent-pane/frontend/src/components/CommandInputBar.tsx +42 -0
  21. package/agent-pane/frontend/src/components/CommandPalette.tsx +196 -0
  22. package/agent-pane/frontend/src/components/DiffViewer.tsx +27 -0
  23. package/agent-pane/frontend/src/components/IdeWorkspace.tsx +25 -0
  24. package/agent-pane/frontend/src/components/NewSessionDialog.tsx +84 -0
  25. package/agent-pane/frontend/src/components/RenderArea.tsx +157 -0
  26. package/agent-pane/frontend/src/components/RightPanel.tsx +40 -0
  27. package/agent-pane/frontend/src/components/RuntimeIcon.tsx +16 -0
  28. package/agent-pane/frontend/src/components/SessionTabs.tsx +41 -0
  29. package/agent-pane/frontend/src/components/StatusBar.tsx +60 -0
  30. package/agent-pane/frontend/src/components/StatusDot.tsx +13 -0
  31. package/agent-pane/frontend/src/components/Toast.tsx +34 -0
  32. package/agent-pane/frontend/src/components/TurnContent.tsx +202 -0
  33. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +60 -0
  34. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +77 -0
  35. package/agent-pane/frontend/src/components/panels/LogsPanel.tsx +25 -0
  36. package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +69 -0
  37. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +84 -0
  38. package/agent-pane/frontend/src/main.tsx +10 -0
  39. package/agent-pane/frontend/src/store/store.ts +492 -0
  40. package/agent-pane/frontend/src/styles/app.css +288 -0
  41. package/agent-pane/frontend/src/styles/tokens.css +62 -0
  42. package/agent-pane/frontend/src/types/index.ts +115 -0
  43. package/agent-pane/frontend/tsconfig.json +22 -0
  44. package/agent-pane/frontend/vite.config.ts +16 -0
  45. package/backend/dist/src/notifications/notifications.service.js +1 -1
  46. package/backend/dist/src/notifications/notifications.service.js.map +1 -1
  47. package/backend/dist/tsconfig.build.tsbuildinfo +1 -1
  48. 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
+ }