glad-web 1.0.21 → 1.0.23
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 +2 -2
- package/README.zh-CN.md +2 -2
- package/bin/cli.js +1 -1
- package/lib/ai-tools/registry.js +1 -1
- package/lib/codex/structured-session.js +668 -0
- package/lib/commands/web.js +63 -2
- package/lib/session/session-manager.js +74 -11
- package/lib/web/index.html +419 -13
- package/package.json +5 -4
package/lib/commands/web.js
CHANGED
|
@@ -93,6 +93,9 @@ async function webCommand(options) {
|
|
|
93
93
|
sessionManager.on('claude-event', ({ sessionId, event }) => {
|
|
94
94
|
broadcastToSession(sessionId, { type: 'claude-event', event });
|
|
95
95
|
});
|
|
96
|
+
sessionManager.on('codex-event', ({ sessionId, event }) => {
|
|
97
|
+
broadcastToSession(sessionId, { type: 'codex-event', event });
|
|
98
|
+
});
|
|
96
99
|
sessionManager.on('exit', ({ sessionId }) => {
|
|
97
100
|
broadcastToSession(sessionId, { type: 'exit' });
|
|
98
101
|
});
|
|
@@ -301,6 +304,46 @@ async function webCommand(options) {
|
|
|
301
304
|
res.json({ success: true });
|
|
302
305
|
});
|
|
303
306
|
|
|
307
|
+
app.patch('/api/sessions/:id/codex-settings', async (req, res) => {
|
|
308
|
+
try {
|
|
309
|
+
const state = await sessionManager.updateCodexSettings(req.params.id, req.body || {});
|
|
310
|
+
if (!state) return res.status(404).json({ error: 'Codex session not found' });
|
|
311
|
+
res.json({ success: true, state });
|
|
312
|
+
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
app.get('/api/sessions/:id/codex-resume-threads', async (req, res) => {
|
|
316
|
+
try {
|
|
317
|
+
const items = await sessionManager.listCodexResumeThreads(req.params.id);
|
|
318
|
+
if (!items) return res.status(404).json({ error: 'Codex session not found' });
|
|
319
|
+
res.json({ success: true, items });
|
|
320
|
+
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
321
|
+
});
|
|
322
|
+
|
|
323
|
+
app.post('/api/sessions/:id/codex-abort', (req, res) => {
|
|
324
|
+
const success = sessionManager.abortCodex(req.params.id);
|
|
325
|
+
if (!success) return res.status(409).json({ error: 'Codex session is idle or unavailable' });
|
|
326
|
+
res.json({ success: true });
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
app.post('/api/sessions/:id/codex-resume', async (req, res) => {
|
|
330
|
+
try {
|
|
331
|
+
const success = await sessionManager.resumeCodex(req.params.id, req.body && req.body.threadId);
|
|
332
|
+
if (!success) return res.status(409).json({ error: 'Codex session is busy or no thread is available' });
|
|
333
|
+
res.json({ success: true });
|
|
334
|
+
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
335
|
+
});
|
|
336
|
+
|
|
337
|
+
app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
|
|
338
|
+
const presentation = req.body && req.body.presentation;
|
|
339
|
+
if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
|
|
340
|
+
try {
|
|
341
|
+
const success = await sessionManager.switchCodexPresentation(req.params.id, presentation);
|
|
342
|
+
if (!success) return res.status(409).json({ error: 'Codex session cannot switch presentation now' });
|
|
343
|
+
res.json({ success: true });
|
|
344
|
+
} catch (e) { res.status(409).json({ error: e.message }); }
|
|
345
|
+
});
|
|
346
|
+
|
|
304
347
|
app.post('/api/debug/client-log', (req, res) => {
|
|
305
348
|
const { sessionId, event, payload } = req.body || {};
|
|
306
349
|
sessionManager.logClientDebug(sessionId, event, payload);
|
|
@@ -435,16 +478,19 @@ async function webCommand(options) {
|
|
|
435
478
|
if (session.kind === 'claude-structured') {
|
|
436
479
|
ws.send(JSON.stringify({ type: 'claude-snapshot', snapshot: sessionManager.getClaudeSnapshot(sessionId) }));
|
|
437
480
|
}
|
|
481
|
+
if (session.kind === 'codex-structured' && session.presentation === 'structured') {
|
|
482
|
+
ws.send(JSON.stringify({ type: 'codex-snapshot', snapshot: sessionManager.getCodexSnapshot(sessionId) }));
|
|
483
|
+
}
|
|
438
484
|
|
|
439
485
|
// Send catchup output. TUI tools may skip the raw circular buffer, so fall
|
|
440
486
|
// back to the rendered/text history snapshot instead of reconnecting blank.
|
|
441
487
|
const catchup = sessionManager.getCatchupOutput(sessionId);
|
|
442
|
-
ws.needsTuiRedraw =
|
|
488
|
+
ws.needsTuiRedraw = !['claude-structured', 'codex-structured'].includes(session.kind)
|
|
443
489
|
&& ['antigravity', 'claude-code', 'codex'].includes(session.tool.key)
|
|
444
490
|
&& (isReconnect || (catchup && catchup.source === 'rendered-history'));
|
|
445
491
|
if (ws.needsTuiRedraw) {
|
|
446
492
|
ws.send(JSON.stringify({ type: 'reset' }));
|
|
447
|
-
} else if (session.kind
|
|
493
|
+
} else if (!(session.kind === 'claude-structured' || (session.kind === 'codex-structured' && session.presentation === 'structured')) && catchup && catchup.data) {
|
|
448
494
|
sessionManager.logWsCatchupOutput(sessionId, catchup);
|
|
449
495
|
ws.send(JSON.stringify({ type: 'output', data: catchup.data }));
|
|
450
496
|
}
|
|
@@ -467,6 +513,21 @@ async function webCommand(options) {
|
|
|
467
513
|
if (payload.type === 'claude-abort') {
|
|
468
514
|
sessionManager.abortClaude(sessionId);
|
|
469
515
|
}
|
|
516
|
+
if (payload.type === 'codex-input') {
|
|
517
|
+
sessionManager.write(sessionId, payload.text || '');
|
|
518
|
+
}
|
|
519
|
+
if (payload.type === 'codex-permission') {
|
|
520
|
+
const codex = sessionManager.get(sessionId);
|
|
521
|
+
if (codex && codex.kind === 'codex-structured') {
|
|
522
|
+
codex.respondPermission(payload.id, payload.decision || Boolean(payload.approved));
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
if (payload.type === 'codex-settings') {
|
|
526
|
+
sessionManager.updateCodexSettings(sessionId, payload.settings || {}).catch(error => logger.error(`Codex settings error: ${error.message}`));
|
|
527
|
+
}
|
|
528
|
+
if (payload.type === 'codex-abort') {
|
|
529
|
+
sessionManager.abortCodex(sessionId);
|
|
530
|
+
}
|
|
470
531
|
if (payload.type === 'claude-resume') {
|
|
471
532
|
sessionManager.resumeClaude(sessionId, payload.resumeSessionId || '');
|
|
472
533
|
}
|
|
@@ -9,6 +9,7 @@ const RenderedHistory = require('./rendered-history');
|
|
|
9
9
|
const CircularBuffer = require('./buffer');
|
|
10
10
|
const { getToolByKey } = require('../ai-tools/registry');
|
|
11
11
|
const ClaudeStructuredSession = require('../claude/structured-session');
|
|
12
|
+
const CodexStructuredSession = require('../codex/structured-session');
|
|
12
13
|
|
|
13
14
|
function previewText(text, maxChars = 320) {
|
|
14
15
|
if (!text) return '';
|
|
@@ -39,7 +40,7 @@ class SessionManager extends EventEmitter {
|
|
|
39
40
|
startTime: session.startTime,
|
|
40
41
|
toolKey: session.tool.key,
|
|
41
42
|
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
42
|
-
mode:
|
|
43
|
+
mode: ['claude-structured', 'codex-structured'].includes(session.kind) ? (session.presentation || 'structured') : 'terminal',
|
|
43
44
|
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
44
45
|
timedInputCount: session.timedInputs
|
|
45
46
|
? Array.from(session.timedInputs.values()).filter(item => item.sendAt > Date.now()).length
|
|
@@ -68,6 +69,9 @@ class SessionManager extends EventEmitter {
|
|
|
68
69
|
if (tool.key === 'claude-code') {
|
|
69
70
|
return this.createClaudeStructuredSession({ tool, workingDirectory, name, claudeOptions });
|
|
70
71
|
}
|
|
72
|
+
if (tool.key === 'codex') {
|
|
73
|
+
return this.createCodexStructuredSession({ tool, workingDirectory, name });
|
|
74
|
+
}
|
|
71
75
|
|
|
72
76
|
const id = uuidv4();
|
|
73
77
|
const buffer = new CircularBuffer(500000);
|
|
@@ -162,11 +166,33 @@ class SessionManager extends EventEmitter {
|
|
|
162
166
|
return session;
|
|
163
167
|
}
|
|
164
168
|
|
|
169
|
+
createCodexStructuredSession({ tool, workingDirectory, name, codexOptions = {} }) {
|
|
170
|
+
const sessionDir = workingDirectory && String(workingDirectory).trim()
|
|
171
|
+
? path.resolve(this.baseDir, String(workingDirectory).trim())
|
|
172
|
+
: this.baseDir;
|
|
173
|
+
if (!fs.existsSync(sessionDir)) {
|
|
174
|
+
const err = new Error(`Directory does not exist: ${sessionDir}`);
|
|
175
|
+
err.statusCode = 400;
|
|
176
|
+
throw err;
|
|
177
|
+
}
|
|
178
|
+
const id = uuidv4();
|
|
179
|
+
const session = new CodexStructuredSession({ id, tool, workingDir: sessionDir, name: name || tool.displayName, logger: this.logger, options: codexOptions });
|
|
180
|
+
this.sessions.set(id, session);
|
|
181
|
+
session.on('event', event => this.emit('codex-event', { sessionId: id, event, session }));
|
|
182
|
+
session.on('output', data => this.emit('output', { sessionId: id, data, session }));
|
|
183
|
+
session.on('exit', () => this.handleExit(session));
|
|
184
|
+
session.ensureProcess().catch(error => {
|
|
185
|
+
session.append({ kind: 'event', level: 'error', text: `Unable to start Codex app-server: ${error.message}` });
|
|
186
|
+
});
|
|
187
|
+
this.logSessionDiagnostics('codex-session-created', session, {}, { compact: true });
|
|
188
|
+
return session;
|
|
189
|
+
}
|
|
190
|
+
|
|
165
191
|
write(id, data) {
|
|
166
192
|
const session = this.get(id);
|
|
167
193
|
if (!session) return false;
|
|
168
194
|
this.markSessionInput(session, data);
|
|
169
|
-
if (
|
|
195
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) return session.write(data);
|
|
170
196
|
return session.ptyManager.write(data);
|
|
171
197
|
}
|
|
172
198
|
|
|
@@ -208,10 +234,47 @@ class SessionManager extends EventEmitter {
|
|
|
208
234
|
return this.scanClaudeProjectSessions(this.getSessionWorkingDirectory(session));
|
|
209
235
|
}
|
|
210
236
|
|
|
237
|
+
getCodexSnapshot(id) {
|
|
238
|
+
const session = this.get(id);
|
|
239
|
+
return session && session.kind === 'codex-structured' ? session.snapshot() : null;
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
updateCodexSettings(id, settings) {
|
|
243
|
+
const session = this.get(id);
|
|
244
|
+
if (!session || session.kind !== 'codex-structured') return null;
|
|
245
|
+
return session.updateSettings(settings || {});
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
abortCodex(id) {
|
|
249
|
+
const session = this.get(id);
|
|
250
|
+
return session && session.kind === 'codex-structured' ? session.abort('Aborted by user') : false;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
resumeCodex(id, threadId) {
|
|
254
|
+
const session = this.get(id);
|
|
255
|
+
return session && session.kind === 'codex-structured' ? session.resume(threadId) : false;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
listCodexResumeThreads(id) {
|
|
259
|
+
const session = this.get(id);
|
|
260
|
+
if (!session || session.kind !== 'codex-structured') return null;
|
|
261
|
+
return session.listResumeThreads();
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
switchCodexPresentation(id, presentation) {
|
|
265
|
+
const session = this.get(id);
|
|
266
|
+
if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
|
|
267
|
+
return presentation === 'terminal' ? session.switchToTerminal() : session.switchToStructured();
|
|
268
|
+
}
|
|
269
|
+
|
|
211
270
|
resize(id, cols, rows) {
|
|
212
271
|
const session = this.get(id);
|
|
213
272
|
if (!session) return false;
|
|
214
273
|
if (session.kind === 'claude-structured') return true;
|
|
274
|
+
if (session.kind === 'codex-structured') {
|
|
275
|
+
if (session.presentation === 'terminal') session.ptyManager.resize(cols, rows);
|
|
276
|
+
return true;
|
|
277
|
+
}
|
|
215
278
|
if (session.renderedHistory) {
|
|
216
279
|
session.renderedHistory.resize(cols, rows);
|
|
217
280
|
}
|
|
@@ -222,7 +285,7 @@ class SessionManager extends EventEmitter {
|
|
|
222
285
|
redraw(id, cols, rows) {
|
|
223
286
|
const session = this.get(id);
|
|
224
287
|
if (!session) return false;
|
|
225
|
-
if (
|
|
288
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) return true;
|
|
226
289
|
if (session.renderedHistory) {
|
|
227
290
|
session.renderedHistory.resize(cols, rows);
|
|
228
291
|
}
|
|
@@ -240,7 +303,7 @@ class SessionManager extends EventEmitter {
|
|
|
240
303
|
markCompletionRead(id) {
|
|
241
304
|
const session = this.get(id);
|
|
242
305
|
if (!session) return null;
|
|
243
|
-
if (
|
|
306
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) {
|
|
244
307
|
session.markCompletionRead();
|
|
245
308
|
this.logSessionDiagnostics('completion-read', session, {}, { compact: true });
|
|
246
309
|
return session;
|
|
@@ -371,7 +434,7 @@ class SessionManager extends EventEmitter {
|
|
|
371
434
|
this.clearTimedInputs(session);
|
|
372
435
|
this.logSessionDiagnostics('session-deleted', session, {}, { compact: true });
|
|
373
436
|
this.disposeSessionHistory(session);
|
|
374
|
-
if (
|
|
437
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) {
|
|
375
438
|
session.ptyManager.kill();
|
|
376
439
|
return true;
|
|
377
440
|
}
|
|
@@ -394,7 +457,7 @@ class SessionManager extends EventEmitter {
|
|
|
394
457
|
getHistory(id) {
|
|
395
458
|
const session = this.get(id);
|
|
396
459
|
if (!session) return null;
|
|
397
|
-
if (
|
|
460
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) return session.getHistory();
|
|
398
461
|
const historySource = session.renderedHistory || session.textHistory;
|
|
399
462
|
return {
|
|
400
463
|
success: true,
|
|
@@ -409,7 +472,7 @@ class SessionManager extends EventEmitter {
|
|
|
409
472
|
getCatchupOutput(id) {
|
|
410
473
|
const session = this.get(id);
|
|
411
474
|
if (!session) return null;
|
|
412
|
-
if (
|
|
475
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) return session.getCatchupOutput();
|
|
413
476
|
|
|
414
477
|
const bufferHistory = session.buffer.getAfter(0);
|
|
415
478
|
if (bufferHistory.length > 0) {
|
|
@@ -741,7 +804,7 @@ class SessionManager extends EventEmitter {
|
|
|
741
804
|
markSessionInput(session, data) {
|
|
742
805
|
if (typeof data !== 'string' || data.length === 0) return;
|
|
743
806
|
session.inputSeq = (session.inputSeq || 0) + 1;
|
|
744
|
-
if (
|
|
807
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) {
|
|
745
808
|
session.hasUnreadCompletion = false;
|
|
746
809
|
this.logSessionDiagnostics('session-input', session, {
|
|
747
810
|
inputSeq: session.inputSeq,
|
|
@@ -760,7 +823,7 @@ class SessionManager extends EventEmitter {
|
|
|
760
823
|
}
|
|
761
824
|
|
|
762
825
|
disposeSessionHistory(session) {
|
|
763
|
-
if (
|
|
826
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) return;
|
|
764
827
|
if (session.renderedHistory) {
|
|
765
828
|
session.renderedHistory.dispose();
|
|
766
829
|
session.renderedHistory = null;
|
|
@@ -776,7 +839,7 @@ class SessionManager extends EventEmitter {
|
|
|
776
839
|
}
|
|
777
840
|
|
|
778
841
|
getSessionDiagnostics(session, extra = {}) {
|
|
779
|
-
if (
|
|
842
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) {
|
|
780
843
|
return {
|
|
781
844
|
sessionId: session.id,
|
|
782
845
|
sessionName: session.name,
|
|
@@ -805,7 +868,7 @@ class SessionManager extends EventEmitter {
|
|
|
805
868
|
}
|
|
806
869
|
|
|
807
870
|
getCompactSessionDiagnostics(session, extra = {}) {
|
|
808
|
-
if (
|
|
871
|
+
if (['claude-structured', 'codex-structured'].includes(session.kind)) {
|
|
809
872
|
return this.getSessionDiagnostics(session, extra);
|
|
810
873
|
}
|
|
811
874
|
const buffer = session.buffer.getDebugSnapshot();
|