@bramblex/codex-workbench 0.1.3 → 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.
- package/README.md +64 -11
- package/bin/codex-workbench +1 -1
- package/package.json +10 -3
- package/src/cli-output.js +109 -0
- package/src/cli.js +55 -741
- package/src/config.js +18 -0
- package/src/model/directories.js +38 -0
- package/src/model/format.js +21 -0
- package/src/model/session-store.js +156 -0
- package/src/model/workbench-config.js +38 -0
- package/src/services/codex-runner.js +75 -0
- package/src/services/session-sources.js +148 -0
- package/src/services/ssh-runner.js +43 -0
- package/src/ui/directory-picker.js +219 -0
- package/src/ui/workbench.js +657 -0
package/src/cli.js
CHANGED
|
@@ -1,314 +1,25 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
'use strict';
|
|
3
3
|
|
|
4
|
-
const
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
codex-workbench show <session>
|
|
24
|
-
codex-workbench rename <session> <name>
|
|
25
|
-
codex-workbench note <session> <note>
|
|
26
|
-
codex-workbench resume <session> [prompt...]
|
|
27
|
-
codex-workbench fork <session>
|
|
28
|
-
codex-workbench archive <session>
|
|
29
|
-
codex-workbench unarchive <session>
|
|
30
|
-
codex-workbench hide <session>
|
|
31
|
-
codex-workbench unhide <session>
|
|
32
|
-
codex-workbench delete <session> [--force] [--file]
|
|
33
|
-
|
|
34
|
-
Environment:
|
|
35
|
-
CODEX_HOME default: ~/.codex
|
|
36
|
-
CODEX_SESSIONS_DIR default: $CODEX_HOME/sessions
|
|
37
|
-
CODEX_WORKBENCH_META default: $CODEX_HOME/codex-workbench.json
|
|
38
|
-
CODEX_BIN default: codex from shell PATH
|
|
39
|
-
`);
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
function readJson(file, fallback) {
|
|
43
|
-
try {
|
|
44
|
-
return JSON.parse(fs.readFileSync(file, 'utf8'));
|
|
45
|
-
} catch {
|
|
46
|
-
return fallback;
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
function writeJson(file, value) {
|
|
51
|
-
fs.mkdirSync(path.dirname(file), { recursive: true });
|
|
52
|
-
fs.writeFileSync(file, JSON.stringify(value, null, 2) + '\n');
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
function walk(dir, out = []) {
|
|
56
|
-
let entries = [];
|
|
57
|
-
try {
|
|
58
|
-
entries = fs.readdirSync(dir, { withFileTypes: true });
|
|
59
|
-
} catch {
|
|
60
|
-
return out;
|
|
61
|
-
}
|
|
62
|
-
for (const entry of entries) {
|
|
63
|
-
const full = path.join(dir, entry.name);
|
|
64
|
-
if (entry.isDirectory()) walk(full, out);
|
|
65
|
-
else if (entry.isFile() && entry.name.endsWith('.jsonl')) out.push(full);
|
|
66
|
-
}
|
|
67
|
-
return out;
|
|
68
|
-
}
|
|
69
|
-
|
|
70
|
-
function textFromContent(content) {
|
|
71
|
-
if (!Array.isArray(content)) return '';
|
|
72
|
-
return content
|
|
73
|
-
.filter((item) => item && (item.type === 'input_text' || item.type === 'output_text'))
|
|
74
|
-
.map((item) => item.text || '')
|
|
75
|
-
.join(' ')
|
|
76
|
-
.replace(/\s+/g, ' ')
|
|
77
|
-
.trim();
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
function isNoiseUserText(text) {
|
|
81
|
-
return text.includes('<environment_context>') || text.includes('<permissions instructions>');
|
|
82
|
-
}
|
|
83
|
-
|
|
84
|
-
function parseSession(file) {
|
|
85
|
-
const stat = fs.statSync(file);
|
|
86
|
-
const raw = fs.readFileSync(file, 'utf8').trim();
|
|
87
|
-
const lines = raw ? raw.split(/\n/) : [];
|
|
88
|
-
let meta = {};
|
|
89
|
-
const messages = [];
|
|
90
|
-
let turns = 0;
|
|
91
|
-
|
|
92
|
-
for (const line of lines) {
|
|
93
|
-
let row;
|
|
94
|
-
try {
|
|
95
|
-
row = JSON.parse(line);
|
|
96
|
-
} catch {
|
|
97
|
-
continue;
|
|
98
|
-
}
|
|
99
|
-
if (row.type === 'session_meta') meta = row.payload || {};
|
|
100
|
-
if (row.type === 'response_item' && row.payload && row.payload.type === 'message') {
|
|
101
|
-
const msg = row.payload;
|
|
102
|
-
if (msg.role === 'developer') continue;
|
|
103
|
-
const text = textFromContent(msg.content);
|
|
104
|
-
if (!text) continue;
|
|
105
|
-
if (msg.role === 'user' && isNoiseUserText(text)) continue;
|
|
106
|
-
messages.push({
|
|
107
|
-
role: msg.role,
|
|
108
|
-
phase: msg.phase || '',
|
|
109
|
-
text,
|
|
110
|
-
});
|
|
111
|
-
if (msg.role === 'user') turns += 1;
|
|
112
|
-
}
|
|
113
|
-
}
|
|
114
|
-
|
|
115
|
-
const id = meta.id || path.basename(file, '.jsonl').split('-').slice(-5).join('-');
|
|
116
|
-
const firstUser = messages.find((msg) => msg.role === 'user');
|
|
117
|
-
const lastUser = [...messages].reverse().find((msg) => msg.role === 'user');
|
|
118
|
-
const lastAssistant = [...messages].reverse().find((msg) => msg.role === 'assistant');
|
|
119
|
-
|
|
120
|
-
return {
|
|
121
|
-
id,
|
|
122
|
-
file,
|
|
123
|
-
cwd: meta.cwd || '(unknown)',
|
|
124
|
-
startedAt: meta.timestamp || null,
|
|
125
|
-
updatedAt: stat.mtime.toISOString(),
|
|
126
|
-
cliVersion: meta.cli_version || '',
|
|
127
|
-
source: meta.source || '',
|
|
128
|
-
provider: meta.model_provider || '',
|
|
129
|
-
turns,
|
|
130
|
-
first: firstUser ? firstUser.text : '',
|
|
131
|
-
last: lastUser ? lastUser.text : '',
|
|
132
|
-
lastAssistant: lastAssistant ? lastAssistant.text : '',
|
|
133
|
-
messages,
|
|
134
|
-
};
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
function loadMeta() {
|
|
138
|
-
const data = readJson(META_PATH, { sessions: {} });
|
|
139
|
-
if (!data.sessions) data.sessions = {};
|
|
140
|
-
return data;
|
|
141
|
-
}
|
|
142
|
-
|
|
143
|
-
function listSessions() {
|
|
144
|
-
const meta = loadMeta();
|
|
145
|
-
return walk(SESSIONS_DIR)
|
|
146
|
-
.map(parseSession)
|
|
147
|
-
.map((session) => ({ ...session, ...(meta.sessions[session.id] || {}) }))
|
|
148
|
-
.sort((a, b) => new Date(b.updatedAt) - new Date(a.updatedAt));
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
function resolveSession(query, sessions = listSessions()) {
|
|
152
|
-
if (!query) throw new Error('Missing session. Run `codex-workbench list` to find a session id.');
|
|
153
|
-
const matches = sessions.filter((session) => {
|
|
154
|
-
return session.id === query ||
|
|
155
|
-
session.id.startsWith(query) ||
|
|
156
|
-
session.name === query ||
|
|
157
|
-
path.basename(session.file) === query;
|
|
158
|
-
});
|
|
159
|
-
if (matches.length === 1) return matches[0];
|
|
160
|
-
if (matches.length === 0) throw new Error(`No session matched: ${query}`);
|
|
161
|
-
throw new Error(`Ambiguous session: ${query}\n${matches.map((s) => ` ${s.id} ${s.name || ''}`).join('\n')}`);
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
function shortId(id) {
|
|
165
|
-
return id.slice(0, 13);
|
|
166
|
-
}
|
|
167
|
-
|
|
168
|
-
function localTime(iso) {
|
|
169
|
-
if (!iso) return '';
|
|
170
|
-
return new Date(iso).toLocaleString();
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
function truncate(text, width) {
|
|
174
|
-
if (!text) return '';
|
|
175
|
-
return text.length > width ? text.slice(0, Math.max(0, width - 1)) + '...' : text;
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
function printList(sessions, opts = {}) {
|
|
179
|
-
const filtered = sessions.filter((session) => {
|
|
180
|
-
if (!opts.all && (session.archived || session.hidden)) return false;
|
|
181
|
-
if (opts.cwd) return path.resolve(session.cwd) === path.resolve(opts.cwd);
|
|
182
|
-
return true;
|
|
183
|
-
});
|
|
184
|
-
if (opts.json) {
|
|
185
|
-
console.log(JSON.stringify(filtered, null, 2));
|
|
186
|
-
return;
|
|
187
|
-
}
|
|
188
|
-
const groups = new Map();
|
|
189
|
-
for (const session of filtered) {
|
|
190
|
-
if (!groups.has(session.cwd)) groups.set(session.cwd, []);
|
|
191
|
-
groups.get(session.cwd).push(session);
|
|
192
|
-
}
|
|
193
|
-
for (const [cwd, group] of groups) {
|
|
194
|
-
console.log(`\n${cwd}`);
|
|
195
|
-
for (const session of group) {
|
|
196
|
-
const label = session.name || truncate(session.first || session.last || '(no prompt)', 56);
|
|
197
|
-
const flags = [session.archived ? 'archived' : '', session.hidden ? 'hidden' : '', session.note ? 'note' : ''].filter(Boolean).join(',');
|
|
198
|
-
console.log(` ${shortId(session.id)} ${localTime(session.updatedAt)} ${String(session.turns).padStart(2)} turns ${flags ? `[${flags}] ` : ''}${label}`);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
if (!filtered.length) console.log('No sessions found.');
|
|
202
|
-
}
|
|
203
|
-
|
|
204
|
-
function printShow(session) {
|
|
205
|
-
console.log(`${session.name || '(unnamed)'} ${session.archived ? '[archived]' : ''}${session.hidden ? '[hidden]' : ''}`);
|
|
206
|
-
console.log(`id: ${session.id}`);
|
|
207
|
-
console.log(`cwd: ${session.cwd}`);
|
|
208
|
-
console.log(`started: ${localTime(session.startedAt)}`);
|
|
209
|
-
console.log(`updated: ${localTime(session.updatedAt)}`);
|
|
210
|
-
console.log(`file: ${session.file}`);
|
|
211
|
-
console.log(`turns: ${session.turns}`);
|
|
212
|
-
if (session.note) console.log(`note: ${session.note}`);
|
|
213
|
-
console.log('\nMessages:');
|
|
214
|
-
for (const msg of session.messages) {
|
|
215
|
-
if (msg.role === 'developer') continue;
|
|
216
|
-
const prefix = msg.role === 'assistant' ? 'A' : msg.role === 'user' ? 'U' : msg.role.slice(0, 1).toUpperCase();
|
|
217
|
-
console.log(` ${prefix}: ${truncate(msg.text, 180)}`);
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
|
|
221
|
-
function printDoctor() {
|
|
222
|
-
const result = inspectCodexBin();
|
|
223
|
-
console.log('codex-workbench doctor');
|
|
224
|
-
console.log(`status: ${result.ok ? 'ok' : 'error'}`);
|
|
225
|
-
if (result.path) console.log(`codex: ${result.path}`);
|
|
226
|
-
if (result.source) console.log(`source: ${result.source}`);
|
|
227
|
-
if (result.error) console.log(`error: ${result.error}`);
|
|
228
|
-
console.log('\nChecks:');
|
|
229
|
-
for (const check of result.checks) {
|
|
230
|
-
const parts = [
|
|
231
|
-
check.source,
|
|
232
|
-
check.mode ? `mode=${check.mode}` : '',
|
|
233
|
-
check.shell ? `shell=${check.shell}` : '',
|
|
234
|
-
check.path ? `path=${check.path}` : '',
|
|
235
|
-
`executable=${check.executable ? 'yes' : 'no'}`,
|
|
236
|
-
].filter(Boolean);
|
|
237
|
-
console.log(` - ${parts.join(' ')}`);
|
|
238
|
-
}
|
|
239
|
-
if (!result.ok) process.exitCode = 1;
|
|
240
|
-
}
|
|
241
|
-
|
|
242
|
-
function updateMetadata(session, patch) {
|
|
243
|
-
const meta = loadMeta();
|
|
244
|
-
meta.sessions[session.id] = { ...(meta.sessions[session.id] || {}), ...patch };
|
|
245
|
-
meta.updatedAt = new Date().toISOString();
|
|
246
|
-
writeJson(META_PATH, meta);
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
function removeMetadata(session) {
|
|
250
|
-
const meta = loadMeta();
|
|
251
|
-
delete meta.sessions[session.id];
|
|
252
|
-
meta.updatedAt = new Date().toISOString();
|
|
253
|
-
writeJson(META_PATH, meta);
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
function deleteSessionFile(session) {
|
|
257
|
-
fs.unlinkSync(session.file);
|
|
258
|
-
removeMetadata(session);
|
|
259
|
-
}
|
|
260
|
-
|
|
261
|
-
function usableCwd(dir) {
|
|
262
|
-
const candidates = [dir, process.cwd(), HOME];
|
|
263
|
-
for (const candidate of candidates) {
|
|
264
|
-
if (!candidate || candidate === '(unknown)') continue;
|
|
265
|
-
try {
|
|
266
|
-
if (fs.statSync(candidate).isDirectory()) return candidate;
|
|
267
|
-
} catch {
|
|
268
|
-
// Try the next fallback.
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
return HOME;
|
|
272
|
-
}
|
|
273
|
-
|
|
274
|
-
function shellQuote(value) {
|
|
275
|
-
return `'${String(value).replace(/'/g, "'\\''")}'`;
|
|
276
|
-
}
|
|
277
|
-
|
|
278
|
-
function commandShell() {
|
|
279
|
-
const shell = process.env.SHELL || '/bin/sh';
|
|
280
|
-
try {
|
|
281
|
-
fs.accessSync(shell, fs.constants.X_OK);
|
|
282
|
-
return shell;
|
|
283
|
-
} catch {
|
|
284
|
-
return '/bin/sh';
|
|
285
|
-
}
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
function codexCommand(command, session, args = [], inherit = false) {
|
|
289
|
-
const executable = resolveCodexBin();
|
|
290
|
-
const argv = [executable, command, session.id, ...args];
|
|
291
|
-
const shellCommand = `exec ${argv.map(shellQuote).join(' ')}`;
|
|
292
|
-
const cwd = usableCwd(session.cwd);
|
|
293
|
-
const shell = commandShell();
|
|
294
|
-
if (inherit) {
|
|
295
|
-
const child = spawn(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
296
|
-
child.on('error', (err) => {
|
|
297
|
-
console.error(`error: failed to start codex: ${err.message}`);
|
|
298
|
-
process.exit(1);
|
|
299
|
-
});
|
|
300
|
-
child.on('exit', (code, signal) => {
|
|
301
|
-
if (signal) process.kill(process.pid, signal);
|
|
302
|
-
process.exit(code || 0);
|
|
303
|
-
});
|
|
304
|
-
return;
|
|
305
|
-
}
|
|
306
|
-
const result = spawnSync(shell, ['-lc', shellCommand], { stdio: 'inherit', cwd, env: process.env });
|
|
307
|
-
if (result.error) throw new Error(`failed to start codex: ${result.error.message}`);
|
|
308
|
-
const status = result.status || 0;
|
|
309
|
-
process.exitCode = status;
|
|
310
|
-
return status;
|
|
311
|
-
}
|
|
4
|
+
const {
|
|
5
|
+
deleteSessionFile,
|
|
6
|
+
listSessions,
|
|
7
|
+
resolveSession,
|
|
8
|
+
updateMetadata,
|
|
9
|
+
} = require('./model/session-store');
|
|
10
|
+
const {
|
|
11
|
+
printDoctor,
|
|
12
|
+
printList,
|
|
13
|
+
printShow,
|
|
14
|
+
usage,
|
|
15
|
+
} = require('./cli-output');
|
|
16
|
+
const {
|
|
17
|
+
runCodexCommand,
|
|
18
|
+
runNewCodexSession,
|
|
19
|
+
usableCwd,
|
|
20
|
+
} = require('./services/codex-runner');
|
|
21
|
+
const { runWorkbench } = require('./ui/workbench');
|
|
22
|
+
const { createChildDirectory, listDirectories } = require('./model/directories');
|
|
312
23
|
|
|
313
24
|
function parseFlags(args) {
|
|
314
25
|
const out = { _: [] };
|
|
@@ -327,460 +38,63 @@ function parseFlags(args) {
|
|
|
327
38
|
return out;
|
|
328
39
|
}
|
|
329
40
|
|
|
330
|
-
async function ui() {
|
|
331
|
-
if (!process.stdin.isTTY || !process.stdout.isTTY) {
|
|
332
|
-
return printList(listSessions());
|
|
333
|
-
}
|
|
334
|
-
|
|
335
|
-
let sessions = [];
|
|
336
|
-
let groups = [];
|
|
337
|
-
let groupIndex = 0;
|
|
338
|
-
let selected = 0;
|
|
339
|
-
let message = '';
|
|
340
|
-
let syncingList = false;
|
|
341
|
-
|
|
342
|
-
const screen = blessed.screen({
|
|
343
|
-
smartCSR: true,
|
|
344
|
-
fullUnicode: true,
|
|
345
|
-
title: 'Codex Workbench',
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const header = blessed.box({
|
|
349
|
-
parent: screen,
|
|
350
|
-
top: 0,
|
|
351
|
-
left: 0,
|
|
352
|
-
right: 0,
|
|
353
|
-
height: 3,
|
|
354
|
-
padding: { left: 1, right: 1 },
|
|
355
|
-
style: { fg: 'white', bg: 'blue' },
|
|
356
|
-
content: 'Codex Workbench',
|
|
357
|
-
});
|
|
358
|
-
|
|
359
|
-
const groupsBar = blessed.box({
|
|
360
|
-
parent: screen,
|
|
361
|
-
label: ' Projects ',
|
|
362
|
-
top: 3,
|
|
363
|
-
left: 0,
|
|
364
|
-
right: 0,
|
|
365
|
-
height: 3,
|
|
366
|
-
border: 'line',
|
|
367
|
-
padding: { left: 1, right: 1 },
|
|
368
|
-
tags: true,
|
|
369
|
-
parseTags: true,
|
|
370
|
-
style: {
|
|
371
|
-
border: { fg: 'green' },
|
|
372
|
-
fg: 'white',
|
|
373
|
-
},
|
|
374
|
-
});
|
|
375
|
-
|
|
376
|
-
const sessionsList = blessed.list({
|
|
377
|
-
parent: screen,
|
|
378
|
-
label: ' Sessions ',
|
|
379
|
-
top: 6,
|
|
380
|
-
left: 0,
|
|
381
|
-
right: 0,
|
|
382
|
-
height: '40%',
|
|
383
|
-
border: 'line',
|
|
384
|
-
mouse: true,
|
|
385
|
-
keys: true,
|
|
386
|
-
vi: false,
|
|
387
|
-
scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
|
|
388
|
-
style: {
|
|
389
|
-
border: { fg: 'cyan' },
|
|
390
|
-
selected: { fg: 'black', bg: 'cyan', bold: true },
|
|
391
|
-
item: { fg: 'white' },
|
|
392
|
-
},
|
|
393
|
-
});
|
|
394
|
-
|
|
395
|
-
const detailBox = blessed.log({
|
|
396
|
-
parent: screen,
|
|
397
|
-
label: ' Details ',
|
|
398
|
-
top: '50%',
|
|
399
|
-
left: 0,
|
|
400
|
-
right: 0,
|
|
401
|
-
bottom: 3,
|
|
402
|
-
border: 'line',
|
|
403
|
-
padding: { left: 1, right: 1 },
|
|
404
|
-
scrollable: true,
|
|
405
|
-
mouse: true,
|
|
406
|
-
keys: true,
|
|
407
|
-
vi: true,
|
|
408
|
-
alwaysScroll: true,
|
|
409
|
-
tags: false,
|
|
410
|
-
parseTags: false,
|
|
411
|
-
scrollbar: { ch: ' ', track: { bg: 'black' }, style: { bg: 'cyan' } },
|
|
412
|
-
style: { border: { fg: 'cyan' }, fg: 'white' },
|
|
413
|
-
});
|
|
414
|
-
|
|
415
|
-
const status = blessed.box({
|
|
416
|
-
parent: screen,
|
|
417
|
-
left: 0,
|
|
418
|
-
right: 0,
|
|
419
|
-
bottom: 0,
|
|
420
|
-
height: 3,
|
|
421
|
-
padding: { left: 1, right: 1 },
|
|
422
|
-
style: { fg: 'white', bg: 'black' },
|
|
423
|
-
});
|
|
424
|
-
|
|
425
|
-
const prompt = blessed.prompt({
|
|
426
|
-
parent: screen,
|
|
427
|
-
border: 'line',
|
|
428
|
-
height: 8,
|
|
429
|
-
width: '70%',
|
|
430
|
-
top: 'center',
|
|
431
|
-
left: 'center',
|
|
432
|
-
padding: { left: 1, right: 1 },
|
|
433
|
-
style: { border: { fg: 'yellow' }, fg: 'white', bg: 'black' },
|
|
434
|
-
});
|
|
435
|
-
|
|
436
|
-
const question = blessed.question({
|
|
437
|
-
parent: screen,
|
|
438
|
-
border: 'line',
|
|
439
|
-
height: 6,
|
|
440
|
-
width: '70%',
|
|
441
|
-
top: 'center',
|
|
442
|
-
left: 'center',
|
|
443
|
-
padding: { left: 1, right: 1 },
|
|
444
|
-
style: { border: { fg: 'red' }, fg: 'white', bg: 'black' },
|
|
445
|
-
});
|
|
446
|
-
|
|
447
|
-
const currentSessions = () => {
|
|
448
|
-
const group = groups[groupIndex];
|
|
449
|
-
return group === 'All' ? sessions : sessions.filter((s) => s.cwd === group);
|
|
450
|
-
};
|
|
451
|
-
|
|
452
|
-
const selectedSession = () => currentSessions()[selected] || null;
|
|
453
|
-
|
|
454
|
-
const groupLabel = (group) => {
|
|
455
|
-
if (group === 'All') return `All (${sessions.length})`;
|
|
456
|
-
const count = sessions.filter((s) => s.cwd === group).length;
|
|
457
|
-
return `${path.basename(group) || group} (${count})`;
|
|
458
|
-
};
|
|
459
|
-
|
|
460
|
-
const tagText = (text) => String(text).replace(/[{}]/g, '');
|
|
461
|
-
|
|
462
|
-
const groupsContent = () => {
|
|
463
|
-
if (!groups.length) return '';
|
|
464
|
-
const width = Math.max(20, (screen.width || 80) - 4);
|
|
465
|
-
const labels = groups.map((group, index) => {
|
|
466
|
-
const max = index === groupIndex ? 34 : 24;
|
|
467
|
-
return tagText(truncate(groupLabel(group), max));
|
|
468
|
-
});
|
|
469
|
-
const chipWidth = (index) => labels[index].length + 2;
|
|
470
|
-
let start = groupIndex;
|
|
471
|
-
let end = groupIndex;
|
|
472
|
-
let used = chipWidth(groupIndex);
|
|
473
|
-
|
|
474
|
-
while (start > 0 || end < groups.length - 1) {
|
|
475
|
-
const reserve = (start > 0 ? 4 : 0) + (end < groups.length - 1 ? 4 : 0);
|
|
476
|
-
const leftCost = start > 0 ? chipWidth(start - 1) + 2 : Infinity;
|
|
477
|
-
const rightCost = end < groups.length - 1 ? chipWidth(end + 1) + 2 : Infinity;
|
|
478
|
-
const preferLeft = groupIndex - start <= end - groupIndex;
|
|
479
|
-
const firstCost = preferLeft ? leftCost : rightCost;
|
|
480
|
-
const secondCost = preferLeft ? rightCost : leftCost;
|
|
481
|
-
|
|
482
|
-
if (used + firstCost + reserve <= width) {
|
|
483
|
-
if (preferLeft) start -= 1;
|
|
484
|
-
else end += 1;
|
|
485
|
-
used += firstCost;
|
|
486
|
-
} else if (used + secondCost + reserve <= width) {
|
|
487
|
-
if (preferLeft) end += 1;
|
|
488
|
-
else start -= 1;
|
|
489
|
-
used += secondCost;
|
|
490
|
-
} else {
|
|
491
|
-
break;
|
|
492
|
-
}
|
|
493
|
-
}
|
|
494
|
-
|
|
495
|
-
const parts = [];
|
|
496
|
-
if (start > 0) parts.push('...');
|
|
497
|
-
for (let index = start; index <= end; index += 1) {
|
|
498
|
-
const label = ` ${labels[index]} `;
|
|
499
|
-
parts.push(index === groupIndex ? `{black-fg}{cyan-bg}{bold}${label}{/bold}{/cyan-bg}{/black-fg}` : label);
|
|
500
|
-
}
|
|
501
|
-
if (end < groups.length - 1) parts.push('...');
|
|
502
|
-
return parts.join(' ');
|
|
503
|
-
};
|
|
504
|
-
|
|
505
|
-
const sessionLabel = (session) => {
|
|
506
|
-
const flags = [
|
|
507
|
-
session.name ? 'renamed' : '',
|
|
508
|
-
session.note ? 'note' : '',
|
|
509
|
-
].filter(Boolean).join(',');
|
|
510
|
-
const title = session.name || session.first || session.last || '(no prompt)';
|
|
511
|
-
const flagText = flags ? `[${flags}]` : '';
|
|
512
|
-
return `${shortId(session.id)} ${String(session.turns).padStart(2)}t ${truncate(localTime(session.updatedAt), 18)} ${flagText} ${truncate(title, 90)}`;
|
|
513
|
-
};
|
|
514
|
-
|
|
515
|
-
const detailContent = (session) => {
|
|
516
|
-
if (!session) return 'No sessions in this project.';
|
|
517
|
-
const title = session.name || session.first || session.last || '(no prompt)';
|
|
518
|
-
return [
|
|
519
|
-
title,
|
|
520
|
-
'',
|
|
521
|
-
`id: ${session.id}`,
|
|
522
|
-
`cwd: ${session.cwd}`,
|
|
523
|
-
`started: ${localTime(session.startedAt)}`,
|
|
524
|
-
`updated: ${localTime(session.updatedAt)}`,
|
|
525
|
-
`turns: ${session.turns}`,
|
|
526
|
-
session.note ? `note: ${session.note}` : '',
|
|
527
|
-
'',
|
|
528
|
-
`last user: ${session.last || session.first || ''}`,
|
|
529
|
-
'',
|
|
530
|
-
`last assistant: ${session.lastAssistant || ''}`,
|
|
531
|
-
].filter((line) => line !== '').join('\n');
|
|
532
|
-
};
|
|
533
|
-
|
|
534
|
-
const setMessage = (text, isError = false) => {
|
|
535
|
-
message = text || 'Ready';
|
|
536
|
-
status.style.fg = isError ? 'red' : 'white';
|
|
537
|
-
};
|
|
538
|
-
|
|
539
|
-
const promptOpen = () => prompt.visible || question.visible;
|
|
540
|
-
|
|
541
|
-
const reload = () => {
|
|
542
|
-
sessions = listSessions().filter((s) => !s.archived && !s.hidden);
|
|
543
|
-
groups = ['All', ...new Set(sessions.map((s) => s.cwd))];
|
|
544
|
-
if (groupIndex >= groups.length) groupIndex = Math.max(0, groups.length - 1);
|
|
545
|
-
const visible = currentSessions();
|
|
546
|
-
if (selected >= visible.length) selected = Math.max(0, visible.length - 1);
|
|
547
|
-
};
|
|
548
|
-
|
|
549
|
-
const syncList = () => {
|
|
550
|
-
const visible = currentSessions();
|
|
551
|
-
const listRows = Math.max(1, (screen.height || 24) - 11);
|
|
552
|
-
const items = visible.length ? visible.map(sessionLabel) : ['No sessions in this project.'];
|
|
553
|
-
while (items.length < listRows) items.push('');
|
|
554
|
-
syncingList = true;
|
|
555
|
-
sessionsList.clearItems();
|
|
556
|
-
sessionsList.setItems(items);
|
|
557
|
-
selected = Math.min(selected, Math.max(0, visible.length - 1));
|
|
558
|
-
sessionsList.childBase = 0;
|
|
559
|
-
sessionsList.childOffset = 0;
|
|
560
|
-
sessionsList.select(selected);
|
|
561
|
-
sessionsList.scrollTo(0);
|
|
562
|
-
syncingList = false;
|
|
563
|
-
};
|
|
564
|
-
|
|
565
|
-
const render = () => {
|
|
566
|
-
const visible = currentSessions();
|
|
567
|
-
header.setContent(` Codex Workbench\n ${visible.length}/${sessions.length} visible ${groups[groupIndex] === 'All' ? 'All projects' : groups[groupIndex]}`);
|
|
568
|
-
groupsBar.setContent(groupsContent());
|
|
569
|
-
detailBox.setLabel(' Details ');
|
|
570
|
-
detailBox.setContent(detailContent(selectedSession()));
|
|
571
|
-
status.setContent(`${message || 'Ready'}\nEnter/r resume tab focus f fork v view n rename o note a archive d delete q quit`);
|
|
572
|
-
screen.render();
|
|
573
|
-
};
|
|
574
|
-
|
|
575
|
-
const askInput = (label, initial = '') => new Promise((resolve) => {
|
|
576
|
-
prompt.input(label, initial, (err, value) => resolve(err ? null : value));
|
|
577
|
-
});
|
|
578
|
-
|
|
579
|
-
const askConfirm = (label) => new Promise((resolve) => {
|
|
580
|
-
question.ask(label, (err, answer) => resolve(!err && Boolean(answer)));
|
|
581
|
-
});
|
|
582
|
-
|
|
583
|
-
const leaveScreen = () => {
|
|
584
|
-
screen.destroy();
|
|
585
|
-
};
|
|
586
|
-
|
|
587
|
-
const refreshAfterAction = (text, isError = false) => {
|
|
588
|
-
setMessage(text, isError);
|
|
589
|
-
reload();
|
|
590
|
-
syncList();
|
|
591
|
-
render();
|
|
592
|
-
};
|
|
593
|
-
|
|
594
|
-
const switchGroup = (offset) => {
|
|
595
|
-
if (!groups.length) return;
|
|
596
|
-
groupIndex = Math.max(0, Math.min(groups.length - 1, groupIndex + offset));
|
|
597
|
-
selected = 0;
|
|
598
|
-
syncList();
|
|
599
|
-
render();
|
|
600
|
-
};
|
|
601
|
-
|
|
602
|
-
const runCodexAndReturn = (command, session, args = [], doneText = `${command} finished.`) => {
|
|
603
|
-
screen.leave();
|
|
604
|
-
let status = 0;
|
|
605
|
-
try {
|
|
606
|
-
status = codexCommand(command, session, args);
|
|
607
|
-
} finally {
|
|
608
|
-
screen.enter();
|
|
609
|
-
}
|
|
610
|
-
if (status === 0) refreshAfterAction(doneText);
|
|
611
|
-
else refreshAfterAction(`${command} exited with code ${status}.`, true);
|
|
612
|
-
return status;
|
|
613
|
-
};
|
|
614
|
-
|
|
615
|
-
const runAction = async (action) => {
|
|
616
|
-
if (promptOpen()) return;
|
|
617
|
-
const session = selectedSession();
|
|
618
|
-
if (!session) return;
|
|
619
|
-
try {
|
|
620
|
-
await action(session);
|
|
621
|
-
} catch (err) {
|
|
622
|
-
setMessage(`error: ${err.message}`, true);
|
|
623
|
-
render();
|
|
624
|
-
}
|
|
625
|
-
};
|
|
626
|
-
|
|
627
|
-
reload();
|
|
628
|
-
setMessage('Ready');
|
|
629
|
-
syncList();
|
|
630
|
-
|
|
631
|
-
sessionsList.on('select item', (_item, index) => {
|
|
632
|
-
if (syncingList) return;
|
|
633
|
-
const visible = currentSessions();
|
|
634
|
-
if (index >= visible.length) {
|
|
635
|
-
selected = Math.max(0, visible.length - 1);
|
|
636
|
-
sessionsList.select(selected);
|
|
637
|
-
return;
|
|
638
|
-
}
|
|
639
|
-
selected = Math.min(index, Math.max(0, visible.length - 1));
|
|
640
|
-
detailBox.setContent(detailContent(selectedSession()));
|
|
641
|
-
screen.render();
|
|
642
|
-
});
|
|
643
|
-
|
|
644
|
-
sessionsList.on('select', () => runAction((session) => {
|
|
645
|
-
runCodexAndReturn('resume', session);
|
|
646
|
-
}));
|
|
647
|
-
|
|
648
|
-
sessionsList.key(['j'], () => {
|
|
649
|
-
if (promptOpen()) return;
|
|
650
|
-
sessionsList.down();
|
|
651
|
-
screen.render();
|
|
652
|
-
});
|
|
653
|
-
|
|
654
|
-
sessionsList.key(['k'], () => {
|
|
655
|
-
if (promptOpen()) return;
|
|
656
|
-
sessionsList.up();
|
|
657
|
-
screen.render();
|
|
658
|
-
});
|
|
659
|
-
|
|
660
|
-
sessionsList.key(['left', 'h'], () => {
|
|
661
|
-
if (promptOpen()) return;
|
|
662
|
-
switchGroup(-1);
|
|
663
|
-
});
|
|
664
|
-
|
|
665
|
-
sessionsList.key(['right', 'l'], () => {
|
|
666
|
-
if (promptOpen()) return;
|
|
667
|
-
switchGroup(1);
|
|
668
|
-
});
|
|
669
|
-
|
|
670
|
-
detailBox.key(['left', 'h'], () => {
|
|
671
|
-
if (promptOpen()) return;
|
|
672
|
-
switchGroup(-1);
|
|
673
|
-
});
|
|
674
|
-
|
|
675
|
-
detailBox.key(['right', 'l'], () => {
|
|
676
|
-
if (promptOpen()) return;
|
|
677
|
-
switchGroup(1);
|
|
678
|
-
});
|
|
679
|
-
|
|
680
|
-
screen.key(['tab'], () => {
|
|
681
|
-
if (promptOpen()) return;
|
|
682
|
-
if (screen.focused === detailBox) sessionsList.focus();
|
|
683
|
-
else detailBox.focus();
|
|
684
|
-
screen.render();
|
|
685
|
-
});
|
|
686
|
-
|
|
687
|
-
screen.key(['q', 'escape', 'C-c'], () => {
|
|
688
|
-
if (promptOpen()) return;
|
|
689
|
-
leaveScreen();
|
|
690
|
-
process.exit(0);
|
|
691
|
-
});
|
|
692
|
-
|
|
693
|
-
screen.key(['r'], () => runAction((session) => {
|
|
694
|
-
runCodexAndReturn('resume', session);
|
|
695
|
-
}));
|
|
696
|
-
|
|
697
|
-
screen.key(['f'], () => runAction((session) => {
|
|
698
|
-
runCodexAndReturn('fork', session);
|
|
699
|
-
}));
|
|
700
|
-
|
|
701
|
-
screen.key(['v'], () => runAction((session) => {
|
|
702
|
-
leaveScreen();
|
|
703
|
-
printShow(session);
|
|
704
|
-
process.exit(0);
|
|
705
|
-
}));
|
|
706
|
-
|
|
707
|
-
screen.key(['n'], () => runAction(async (session) => {
|
|
708
|
-
const name = await askInput('Name', session.name || '');
|
|
709
|
-
if (name === null) return render();
|
|
710
|
-
updateMetadata(session, { name });
|
|
711
|
-
refreshAfterAction('Renamed.');
|
|
712
|
-
}));
|
|
713
|
-
|
|
714
|
-
screen.key(['o'], () => runAction(async (session) => {
|
|
715
|
-
const note = await askInput('Note', session.note || '');
|
|
716
|
-
if (note === null) return render();
|
|
717
|
-
updateMetadata(session, { note });
|
|
718
|
-
refreshAfterAction('Note saved.');
|
|
719
|
-
}));
|
|
720
|
-
|
|
721
|
-
screen.key(['a'], () => runAction((session) => {
|
|
722
|
-
runCodexAndReturn('archive', session, [], `Archived ${shortId(session.id)}.`);
|
|
723
|
-
}));
|
|
724
|
-
|
|
725
|
-
screen.key(['d'], () => runAction(async (session) => {
|
|
726
|
-
const confirmed = await askConfirm(`Delete ${shortId(session.id)}? Enter/y to confirm, n/Esc to cancel`);
|
|
727
|
-
if (!confirmed) {
|
|
728
|
-
setMessage('Delete cancelled.');
|
|
729
|
-
return render();
|
|
730
|
-
}
|
|
731
|
-
const status = runCodexAndReturn('delete', session, ['--force'], `Deleted ${shortId(session.id)}.`);
|
|
732
|
-
if (status !== 0) {
|
|
733
|
-
const removeFile = await askConfirm(`Codex could not delete ${shortId(session.id)}. Delete its session file?`);
|
|
734
|
-
if (removeFile) {
|
|
735
|
-
deleteSessionFile(session);
|
|
736
|
-
refreshAfterAction(`Deleted file for ${shortId(session.id)}.`);
|
|
737
|
-
return;
|
|
738
|
-
}
|
|
739
|
-
const hideSession = await askConfirm(`Hide ${shortId(session.id)} from workbench instead?`);
|
|
740
|
-
if (hideSession) {
|
|
741
|
-
updateMetadata(session, { hidden: true });
|
|
742
|
-
refreshAfterAction(`Hidden ${shortId(session.id)}.`);
|
|
743
|
-
}
|
|
744
|
-
}
|
|
745
|
-
}));
|
|
746
|
-
|
|
747
|
-
sessionsList.focus();
|
|
748
|
-
render();
|
|
749
|
-
|
|
750
|
-
return new Promise(() => {});
|
|
751
|
-
}
|
|
752
|
-
|
|
753
41
|
async function main() {
|
|
754
42
|
const [cmd = 'ui', ...rest] = process.argv.slice(2);
|
|
755
43
|
if (cmd === '-h' || cmd === '--help' || cmd === 'help') return usage();
|
|
756
44
|
|
|
757
45
|
const flags = parseFlags(rest);
|
|
758
46
|
if (cmd === 'doctor') return printDoctor();
|
|
47
|
+
if (cmd === 'dirs') {
|
|
48
|
+
const payload = listDirectories(flags.cwd || process.cwd(), usableCwd);
|
|
49
|
+
if (flags.json) console.log(JSON.stringify(payload, null, 2));
|
|
50
|
+
else {
|
|
51
|
+
console.log(payload.cwd);
|
|
52
|
+
for (const entry of payload.entries) console.log(entry.path);
|
|
53
|
+
}
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
if (cmd === 'mkdir') {
|
|
57
|
+
const target = createChildDirectory(usableCwd(flags.cwd || process.cwd()), flags._[0] || '');
|
|
58
|
+
if (flags.json) console.log(JSON.stringify({ path: target }, null, 2));
|
|
59
|
+
else console.log(target);
|
|
60
|
+
return undefined;
|
|
61
|
+
}
|
|
759
62
|
|
|
760
63
|
const sessions = listSessions();
|
|
761
64
|
|
|
762
|
-
if (cmd === 'ui') return
|
|
65
|
+
if (cmd === 'ui') return runWorkbench();
|
|
763
66
|
if (cmd === 'list' || cmd === 'ls') return printList(sessions, flags);
|
|
764
67
|
if (cmd === 'show') return printShow(resolveSession(flags._[0], sessions));
|
|
765
68
|
if (cmd === 'rename') return updateMetadata(resolveSession(flags._[0], sessions), { name: flags._.slice(1).join(' ') });
|
|
766
69
|
if (cmd === 'note') return updateMetadata(resolveSession(flags._[0], sessions), { note: flags._.slice(1).join(' ') });
|
|
767
|
-
if (cmd === '
|
|
768
|
-
if (cmd === '
|
|
769
|
-
if (cmd === '
|
|
770
|
-
if (cmd === '
|
|
70
|
+
if (cmd === 'new' || cmd === 'start') return runNewCodexSession(flags.cwd || process.cwd(), flags._, true);
|
|
71
|
+
if (cmd === 'resume') return runCodexCommand('resume', resolveSession(flags._[0], sessions), flags._.slice(1), true);
|
|
72
|
+
if (cmd === 'fork') return runCodexCommand('fork', resolveSession(flags._[0], sessions), [], true);
|
|
73
|
+
if (cmd === 'archive') return runCodexCommand('archive', resolveSession(flags._[0], sessions));
|
|
74
|
+
if (cmd === 'unarchive') return runCodexCommand('unarchive', resolveSession(flags._[0], sessions));
|
|
771
75
|
if (cmd === 'hide') return updateMetadata(resolveSession(flags._[0], sessions), { hidden: true });
|
|
772
76
|
if (cmd === 'unhide') return updateMetadata(resolveSession(flags._[0], sessions), { hidden: false });
|
|
773
77
|
if (cmd === 'delete') {
|
|
774
78
|
const session = resolveSession(flags._[0], sessions);
|
|
775
79
|
if (flags.file) return deleteSessionFile(session);
|
|
776
|
-
return
|
|
80
|
+
return runCodexCommand('delete', session, flags.force ? ['--force'] : []);
|
|
777
81
|
}
|
|
778
82
|
|
|
779
83
|
usage();
|
|
780
84
|
process.exitCode = 2;
|
|
781
85
|
}
|
|
782
86
|
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
87
|
+
function run() {
|
|
88
|
+
return main().catch((err) => {
|
|
89
|
+
console.error(`error: ${err.message}`);
|
|
90
|
+
process.exit(1);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (require.main === module) run();
|
|
95
|
+
|
|
96
|
+
module.exports = {
|
|
97
|
+
main,
|
|
98
|
+
parseFlags,
|
|
99
|
+
run,
|
|
100
|
+
};
|