glad-web 1.0.23 → 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.
|
@@ -122,6 +122,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
122
122
|
this.threadId = options.resume || null;
|
|
123
123
|
this.currentTurnId = null;
|
|
124
124
|
this.currentTurnStartedAt = null;
|
|
125
|
+
this.tokenUsage = null;
|
|
125
126
|
this.permissionMode = normalizePermissionMode(options.permissionMode);
|
|
126
127
|
this.sandboxMode = normalizeSandboxMode(options.sandboxMode);
|
|
127
128
|
this.effectivePermissionMode = null;
|
|
@@ -129,6 +130,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
129
130
|
this.configPermissionMode = null;
|
|
130
131
|
this.configSandboxMode = null;
|
|
131
132
|
this.configSandboxWorkspaceWrite = {};
|
|
133
|
+
this.configModel = null;
|
|
134
|
+
this.configEffort = null;
|
|
135
|
+
this.hasModelOverride = Boolean(options.model);
|
|
136
|
+
this.hasEffortOverride = Boolean(options.effort);
|
|
132
137
|
this.model = options.model || null;
|
|
133
138
|
this.effort = options.effort || null;
|
|
134
139
|
this.models = [];
|
|
@@ -325,6 +330,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
325
330
|
}
|
|
326
331
|
|
|
327
332
|
handleNotification(method, params) {
|
|
333
|
+
if (method === 'thread/tokenUsage/updated') {
|
|
334
|
+
this.tokenUsage = params.tokenUsage || params.usage || params;
|
|
335
|
+
return;
|
|
336
|
+
}
|
|
328
337
|
if (method === 'turn/started') {
|
|
329
338
|
this.currentTurnId = params.turn?.id || params.turnId || this.currentTurnId;
|
|
330
339
|
this.currentTurnStartedAt = Date.now();
|
|
@@ -338,6 +347,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
338
347
|
: params.turn?.status === 'interrupted' ? 'cancelled' : 'completed';
|
|
339
348
|
this.append({ kind: 'turn-end', turnId: completedTurnId, status: turnStatus,
|
|
340
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
|
+
}
|
|
341
354
|
for (const pending of this.pendingPermissions.values()) {
|
|
342
355
|
this.recordPermission(pending.public, 'denied', 'abort');
|
|
343
356
|
}
|
|
@@ -401,10 +414,13 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
401
414
|
else this.append({ kind, providerId: itemId, text: delta, streaming: true });
|
|
402
415
|
return;
|
|
403
416
|
}
|
|
404
|
-
if (method.startsWith('item/'))
|
|
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
|
+
}
|
|
405
421
|
}
|
|
406
422
|
|
|
407
|
-
applyProviderItem(raw) {
|
|
423
|
+
applyProviderItem(raw, inferredStatus = null) {
|
|
408
424
|
if (!raw || typeof raw !== 'object') return;
|
|
409
425
|
const providerId = String(raw.id || '');
|
|
410
426
|
const existing = providerId && this.messages.find(item => item.providerId === providerId);
|
|
@@ -413,8 +429,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
413
429
|
if (!kind) return;
|
|
414
430
|
const text = kind === 'user' ? textFromInputItems(raw.content) : kind === 'assistant' ? String(raw.text || '')
|
|
415
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;
|
|
416
434
|
const patch = kind === 'tool' ? { ...toolDetails(raw), turnId: raw.turnId || this.currentTurnId,
|
|
417
|
-
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 };
|
|
418
436
|
if (existing) {
|
|
419
437
|
this.patch(existing.id, patch);
|
|
420
438
|
} else if (kind === 'user') {
|
|
@@ -432,11 +450,12 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
432
450
|
do {
|
|
433
451
|
const result = await this.request('model/list', { cursor, limit: 100, includeHidden: false });
|
|
434
452
|
for (const item of result?.data || []) models.push({ id: item.id || item.model, label: item.displayName || item.model || item.id,
|
|
435
|
-
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 });
|
|
436
455
|
cursor = result?.nextCursor || null;
|
|
437
456
|
} while (cursor);
|
|
438
457
|
this.models = models;
|
|
439
|
-
if (!this.model
|
|
458
|
+
if (!this.model) this.model = (models.find(item => item.isDefault) || models[0])?.id || null;
|
|
440
459
|
if (!this.effort) this.effort = models.find(item => item.id === this.model)?.defaultEffort || 'medium';
|
|
441
460
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
442
461
|
return models;
|
|
@@ -448,8 +467,12 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
448
467
|
this.configPermissionMode = config.approval_policy || null;
|
|
449
468
|
this.configSandboxMode = normalizeSandboxMode(config.sandbox_mode);
|
|
450
469
|
this.configSandboxWorkspaceWrite = config.sandbox_workspace_write || {};
|
|
470
|
+
this.configModel = config.model || null;
|
|
471
|
+
this.configEffort = config.model_reasoning_effort || null;
|
|
451
472
|
if (!this.permissionMode) this.effectivePermissionMode = this.configPermissionMode;
|
|
452
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;
|
|
453
476
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
454
477
|
return config;
|
|
455
478
|
}
|
|
@@ -474,11 +497,66 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
474
497
|
}));
|
|
475
498
|
}
|
|
476
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
|
+
|
|
477
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
|
+
}
|
|
478
550
|
if (settings.permissionMode !== undefined) this.permissionMode = normalizePermissionMode(settings.permissionMode);
|
|
479
551
|
if (settings.sandboxMode !== undefined) this.sandboxMode = normalizeSandboxMode(settings.sandboxMode);
|
|
480
|
-
if (settings.model !== undefined)
|
|
481
|
-
|
|
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
|
+
}
|
|
482
560
|
const needsConfigDefaults = (settings.permissionMode !== undefined && !this.permissionMode)
|
|
483
561
|
|| (settings.sandboxMode !== undefined && !this.sandboxMode);
|
|
484
562
|
if (needsConfigDefaults && this.presentation === 'structured') {
|
|
@@ -497,8 +575,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
497
575
|
this.sandboxMode ? {} : this.configSandboxWorkspaceWrite);
|
|
498
576
|
if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
|
|
499
577
|
}
|
|
500
|
-
if (settings.model !== undefined) params.model = this.model;
|
|
501
|
-
if (settings.effort !== undefined) params.effort = this.effort;
|
|
578
|
+
if (settings.model !== undefined) params.model = this.hasModelOverride ? this.model : null;
|
|
579
|
+
if (settings.effort !== undefined) params.effort = this.hasEffortOverride ? this.effort : null;
|
|
502
580
|
if (Object.keys(params).length > 1) await this.request('thread/settings/update', params);
|
|
503
581
|
}
|
|
504
582
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
@@ -512,7 +590,8 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
512
590
|
this.append({ kind: 'user', text: prompt });
|
|
513
591
|
await this.ensureProcess();
|
|
514
592
|
if (!this.threadId) {
|
|
515
|
-
const params = {
|
|
593
|
+
const params = { cwd: this.workingDir };
|
|
594
|
+
if (this.hasModelOverride) params.model = this.model;
|
|
516
595
|
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
517
596
|
if (this.sandboxMode) params.sandbox = this.sandboxMode;
|
|
518
597
|
const started = await this.request('thread/start', params);
|
|
@@ -524,8 +603,9 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
524
603
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
525
604
|
}
|
|
526
605
|
this.setStatus('running');
|
|
527
|
-
const params = { threadId: this.threadId, input: [{ type: 'text', text: prompt }], cwd: this.workingDir,
|
|
528
|
-
|
|
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;
|
|
529
609
|
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
530
610
|
const sandboxPolicy = sandboxPolicyFor(this.sandboxMode, this.workingDir);
|
|
531
611
|
if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
|
|
@@ -596,22 +676,36 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
596
676
|
const target = String(threadId || this.threadId || '').trim();
|
|
597
677
|
if (!target || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
598
678
|
await this.ensureProcess();
|
|
599
|
-
const params = { threadId: target,
|
|
679
|
+
const params = { threadId: target, cwd: this.workingDir };
|
|
600
680
|
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
601
681
|
if (this.sandboxMode) params.sandbox = this.sandboxMode;
|
|
602
682
|
const result = await this.request('thread/resume', params);
|
|
603
683
|
this.threadId = result.thread?.id || target;
|
|
604
|
-
this.
|
|
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;
|
|
605
688
|
this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
|
|
606
689
|
this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
|
|
607
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;
|
|
608
694
|
this.messages = [];
|
|
609
695
|
this.completedPermissions = [];
|
|
610
696
|
for (const turn of history?.thread?.turns || []) {
|
|
611
|
-
this.append({ kind: 'turn-start', turnId: turn.id });
|
|
612
|
-
for (const item of turn.items || []) this.applyProviderItem({ ...item, turnId: turn.id });
|
|
613
697
|
const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
|
|
614
|
-
|
|
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 } : {}) });
|
|
615
709
|
}
|
|
616
710
|
this.append({ kind: 'event', level: 'info', text: `Resumed Codex thread ${this.threadId}` });
|
|
617
711
|
this.emitEvent({ type: 'history-reset', messages: this.messages });
|
package/lib/commands/web.js
CHANGED
|
@@ -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
|
}
|
|
@@ -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;
|
package/lib/web/index.html
CHANGED
|
@@ -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,9 @@
|
|
|
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(
|
|
121
|
-
.codex-control-row > * { width: 100%; }
|
|
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; }
|
|
122
131
|
.codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
|
|
123
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; }
|
|
124
133
|
.codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
|
|
@@ -293,7 +302,7 @@
|
|
|
293
302
|
#nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
|
|
294
303
|
#input-row { padding-left: 10px; padding-right: 10px; }
|
|
295
304
|
#codex-control-panel { padding-left: 8px; padding-right: 8px; }
|
|
296
|
-
.codex-control-row { grid-template-columns: repeat(
|
|
305
|
+
.codex-control-row { grid-template-columns: repeat(6, minmax(0, 1fr)); }
|
|
297
306
|
.codex-control-row .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
|
|
298
307
|
.codex-select-control { height: 36px; }
|
|
299
308
|
#codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
|
|
@@ -425,6 +434,7 @@
|
|
|
425
434
|
</select>
|
|
426
435
|
</label>
|
|
427
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>
|
|
428
438
|
<button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
|
|
429
439
|
<button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
|
|
430
440
|
</div>
|
|
@@ -2078,6 +2088,39 @@
|
|
|
2078
2088
|
if (inline) return `<div class="codex-inline-permission">${content}</div>`;
|
|
2079
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>`;
|
|
2080
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
|
+
}
|
|
2081
2124
|
function renderCodexTool(item, permission = null) {
|
|
2082
2125
|
const status = codexToolStatus(item);
|
|
2083
2126
|
const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
|
|
@@ -2091,17 +2134,22 @@
|
|
|
2091
2134
|
const result = item.result || (item.name === 'McpTool' || item.name === 'Agent' ? codexJson(item.input) : '');
|
|
2092
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>`;
|
|
2093
2136
|
}
|
|
2094
|
-
function renderCodexToolGroup(items, permissionById, usedPermissions) {
|
|
2137
|
+
function renderCodexToolGroup(items, permissionById, usedPermissions, turnEnd = null) {
|
|
2095
2138
|
const running = items.some(item => codexToolStatus(item) === 'running');
|
|
2096
2139
|
const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
|
|
2097
2140
|
const startedAt = Math.min(...items.map(item => Number(item.createdAt || Date.now())));
|
|
2098
|
-
const
|
|
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;
|
|
2099
2146
|
const tools = items.map(item => {
|
|
2100
2147
|
const permission = permissionById.get(String(item.providerId || ''));
|
|
2101
2148
|
if (permission) usedPermissions.add(permission.id);
|
|
2102
2149
|
return renderCodexTool(item, permission);
|
|
2103
2150
|
}).join('');
|
|
2104
|
-
|
|
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>`;
|
|
2105
2153
|
}
|
|
2106
2154
|
function renderCodexChat() {
|
|
2107
2155
|
const container = document.getElementById('codex-chat-container');
|
|
@@ -2109,13 +2157,17 @@
|
|
|
2109
2157
|
const parts = [];
|
|
2110
2158
|
const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
|
|
2111
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]));
|
|
2112
2162
|
const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
|
|
2113
2163
|
for (let i = 0; i < visible.length;) {
|
|
2114
2164
|
const item = visible[i];
|
|
2115
2165
|
if (item.kind === 'tool') {
|
|
2116
2166
|
const tools = [];
|
|
2117
|
-
|
|
2118
|
-
|
|
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 || ''))));
|
|
2119
2171
|
else {
|
|
2120
2172
|
const permission = permissionById.get(String(tools[0].providerId || ''));
|
|
2121
2173
|
if (permission) usedPermissions.add(permission.id);
|
|
@@ -2125,14 +2177,14 @@
|
|
|
2125
2177
|
}
|
|
2126
2178
|
if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>`);
|
|
2127
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));
|
|
2128
2181
|
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
2129
2182
|
i += 1;
|
|
2130
2183
|
}
|
|
2131
2184
|
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
2132
|
-
|
|
2133
|
-
|
|
2134
|
-
}
|
|
2135
|
-
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>`;
|
|
2136
2188
|
requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
|
|
2137
2189
|
}
|
|
2138
2190
|
|
|
@@ -2175,7 +2227,6 @@
|
|
|
2175
2227
|
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
2176
2228
|
const parts = [];
|
|
2177
2229
|
if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
|
|
2178
|
-
else if (codexState.status === 'running') parts.push('<span class="claude-state-pill">Working</span>');
|
|
2179
2230
|
el.innerHTML = parts.join('');
|
|
2180
2231
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
2181
2232
|
}
|
|
@@ -2228,6 +2279,7 @@
|
|
|
2228
2279
|
sendCodexSettings({ permissionMode, sandboxMode });
|
|
2229
2280
|
}
|
|
2230
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' })); }
|
|
2231
2283
|
function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
|
|
2232
2284
|
async function toggleCodexResumePanel() {
|
|
2233
2285
|
codexResumePanelOpen = !codexResumePanelOpen;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "glad-web",
|
|
3
|
-
"version": "1.0.
|
|
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": "^
|
|
55
|
+
"uuid": "^11.1.1",
|
|
56
56
|
"ws": "^8.19.0",
|
|
57
|
-
"xterm
|
|
57
|
+
"@xterm/headless": "^6.0.0"
|
|
58
58
|
},
|
|
59
59
|
"devDependencies": {
|
|
60
60
|
"caxa": "^3.0.1"
|