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,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',
|
|
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 '
|
|
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
|
}
|