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.
Files changed (25) hide show
  1. package/agent-pane/backend/src/codex/codex-provider.ts +105 -2
  2. package/agent-pane/backend/src/db/index.ts +24 -0
  3. package/agent-pane/backend/src/db/schema.ts +11 -0
  4. package/agent-pane/backend/src/index.ts +2 -0
  5. package/agent-pane/backend/src/routes/sessions.ts +37 -0
  6. package/agent-pane/backend/src/routes/workspaces.ts +110 -0
  7. package/agent-pane/frontend/src/api/client.ts +12 -0
  8. package/agent-pane/frontend/src/api/sessions.ts +17 -1
  9. package/agent-pane/frontend/src/api/websocket.ts +11 -1
  10. package/agent-pane/frontend/src/api/workspaces.ts +24 -0
  11. package/agent-pane/frontend/src/components/CollapsibleToolCalls.tsx +50 -0
  12. package/agent-pane/frontend/src/components/CommandPalette.tsx +11 -0
  13. package/agent-pane/frontend/src/components/RenderArea.tsx +61 -48
  14. package/agent-pane/frontend/src/components/SessionTabs.tsx +6 -3
  15. package/agent-pane/frontend/src/components/StatusBar.tsx +13 -5
  16. package/agent-pane/frontend/src/components/Toast.tsx +60 -14
  17. package/agent-pane/frontend/src/components/TurnContent.tsx +104 -12
  18. package/agent-pane/frontend/src/components/WorkspaceSidebar.tsx +325 -35
  19. package/agent-pane/frontend/src/components/panels/ChangesPanel.tsx +6 -2
  20. package/agent-pane/frontend/src/components/panels/MetricsPanel.tsx +15 -2
  21. package/agent-pane/frontend/src/components/panels/TurnsPanel.tsx +18 -57
  22. package/agent-pane/frontend/src/store/store.ts +159 -16
  23. package/agent-pane/frontend/src/styles/app.css +79 -2
  24. package/agent-pane/frontend/src/types/index.ts +46 -8
  25. package/package.json +1 -1
@@ -1,5 +1,5 @@
1
1
  import { create } from 'zustand';
2
- import type { Session, SessionDetail, Turn, Item, LogEntry, RightTab, ChangesScope, TokenUsage } from '../types/index.ts';
2
+ import type { Session, SessionDetail, Turn, Item, LogEntry, RightTab, ChangesScope, TokenUsage, Workspace } from '../types/index.ts';
3
3
  import {
4
4
  fetchSessions,
5
5
  fetchSessionDetail,
@@ -7,7 +7,17 @@ import {
7
7
  deleteSession as apiDeleteSession,
8
8
  submitPrompt as apiSubmitPrompt,
9
9
  cancelTurn as apiCancelTurn,
10
+ renameSession as apiRenameSession,
11
+ markFinished as apiMarkFinished,
12
+ reopenSession as apiReopenSession,
13
+ clearUnreviewed as apiClearUnreviewed,
10
14
  } from '../api/sessions.ts';
15
+ import {
16
+ fetchWorkspaces as apiFetchWorkspaces,
17
+ createWorkspace as apiCreateWorkspace,
18
+ hideWorkspace as apiHideWorkspace,
19
+ pickDirectory as apiPickDirectory,
20
+ } from '../api/workspaces.ts';
11
21
 
12
22
  export interface CreateSessionBody {
13
23
  title?: string;
@@ -20,11 +30,14 @@ export interface Toast {
20
30
  id: string;
21
31
  type: 'success' | 'error' | 'info' | 'warn';
22
32
  message: string;
33
+ undoAction?: () => void;
34
+ undoLabel?: string;
23
35
  }
24
36
 
25
37
  interface StoreState {
26
38
  // Data
27
39
  sessions: Session[];
40
+ workspaces: Workspace[];
28
41
  sessionDetails: Record<string, SessionDetail>;
29
42
  activeSessionId: string | null;
30
43
  openTabs: string[];
@@ -39,6 +52,7 @@ interface StoreState {
39
52
  toasts: Toast[];
40
53
  showCommandPalette: boolean;
41
54
  submittingPrompt: string | null;
55
+ selectedChangePath: string | null;
42
56
 
43
57
  // Actions — REST
44
58
  init: () => Promise<void>;
@@ -49,27 +63,37 @@ interface StoreState {
49
63
  setChangesScope: (scope: ChangesScope) => void;
50
64
  createSession: (body: CreateSessionBody) => Promise<void>;
51
65
  deleteSession: (id: string) => Promise<void>;
66
+ renameSession: (id: string, title: string) => Promise<void>;
67
+ markFinished: (id: string) => Promise<void>;
68
+ reopenSession: (id: string) => Promise<void>;
52
69
  submitPrompt: (prompt: string) => Promise<void>;
53
70
  cancelTurn: () => Promise<void>;
54
71
  refreshActiveSession: () => Promise<void>;
55
72
  setShowNewSessionDialog: (show: boolean) => void;
56
73
  setWsConnected: (connected: boolean) => void;
57
74
  addLogEntry: (level: LogEntry['level'], message: string) => void;
58
- addToast: (type: Toast['type'], message: string) => void;
75
+ addToast: (type: Toast['type'], message: string, undoAction?: () => void, undoLabel?: string) => void;
59
76
  removeToast: (id: string) => void;
60
77
  setShowCommandPalette: (show: boolean) => void;
78
+ setSelectedChangePath: (path: string | null) => void;
79
+ addWorkspace: (path: string) => Promise<void>;
80
+ removeWorkspace: (path: string) => Promise<void>;
81
+ createSessionInWorkspace: (cwd: string) => Promise<void>;
82
+ pickAndAddWorkspace: () => Promise<void>;
61
83
 
62
84
  // Actions — WebSocket event handlers
63
85
  onTurnStarted: (sessionId: string, turnId: string) => void;
64
86
  onItemStarted: (sessionId: string, turnId: string, item: Item) => void;
65
87
  onItemUpdated: (sessionId: string, turnId: string, item: Item) => void;
66
88
  onItemCompleted: (sessionId: string, turnId: string, item: Item) => void;
67
- onTurnCompleted: (sessionId: string, turnId: string, usage: TokenUsage) => void;
89
+ onTurnCompleted: (sessionId: string, turnId: string, usage: TokenUsage, hasUnreviewed?: boolean) => void;
68
90
  onTurnFailed: (sessionId: string, turnId: string, error: string) => void;
91
+ onSessionTitle: (sessionId: string, title: string) => void;
69
92
  }
70
93
 
71
94
  export const useStore = create<StoreState>((set, get) => ({
72
95
  sessions: [],
96
+ workspaces: [],
73
97
  sessionDetails: {},
74
98
  activeSessionId: null,
75
99
  openTabs: [],
@@ -84,14 +108,15 @@ export const useStore = create<StoreState>((set, get) => ({
84
108
  toasts: [],
85
109
  showCommandPalette: false,
86
110
  submittingPrompt: null,
111
+ selectedChangePath: null,
87
112
 
88
113
  // === REST Actions ===
89
114
 
90
115
  init: async () => {
91
116
  set({ loading: true, error: null });
92
117
  try {
93
- const sessions = await fetchSessions();
94
- set({ sessions, loading: false });
118
+ const [sessions, workspaces] = await Promise.all([fetchSessions(), apiFetchWorkspaces()]);
119
+ set({ sessions, workspaces, loading: false });
95
120
  if (sessions.length > 0 && !get().activeSessionId) {
96
121
  await get().selectSession(sessions[0].id);
97
122
  }
@@ -110,6 +135,14 @@ export const useStore = create<StoreState>((set, get) => ({
110
135
  activeRightTab: 'turns',
111
136
  openTabs: get().openTabs.includes(id) ? get().openTabs : [...get().openTabs, id],
112
137
  });
138
+ // Clear review pending indicator if set
139
+ if (detail.has_unreviewed === 1) {
140
+ apiClearUnreviewed(id).catch(() => {});
141
+ set((state) => ({
142
+ sessions: state.sessions.map((s) => s.id === id ? { ...s, has_unreviewed: 0 } : s),
143
+ sessionDetails: { ...state.sessionDetails, [id]: { ...detail, has_unreviewed: 0 } },
144
+ }));
145
+ }
113
146
  return;
114
147
  }
115
148
 
@@ -121,9 +154,15 @@ export const useStore = create<StoreState>((set, get) => ({
121
154
  activeSessionId: id,
122
155
  activeTurnIndex: detail.turns.length > 0 ? detail.turns.length - 1 : 0,
123
156
  activeRightTab: 'turns',
124
- openTabs: state.openTabs.includes(id) ? state.openTabs : [...state.openTabs, id],
125
- loading: false,
126
157
  }));
158
+ // Clear review pending indicator if set
159
+ if (detail.has_unreviewed === 1) {
160
+ apiClearUnreviewed(id).catch(() => {});
161
+ set((state) => ({
162
+ sessions: state.sessions.map((s) => s.id === id ? { ...s, has_unreviewed: 0 } : s),
163
+ sessionDetails: { ...state.sessionDetails, [id]: { ...detail, has_unreviewed: 0 } },
164
+ }));
165
+ }
127
166
  } catch (err) {
128
167
  set({ loading: false, error: (err as Error).message });
129
168
  }
@@ -153,15 +192,62 @@ export const useStore = create<StoreState>((set, get) => ({
153
192
  setChangesScope: (scope) => set({ changesScope: scope }),
154
193
  setShowNewSessionDialog: (show) => set({ showNewSessionDialog: show }),
155
194
  setShowCommandPalette: (show) => set({ showCommandPalette: show }),
195
+ setSelectedChangePath: (path) => set({ selectedChangePath: path }),
196
+
197
+ addWorkspace: async (path) => {
198
+ set({ error: null });
199
+ try {
200
+ const workspace = await apiCreateWorkspace(path.trim());
201
+ set((state) => ({ workspaces: [...state.workspaces, workspace] }));
202
+ get().addToast('success', `Workspace added — ${workspace.path}`);
203
+ } catch (err) {
204
+ get().addToast('error', `Failed to add workspace — ${(err as Error).message}`);
205
+ }
206
+ },
207
+
208
+ removeWorkspace: async (path) => {
209
+ set({ error: null });
210
+ try {
211
+ await apiHideWorkspace(path);
212
+ // Re-fetch all workspaces (includes both active + deleted)
213
+ const workspaces = await apiFetchWorkspaces();
214
+ set({ workspaces });
215
+ get().addToast('info', 'Workspace hidden');
216
+ } catch (err) {
217
+ get().addToast('error', `Failed to hide workspace — ${(err as Error).message}`);
218
+ }
219
+ },
220
+
221
+ createSessionInWorkspace: async (cwd) => {
222
+ set({ error: null });
223
+ try {
224
+ const session = await apiCreateSession({ cwd, runtime_type: 'codex' });
225
+ set((state) => ({
226
+ sessions: [session, ...state.sessions],
227
+ }));
228
+ await get().selectSession(session.id);
229
+ get().addLogEntry('INFO', `Session created in ${cwd} — ${session.id.slice(0, 8)}`);
230
+ get().addToast('success', `Session created in ${cwd.split('/').pop() || cwd}`);
231
+ } catch (err) {
232
+ set({ error: (err as Error).message });
233
+ get().addToast('error', `Failed to create session — ${(err as Error).message}`);
234
+ }
235
+ },
236
+
237
+ pickAndAddWorkspace: async () => {
238
+ try {
239
+ const result = await apiPickDirectory();
240
+ if (result.cancelled || !result.path) return;
241
+ await get().addWorkspace(result.path);
242
+ } catch (err) {
243
+ get().addToast('error', `Failed to pick directory — ${(err as Error).message}`);
244
+ }
245
+ },
156
246
  setWsConnected: (connected) => set({ wsConnected: connected }),
157
247
 
158
- addToast: (type, message) => {
248
+ addToast: (type, message, undoAction, undoLabel) => {
159
249
  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);
250
+ set((state) => ({ toasts: [...state.toasts, { id, type, message, undoAction, undoLabel }] }));
165
251
  },
166
252
 
167
253
  removeToast: (id) => {
@@ -194,6 +280,7 @@ export const useStore = create<StoreState>((set, get) => ({
194
280
 
195
281
  deleteSession: async (id) => {
196
282
  set({ error: null });
283
+ const sessionTitle = get().sessions.find((s) => s.id === id)?.title;
197
284
  try {
198
285
  await apiDeleteSession(id);
199
286
  set((state) => {
@@ -216,13 +303,57 @@ export const useStore = create<StoreState>((set, get) => ({
216
303
  };
217
304
  });
218
305
  get().addLogEntry('INFO', `Session deleted — ${id.slice(0, 8)}`);
219
- get().addToast('info', 'Session deleted');
306
+ get().addToast('info', `Session "${sessionTitle || id.slice(0, 8)}" deleted`);
220
307
  } catch (err) {
221
308
  set({ error: (err as Error).message });
222
309
  get().addToast('error', `Failed to delete session — ${(err as Error).message}`);
223
310
  }
224
311
  },
225
312
 
313
+ renameSession: async (id, title) => {
314
+ set({ error: null });
315
+ try {
316
+ const updated = await apiRenameSession(id, title.trim());
317
+ set((state) => ({
318
+ sessions: state.sessions.map((s) => (s.id === id ? updated : s)),
319
+ sessionDetails: state.sessionDetails[id]
320
+ ? { ...state.sessionDetails, [id]: { ...state.sessionDetails[id], title: updated.title } }
321
+ : state.sessionDetails,
322
+ }));
323
+ } catch (err) {
324
+ set({ error: (err as Error).message });
325
+ get().addToast('error', `Failed to rename session — ${(err as Error).message}`);
326
+ }
327
+ },
328
+
329
+ markFinished: async (id) => {
330
+ set({ error: null });
331
+ try {
332
+ const updated = await apiMarkFinished(id);
333
+ set((state) => ({
334
+ sessions: state.sessions.map((s) => (s.id === id ? updated : s)),
335
+ }));
336
+ get().addToast('info', 'Session marked as finished');
337
+ } catch (err) {
338
+ set({ error: (err as Error).message });
339
+ get().addToast('error', `Failed to mark as finished — ${(err as Error).message}`);
340
+ }
341
+ },
342
+
343
+ reopenSession: async (id) => {
344
+ set({ error: null });
345
+ try {
346
+ const updated = await apiReopenSession(id);
347
+ set((state) => ({
348
+ sessions: state.sessions.map((s) => (s.id === id ? updated : s)),
349
+ }));
350
+ get().addToast('info', 'Session reopened');
351
+ } catch (err) {
352
+ set({ error: (err as Error).message });
353
+ get().addToast('error', `Failed to reopen session — ${(err as Error).message}`);
354
+ }
355
+ },
356
+
226
357
  submitPrompt: async (prompt) => {
227
358
  const sessionId = get().activeSessionId;
228
359
  if (!sessionId) return;
@@ -409,7 +540,7 @@ export const useStore = create<StoreState>((set, get) => ({
409
540
  });
410
541
  },
411
542
 
412
- onTurnCompleted: (sessionId, turnId, usage) => {
543
+ onTurnCompleted: (sessionId, turnId, usage, hasUnreviewed) => {
413
544
  get().addLogEntry('INFO', `turn:completed — ${usage.input_tokens} in / ${usage.output_tokens} out`);
414
545
 
415
546
  set((state) => {
@@ -423,13 +554,14 @@ export const useStore = create<StoreState>((set, get) => ({
423
554
  ...detail,
424
555
  is_running: 0,
425
556
  status: 'active',
557
+ has_unreviewed: hasUnreviewed ? 1 : detail.has_unreviewed,
426
558
  turns: detail.turns.map((t) =>
427
559
  t.id === turnId ? { ...t, status: 'completed', token_usage: usage } : t,
428
560
  ),
429
561
  },
430
562
  },
431
563
  sessions: state.sessions.map((s) =>
432
- s.id === sessionId ? { ...s, is_running: 0, status: 'active' } : s,
564
+ s.id === sessionId ? { ...s, is_running: 0, status: 'active', has_unreviewed: hasUnreviewed ? 1 : s.has_unreviewed } : s,
433
565
  ),
434
566
  };
435
567
  });
@@ -464,6 +596,17 @@ export const useStore = create<StoreState>((set, get) => ({
464
596
  };
465
597
  });
466
598
  },
599
+
600
+ onSessionTitle: (sessionId, title) => {
601
+ set((state) => ({
602
+ sessions: state.sessions.map((s) =>
603
+ s.id === sessionId ? { ...s, title } : s,
604
+ ),
605
+ sessionDetails: state.sessionDetails[sessionId]
606
+ ? { ...state.sessionDetails, [sessionId]: { ...state.sessionDetails[sessionId], title } }
607
+ : state.sessionDetails,
608
+ }));
609
+ },
467
610
  }));
468
611
 
469
612
  /** Insert or replace an item in the correct turn within a SessionDetail. */
@@ -96,6 +96,26 @@ body {
96
96
  .turn-header-icon { width: 24px; height: 24px; border-radius: var(--r-sm); display: flex; align-items: center; justify-content: center; flex-shrink: 0; font-size: 9px; font-weight: 700; color: #fff; }
97
97
  .turn-header-title { font-size: 14px; font-weight: 600; color: var(--text); }
98
98
  .turn-header-meta { font-size: 11px; color: var(--text-muted); }
99
+
100
+ /* Collapsible tool calls */
101
+ .tool-calls { margin-bottom: 16px; }
102
+ .tool-calls-header {
103
+ display: flex; align-items: center; gap: 8px;
104
+ padding: 6px 12px;
105
+ background: var(--surface-1);
106
+ border: 1px solid var(--border);
107
+ border-radius: var(--r-sm);
108
+ cursor: pointer;
109
+ transition: background 150ms ease-out;
110
+ user-select: none;
111
+ }
112
+ .tool-calls-header:hover { background: var(--surface-2); }
113
+ .tool-calls-icon { display: flex; align-items: center; color: var(--text-3); flex-shrink: 0; }
114
+ .tool-calls-summary { font-size: 13px; font-weight: 500; color: var(--text-2); flex: 1; }
115
+ .tool-calls-chevron { color: var(--text-muted); transition: transform 150ms ease-out; flex-shrink: 0; }
116
+ .tool-calls-body { margin-top: 8px; padding-left: 16px; border-left: 2px solid var(--border); }
117
+ .intermediate-segment { margin-bottom: 16px; }
118
+ .intermediate-segment:last-child { margin-bottom: 0; }
99
119
  .user-message { font-size: 14px; color: var(--text); background: var(--accent-soft); padding: 12px 16px; border-radius: var(--r-md); border: 1px solid rgba(217,119,6,0.15); }
100
120
 
101
121
  /* === Command input === */
@@ -202,7 +222,7 @@ body {
202
222
 
203
223
  /* === Status dots === */
204
224
  .status-dot { width: 6px; height: 6px; border-radius: 50%; display: inline-block; flex-shrink: 0; }
205
- .status-dot.running { background: var(--green); }
225
+ .status-dot.running { background: var(--green); animation: dot-pulse 1.5s ease-in-out infinite; }
206
226
  .status-dot.queued { background: var(--amber); }
207
227
  .status-dot.error { background: var(--red); }
208
228
  .status-dot.done { background: var(--accent); }
@@ -233,6 +253,8 @@ body {
233
253
 
234
254
  /* === Turn group === */
235
255
  .turn-group { margin-bottom: 24px; }
256
+ .turn-block { margin-bottom: 32px; padding-bottom: 8px; border-bottom: 1px solid var(--border); }
257
+ .turn-block:last-child { border-bottom: none; }
236
258
  .cmd-block { background: var(--surface-0); border: 1px solid var(--border); border-radius: var(--r-md); padding: 10px 14px; overflow-x: auto; margin-bottom: 12px; }
237
259
  .cmd-block code { font-family: var(--font-mono); font-size: 12px; color: var(--text); }
238
260
  .cmd-output { background: var(--surface-0); border: 1px solid var(--border-muted); border-radius: var(--r-md); padding: 10px 14px; overflow-x: auto; margin-bottom: 12px; max-height: 200px; overflow-y: auto; }
@@ -285,4 +307,59 @@ body {
285
307
 
286
308
  @media (prefers-reduced-motion: reduce) {
287
309
  *, *::before, *::after { animation-duration: 0.01ms !important; animation-iteration-count: 1 !important; transition-duration: 0.01ms !important; }
288
- }
310
+ }
311
+
312
+ /* === Skeleton Loading (F2) === */
313
+ .skeleton-bars { width: 80%; max-width: 500px; margin: 20px 0; }
314
+ .skeleton-bar { height: 10px; border-radius: var(--r-sm); background: var(--surface-1); margin-bottom: 8px; animation: shimmer 1.2s infinite; }
315
+ .skeleton-bar:nth-child(1) { width: 60%; }
316
+ .skeleton-bar:nth-child(2) { width: 85%; }
317
+ .skeleton-bar:nth-child(3) { width: 70%; }
318
+ .skeleton-bar:nth-child(4) { width: 90%; }
319
+ .skeleton-bar:nth-child(5) { width: 50%; }
320
+ @keyframes shimmer { 0% { opacity: 0.3; } 50% { opacity: 0.6; } 100% { opacity: 0.3; } }
321
+
322
+ @keyframes dot-pulse {
323
+ from { opacity: 1; }
324
+ to { opacity: 0.4; }
325
+ }
326
+
327
+ /* === Diff Highlight (F4) === */
328
+ .diff-file-header { transition: background 300ms ease-out; }
329
+ .diff-file-header.diff-highlight { background: var(--accent-soft); }
330
+
331
+ /* === Palette Footer (F5) === */
332
+ .palette-footer { display: flex; gap: 16px; padding: 8px 16px; border-top: 1px solid var(--border-muted); font-size: 11px; color: var(--text-muted); }
333
+ .palette-footer-hint { display: flex; align-items: center; gap: 4px; }
334
+ .kbd { font-family: var(--font-mono); font-size: 10px; padding: 1px 5px; border-radius: var(--r-sm); background: var(--surface-2); border: 1px solid var(--border); }
335
+
336
+ /* === Toast Undo (F6) === */
337
+ .toast-undo { font-size: 12px; font-weight: 500; color: var(--accent); background: transparent; border: none; cursor: pointer; padding: 2px 8px; border-radius: var(--r-sm); display: flex; align-items: center; gap: 4px; transition: background var(--dur-fast) var(--ease); }
338
+ .toast-undo:hover { background: var(--accent-soft); }
339
+
340
+ /* === Workspace Sidebar (hierarchy + quick create) === */
341
+ .tree-section-label { flex: 1; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; font-size: 11px; }
342
+ .tree-section-add { width: 18px; height: 18px; border: none; border-radius: var(--r-sm); background: transparent; color: var(--text-muted); cursor: pointer; display: flex; align-items: center; justify-content: center; transition: all var(--dur-fast) var(--ease); flex-shrink: 0; }
343
+ .tree-section-add:hover { background: var(--accent-soft); color: var(--accent); }
344
+ .tree-empty { padding: 4px 8px 4px 26px; font-size: 11px; color: var(--text-muted); font-style: italic; }
345
+
346
+ /* === Sidebar sub-groups (Working / Finished) === */
347
+ .tree-subgroup { margin-bottom: 1px; }
348
+ .tree-subgroup-header { padding: 3px 8px 3px 18px; display: flex; align-items: center; gap: 5px; font-size: 10px; font-weight: 590; color: var(--text-muted); cursor: pointer; user-select: none; border-radius: var(--r-sm); transition: background var(--dur-fast) var(--ease); }
349
+ .tree-subgroup-header:hover { background: rgba(0,0,0,0.04); color: var(--text-2); }
350
+ .tree-subgroup.collapsed .tree-chevron { transform: rotate(-90deg); }
351
+ .tree-subgroup.collapsed .tree-subgroup-items { display: none; }
352
+ .tree-subgroup-label { text-transform: uppercase; letter-spacing: 0.05em; }
353
+ .tree-subgroup-count { margin-left: auto; font-size: 9px; color: var(--text-muted); font-variant-numeric: tabular-nums; }
354
+ .tree-subgroup-items { padding-left: 0; }
355
+ .tree-item-finished .tree-label { color: var(--text-muted); opacity: 0.6; }
356
+ .tree-item-finished:hover .tree-label { opacity: 0.85; }
357
+
358
+ /* === Context Menu === */
359
+ .ctx-menu { background: var(--surface-0); border: 1px solid var(--border); border-radius: var(--r-md); box-shadow: 0 4px 16px rgba(0,0,0,0.12); padding: 4px; min-width: 160px; }
360
+ .ctx-menu-item { display: flex; align-items: center; gap: 8px; padding: 6px 10px; border-radius: var(--r-sm); font-size: 12px; color: var(--text-2); cursor: pointer; transition: background var(--dur-fast) var(--ease); }
361
+ .ctx-menu-item:hover { background: var(--accent-soft); color: var(--text); }
362
+ .ctx-menu-item.ctx-danger:hover { background: rgba(220,38,38,0.08); color: var(--red); }
363
+
364
+ /* === Inline rename input in sidebar === */
365
+ .tree-rename-input { flex: 1; border: 1px solid var(--accent); border-radius: var(--r-sm); padding: 1px 6px; font-size: 12px; font-family: var(--font-sans); color: var(--text); background: var(--canvas); outline: none; box-shadow: var(--focus-ring); min-width: 0; }
@@ -22,6 +22,8 @@ export interface Session {
22
22
  external_thread_id: string | null;
23
23
  status: SessionStatus;
24
24
  is_running: number; // 0 | 1
25
+ lifecycle: 'working' | 'finished';
26
+ has_unreviewed: number; // 0 | 1
25
27
  created_at: string;
26
28
  updated_at: string;
27
29
  }
@@ -64,6 +66,15 @@ export interface SessionDetail extends Session {
64
66
  turns: Turn[];
65
67
  }
66
68
 
69
+ // Workspace — directory path for grouping sessions in sidebar
70
+ export interface Workspace {
71
+ id: string;
72
+ path: string;
73
+ label: string | null;
74
+ deleted: number; // 0 = active, 1 = soft-deleted (hidden from sidebar)
75
+ created_at: string;
76
+ }
77
+
67
78
  // UI-only types (not from backend)
68
79
 
69
80
  export interface LogEntry {
@@ -89,27 +100,54 @@ export interface PaletteCommand {
89
100
  export type RightTab = 'turns' | 'metrics' | 'changes' | 'logs';
90
101
  export type ChangesScope = 'session' | 'turngroup';
91
102
 
103
+ // Helper: map runtime_type to icon class + text badge
104
+ export function runtimeIcon(runtimeType: string): { iconClass: string; iconText: string } {
105
+ switch (runtimeType) {
106
+ case 'claude': return { iconClass: 'ic-claude', iconText: 'CL' };
107
+ case 'codex': return { iconClass: 'ic-codex', iconText: 'CX' };
108
+ case 'repoburg': return { iconClass: 'ic-repoburg', iconText: 'RB' };
109
+ case 'cursor': return { iconClass: 'ic-cursor', iconText: 'CR' };
110
+ default: return { iconClass: 'ic-codex', iconText: 'CX' };
111
+ }
112
+ }
113
+
114
+ // Helper: format duration from ISO timestamps range
115
+ export function formatDuration(startISO: string, endISO: string | null): string {
116
+ const start = new Date(startISO).getTime();
117
+ const end = endISO ? new Date(endISO).getTime() : Date.now();
118
+ const seconds = Math.max(0, Math.floor((end - start) / 1000));
119
+ if (seconds < 60) return `${seconds}s`;
120
+ const minutes = Math.floor(seconds / 60);
121
+ const remSec = seconds % 60;
122
+ if (minutes < 60) return `${minutes}m ${remSec}s`;
123
+ const hours = Math.floor(minutes / 60);
124
+ const remMin = minutes % 60;
125
+ return `${hours}h ${remMin}m`;
126
+ }
127
+
92
128
  // Helper: map SessionStatus to UI status dot class
93
- export function statusToDot(status: SessionStatus, isRunning: boolean): string {
129
+ export function statusToDot(status: SessionStatus, isRunning: boolean, hasUnreviewed: boolean = false, lifecycle: string = 'working'): string {
130
+ if (lifecycle === 'finished') return '';
94
131
  if (isRunning) return 'running';
132
+ if (hasUnreviewed && status !== 'failed') return 'queued';
95
133
  switch (status) {
96
- case 'active': return 'done';
97
- case 'completed': return 'done';
98
134
  case 'failed': return 'error';
99
135
  case 'aborted': return 'paused';
100
- default: return 'done';
136
+ default: return 'paused';
101
137
  }
102
138
  }
103
139
 
104
140
  // Helper: map SessionStatus to UI status label
105
- export function statusToLabel(status: SessionStatus, isRunning: boolean): string {
141
+ export function statusToLabel(status: SessionStatus, isRunning: boolean, hasUnreviewed: boolean = false, lifecycle: string = 'working'): string {
142
+ if (lifecycle === 'finished') return 'Finished';
106
143
  if (isRunning) return 'Running';
144
+ if (hasUnreviewed && status !== 'failed') return 'Review Pending';
107
145
  switch (status) {
108
- case 'active': return 'Active';
109
- case 'completed': return 'Completed';
110
- case 'running': return 'Running';
111
146
  case 'failed': return 'Failed';
112
147
  case 'aborted': return 'Aborted';
148
+ case 'active': return 'Idle';
149
+ case 'completed': return 'Completed';
150
+ case 'running': return 'Running';
113
151
  default: return status;
114
152
  }
115
153
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "repoburg",
3
- "version": "1.3.186",
3
+ "version": "1.3.188",
4
4
  "description": "A local AI-powered software developer assistant that runs on your own machine.",
5
5
  "author": "Celal Ertug",
6
6
  "license": "SEE LICENSE IN LICENSE",