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 || '
|
|
12
|
-
return PERMISSION_MODES.has(mode) ? mode :
|
|
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,
|
|
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.
|
|
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/'))
|
|
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
|
|
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.
|
|
431
|
-
if (settings.
|
|
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
|
-
|
|
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
|
|
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
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
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
|
|
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.
|
|
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
|
-
|
|
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 });
|
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,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(
|
|
121
|
-
.codex-
|
|
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:
|
|
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:
|
|
291
|
-
.codex-control-row {
|
|
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
|
-
<
|
|
409
|
-
<
|
|
410
|
-
|
|
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: '
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
2102
|
-
|
|
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
|
-
|
|
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)
|
|
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 =
|
|
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() {
|
|
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: '
|
|
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.
|
|
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"
|