glad-web 1.0.22 → 1.0.24

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.
@@ -5,11 +5,38 @@ const crypto = require('crypto');
5
5
  const PTYManager = require('../session/pty-manager');
6
6
 
7
7
  const PERMISSION_MODES = new Set(['untrusted', 'on-request', 'never']);
8
+ const SANDBOX_MODES = new Set(['read-only', 'workspace-write', 'danger-full-access']);
8
9
  const FALLBACK_EFFORTS = ['minimal', 'low', 'medium', 'high', 'xhigh', 'ultra'];
9
10
 
10
11
  function normalizePermissionMode(value) {
11
- const mode = String(value || 'on-request');
12
- return PERMISSION_MODES.has(mode) ? mode : 'on-request';
12
+ const mode = String(value || 'default');
13
+ return PERMISSION_MODES.has(mode) ? mode : null;
14
+ }
15
+
16
+ function normalizeSandboxMode(value) {
17
+ const mode = String(value || 'default');
18
+ return SANDBOX_MODES.has(mode) ? mode : null;
19
+ }
20
+
21
+ function sandboxPolicyFor(mode, workingDir, workspaceOptions = {}) {
22
+ if (mode === 'danger-full-access') return { type: 'dangerFullAccess' };
23
+ if (mode === 'read-only') return { type: 'readOnly', networkAccess: false };
24
+ if (mode === 'workspace-write') {
25
+ const roots = Array.isArray(workspaceOptions.writable_roots) ? workspaceOptions.writable_roots : [];
26
+ return { type: 'workspaceWrite', writableRoots: [workingDir, ...roots.filter(root => root !== workingDir)],
27
+ networkAccess: Boolean(workspaceOptions.network_access),
28
+ excludeTmpdirEnvVar: Boolean(workspaceOptions.exclude_tmpdir_env_var),
29
+ excludeSlashTmp: Boolean(workspaceOptions.exclude_slash_tmp) };
30
+ }
31
+ return null;
32
+ }
33
+
34
+ function sandboxModeFromPolicy(policy) {
35
+ const type = typeof policy === 'string' ? policy : policy?.type;
36
+ if (type === 'dangerFullAccess' || type === 'danger-full-access') return 'danger-full-access';
37
+ if (type === 'readOnly' || type === 'read-only') return 'read-only';
38
+ if (type === 'workspaceWrite' || type === 'workspace-write') return 'workspace-write';
39
+ return null;
13
40
  }
14
41
 
15
42
  function safeJson(value) {
@@ -95,7 +122,18 @@ class CodexStructuredSession extends EventEmitter {
95
122
  this.threadId = options.resume || null;
96
123
  this.currentTurnId = null;
97
124
  this.currentTurnStartedAt = null;
125
+ this.tokenUsage = null;
98
126
  this.permissionMode = normalizePermissionMode(options.permissionMode);
127
+ this.sandboxMode = normalizeSandboxMode(options.sandboxMode);
128
+ this.effectivePermissionMode = null;
129
+ this.effectiveSandboxMode = null;
130
+ this.configPermissionMode = null;
131
+ this.configSandboxMode = null;
132
+ this.configSandboxWorkspaceWrite = {};
133
+ this.configModel = null;
134
+ this.configEffort = null;
135
+ this.hasModelOverride = Boolean(options.model);
136
+ this.hasEffortOverride = Boolean(options.effort);
99
137
  this.model = options.model || null;
100
138
  this.effort = options.effort || null;
101
139
  this.models = [];
@@ -135,7 +173,9 @@ class CodexStructuredSession extends EventEmitter {
135
173
  }
136
174
 
137
175
  getControlState() {
138
- return { permissionMode: this.permissionMode, model: this.model, effort: this.effort,
176
+ return { permissionMode: this.permissionMode || 'default', sandboxMode: this.sandboxMode || 'default',
177
+ effectivePermissionMode: this.effectivePermissionMode, effectiveSandboxMode: this.effectiveSandboxMode,
178
+ model: this.model, effort: this.effort,
139
179
  status: this.status, threadId: this.threadId, presentation: this.presentation,
140
180
  canAbort: this.presentation === 'structured' && this.status !== 'idle',
141
181
  canSwitchToTerminal: this.presentation === 'structured' && this.status === 'idle' && Boolean(this.threadId),
@@ -209,6 +249,7 @@ class CodexStructuredSession extends EventEmitter {
209
249
  lines.on('line', line => this.handleRpcLine(line));
210
250
  this.request('initialize', { clientInfo: { name: 'glad-web', title: 'Glad', version: '1.0' }, capabilities: { experimentalApi: true } })
211
251
  .then(async () => {
252
+ try { await this.refreshConfigDefaults(); } catch (error) { this.logger.debugInfo?.(`[codex-app-server] config/read failed: ${error.message}`); }
212
253
  try { await this.refreshModels(); } catch (error) { this.logger.debugInfo?.(`[codex-app-server] model/list failed: ${error.message}`); }
213
254
  resolve();
214
255
  }).catch(fail);
@@ -289,6 +330,10 @@ class CodexStructuredSession extends EventEmitter {
289
330
  }
290
331
 
291
332
  handleNotification(method, params) {
333
+ if (method === 'thread/tokenUsage/updated') {
334
+ this.tokenUsage = params.tokenUsage || params.usage || params;
335
+ return;
336
+ }
292
337
  if (method === 'turn/started') {
293
338
  this.currentTurnId = params.turn?.id || params.turnId || this.currentTurnId;
294
339
  this.currentTurnStartedAt = Date.now();
@@ -302,6 +347,10 @@ class CodexStructuredSession extends EventEmitter {
302
347
  : params.turn?.status === 'interrupted' ? 'cancelled' : 'completed';
303
348
  this.append({ kind: 'turn-end', turnId: completedTurnId, status: turnStatus,
304
349
  durationMs: this.currentTurnStartedAt ? Date.now() - this.currentTurnStartedAt : null });
350
+ for (const item of this.messages.filter(message => message.kind === 'tool'
351
+ && message.turnId === completedTurnId && ['running', 'inProgress'].includes(message.toolStatus))) {
352
+ this.patch(item.id, { toolStatus: turnStatus === 'failed' ? 'failed' : 'completed' });
353
+ }
305
354
  for (const pending of this.pendingPermissions.values()) {
306
355
  this.recordPermission(pending.public, 'denied', 'abort');
307
356
  }
@@ -330,7 +379,8 @@ class CodexStructuredSession extends EventEmitter {
330
379
  const settings = params.threadSettings || {};
331
380
  this.model = settings.model || this.model;
332
381
  this.effort = settings.effort || this.effort;
333
- this.permissionMode = settings.approvalPolicy || this.permissionMode;
382
+ this.effectivePermissionMode = settings.approvalPolicy || this.effectivePermissionMode;
383
+ this.effectiveSandboxMode = sandboxModeFromPolicy(settings.sandboxPolicy) || this.effectiveSandboxMode;
334
384
  this.emitEvent({ type: 'state', state: this.getControlState() });
335
385
  return;
336
386
  }
@@ -364,10 +414,13 @@ class CodexStructuredSession extends EventEmitter {
364
414
  else this.append({ kind, providerId: itemId, text: delta, streaming: true });
365
415
  return;
366
416
  }
367
- if (method.startsWith('item/')) this.applyProviderItem(params.item || params);
417
+ if (method.startsWith('item/')) {
418
+ const inferredStatus = method === 'item/completed' ? 'completed' : method === 'item/started' ? 'running' : null;
419
+ this.applyProviderItem(params.item || params, inferredStatus);
420
+ }
368
421
  }
369
422
 
370
- applyProviderItem(raw) {
423
+ applyProviderItem(raw, inferredStatus = null) {
371
424
  if (!raw || typeof raw !== 'object') return;
372
425
  const providerId = String(raw.id || '');
373
426
  const existing = providerId && this.messages.find(item => item.providerId === providerId);
@@ -376,8 +429,10 @@ class CodexStructuredSession extends EventEmitter {
376
429
  if (!kind) return;
377
430
  const text = kind === 'user' ? textFromInputItems(raw.content) : kind === 'assistant' ? String(raw.text || '')
378
431
  : kind === 'reasoning' ? (raw.text || (Array.isArray(raw.summary) ? raw.summary.join('\n') : Array.isArray(raw.content) ? raw.content.join('\n') : '')) : '';
432
+ const inferredToolStatus = inferredStatus === 'completed' && ['failed', 'declined'].includes(raw.status)
433
+ ? raw.status : inferredStatus;
379
434
  const patch = kind === 'tool' ? { ...toolDetails(raw), turnId: raw.turnId || this.currentTurnId,
380
- toolStatus: raw.status || 'running' } : { text, turnId: raw.turnId || this.currentTurnId, streaming: false };
435
+ toolStatus: inferredToolStatus || raw.status || 'running' } : { text, turnId: raw.turnId || this.currentTurnId, streaming: false };
381
436
  if (existing) {
382
437
  this.patch(existing.id, patch);
383
438
  } else if (kind === 'user') {
@@ -395,16 +450,33 @@ class CodexStructuredSession extends EventEmitter {
395
450
  do {
396
451
  const result = await this.request('model/list', { cursor, limit: 100, includeHidden: false });
397
452
  for (const item of result?.data || []) models.push({ id: item.id || item.model, label: item.displayName || item.model || item.id,
398
- efforts: (item.supportedReasoningEfforts || []).map(value => value.reasoningEffort), defaultEffort: item.defaultReasoningEffort || null });
453
+ efforts: (item.supportedReasoningEfforts || []).map(value => value.reasoningEffort), defaultEffort: item.defaultReasoningEffort || null,
454
+ isDefault: Boolean(item.isDefault), contextWindow: Number(item.contextWindow || item.context_window || 0) || null });
399
455
  cursor = result?.nextCursor || null;
400
456
  } while (cursor);
401
457
  this.models = models;
402
- if (!this.model && models[0]) this.model = models[0].id;
458
+ if (!this.model) this.model = (models.find(item => item.isDefault) || models[0])?.id || null;
403
459
  if (!this.effort) this.effort = models.find(item => item.id === this.model)?.defaultEffort || 'medium';
404
460
  this.emitEvent({ type: 'state', state: this.getControlState() });
405
461
  return models;
406
462
  }
407
463
 
464
+ async refreshConfigDefaults() {
465
+ const result = await this.request('config/read', { cwd: this.workingDir, includeLayers: false });
466
+ const config = result?.config || {};
467
+ this.configPermissionMode = config.approval_policy || null;
468
+ this.configSandboxMode = normalizeSandboxMode(config.sandbox_mode);
469
+ this.configSandboxWorkspaceWrite = config.sandbox_workspace_write || {};
470
+ this.configModel = config.model || null;
471
+ this.configEffort = config.model_reasoning_effort || null;
472
+ if (!this.permissionMode) this.effectivePermissionMode = this.configPermissionMode;
473
+ if (!this.sandboxMode) this.effectiveSandboxMode = this.configSandboxMode;
474
+ if (!this.hasModelOverride && this.configModel) this.model = this.configModel;
475
+ if (!this.hasEffortOverride && this.configEffort) this.effort = this.configEffort;
476
+ this.emitEvent({ type: 'state', state: this.getControlState() });
477
+ return config;
478
+ }
479
+
408
480
  async listResumeThreads() {
409
481
  await this.ensureProcess();
410
482
  const result = await this.request('thread/list', {
@@ -425,13 +497,87 @@ class CodexStructuredSession extends EventEmitter {
425
497
  }));
426
498
  }
427
499
 
500
+ contextStatus() {
501
+ const usage = this.tokenUsage || {};
502
+ const selectedModel = this.models.find(item => item.id === this.model);
503
+ const contextWindow = Number(usage.modelContextWindow || usage.model_context_window
504
+ || usage.contextWindow || usage.context_window || selectedModel?.contextWindow || 0);
505
+ const last = usage.last || usage.lastTokenUsage || usage.last_token_usage || {};
506
+ const usedTokens = Number(last.totalTokens || last.total_tokens || usage.contextTokens
507
+ || usage.context_tokens || 0);
508
+ if (!contextWindow) {
509
+ return !this.threadId && !this.tokenUsage
510
+ ? { usedTokens: 0, contextWindow: null, remainingTokens: null, remainingPercent: 100 }
511
+ : null;
512
+ }
513
+ return {
514
+ usedTokens: Math.max(0, usedTokens),
515
+ contextWindow,
516
+ remainingTokens: Math.max(0, contextWindow - usedTokens),
517
+ remainingPercent: Math.max(0, Math.min(100, Math.round((contextWindow - usedTokens) / contextWindow * 100)))
518
+ };
519
+ }
520
+
521
+ async showStatus() {
522
+ if (this.presentation !== 'structured') return false;
523
+ await this.ensureProcess();
524
+ const accountResult = await this.request('account/read', { refreshToken: false });
525
+ const account = accountResult?.account || null;
526
+ let rateLimits = null;
527
+ if (account?.type === 'chatgpt') {
528
+ try {
529
+ const result = await this.request('account/rateLimits/read', {});
530
+ rateLimits = result?.rateLimits || null;
531
+ } catch (error) {
532
+ this.logger.debugInfo?.(`[codex-app-server] account/rateLimits/read failed: ${error.message}`);
533
+ }
534
+ }
535
+ this.append({ kind: 'status', title: 'Codex status', model: this.model, effort: this.effort,
536
+ account, rateLimits, context: this.contextStatus() });
537
+ return true;
538
+ }
539
+
428
540
  async updateSettings(settings = {}) {
541
+ const configEdits = [];
542
+ if (settings.model) configEdits.push({ keyPath: 'model', value: String(settings.model), mergeStrategy: 'upsert' });
543
+ if (settings.effort) configEdits.push({ keyPath: 'model_reasoning_effort', value: String(settings.effort), mergeStrategy: 'upsert' });
544
+ if (configEdits.length) {
545
+ await this.ensureProcess();
546
+ await this.request('config/batchWrite', { edits: configEdits });
547
+ if (settings.model) this.configModel = String(settings.model);
548
+ if (settings.effort) this.configEffort = String(settings.effort);
549
+ }
429
550
  if (settings.permissionMode !== undefined) this.permissionMode = normalizePermissionMode(settings.permissionMode);
430
- if (settings.model !== undefined) this.model = settings.model || null;
431
- if (settings.effort !== undefined) this.effort = settings.effort || null;
551
+ if (settings.sandboxMode !== undefined) this.sandboxMode = normalizeSandboxMode(settings.sandboxMode);
552
+ if (settings.model !== undefined) {
553
+ this.hasModelOverride = Boolean(settings.model);
554
+ this.model = settings.model || this.configModel || null;
555
+ }
556
+ if (settings.effort !== undefined) {
557
+ this.hasEffortOverride = Boolean(settings.effort);
558
+ this.effort = settings.effort || this.configEffort || null;
559
+ }
560
+ const needsConfigDefaults = (settings.permissionMode !== undefined && !this.permissionMode)
561
+ || (settings.sandboxMode !== undefined && !this.sandboxMode);
562
+ if (needsConfigDefaults && this.presentation === 'structured') {
563
+ await this.ensureProcess();
564
+ await this.refreshConfigDefaults();
565
+ }
432
566
  if (this.threadId && this.presentation === 'structured') {
433
567
  await this.ensureProcess();
434
- await this.request('thread/settings/update', { threadId: this.threadId, approvalPolicy: this.permissionMode, model: this.model, effort: this.effort });
568
+ const params = { threadId: this.threadId };
569
+ if (settings.permissionMode !== undefined) {
570
+ const approvalPolicy = this.permissionMode || this.configPermissionMode;
571
+ if (approvalPolicy) params.approvalPolicy = approvalPolicy;
572
+ }
573
+ if (settings.sandboxMode !== undefined) {
574
+ const sandboxPolicy = sandboxPolicyFor(this.sandboxMode || this.configSandboxMode, this.workingDir,
575
+ this.sandboxMode ? {} : this.configSandboxWorkspaceWrite);
576
+ if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
577
+ }
578
+ if (settings.model !== undefined) params.model = this.hasModelOverride ? this.model : null;
579
+ if (settings.effort !== undefined) params.effort = this.hasEffortOverride ? this.effort : null;
580
+ if (Object.keys(params).length > 1) await this.request('thread/settings/update', params);
435
581
  }
436
582
  this.emitEvent({ type: 'state', state: this.getControlState() });
437
583
  return this.getControlState();
@@ -444,17 +590,26 @@ class CodexStructuredSession extends EventEmitter {
444
590
  this.append({ kind: 'user', text: prompt });
445
591
  await this.ensureProcess();
446
592
  if (!this.threadId) {
447
- const started = await this.request('thread/start', { model: this.model, cwd: this.workingDir, approvalPolicy: this.permissionMode, sandbox: 'workspace-write' });
593
+ const params = { cwd: this.workingDir };
594
+ if (this.hasModelOverride) params.model = this.model;
595
+ if (this.permissionMode) params.approvalPolicy = this.permissionMode;
596
+ if (this.sandboxMode) params.sandbox = this.sandboxMode;
597
+ const started = await this.request('thread/start', params);
448
598
  this.threadId = started.thread?.id;
449
599
  this.model = started.model || this.model;
450
600
  this.effort = started.reasoningEffort || this.effort;
601
+ this.effectivePermissionMode = started.approvalPolicy || this.effectivePermissionMode;
602
+ this.effectiveSandboxMode = sandboxModeFromPolicy(started.sandbox) || this.effectiveSandboxMode;
451
603
  this.emitEvent({ type: 'state', state: this.getControlState() });
452
604
  }
453
605
  this.setStatus('running');
454
- const started = await this.request('turn/start', { threadId: this.threadId, input: [{ type: 'text', text: prompt }], cwd: this.workingDir,
455
- approvalPolicy: this.permissionMode,
456
- sandboxPolicy: { type: 'workspaceWrite', writableRoots: [this.workingDir], networkAccess: false, excludeTmpdirEnvVar: false, excludeSlashTmp: false },
457
- model: this.model, effort: this.effort, summary: 'auto' });
606
+ const params = { threadId: this.threadId, input: [{ type: 'text', text: prompt }], cwd: this.workingDir, summary: 'auto' };
607
+ if (this.hasModelOverride) params.model = this.model;
608
+ if (this.hasEffortOverride) params.effort = this.effort;
609
+ if (this.permissionMode) params.approvalPolicy = this.permissionMode;
610
+ const sandboxPolicy = sandboxPolicyFor(this.sandboxMode, this.workingDir);
611
+ if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
612
+ const started = await this.request('turn/start', params);
458
613
  this.currentTurnId = started?.turn?.id || started?.turnId || this.currentTurnId;
459
614
  return true;
460
615
  }
@@ -521,17 +676,36 @@ class CodexStructuredSession extends EventEmitter {
521
676
  const target = String(threadId || this.threadId || '').trim();
522
677
  if (!target || this.presentation !== 'structured' || this.status !== 'idle') return false;
523
678
  await this.ensureProcess();
524
- const result = await this.request('thread/resume', { threadId: target, model: this.model, cwd: this.workingDir, approvalPolicy: this.permissionMode, sandbox: 'workspace-write' });
679
+ const params = { threadId: target, cwd: this.workingDir };
680
+ if (this.permissionMode) params.approvalPolicy = this.permissionMode;
681
+ if (this.sandboxMode) params.sandbox = this.sandboxMode;
682
+ const result = await this.request('thread/resume', params);
525
683
  this.threadId = result.thread?.id || target;
526
- this.model = result.model || this.model;
684
+ this.hasModelOverride = false;
685
+ this.hasEffortOverride = false;
686
+ this.model = result.model || result.thread?.model || this.model;
687
+ this.effort = result.reasoningEffort || result.thread?.reasoningEffort || this.effort;
688
+ this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
689
+ this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
527
690
  const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
691
+ this.model = history?.thread?.model || this.model;
692
+ this.effort = history?.thread?.reasoningEffort || history?.thread?.reasoning_effort || this.effort;
693
+ this.tokenUsage = history?.thread?.tokenUsage || history?.thread?.token_usage || this.tokenUsage;
528
694
  this.messages = [];
529
695
  this.completedPermissions = [];
530
696
  for (const turn of history?.thread?.turns || []) {
531
- this.append({ kind: 'turn-start', turnId: turn.id });
532
- for (const item of turn.items || []) this.applyProviderItem({ ...item, turnId: turn.id });
533
697
  const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
534
- this.append({ kind: 'turn-end', turnId: turn.id, status });
698
+ const startedAt = Number(turn.startedAt || turn.createdAt || 0);
699
+ const completedAt = Number(turn.completedAt || turn.updatedAt || 0);
700
+ const toMilliseconds = value => value > 0 && value < 100000000000 ? value * 1000 : value;
701
+ const startedAtMs = toMilliseconds(startedAt);
702
+ const completedAtMs = toMilliseconds(completedAt);
703
+ this.append({ kind: 'turn-start', turnId: turn.id, ...(startedAtMs ? { createdAt: startedAtMs } : {}) });
704
+ for (const item of turn.items || []) this.applyProviderItem({ ...item, turnId: turn.id }, status === 'failed' ? 'failed' : 'completed');
705
+ const durationMs = Number(turn.durationMs || turn.duration_ms || 0)
706
+ || (startedAtMs && completedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null);
707
+ this.append({ kind: 'turn-end', turnId: turn.id, status, durationMs,
708
+ ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
535
709
  }
536
710
  this.append({ kind: 'event', level: 'info', text: `Resumed Codex thread ${this.threadId}` });
537
711
  this.emitEvent({ type: 'history-reset', messages: this.messages });
@@ -525,6 +525,9 @@ async function webCommand(options) {
525
525
  if (payload.type === 'codex-settings') {
526
526
  sessionManager.updateCodexSettings(sessionId, payload.settings || {}).catch(error => logger.error(`Codex settings error: ${error.message}`));
527
527
  }
528
+ if (payload.type === 'codex-status') {
529
+ sessionManager.showCodexStatus(sessionId).catch(error => logger.error(`Codex status error: ${error.message}`));
530
+ }
528
531
  if (payload.type === 'codex-abort') {
529
532
  sessionManager.abortCodex(sessionId);
530
533
  }
@@ -5,7 +5,7 @@ function getTerminalCtor() {
5
5
  if (typeof global.window === 'undefined') {
6
6
  global.window = {};
7
7
  }
8
- ({ Terminal: TerminalCtor } = require('xterm-headless'));
8
+ ({ Terminal: TerminalCtor } = require('@xterm/headless'));
9
9
  return TerminalCtor;
10
10
  }
11
11
 
@@ -245,6 +245,12 @@ class SessionManager extends EventEmitter {
245
245
  return session.updateSettings(settings || {});
246
246
  }
247
247
 
248
+ showCodexStatus(id) {
249
+ const session = this.get(id);
250
+ if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
251
+ return session.showStatus();
252
+ }
253
+
248
254
  abortCodex(id) {
249
255
  const session = this.get(id);
250
256
  return session && session.kind === 'codex-structured' ? session.abort('Aborted by user') : false;
@@ -80,9 +80,17 @@
80
80
  #claude-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
81
81
  #codex-chat-container { display: none; flex: 1; min-height: 0; overflow-y: auto; background: #050505; padding: 12px 12px 24px 12px; box-sizing: border-box; -webkit-overflow-scrolling: touch; }
82
82
  .codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
83
+ .codex-working-indicator { position: sticky; top: 0; z-index: 4; width: 28px; height: 28px; margin: 0 0 -28px auto; border: 1px solid rgba(255,255,255,.1); border-radius: 50%; background: rgba(28,28,30,.68); box-shadow: 0 5px 16px rgba(0,0,0,.24); backdrop-filter: blur(8px); pointer-events: none; }
84
+ .codex-working-indicator::after { content: ''; position: absolute; inset: 8px; border: 1.5px solid rgba(255,255,255,.7); border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; }
83
85
  .codex-message { max-width: 100%; margin: 0 0 12px; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
84
86
  .codex-message.user { max-width: 92%; margin-left: auto; padding: 8px 12px; border: 1px solid rgba(0,122,255,.32); border-radius: 12px; background: rgba(0,122,255,.24); }
85
87
  .codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
88
+ .codex-status-card { margin: 10px 0 14px; border: 1px solid rgba(100,210,255,.2); border-radius: 10px; background: rgba(28,28,30,.72); padding: 11px; color: #f5f5f7; }
89
+ .codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
90
+ .codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
91
+ .codex-status-item { min-width: 0; border: 1px solid rgba(255,255,255,.07); border-radius: 8px; background: rgba(255,255,255,.035); padding: 8px; }
92
+ .codex-status-label { color: #8e8e93; font-size: 10px; font-weight: 800; text-transform: uppercase; }
93
+ .codex-status-value { margin-top: 3px; color: #f5f5f7; font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
86
94
  .codex-tool { border: 1px solid rgba(255,255,255,.1); border-radius: 8px; background: rgba(255,255,255,.045); overflow: hidden; margin: 8px 0; color: #d1d5db; }
87
95
  .codex-tool summary { list-style: none; cursor: pointer; }
88
96
  .codex-tool summary::-webkit-details-marker { display: none; }
@@ -117,8 +125,13 @@
117
125
  .codex-inline-permission .claude-permission-actions { margin-top: 8px; }
118
126
  @keyframes codex-spin { to { transform: rotate(360deg); } }
119
127
  #codex-control-panel { display: none; width: min(100%, var(--control-content-max)); margin: 0 auto; padding: 8px 14px 10px; border-bottom: 1px solid #222; background: #121212; box-sizing: border-box; }
120
- .codex-control-row { display: grid; grid-template-columns: repeat(4, minmax(0, 1fr)); gap: 7px; align-items: center; }
121
- .codex-select { appearance: none; max-width: 116px; min-width: 0; height: 32px; padding: 0 8px; border: 1px solid rgba(255,255,255,.12); border-radius: 7px; background: rgba(255,255,255,.08); color: #f5f5f7; font-weight: 700; font-size: 12px; }
128
+ .codex-control-row { display: grid; grid-template-columns: repeat(6, minmax(0, 116px)); grid-template-rows: auto; justify-content: center; gap: clamp(2px, .8vw, 7px); align-items: center; white-space: nowrap; }
129
+ .codex-control-row > * { width: 100%; min-width: 0; overflow: hidden; }
130
+ .codex-control-row .claude-ctrl-btn, .codex-control-row .codex-select-label { padding-left: clamp(2px, 1vw, 10px); padding-right: clamp(2px, 1vw, 10px); font-size: clamp(8px, 2.2vw, 11px); text-overflow: ellipsis; overflow: hidden; }
131
+ .codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
132
+ .codex-select-label { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; box-sizing: border-box; border: 1px solid rgba(255,255,255,.1); border-radius: 16px; background: rgba(255,255,255,.08); color: #f5f5f7; font-size: 11px; font-weight: 800; white-space: nowrap; }
133
+ .codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
134
+ .codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
122
135
  .codex-select option { background: #1c1c1e; color: #fff; }
123
136
  #codex-model-panel, #codex-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
124
137
  #codex-model-panel.active, #codex-resume-panel.active { display: block; }
@@ -128,7 +141,8 @@
128
141
  .codex-picker-column + .codex-picker-column { border-left: 1px solid rgba(255,255,255,.08); }
129
142
  .codex-picker-option { display: block; width: 100%; min-height: 38px; padding: 8px 10px; border: 0; border-bottom: 1px solid rgba(255,255,255,.06); background: transparent; color: #f5f5f7; text-align: left; font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
130
143
  .codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
131
- #codex-state-bar { display: none; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; }
144
+ #codex-state-bar { display: flex; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; overflow-x: auto; scrollbar-width: none; white-space: nowrap; }
145
+ #codex-state-bar::-webkit-scrollbar { display: none; }
132
146
  #codex-resume-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
133
147
  .claude-context-size-badge { position: sticky; top: 0; z-index: 2; width: max-content; max-width: 100%; margin: 0 0 8px auto; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.94); border-radius: 999px; padding: 4px 9px; color: #d1d5db; font-size: 11px; font-weight: 800; box-shadow: 0 8px 18px rgba(0,0,0,0.24); }
134
148
  .claude-message { max-width: 92%; margin: 0 0 10px 0; padding: 10px 12px; border-radius: 8px; overflow-wrap: anywhere; line-height: 1.45; font-size: 14px; }
@@ -287,8 +301,10 @@
287
301
  #nav-bar > div:last-child { gap: 5px !important; }
288
302
  #nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
289
303
  #input-row { padding-left: 10px; padding-right: 10px; }
290
- #codex-control-panel { padding-left: 10px; padding-right: 10px; }
291
- .codex-control-row { gap: 6px; }
304
+ #codex-control-panel { padding-left: 8px; padding-right: 8px; }
305
+ .codex-control-row { grid-template-columns: repeat(6, minmax(0, 1fr)); }
306
+ .codex-control-row .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
307
+ .codex-select-control { height: 36px; }
292
308
  #codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
293
309
  .codex-message.user { max-width: 94%; }
294
310
  .claude-permission-actions { justify-content: flex-start; }
@@ -405,10 +421,20 @@
405
421
  </div>
406
422
  <div id="codex-control-panel">
407
423
  <div class="codex-control-row">
408
- <select id="codex-permission-select" class="codex-select" title="Permission mode" onchange="updateCodexSettingsFromControls()">
409
- <option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
410
- </select>
424
+ <label class="codex-select-control" title="Sandbox mode">
425
+ <span class="codex-select-label" aria-hidden="true">Sandbox</span>
426
+ <select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
427
+ <option value="default">Default</option><option value="read-only">Read only</option><option value="workspace-write">Workspace write</option><option value="danger-full-access">Full access</option>
428
+ </select>
429
+ </label>
430
+ <label class="codex-select-control" title="Approval policy">
431
+ <span class="codex-select-label" aria-hidden="true">Ask</span>
432
+ <select id="codex-permission-select" class="codex-select" aria-label="Approval policy" onchange="updateCodexSettingsFromControls()">
433
+ <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
434
+ </select>
435
+ </label>
411
436
  <button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
437
+ <button id="codex-status-btn" class="claude-ctrl-btn" onclick="requestCodexStatus()" title="Show Codex account, limits, and context status">Status</button>
412
438
  <button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
413
439
  <button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
414
440
  </div>
@@ -607,7 +633,7 @@
607
633
  let claudeResumeItemsLoaded = false;
608
634
  let codexMessages = [];
609
635
  let codexPendingPermissions = [];
610
- let codexState = { permissionMode: 'on-request', model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
636
+ let codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
611
637
  let codexModelPanelOpen = false;
612
638
  let codexModelCandidate = null;
613
639
  let codexResumePanelOpen = false;
@@ -2062,6 +2088,39 @@
2062
2088
  if (inline) return `<div class="codex-inline-permission">${content}</div>`;
2063
2089
  return `<div class="claude-tool claude-permission"><div class="claude-tool-header"><strong>${escapeHtml(request.title || 'Permission required')}</strong></div><div class="claude-tool-body">${content}</div></div>`;
2064
2090
  }
2091
+ function formatCodexReset(timestamp) {
2092
+ const value = Number(timestamp || 0);
2093
+ return value ? new Date(value * 1000).toLocaleString() : '';
2094
+ }
2095
+ function formatCodexTokens(value) {
2096
+ const count = Number(value || 0);
2097
+ return count >= 1000000 ? `${(count / 1000000).toFixed(1)}M`
2098
+ : count >= 1000 ? `${Math.round(count / 1000)}K` : String(count);
2099
+ }
2100
+ function codexStatusItem(label, value) {
2101
+ if (value == null || value === '') return '';
2102
+ return `<div class="codex-status-item"><div class="codex-status-label">${escapeHtml(label)}</div><div class="codex-status-value">${escapeHtml(value)}</div></div>`;
2103
+ }
2104
+ function renderCodexStatus(item) {
2105
+ const account = item.account || {};
2106
+ const limit = item.rateLimits || {};
2107
+ const primary = limit.primary;
2108
+ const secondary = limit.secondary;
2109
+ const context = item.context;
2110
+ const accountLabel = account.type === 'chatgpt'
2111
+ ? [account.email, account.planType].filter(Boolean).join(' · ')
2112
+ : account.type === 'apiKey' ? 'API key' : account.type || 'Not signed in';
2113
+ const fiveHour = primary ? `${Math.max(0, 100 - Number(primary.usedPercent || 0))}% left${primary.resetsAt ? ` · resets ${formatCodexReset(primary.resetsAt)}` : ''}` : '';
2114
+ const weekly = secondary ? `${Math.max(0, 100 - Number(secondary.usedPercent || 0))}% left${secondary.resetsAt ? ` · resets ${formatCodexReset(secondary.resetsAt)}` : ''}` : '';
2115
+ const contextLabel = context ? `${context.remainingPercent}% left${context.contextWindow ? ` · ${formatCodexTokens(context.remainingTokens)} / ${formatCodexTokens(context.contextWindow)}` : ''}` : 'Available after the first usage update';
2116
+ return `<div class="codex-status-card"><div class="codex-status-title">${escapeHtml(item.title || 'Codex status')}</div><div class="codex-status-grid">
2117
+ ${codexStatusItem('Account', accountLabel)}
2118
+ ${codexStatusItem('Model', [item.model, item.effort].filter(Boolean).join(' · '))}
2119
+ ${codexStatusItem('5h limit', fiveHour)}
2120
+ ${codexStatusItem('Weekly limit', weekly)}
2121
+ ${codexStatusItem('Context', contextLabel)}
2122
+ </div></div>`;
2123
+ }
2065
2124
  function renderCodexTool(item, permission = null) {
2066
2125
  const status = codexToolStatus(item);
2067
2126
  const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
@@ -2075,17 +2134,22 @@
2075
2134
  const result = item.result || (item.name === 'McpTool' || item.name === 'Agent' ? codexJson(item.input) : '');
2076
2135
  return `<details class="codex-tool${isError ? ' error' : ''}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
2077
2136
  }
2078
- function renderCodexToolGroup(items, permissionById, usedPermissions) {
2137
+ function renderCodexToolGroup(items, permissionById, usedPermissions, turnEnd = null) {
2079
2138
  const running = items.some(item => codexToolStatus(item) === 'running');
2080
2139
  const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
2081
2140
  const startedAt = Math.min(...items.map(item => Number(item.createdAt || Date.now())));
2082
- const seconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
2141
+ const storedDuration = Number(turnEnd?.durationMs || 0);
2142
+ const completedAt = Number(turnEnd?.createdAt || 0);
2143
+ const durationMs = storedDuration > 0 ? storedDuration
2144
+ : (!running && completedAt >= startedAt ? completedAt - startedAt : 0);
2145
+ const seconds = durationMs > 0 ? Math.max(1, Math.round(durationMs / 1000)) : null;
2083
2146
  const tools = items.map(item => {
2084
2147
  const permission = permissionById.get(String(item.providerId || ''));
2085
2148
  if (permission) usedPermissions.add(permission.id);
2086
2149
  return renderCodexTool(item, permission);
2087
2150
  }).join('');
2088
- return `<details class="codex-work-group"${running ? ' open' : ''}><summary>${running ? 'Working' : failed ? 'Work finished with errors' : `Worked for ${seconds}s`} · ${items.length} tools</summary><div class="codex-work-group-body">${tools}</div></details>`;
2151
+ const label = running ? 'Working' : failed ? 'Work finished with errors' : seconds ? `Worked for ${seconds}s` : 'Worked';
2152
+ return `<details class="codex-work-group"${running ? ' open' : ''}><summary>${label} · ${items.length} tools</summary><div class="codex-work-group-body">${tools}</div></details>`;
2089
2153
  }
2090
2154
  function renderCodexChat() {
2091
2155
  const container = document.getElementById('codex-chat-container');
@@ -2093,13 +2157,17 @@
2093
2157
  const parts = [];
2094
2158
  const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
2095
2159
  const usedPermissions = new Set();
2160
+ const turnEndById = new Map(codexMessages.filter(item => item.kind === 'turn-end' && item.turnId)
2161
+ .map(item => [String(item.turnId), item]));
2096
2162
  const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
2097
2163
  for (let i = 0; i < visible.length;) {
2098
2164
  const item = visible[i];
2099
2165
  if (item.kind === 'tool') {
2100
2166
  const tools = [];
2101
- while (i < visible.length && visible[i].kind === 'tool') tools.push(visible[i++]);
2102
- if (tools.length > 1) parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions));
2167
+ const turnId = item.turnId;
2168
+ while (i < visible.length && visible[i].kind === 'tool' && visible[i].turnId === turnId) tools.push(visible[i++]);
2169
+ if (tools.length > 1) parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions,
2170
+ turnEndById.get(String(turnId || ''))));
2103
2171
  else {
2104
2172
  const permission = permissionById.get(String(tools[0].providerId || ''));
2105
2173
  if (permission) usedPermissions.add(permission.id);
@@ -2109,27 +2177,47 @@
2109
2177
  }
2110
2178
  if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>`);
2111
2179
  else if (item.kind === 'user') parts.push(`<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>`);
2180
+ else if (item.kind === 'status') parts.push(renderCodexStatus(item));
2112
2181
  else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
2113
2182
  i += 1;
2114
2183
  }
2115
2184
  for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
2116
- container.innerHTML = `<div class="codex-conversation">${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
2185
+ const working = codexState.status === 'running'
2186
+ ? '<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"></div>' : '';
2187
+ container.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
2117
2188
  requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
2118
2189
  }
2119
2190
 
2120
2191
  function applyCodexState(state = {}) {
2192
+ const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
2121
2193
  codexState = { ...codexState, ...state };
2122
2194
  const permission = document.getElementById('codex-permission-select');
2123
- if (permission) permission.value = codexState.permissionMode || 'on-request';
2195
+ if (permission) {
2196
+ const defaultOption = permission.querySelector('option[value="default"]');
2197
+ const labels = { untrusted: 'Untrusted', 'on-request': 'On request', never: 'Never ask' };
2198
+ if (defaultOption) defaultOption.textContent = codexState.effectivePermissionMode
2199
+ ? `Default (${labels[codexState.effectivePermissionMode] || codexState.effectivePermissionMode})`
2200
+ : 'Default';
2201
+ permission.value = codexState.permissionMode || 'default';
2202
+ }
2203
+ const sandbox = document.getElementById('codex-sandbox-select');
2204
+ if (sandbox) {
2205
+ const defaultOption = sandbox.querySelector('option[value="default"]');
2206
+ const labels = { 'read-only': 'Read only', 'workspace-write': 'Workspace write', 'danger-full-access': 'Full access' };
2207
+ if (defaultOption) defaultOption.textContent = codexState.effectiveSandboxMode
2208
+ ? `Default (${labels[codexState.effectiveSandboxMode] || codexState.effectiveSandboxMode})`
2209
+ : 'Default';
2210
+ sandbox.value = codexState.sandboxMode || 'default';
2211
+ }
2124
2212
  const modelButton = document.getElementById('codex-model-btn');
2125
- if (modelButton) modelButton.textContent = codexState.model ? codexState.model.replace(/^gpt-/, '').slice(0, 10) : 'Model';
2213
+ if (modelButton) modelButton.textContent = 'Model';
2126
2214
  const abort = document.getElementById('codex-abort-btn');
2127
2215
  if (abort) abort.disabled = !codexState.canAbort;
2128
2216
  const terminal = document.getElementById('codex-terminal-switch');
2129
2217
  if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
2130
2218
  renderCodexStateBar();
2131
2219
  renderCodexModelPanel();
2132
- setClaudeModeEnabled(isClaudeSession());
2220
+ if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
2133
2221
  renderCodexChat();
2134
2222
  }
2135
2223
 
@@ -2139,7 +2227,6 @@
2139
2227
  const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
2140
2228
  const parts = [];
2141
2229
  if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
2142
- else if (codexState.status === 'running') parts.push('<span class="claude-state-pill">Working</span>');
2143
2230
  el.innerHTML = parts.join('');
2144
2231
  el.style.display = parts.length ? 'flex' : 'none';
2145
2232
  }
@@ -2185,8 +2272,14 @@
2185
2272
  }
2186
2273
 
2187
2274
  function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
2188
- function updateCodexSettingsFromControls() { const permissionMode = document.getElementById('codex-permission-select').value; applyCodexState({ permissionMode }); sendCodexSettings({ permissionMode }); }
2275
+ function updateCodexSettingsFromControls() {
2276
+ const permissionMode = document.getElementById('codex-permission-select').value;
2277
+ const sandboxMode = document.getElementById('codex-sandbox-select').value;
2278
+ applyCodexState({ permissionMode, sandboxMode });
2279
+ sendCodexSettings({ permissionMode, sandboxMode });
2280
+ }
2189
2281
  function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
2282
+ function requestCodexStatus() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-status' })); }
2190
2283
  function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
2191
2284
  async function toggleCodexResumePanel() {
2192
2285
  codexResumePanelOpen = !codexResumePanelOpen;
@@ -2345,7 +2438,7 @@
2345
2438
  codexResumePanelOpen = false;
2346
2439
  document.getElementById('codex-model-panel').classList.remove('active');
2347
2440
  document.getElementById('codex-resume-panel').classList.remove('active');
2348
- codexState = { permissionMode: 'on-request', model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
2441
+ codexState = { permissionMode: 'default', sandboxMode: 'default', effectivePermissionMode: null, effectiveSandboxMode: null, model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
2349
2442
  setClaudeModeEnabled(false);
2350
2443
  applyCodexState(codexState);
2351
2444
  const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.22",
3
+ "version": "1.0.24",
4
4
  "description": "Glad transforms terminal-based AI coding tools into a polished, mobile-friendly local Web interface.",
5
5
  "main": "index.js",
6
6
  "bin": {
@@ -52,9 +52,9 @@
52
52
  "conf": "^10.2.0",
53
53
  "express": "^5.2.1",
54
54
  "node-pty": "npm:@lydell/node-pty@^1.1.0",
55
- "uuid": "^9.0.0",
55
+ "uuid": "^11.1.1",
56
56
  "ws": "^8.19.0",
57
- "xterm-headless": "^5.3.0"
57
+ "@xterm/headless": "^6.0.0"
58
58
  },
59
59
  "devDependencies": {
60
60
  "caxa": "^3.0.1"