@bramblex/codex-workbench 0.1.2 → 0.1.4

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.
@@ -0,0 +1,657 @@
1
+ 'use strict';
2
+
3
+ const path = require('path');
4
+ const blessed = require('blessed');
5
+ const { printList, printShow } = require('../cli-output');
6
+ const { deleteSessionFile } = require('../model/session-store');
7
+ const { localTime, shortId, truncate } = require('../model/format');
8
+ const {
9
+ LOCAL_SOURCE,
10
+ createSourceDirectory,
11
+ listSourceDirectories,
12
+ loadWorkbenchSessions,
13
+ runSourceNewSession,
14
+ runSourceSessionCommand,
15
+ sourceById,
16
+ updateSourceMetadata,
17
+ } = require('../services/session-sources');
18
+ const { usableCwd } = require('../services/codex-runner');
19
+ const { createDirectoryPicker } = require('./directory-picker');
20
+
21
+ async function runWorkbench() {
22
+ if (!process.stdin.isTTY || !process.stdout.isTTY) {
23
+ return printList(loadWorkbenchSessions().sessions);
24
+ }
25
+
26
+ let sessions = [];
27
+ let sources = [];
28
+ let sourceErrors = [];
29
+ let groups = [];
30
+ let groupIndex = 0;
31
+ let selected = 0;
32
+ let message = '';
33
+ let syncingList = false;
34
+ let syncingProjects = false;
35
+ let projectWidth = 32;
36
+ let activePanel = 'projects';
37
+
38
+ const screen = blessed.screen({
39
+ smartCSR: true,
40
+ fullUnicode: true,
41
+ title: 'Codex Workbench',
42
+ });
43
+
44
+ const header = blessed.box({
45
+ parent: screen,
46
+ top: 0,
47
+ left: 0,
48
+ right: 0,
49
+ height: 3,
50
+ padding: { left: 1, right: 1 },
51
+ style: { fg: 'white', bg: 'blue' },
52
+ content: 'Codex Workbench',
53
+ });
54
+
55
+ const projectsList = blessed.list({
56
+ parent: screen,
57
+ label: ' Projects ',
58
+ top: 3,
59
+ left: 0,
60
+ width: projectWidth,
61
+ bottom: 3,
62
+ border: 'line',
63
+ mouse: true,
64
+ keys: true,
65
+ vi: false,
66
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'green' } },
67
+ style: {
68
+ border: { fg: 'green' },
69
+ selected: { fg: 'black', bg: 'green', bold: true },
70
+ item: { fg: 'white' },
71
+ },
72
+ });
73
+
74
+ const sessionsList = blessed.list({
75
+ parent: screen,
76
+ label: ' Sessions ',
77
+ top: 3,
78
+ left: projectWidth,
79
+ right: 0,
80
+ height: '40%',
81
+ border: 'line',
82
+ mouse: true,
83
+ keys: true,
84
+ vi: false,
85
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
86
+ style: {
87
+ border: { fg: 'cyan' },
88
+ selected: { fg: 'black', bg: 'cyan', bold: true },
89
+ item: { fg: 'white' },
90
+ },
91
+ });
92
+
93
+ const detailBox = blessed.log({
94
+ parent: screen,
95
+ label: ' Details ',
96
+ top: '50%',
97
+ left: projectWidth,
98
+ right: 0,
99
+ bottom: 3,
100
+ border: 'line',
101
+ padding: { left: 1, right: 1 },
102
+ scrollable: true,
103
+ mouse: true,
104
+ keys: true,
105
+ vi: true,
106
+ alwaysScroll: true,
107
+ tags: false,
108
+ parseTags: false,
109
+ scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
110
+ style: { border: { fg: 'cyan' }, fg: 'white' },
111
+ });
112
+
113
+ const status = blessed.box({
114
+ parent: screen,
115
+ left: 0,
116
+ right: 0,
117
+ bottom: 0,
118
+ height: 3,
119
+ padding: { left: 1, right: 1 },
120
+ style: { fg: 'white', bg: 'black' },
121
+ });
122
+
123
+ const prompt = blessed.prompt({
124
+ parent: screen,
125
+ border: 'line',
126
+ height: 8,
127
+ width: '70%',
128
+ top: 'center',
129
+ left: 'center',
130
+ padding: { left: 1, right: 1 },
131
+ style: { border: { fg: 'yellow' }, fg: 'white', bg: 'black' },
132
+ });
133
+
134
+ const question = blessed.question({
135
+ parent: screen,
136
+ border: 'line',
137
+ height: 6,
138
+ width: '70%',
139
+ top: 'center',
140
+ left: 'center',
141
+ padding: { left: 1, right: 1 },
142
+ style: { border: { fg: 'red' }, fg: 'white', bg: 'black' },
143
+ });
144
+
145
+ const sessionsForSource = (sourceId) => sessions.filter((session) => session.sourceId === sourceId);
146
+
147
+ const buildGroups = () => {
148
+ const nextGroups = [{ kind: 'all', label: 'All', source: null, cwd: null }];
149
+ for (const source of sources) {
150
+ const sourceSessions = sessionsForSource(source.id);
151
+ nextGroups.push({ kind: 'source', source, cwd: null });
152
+ const cwds = [...new Set(sourceSessions.map((session) => session.cwd))];
153
+ for (const cwd of cwds) {
154
+ nextGroups.push({ kind: 'project', source, cwd });
155
+ }
156
+ }
157
+ return nextGroups;
158
+ };
159
+
160
+ const currentGroup = () => groups[groupIndex] || groups[0] || { kind: 'all', source: null, cwd: null };
161
+
162
+ const currentSessions = () => {
163
+ const group = currentGroup();
164
+ if (group.kind === 'all') return sessions;
165
+ if (group.kind === 'source') return sessionsForSource(group.source.id);
166
+ return sessions.filter((session) => session.sourceId === group.source.id && session.cwd === group.cwd);
167
+ };
168
+
169
+ const selectedSession = () => currentSessions()[selected] || null;
170
+
171
+ const groupDisplayName = (group) => {
172
+ if (group.kind === 'all') return 'All sources';
173
+ if (group.kind === 'source') return group.source.label;
174
+ return `${group.source.label}: ${group.cwd}`;
175
+ };
176
+
177
+ const projectLabel = (group) => {
178
+ if (group.kind === 'all') return `All (${sessions.length})`;
179
+ if (group.kind === 'source') {
180
+ const count = sessionsForSource(group.source.id).length;
181
+ return `${truncate(group.source.label, Math.max(10, projectWidth - 6))} (${count})`;
182
+ }
183
+ const count = sessions.filter((session) => session.sourceId === group.source.id && session.cwd === group.cwd).length;
184
+ const base = path.basename(group.cwd) || group.cwd;
185
+ return ` ${truncate(base, Math.max(10, projectWidth - 12))} (${count})`;
186
+ };
187
+
188
+ const sessionLabel = (session) => {
189
+ const flags = [
190
+ session.name ? 'renamed' : '',
191
+ session.note ? 'note' : '',
192
+ ].filter(Boolean).join(',');
193
+ const title = session.name || session.first || session.last || '(no prompt)';
194
+ const flagText = flags ? `[${flags}]` : '';
195
+ return `${shortId(session.id)} ${String(session.turns).padStart(2)}t ${truncate(localTime(session.updatedAt), 18)} ${flagText} ${truncate(title, 90)}`;
196
+ };
197
+
198
+ const detailContent = (session) => {
199
+ if (!session) return 'No sessions in this project.';
200
+ const title = session.name || session.first || session.last || '(no prompt)';
201
+ return [
202
+ title,
203
+ '',
204
+ `id: ${session.id}`,
205
+ `source: ${session.sourceLabel || 'Local'}`,
206
+ `cwd: ${session.cwd}`,
207
+ `started: ${localTime(session.startedAt)}`,
208
+ `updated: ${localTime(session.updatedAt)}`,
209
+ `turns: ${session.turns}`,
210
+ session.note ? `note: ${session.note}` : '',
211
+ '',
212
+ `last user: ${session.last || session.first || ''}`,
213
+ '',
214
+ `last assistant: ${session.lastAssistant || ''}`,
215
+ ].filter((line) => line !== '').join('\n');
216
+ };
217
+
218
+ const setMessage = (text, isError = false) => {
219
+ message = text || 'Ready';
220
+ status.style.fg = isError ? 'red' : 'white';
221
+ };
222
+
223
+ const reload = () => {
224
+ const state = loadWorkbenchSessions();
225
+ sources = state.sources;
226
+ sourceErrors = state.errors;
227
+ sessions = state.sessions.filter((s) => !s.archived && !s.hidden);
228
+ groups = buildGroups();
229
+ if (groupIndex >= groups.length) groupIndex = Math.max(0, groups.length - 1);
230
+ const visible = currentSessions();
231
+ if (selected >= visible.length) selected = Math.max(0, visible.length - 1);
232
+ if (sourceErrors.length && (!message || message === 'Ready')) {
233
+ const first = sourceErrors[0];
234
+ const detail = `${first.source.label}: ${first.error}`;
235
+ const prefix = sourceErrors.length === 1 ? 'Remote source failed' : `${sourceErrors.length} remote sources failed`;
236
+ setMessage(`${prefix}: ${truncate(detail, 100)}`, true);
237
+ }
238
+ };
239
+
240
+ const applyLayout = () => {
241
+ const width = screen.width || 80;
242
+ const height = screen.height || 24;
243
+ projectWidth = Math.min(42, Math.max(24, Math.floor(width * 0.28)));
244
+ const top = 3;
245
+ const bottom = 3;
246
+ const available = Math.max(10, height - top - bottom);
247
+ const sessionsHeight = Math.max(7, Math.floor(available * 0.45));
248
+
249
+ projectsList.width = projectWidth;
250
+ projectsList.top = top;
251
+ projectsList.bottom = bottom;
252
+
253
+ sessionsList.left = projectWidth;
254
+ sessionsList.top = top;
255
+ sessionsList.height = sessionsHeight;
256
+
257
+ detailBox.left = projectWidth;
258
+ detailBox.top = top + sessionsHeight;
259
+ detailBox.bottom = bottom;
260
+ };
261
+
262
+ const syncProjects = () => {
263
+ const items = groups.length ? groups.map(projectLabel) : ['No projects'];
264
+ syncingProjects = true;
265
+ projectsList.clearItems();
266
+ projectsList.setItems(items);
267
+ projectsList.select(groupIndex);
268
+ projectsList.scrollTo(groupIndex);
269
+ syncingProjects = false;
270
+ };
271
+
272
+ const syncList = () => {
273
+ const visible = currentSessions();
274
+ const listRows = Math.max(1, (sessionsList.height || Math.floor((screen.height || 24) * 0.4)) - 2);
275
+ const items = visible.length ? visible.map(sessionLabel) : ['No sessions in this project.'];
276
+ while (items.length < listRows) items.push('');
277
+ syncingList = true;
278
+ sessionsList.clearItems();
279
+ sessionsList.setItems(items);
280
+ selected = Math.min(selected, Math.max(0, visible.length - 1));
281
+ sessionsList.childBase = 0;
282
+ sessionsList.childOffset = 0;
283
+ sessionsList.select(selected);
284
+ sessionsList.scrollTo(0);
285
+ syncingList = false;
286
+ };
287
+
288
+ const setPanelLabel = (panel, title, focused, fg) => {
289
+ panel.setLabel(focused ? ` > ${title} ` : ` ${title} `);
290
+ if (!panel._label) return;
291
+ panel._label.style.fg = focused ? fg : 'white';
292
+ panel._label.style.bg = 'default';
293
+ panel._label.style.bold = focused;
294
+ };
295
+
296
+ const updateFocusStyles = () => {
297
+ const projectFocused = activePanel === 'projects';
298
+ const sessionsFocused = activePanel === 'sessions';
299
+ const detailFocused = activePanel === 'details';
300
+
301
+ projectsList.style.border.fg = projectFocused ? 'green' : 'gray';
302
+ sessionsList.style.border.fg = sessionsFocused ? 'cyan' : 'gray';
303
+ detailBox.style.border.fg = detailFocused ? 'yellow' : 'gray';
304
+ projectsList.style.selected.bg = projectFocused ? 'green' : 'gray';
305
+ projectsList.style.selected.fg = 'black';
306
+ sessionsList.style.selected.bg = sessionsFocused ? 'cyan' : 'gray';
307
+ sessionsList.style.selected.fg = 'black';
308
+
309
+ setPanelLabel(projectsList, `Sources (${sources.length})`, projectFocused, 'green');
310
+ setPanelLabel(sessionsList, 'Sessions', sessionsFocused, 'cyan');
311
+ setPanelLabel(detailBox, 'Details', detailFocused, 'yellow');
312
+
313
+ const firstLine = message || 'Ready';
314
+ if (projectFocused) {
315
+ status.setContent(`${firstLine}\nProjects: ↑/↓ select project n new project →/Enter sessions Tab focus q quit`);
316
+ } else if (detailFocused) {
317
+ status.setContent(`${firstLine}\nDetails: ↑/↓ scroll n new session ← sessions → projects Tab focus q quit`);
318
+ } else {
319
+ status.setContent(`${firstLine}\nSessions: ↑/↓ select Enter resume r rename n new session f fork v view o note a archive d delete q quit`);
320
+ }
321
+ };
322
+
323
+ const render = () => {
324
+ applyLayout();
325
+ const visible = currentSessions();
326
+ header.setContent(` Codex Workbench\n ${visible.length}/${sessions.length} visible ${groupDisplayName(currentGroup())}`);
327
+ detailBox.setContent(detailContent(selectedSession()));
328
+ updateFocusStyles();
329
+ screen.render();
330
+ };
331
+
332
+ const focusPanel = (panel, panelName) => {
333
+ activePanel = panelName;
334
+ panel.focus();
335
+ updateFocusStyles();
336
+ screen.render();
337
+ };
338
+
339
+ const askInput = (label, initial = '') => new Promise((resolve) => {
340
+ prompt.setFront();
341
+ prompt.input(label, initial, (err, value) => resolve(err ? null : value));
342
+ });
343
+
344
+ const askConfirm = (label) => new Promise((resolve) => {
345
+ question.setFront();
346
+ question.ask(label, (err, answer) => resolve(!err && Boolean(answer)));
347
+ });
348
+
349
+ const directoryPicker = createDirectoryPicker({
350
+ askInput,
351
+ focusOnClose: () => focusPanel(projectsList, 'projects'),
352
+ screen,
353
+ truncate,
354
+ });
355
+
356
+ const promptOpen = () => prompt.visible || question.visible || directoryPicker.isOpen();
357
+
358
+ const leaveScreen = () => {
359
+ screen.destroy();
360
+ };
361
+
362
+ const refreshAfterAction = (text, isError = false, focusCwd = null, focusSourceId = null) => {
363
+ setMessage(text, isError);
364
+ reload();
365
+ if (focusCwd) {
366
+ const nextGroupIndex = groups.findIndex((group) => {
367
+ return group.kind === 'project' &&
368
+ group.cwd === focusCwd &&
369
+ (!focusSourceId || group.source.id === focusSourceId);
370
+ });
371
+ if (nextGroupIndex !== -1) groupIndex = nextGroupIndex;
372
+ }
373
+ syncProjects();
374
+ syncList();
375
+ render();
376
+ };
377
+
378
+ const selectGroup = (index) => {
379
+ if (!groups.length) return;
380
+ groupIndex = Math.max(0, Math.min(groups.length - 1, index));
381
+ selected = 0;
382
+ syncProjects();
383
+ syncList();
384
+ render();
385
+ };
386
+
387
+ const runCodexAndReturn = (command, session, args = [], doneText = `${command} finished.`) => {
388
+ screen.leave();
389
+ let status = 0;
390
+ try {
391
+ status = runSourceSessionCommand(session, command, args);
392
+ } finally {
393
+ screen.enter();
394
+ }
395
+ if (status === 0) refreshAfterAction(doneText);
396
+ else refreshAfterAction(`${command} exited with code ${status}.`, true);
397
+ return status;
398
+ };
399
+
400
+ const currentProjectCwd = () => {
401
+ const group = currentGroup();
402
+ if (group.kind === 'project') return group.cwd;
403
+ const session = selectedSession();
404
+ if (session && session.cwd && session.cwd !== '(unknown)') return session.cwd;
405
+ if (group.kind === 'source' && group.source.remote) return '.';
406
+ return process.cwd();
407
+ };
408
+
409
+ const currentSource = () => {
410
+ const group = currentGroup();
411
+ if (group.kind === 'source' || group.kind === 'project') return group.source;
412
+ if (activePanel === 'projects') return LOCAL_SOURCE;
413
+ const session = selectedSession();
414
+ return session ? sourceById(sources, session.sourceId) : LOCAL_SOURCE;
415
+ };
416
+
417
+ const currentDirectoryStart = () => {
418
+ const group = currentGroup();
419
+ if (group.kind === 'project') return group.cwd;
420
+ return currentSource().remote ? '.' : currentProjectCwd();
421
+ };
422
+
423
+ const directoryOpsForSource = (source) => ({
424
+ listDirectories: (dir) => listSourceDirectories(source, dir),
425
+ createDirectory: (parent, name) => createSourceDirectory(source, parent, name),
426
+ });
427
+
428
+ const runNewCodexAndReturn = (cwd, args = []) => {
429
+ const source = currentSource();
430
+ const resolvedCwd = source.remote ? cwd : usableCwd(cwd);
431
+ screen.leave();
432
+ let status = 0;
433
+ try {
434
+ status = runSourceNewSession(source, resolvedCwd, args);
435
+ } finally {
436
+ screen.enter();
437
+ }
438
+ const label = source.remote ? `${source.label}: ${resolvedCwd}` : resolvedCwd;
439
+ if (status === 0) refreshAfterAction(`New session finished in ${label}.`, false, resolvedCwd, source.id);
440
+ else refreshAfterAction(`new session exited with code ${status}.`, true, resolvedCwd, source.id);
441
+ return status;
442
+ };
443
+
444
+ const runAction = async (action) => {
445
+ if (promptOpen()) return;
446
+ const session = selectedSession();
447
+ if (!session) return;
448
+ try {
449
+ await action(session);
450
+ } catch (err) {
451
+ setMessage(`error: ${err.message}`, true);
452
+ render();
453
+ }
454
+ };
455
+
456
+ reload();
457
+ if (!sourceErrors.length) setMessage('Ready');
458
+ applyLayout();
459
+ syncProjects();
460
+ syncList();
461
+
462
+ projectsList.on('select item', (_item, index) => {
463
+ if (syncingProjects) return;
464
+ activePanel = 'projects';
465
+ selectGroup(index);
466
+ });
467
+
468
+ projectsList.on('focus', () => {
469
+ activePanel = 'projects';
470
+ updateFocusStyles();
471
+ });
472
+
473
+ sessionsList.on('focus', () => {
474
+ activePanel = 'sessions';
475
+ updateFocusStyles();
476
+ });
477
+
478
+ detailBox.on('focus', () => {
479
+ activePanel = 'details';
480
+ updateFocusStyles();
481
+ });
482
+
483
+ projectsList.key(['j', 'down'], () => {
484
+ if (promptOpen()) return;
485
+ selectGroup(groupIndex + 1);
486
+ });
487
+
488
+ projectsList.key(['k', 'up'], () => {
489
+ if (promptOpen()) return;
490
+ selectGroup(groupIndex - 1);
491
+ });
492
+
493
+ projectsList.key(['right', 'l', 'enter'], () => {
494
+ if (promptOpen()) return;
495
+ focusPanel(sessionsList, 'sessions');
496
+ });
497
+
498
+ sessionsList.on('select item', (_item, index) => {
499
+ if (syncingList) return;
500
+ activePanel = 'sessions';
501
+ const visible = currentSessions();
502
+ if (index >= visible.length) {
503
+ selected = Math.max(0, visible.length - 1);
504
+ sessionsList.select(selected);
505
+ return;
506
+ }
507
+ selected = Math.min(index, Math.max(0, visible.length - 1));
508
+ detailBox.setContent(detailContent(selectedSession()));
509
+ screen.render();
510
+ });
511
+
512
+ sessionsList.key(['enter'], () => runAction((session) => {
513
+ runCodexAndReturn('resume', session);
514
+ }));
515
+
516
+ sessionsList.key(['j'], () => {
517
+ if (promptOpen()) return;
518
+ sessionsList.down();
519
+ screen.render();
520
+ });
521
+
522
+ sessionsList.key(['k'], () => {
523
+ if (promptOpen()) return;
524
+ sessionsList.up();
525
+ screen.render();
526
+ });
527
+
528
+ sessionsList.key(['left', 'h'], () => {
529
+ if (promptOpen()) return;
530
+ focusPanel(projectsList, 'projects');
531
+ });
532
+
533
+ sessionsList.key(['right', 'l'], () => {
534
+ if (promptOpen()) return;
535
+ focusPanel(detailBox, 'details');
536
+ });
537
+
538
+ detailBox.key(['left', 'h'], () => {
539
+ if (promptOpen()) return;
540
+ focusPanel(sessionsList, 'sessions');
541
+ });
542
+
543
+ detailBox.key(['right', 'l'], () => {
544
+ if (promptOpen()) return;
545
+ focusPanel(projectsList, 'projects');
546
+ });
547
+
548
+ screen.on('resize', () => {
549
+ applyLayout();
550
+ if (directoryPicker.isOpen()) directoryPicker.applyLayout();
551
+ syncProjects();
552
+ syncList();
553
+ render();
554
+ });
555
+
556
+ screen.key(['tab'], () => {
557
+ if (promptOpen()) return;
558
+ if (activePanel === 'projects') focusPanel(sessionsList, 'sessions');
559
+ else if (activePanel === 'sessions') focusPanel(detailBox, 'details');
560
+ else focusPanel(projectsList, 'projects');
561
+ });
562
+
563
+ screen.key(['S-tab'], () => {
564
+ if (promptOpen()) return;
565
+ if (activePanel === 'details') focusPanel(sessionsList, 'sessions');
566
+ else if (activePanel === 'sessions') focusPanel(projectsList, 'projects');
567
+ else focusPanel(detailBox, 'details');
568
+ });
569
+
570
+ screen.key(['q', 'escape', 'C-c'], () => {
571
+ if (promptOpen()) return;
572
+ leaveScreen();
573
+ process.exit(0);
574
+ });
575
+
576
+ screen.key(['f'], () => runAction((session) => {
577
+ runCodexAndReturn('fork', session);
578
+ }));
579
+
580
+ screen.key(['v'], () => runAction((session) => {
581
+ leaveScreen();
582
+ printShow(session);
583
+ process.exit(0);
584
+ }));
585
+
586
+ screen.key(['n'], async () => {
587
+ if (promptOpen()) return;
588
+ if (activePanel === 'projects') {
589
+ const source = currentSource();
590
+ const dir = await directoryPicker.ask(currentDirectoryStart(), directoryOpsForSource(source));
591
+ if (!dir) {
592
+ setMessage('New project cancelled.');
593
+ return render();
594
+ }
595
+ runNewCodexAndReturn(dir);
596
+ return;
597
+ }
598
+ runNewCodexAndReturn(currentProjectCwd());
599
+ });
600
+
601
+ screen.key(['r'], () => runAction(async (session) => {
602
+ const name = await askInput('Name', session.name || '');
603
+ if (name === null) return render();
604
+ const status = updateSourceMetadata(session, { name });
605
+ refreshAfterAction(status === 0 ? 'Renamed.' : `rename exited with code ${status}.`, status !== 0);
606
+ }));
607
+
608
+ screen.key(['o'], () => runAction(async (session) => {
609
+ const note = await askInput('Note', session.note || '');
610
+ if (note === null) return render();
611
+ const status = updateSourceMetadata(session, { note });
612
+ refreshAfterAction(status === 0 ? 'Note saved.' : `note exited with code ${status}.`, status !== 0);
613
+ }));
614
+
615
+ screen.key(['a'], () => runAction((session) => {
616
+ runCodexAndReturn('archive', session, [], `Archived ${shortId(session.id)}.`);
617
+ }));
618
+
619
+ screen.key(['d'], () => runAction(async (session) => {
620
+ const confirmed = await askConfirm(`Delete ${shortId(session.id)}? Enter/y to confirm, n/Esc to cancel`);
621
+ if (!confirmed) {
622
+ setMessage('Delete cancelled.');
623
+ return render();
624
+ }
625
+ const status = runCodexAndReturn('delete', session, ['--force'], `Deleted ${shortId(session.id)}.`);
626
+ if (status !== 0) {
627
+ if (session.sourceRemote) {
628
+ const hideSession = await askConfirm(`Remote delete failed for ${shortId(session.id)}. Hide from remote workbench instead?`);
629
+ if (hideSession) {
630
+ updateSourceMetadata(session, { hidden: true });
631
+ refreshAfterAction(`Hidden ${shortId(session.id)}.`);
632
+ }
633
+ return;
634
+ }
635
+ const removeFile = await askConfirm(`Codex could not delete ${shortId(session.id)}. Delete its session file?`);
636
+ if (removeFile) {
637
+ deleteSessionFile(session);
638
+ refreshAfterAction(`Deleted file for ${shortId(session.id)}.`);
639
+ return;
640
+ }
641
+ const hideSession = await askConfirm(`Hide ${shortId(session.id)} from workbench instead?`);
642
+ if (hideSession) {
643
+ updateSourceMetadata(session, { hidden: true });
644
+ refreshAfterAction(`Hidden ${shortId(session.id)}.`);
645
+ }
646
+ }
647
+ }));
648
+
649
+ projectsList.focus();
650
+ render();
651
+
652
+ return new Promise(() => {});
653
+ }
654
+
655
+ module.exports = {
656
+ runWorkbench,
657
+ };