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.
@@ -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 = session.kind !== 'claude-structured'
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 !== 'claude-structured' && catchup && catchup.data) {
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: session.kind === 'claude-structured' ? 'structured' : 'terminal',
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 (session.kind === 'claude-structured') return session.write(data);
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 (session.kind === 'claude-structured') return true;
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 (session.kind === 'claude-structured') {
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 (session.kind === 'claude-structured') {
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 (session.kind === 'claude-structured') return session.getHistory();
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 (session.kind === 'claude-structured') return session.getCatchupOutput();
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 (session.kind === 'claude-structured') {
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 (session.kind === 'claude-structured') return;
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 (session.kind === 'claude-structured') {
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 (session.kind === 'claude-structured') {
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();