glad-web 1.0.26 → 1.0.28
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/lib/codex/structured-session.js +82 -39
- package/lib/commands/web.js +8 -0
- package/lib/session/session-manager.js +20 -0
- package/lib/web/index.html +103 -39
- package/package.json +1 -1
|
@@ -43,6 +43,12 @@ function safeJson(value) {
|
|
|
43
43
|
try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
|
|
44
44
|
}
|
|
45
45
|
|
|
46
|
+
function toTimestampMs(value) {
|
|
47
|
+
const timestamp = Number(value || 0);
|
|
48
|
+
if (!timestamp) return null;
|
|
49
|
+
return timestamp < 100000000000 ? timestamp * 1000 : timestamp;
|
|
50
|
+
}
|
|
51
|
+
|
|
46
52
|
function appServerSpawnOptions({ cwd, env, platform = process.platform }) {
|
|
47
53
|
const options = { cwd, env, stdio: ['pipe', 'pipe', 'pipe'] };
|
|
48
54
|
|
|
@@ -247,7 +253,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
247
253
|
patch(id, patch) {
|
|
248
254
|
const item = this.messages.find(message => message.id === id);
|
|
249
255
|
if (!item) return null;
|
|
250
|
-
Object.assign(item, patch);
|
|
256
|
+
Object.assign(item, { updatedAt: Date.now() }, patch);
|
|
251
257
|
this.emitEvent({ type: 'message-updated', message: item });
|
|
252
258
|
return item;
|
|
253
259
|
}
|
|
@@ -514,8 +520,10 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
514
520
|
const trackedTurnId = threadId ? this.threadTurns.get(threadId)?.turnId : null;
|
|
515
521
|
const turnId = raw.turnId || context.turnId || trackedTurnId || this.currentTurnId;
|
|
516
522
|
const existingStartedAtMs = existing?.startedAtMs || existing?.createdAt || null;
|
|
517
|
-
const startedAtMs = context.startedAtMs || raw.startedAtMs ||
|
|
518
|
-
|
|
523
|
+
const startedAtMs = Number(context.startedAtMs || raw.startedAtMs || 0)
|
|
524
|
+
|| toTimestampMs(raw.startedAt || raw.createdAt) || existingStartedAtMs;
|
|
525
|
+
const completedAtMs = Number(context.completedAtMs || raw.completedAtMs || 0)
|
|
526
|
+
|| toTimestampMs(raw.completedAt || raw.updatedAt);
|
|
519
527
|
const durationMs = Number(raw.durationMs || 0)
|
|
520
528
|
|| (completedAtMs && startedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null)
|
|
521
529
|
|| Number(existing?.durationMs || 0) || null;
|
|
@@ -526,13 +534,13 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
526
534
|
};
|
|
527
535
|
const patch = kind === 'tool' ? { ...toolDetails(raw), threadId, turnId,
|
|
528
536
|
...timing, toolStatus: inferredToolStatus || raw.status || 'running' }
|
|
529
|
-
: { text, threadId, turnId, streaming: false };
|
|
537
|
+
: { text, threadId, turnId, streaming: false, ...(completedAtMs ? { completedAtMs } : {}) };
|
|
530
538
|
if (existing) {
|
|
531
539
|
this.patch(existing.id, patch);
|
|
532
540
|
} else if (kind === 'user') {
|
|
533
541
|
const local = [...this.messages].reverse().find(item => item.kind === 'user' && !item.providerId && item.text === text);
|
|
534
542
|
if (local) this.patch(local.id, { providerId, ...patch });
|
|
535
|
-
else this.append({ kind, providerId, ...patch });
|
|
543
|
+
else this.append({ kind, providerId, ...(startedAtMs ? { createdAt: startedAtMs } : {}), ...patch });
|
|
536
544
|
} else {
|
|
537
545
|
this.append({ kind, providerId, ...(startedAtMs ? { createdAt: startedAtMs } : {}), ...patch });
|
|
538
546
|
}
|
|
@@ -702,33 +710,41 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
702
710
|
text: prompt || '📷 Image attachment',
|
|
703
711
|
attachments: images.map(item => ({ id: item.id, name: item.name || 'image' }))
|
|
704
712
|
});
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
713
|
+
try {
|
|
714
|
+
await this.ensureProcess();
|
|
715
|
+
if (!this.threadId) {
|
|
716
|
+
const params = { cwd: this.workingDir };
|
|
717
|
+
if (this.hasModelOverride) params.model = this.model;
|
|
718
|
+
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
719
|
+
if (this.sandboxMode) params.sandbox = this.sandboxMode;
|
|
720
|
+
const started = await this.request('thread/start', params);
|
|
721
|
+
this.threadId = started.thread?.id;
|
|
722
|
+
this.model = started.model || this.model;
|
|
723
|
+
this.effort = started.reasoningEffort || this.effort;
|
|
724
|
+
this.effectivePermissionMode = started.approvalPolicy || this.effectivePermissionMode;
|
|
725
|
+
this.effectiveSandboxMode = sandboxModeFromPolicy(started.sandbox) || this.effectiveSandboxMode;
|
|
726
|
+
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
727
|
+
}
|
|
728
|
+
this.setStatus('running');
|
|
729
|
+
const input = [];
|
|
730
|
+
if (prompt) input.push({ type: 'text', text: prompt });
|
|
731
|
+
for (const image of images) input.push({ type: 'localImage', path: image.path });
|
|
732
|
+
const params = { threadId: this.threadId, input, cwd: this.workingDir, summary: 'auto' };
|
|
708
733
|
if (this.hasModelOverride) params.model = this.model;
|
|
734
|
+
if (this.hasEffortOverride) params.effort = this.effort;
|
|
709
735
|
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
this.
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
this.
|
|
717
|
-
this.
|
|
736
|
+
const sandboxPolicy = sandboxPolicyFor(this.sandboxMode, this.workingDir);
|
|
737
|
+
if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
|
|
738
|
+
const started = await this.request('turn/start', params);
|
|
739
|
+
this.currentTurnId = started?.turn?.id || started?.turnId || this.currentTurnId;
|
|
740
|
+
return true;
|
|
741
|
+
} catch (error) {
|
|
742
|
+
this.currentTurnId = null;
|
|
743
|
+
this.currentTurnStartedAt = null;
|
|
744
|
+
this.setStatus('idle');
|
|
745
|
+
this.append({ kind: 'event', level: 'error', text: `Unable to send message: ${error.message}` });
|
|
746
|
+
throw error;
|
|
718
747
|
}
|
|
719
|
-
this.setStatus('running');
|
|
720
|
-
const input = [];
|
|
721
|
-
if (prompt) input.push({ type: 'text', text: prompt });
|
|
722
|
-
for (const image of images) input.push({ type: 'localImage', path: image.path });
|
|
723
|
-
const params = { threadId: this.threadId, input, cwd: this.workingDir, summary: 'auto' };
|
|
724
|
-
if (this.hasModelOverride) params.model = this.model;
|
|
725
|
-
if (this.hasEffortOverride) params.effort = this.effort;
|
|
726
|
-
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
727
|
-
const sandboxPolicy = sandboxPolicyFor(this.sandboxMode, this.workingDir);
|
|
728
|
-
if (sandboxPolicy) params.sandboxPolicy = sandboxPolicy;
|
|
729
|
-
const started = await this.request('turn/start', params);
|
|
730
|
-
this.currentTurnId = started?.turn?.id || started?.turnId || this.currentTurnId;
|
|
731
|
-
return true;
|
|
732
748
|
}
|
|
733
749
|
|
|
734
750
|
write(data) {
|
|
@@ -736,9 +752,7 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
736
752
|
const text = String(data || '').replace(/\r/g, '\n');
|
|
737
753
|
const prompt = text.trim();
|
|
738
754
|
if (prompt) void this.sendUserMessage(prompt).catch(error => {
|
|
739
|
-
this.
|
|
740
|
-
this.setStatus('idle');
|
|
741
|
-
this.append({ kind: 'event', level: 'error', text: error.message });
|
|
755
|
+
this.logger.debugInfo?.(`[codex-app-server] send failed: ${error.message}`);
|
|
742
756
|
});
|
|
743
757
|
return true;
|
|
744
758
|
}
|
|
@@ -812,12 +826,17 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
812
826
|
this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
|
|
813
827
|
this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
|
|
814
828
|
const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
|
|
815
|
-
this.
|
|
816
|
-
|
|
817
|
-
|
|
829
|
+
this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`);
|
|
830
|
+
return true;
|
|
831
|
+
}
|
|
832
|
+
|
|
833
|
+
restoreThreadHistory(thread, eventText = '') {
|
|
834
|
+
this.model = thread?.model || this.model;
|
|
835
|
+
this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
|
|
836
|
+
this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
|
|
818
837
|
this.messages = [];
|
|
819
838
|
this.completedPermissions = [];
|
|
820
|
-
for (const turn of
|
|
839
|
+
for (const turn of thread?.turns || []) {
|
|
821
840
|
const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
|
|
822
841
|
const startedAt = Number(turn.startedAt || turn.createdAt || 0);
|
|
823
842
|
const completedAt = Number(turn.completedAt || turn.updatedAt || 0);
|
|
@@ -825,16 +844,40 @@ class CodexStructuredSession extends EventEmitter {
|
|
|
825
844
|
const startedAtMs = toMilliseconds(startedAt);
|
|
826
845
|
const completedAtMs = toMilliseconds(completedAt);
|
|
827
846
|
this.append({ kind: 'turn-start', turnId: turn.id, ...(startedAtMs ? { createdAt: startedAtMs } : {}) });
|
|
828
|
-
for (const item of turn.items || [])
|
|
847
|
+
for (const item of turn.items || []) {
|
|
848
|
+
this.applyProviderItem({ ...item, turnId: turn.id }, status === 'failed' ? 'failed' : 'completed', {
|
|
849
|
+
startedAtMs,
|
|
850
|
+
completedAtMs
|
|
851
|
+
});
|
|
852
|
+
}
|
|
829
853
|
const durationMs = Number(turn.durationMs || turn.duration_ms || 0)
|
|
830
854
|
|| (startedAtMs && completedAtMs && completedAtMs >= startedAtMs ? completedAtMs - startedAtMs : null);
|
|
831
855
|
this.append({ kind: 'turn-end', turnId: turn.id, status, durationMs,
|
|
832
856
|
...(completedAtMs ? { createdAt: completedAtMs } : {}) });
|
|
833
857
|
}
|
|
834
|
-
this.append({ kind: 'event', level: 'info', text:
|
|
858
|
+
if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
|
|
835
859
|
this.emitEvent({ type: 'history-reset', messages: this.messages });
|
|
836
860
|
this.emitEvent({ type: 'state', state: this.getControlState() });
|
|
837
|
-
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
async forkFrom(threadId) {
|
|
864
|
+
const sourceThreadId = String(threadId || '').trim();
|
|
865
|
+
if (!sourceThreadId || this.presentation !== 'structured' || this.status !== 'idle') return false;
|
|
866
|
+
await this.ensureProcess();
|
|
867
|
+
const params = { threadId: sourceThreadId, cwd: this.workingDir, ephemeral: false, threadSource: null };
|
|
868
|
+
if (this.hasModelOverride) params.model = this.model;
|
|
869
|
+
if (this.permissionMode) params.approvalPolicy = this.permissionMode;
|
|
870
|
+
if (this.sandboxMode) params.sandbox = this.sandboxMode;
|
|
871
|
+
const result = await this.request('thread/fork', params);
|
|
872
|
+
const forkedThread = result?.thread;
|
|
873
|
+
if (!forkedThread?.id) throw new Error('Codex did not return a forked thread');
|
|
874
|
+
this.threadId = forkedThread.id;
|
|
875
|
+
this.model = result.model || forkedThread.model || this.model;
|
|
876
|
+
this.effort = result.reasoningEffort || forkedThread.reasoningEffort || forkedThread.reasoning_effort || this.effort;
|
|
877
|
+
this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
|
|
878
|
+
this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
|
|
879
|
+
this.restoreThreadHistory(forkedThread, `Forked from Codex thread ${sourceThreadId}`);
|
|
880
|
+
return { threadId: this.threadId };
|
|
838
881
|
}
|
|
839
882
|
|
|
840
883
|
async switchToTerminal() {
|
package/lib/commands/web.js
CHANGED
|
@@ -379,6 +379,14 @@ async function webCommand(options) {
|
|
|
379
379
|
} catch (e) { res.status(400).json({ error: e.message }); }
|
|
380
380
|
});
|
|
381
381
|
|
|
382
|
+
app.post('/api/sessions/:id/codex-fork', async (req, res) => {
|
|
383
|
+
try {
|
|
384
|
+
const session = await sessionManager.forkCodex(req.params.id, req.body && req.body.threadId);
|
|
385
|
+
if (!session) return res.status(404).json({ error: 'Codex session not found' });
|
|
386
|
+
res.json({ success: true, id: session.id, name: session.name, threadId: session.threadId });
|
|
387
|
+
} catch (e) { res.status(e.statusCode || 400).json({ error: e.message }); }
|
|
388
|
+
});
|
|
389
|
+
|
|
382
390
|
app.post('/api/sessions/:id/codex-presentation', async (req, res) => {
|
|
383
391
|
const presentation = req.body && req.body.presentation;
|
|
384
392
|
if (!['terminal', 'structured'].includes(presentation)) return res.status(400).json({ error: 'Invalid presentation' });
|
|
@@ -438,6 +438,26 @@ class SessionManager extends EventEmitter {
|
|
|
438
438
|
return session && session.kind === 'codex-structured' ? session.resume(threadId) : false;
|
|
439
439
|
}
|
|
440
440
|
|
|
441
|
+
async forkCodex(id, threadId) {
|
|
442
|
+
const session = this.get(id);
|
|
443
|
+
if (!session || session.kind !== 'codex-structured') return null;
|
|
444
|
+
if (session.presentation !== 'structured' || session.status !== 'idle') {
|
|
445
|
+
const error = new Error('Codex must be idle in chat mode before forking');
|
|
446
|
+
error.statusCode = 409;
|
|
447
|
+
throw error;
|
|
448
|
+
}
|
|
449
|
+
const sourceThreadId = String(threadId || session.threadId || '').trim();
|
|
450
|
+
if (!sourceThreadId) {
|
|
451
|
+
const error = new Error('Choose a Codex thread to fork');
|
|
452
|
+
error.statusCode = 400;
|
|
453
|
+
throw error;
|
|
454
|
+
}
|
|
455
|
+
const result = await session.forkFrom(sourceThreadId);
|
|
456
|
+
if (!result) throw new Error('Unable to fork the selected Codex thread');
|
|
457
|
+
session.forkedFromThreadId = sourceThreadId;
|
|
458
|
+
return session;
|
|
459
|
+
}
|
|
460
|
+
|
|
441
461
|
listCodexResumeThreads(id) {
|
|
442
462
|
const session = this.get(id);
|
|
443
463
|
if (!session || session.kind !== 'codex-structured') return null;
|
package/lib/web/index.html
CHANGED
|
@@ -99,9 +99,13 @@
|
|
|
99
99
|
.codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
|
|
100
100
|
.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; }
|
|
101
101
|
.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; }
|
|
102
|
-
.codex-message { max-width: 100%; margin: 0 0 12px;
|
|
103
|
-
.codex-message.user { max-width: 92%; margin-left: auto;
|
|
102
|
+
.codex-message-block { max-width: 100%; margin: 0 0 12px; }
|
|
103
|
+
.codex-message-block.user { max-width: 92%; margin-left: auto; }
|
|
104
|
+
.codex-message { max-width: 100%; margin: 0; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
|
|
105
|
+
.codex-message.user { padding: 8px 12px; border: 1px solid rgba(0,122,255,.32); border-radius: 12px; background: rgba(0,122,255,.24); }
|
|
104
106
|
.codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
|
|
107
|
+
.codex-message-time { display: block; width: max-content; margin-top: 4px; padding: 0 2px; color: #8e8e93; font-size: 10px; font-weight: 500; line-height: 1; font-variant-numeric: tabular-nums; }
|
|
108
|
+
.codex-message-block.user .codex-message-time { margin-left: auto; }
|
|
105
109
|
.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; }
|
|
106
110
|
.codex-status-title { margin-bottom: 9px; color: #64d2ff; font-size: 12px; font-weight: 800; text-transform: uppercase; letter-spacing: .04em; }
|
|
107
111
|
.codex-status-grid { display: grid; grid-template-columns: repeat(2, minmax(0, 1fr)); gap: 7px; }
|
|
@@ -143,16 +147,18 @@
|
|
|
143
147
|
.codex-inline-permission .claude-permission-actions { margin-top: 8px; }
|
|
144
148
|
@keyframes codex-spin { to { transform: rotate(360deg); } }
|
|
145
149
|
#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; }
|
|
146
|
-
.codex-control-
|
|
147
|
-
.codex-control-
|
|
148
|
-
.codex-control-
|
|
150
|
+
.codex-control-rail { display: flex; gap: 14px; overflow-x: auto; overscroll-behavior-x: contain; scroll-snap-type: x mandatory; scrollbar-width: none; -webkit-overflow-scrolling: touch; }
|
|
151
|
+
.codex-control-rail::-webkit-scrollbar { display: none; }
|
|
152
|
+
.codex-control-page { flex: 0 0 100%; display: grid; grid-template-columns: repeat(5, minmax(0, 116px)); grid-template-rows: auto; justify-content: center; gap: clamp(2px, .8vw, 7px); align-items: center; white-space: nowrap; scroll-snap-align: start; box-sizing: border-box; }
|
|
153
|
+
.codex-control-page > * { width: 100%; min-width: 0; overflow: hidden; }
|
|
154
|
+
.codex-control-page .claude-ctrl-btn, .codex-control-page .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; }
|
|
149
155
|
.codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
|
|
150
156
|
.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; }
|
|
151
157
|
.codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
|
|
152
158
|
.codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
|
|
153
159
|
.codex-select option { background: #1c1c1e; color: #fff; }
|
|
154
|
-
#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; }
|
|
155
|
-
#codex-model-panel.active, #codex-resume-panel.active { display: block; }
|
|
160
|
+
#codex-model-panel, #codex-resume-panel, #codex-fork-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; }
|
|
161
|
+
#codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active { display: block; }
|
|
156
162
|
#codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
|
|
157
163
|
#codex-model-panel.active { display: grid; }
|
|
158
164
|
.codex-picker-column { min-width: 0; overflow-y: auto; }
|
|
@@ -161,7 +167,7 @@
|
|
|
161
167
|
.codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
|
|
162
168
|
#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; }
|
|
163
169
|
#codex-state-bar::-webkit-scrollbar { display: none; }
|
|
164
|
-
#codex-resume-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
|
|
170
|
+
#codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
|
|
165
171
|
.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); }
|
|
166
172
|
.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; }
|
|
167
173
|
.claude-message.user { margin-left: auto; background: rgba(0,122,255,0.24); border: 1px solid rgba(0,122,255,0.32); color: #fff; border-radius: 12px; padding-top: 7px; padding-bottom: 7px; }
|
|
@@ -321,11 +327,11 @@
|
|
|
321
327
|
#nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
|
|
322
328
|
#input-row { padding-left: 10px; padding-right: 10px; }
|
|
323
329
|
#codex-control-panel { padding-left: 8px; padding-right: 8px; }
|
|
324
|
-
.codex-control-
|
|
325
|
-
.codex-control-
|
|
330
|
+
.codex-control-page { grid-template-columns: repeat(5, minmax(0, 1fr)); }
|
|
331
|
+
.codex-control-page .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
|
|
326
332
|
.codex-select-control { height: 36px; }
|
|
327
333
|
#codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
|
|
328
|
-
.codex-message.user { max-width: 94%; }
|
|
334
|
+
.codex-message-block.user { max-width: 94%; }
|
|
329
335
|
.claude-permission-actions { justify-content: flex-start; }
|
|
330
336
|
}
|
|
331
337
|
@media (max-width: 430px) {
|
|
@@ -443,27 +449,33 @@
|
|
|
443
449
|
<div id="claude-resume-panel"></div>
|
|
444
450
|
</div>
|
|
445
451
|
<div id="codex-control-panel">
|
|
446
|
-
<div class="codex-control-
|
|
447
|
-
<
|
|
448
|
-
<
|
|
449
|
-
<
|
|
450
|
-
|
|
451
|
-
</
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
|
|
455
|
-
<
|
|
456
|
-
<
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
452
|
+
<div id="codex-control-rail" class="codex-control-rail">
|
|
453
|
+
<div class="codex-control-page">
|
|
454
|
+
<button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
|
|
455
|
+
<button id="codex-status-btn" class="claude-ctrl-btn" onclick="requestCodexStatus()" title="Show Codex account, limits, and context status">Status</button>
|
|
456
|
+
<button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
|
|
457
|
+
<button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
|
|
458
|
+
<button id="codex-fork-btn" class="claude-ctrl-btn primary" onclick="toggleCodexForkPanel()" title="Fork a Codex thread in this conversation">Fork</button>
|
|
459
|
+
</div>
|
|
460
|
+
<div class="codex-control-page">
|
|
461
|
+
<label class="codex-select-control" title="Sandbox mode">
|
|
462
|
+
<span class="codex-select-label" aria-hidden="true">Sandbox</span>
|
|
463
|
+
<select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
|
|
464
|
+
<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>
|
|
465
|
+
</select>
|
|
466
|
+
</label>
|
|
467
|
+
<label class="codex-select-control" title="Approval policy">
|
|
468
|
+
<span class="codex-select-label" aria-hidden="true">Ask</span>
|
|
469
|
+
<select id="codex-permission-select" class="codex-select" aria-label="Approval policy" onchange="updateCodexSettingsFromControls()">
|
|
470
|
+
<option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
|
|
471
|
+
</select>
|
|
472
|
+
</label>
|
|
473
|
+
</div>
|
|
463
474
|
</div>
|
|
464
475
|
<div id="codex-state-bar"></div>
|
|
465
476
|
<div id="codex-model-panel"></div>
|
|
466
477
|
<div id="codex-resume-panel"></div>
|
|
478
|
+
<div id="codex-fork-panel"></div>
|
|
467
479
|
</div>
|
|
468
480
|
<div id="timed-send-panel">
|
|
469
481
|
<div class="timed-row">
|
|
@@ -660,6 +672,7 @@
|
|
|
660
672
|
let codexModelPanelOpen = false;
|
|
661
673
|
let codexModelCandidate = null;
|
|
662
674
|
let codexResumePanelOpen = false;
|
|
675
|
+
let codexForkPanelOpen = false;
|
|
663
676
|
let codexRenderFrame = null;
|
|
664
677
|
const modifiers = { ctrl: false };
|
|
665
678
|
|
|
@@ -2188,7 +2201,16 @@
|
|
|
2188
2201
|
}).join('');
|
|
2189
2202
|
const label = running ? 'Working' : failed ? 'Work finished with errors' : duration ? `Worked for ${duration}` : 'Worked';
|
|
2190
2203
|
const key = items.map(item => item.id || item.providerId || '').join('-');
|
|
2191
|
-
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"
|
|
2204
|
+
return `<details class="codex-work-group" data-codex-key="group-${escapeHtml(key)}"><summary>${label} · ${items.length} ${items.length === 1 ? 'tool' : 'tools'}</summary><div class="codex-work-group-body">${tools}</div></details>`;
|
|
2205
|
+
}
|
|
2206
|
+
function renderCodexMessageTime(item, finalOnly = false) {
|
|
2207
|
+
if (!item || (finalOnly && item.streaming)) return '';
|
|
2208
|
+
const timestamp = Number(finalOnly ? (item.completedAtMs || item.updatedAt || item.createdAt) : item.createdAt);
|
|
2209
|
+
if (!Number.isFinite(timestamp) || timestamp <= 0) return '';
|
|
2210
|
+
const date = new Date(timestamp);
|
|
2211
|
+
if (Number.isNaN(date.getTime())) return '';
|
|
2212
|
+
const label = date.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
|
|
2213
|
+
return `<time class="codex-message-time" datetime="${escapeHtml(date.toISOString())}" title="${escapeHtml(date.toLocaleString())}">${escapeHtml(label)}</time>`;
|
|
2192
2214
|
}
|
|
2193
2215
|
function syncCodexDom(current, next) {
|
|
2194
2216
|
if (!current || !next) return;
|
|
@@ -2234,6 +2256,10 @@
|
|
|
2234
2256
|
function commitCodexChatRender() {
|
|
2235
2257
|
const container = document.getElementById('codex-chat-container');
|
|
2236
2258
|
if (!container) return;
|
|
2259
|
+
const wasEmpty = !container.firstElementChild;
|
|
2260
|
+
const previousScrollTop = container.scrollTop;
|
|
2261
|
+
const distanceFromBottom = container.scrollHeight - container.clientHeight - previousScrollTop;
|
|
2262
|
+
const shouldStickToBottom = wasEmpty || distanceFromBottom <= 64;
|
|
2237
2263
|
const parts = [];
|
|
2238
2264
|
const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
|
|
2239
2265
|
const usedPermissions = new Set();
|
|
@@ -2246,17 +2272,12 @@
|
|
|
2246
2272
|
const tools = [];
|
|
2247
2273
|
const turnId = item.turnId;
|
|
2248
2274
|
while (i < visible.length && visible[i].kind === 'tool' && visible[i].turnId === turnId) tools.push(visible[i++]);
|
|
2249
|
-
|
|
2275
|
+
parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions,
|
|
2250
2276
|
turnEndById.get(String(turnId || ''))));
|
|
2251
|
-
else {
|
|
2252
|
-
const permission = permissionById.get(String(tools[0].providerId || ''));
|
|
2253
|
-
if (permission) usedPermissions.add(permission.id);
|
|
2254
|
-
parts.push(renderCodexTool(tools[0], permission));
|
|
2255
|
-
}
|
|
2256
2277
|
continue;
|
|
2257
2278
|
}
|
|
2258
|
-
if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant
|
|
2259
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message user
|
|
2279
|
+
if (item.kind === 'assistant') parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageTime(item, true)}</div>`);
|
|
2280
|
+
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageTime(item)}</div>`);
|
|
2260
2281
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
2261
2282
|
else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
|
|
2262
2283
|
i += 1;
|
|
@@ -2269,7 +2290,7 @@
|
|
|
2269
2290
|
const current = container.firstElementChild;
|
|
2270
2291
|
if (!current) container.appendChild(next);
|
|
2271
2292
|
else syncCodexDom(current, next);
|
|
2272
|
-
|
|
2293
|
+
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
2273
2294
|
}
|
|
2274
2295
|
|
|
2275
2296
|
function applyCodexState(state = {}) {
|
|
@@ -2297,6 +2318,8 @@
|
|
|
2297
2318
|
if (modelButton) modelButton.textContent = 'Model';
|
|
2298
2319
|
const abort = document.getElementById('codex-abort-btn');
|
|
2299
2320
|
if (abort) abort.disabled = !codexState.canAbort;
|
|
2321
|
+
const fork = document.getElementById('codex-fork-btn');
|
|
2322
|
+
if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle');
|
|
2300
2323
|
const terminal = document.getElementById('codex-terminal-switch');
|
|
2301
2324
|
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'; }
|
|
2302
2325
|
renderCodexStateBar();
|
|
@@ -2320,8 +2343,10 @@
|
|
|
2320
2343
|
function toggleCodexModelPanel() {
|
|
2321
2344
|
codexModelPanelOpen = !codexModelPanelOpen;
|
|
2322
2345
|
codexResumePanelOpen = false;
|
|
2346
|
+
codexForkPanelOpen = false;
|
|
2323
2347
|
codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
|
|
2324
2348
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2349
|
+
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2325
2350
|
renderCodexModelPanel();
|
|
2326
2351
|
updateTerminalControlsHeight();
|
|
2327
2352
|
}
|
|
@@ -2370,11 +2395,29 @@
|
|
|
2370
2395
|
async function toggleCodexResumePanel() {
|
|
2371
2396
|
codexResumePanelOpen = !codexResumePanelOpen;
|
|
2372
2397
|
codexModelPanelOpen = false;
|
|
2398
|
+
codexForkPanelOpen = false;
|
|
2373
2399
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2400
|
+
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2374
2401
|
const panel = document.getElementById('codex-resume-panel');
|
|
2375
2402
|
panel.classList.toggle('active', codexResumePanelOpen);
|
|
2376
2403
|
updateTerminalControlsHeight();
|
|
2377
2404
|
if (!codexResumePanelOpen) return;
|
|
2405
|
+
await loadCodexThreadPanel(panel, 'resume');
|
|
2406
|
+
}
|
|
2407
|
+
async function toggleCodexForkPanel() {
|
|
2408
|
+
if (!(codexState.presentation === 'structured' && codexState.status === 'idle')) return;
|
|
2409
|
+
codexForkPanelOpen = !codexForkPanelOpen;
|
|
2410
|
+
codexModelPanelOpen = false;
|
|
2411
|
+
codexResumePanelOpen = false;
|
|
2412
|
+
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2413
|
+
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2414
|
+
const panel = document.getElementById('codex-fork-panel');
|
|
2415
|
+
panel.classList.toggle('active', codexForkPanelOpen);
|
|
2416
|
+
updateTerminalControlsHeight();
|
|
2417
|
+
if (!codexForkPanelOpen) return;
|
|
2418
|
+
await loadCodexThreadPanel(panel, 'fork');
|
|
2419
|
+
}
|
|
2420
|
+
async function loadCodexThreadPanel(panel, action) {
|
|
2378
2421
|
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
|
|
2379
2422
|
try {
|
|
2380
2423
|
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
|
|
@@ -2383,9 +2426,11 @@
|
|
|
2383
2426
|
const items = data.items || [];
|
|
2384
2427
|
panel.innerHTML = items.length ? items.map(item => {
|
|
2385
2428
|
const questions = Array.isArray(item.questions) ? item.questions : [];
|
|
2386
|
-
|
|
2429
|
+
const handler = action === 'fork' ? 'selectCodexForkThread' : 'selectCodexResumeThread';
|
|
2430
|
+
return `<button class="claude-resume-item" onclick="${handler}(decodePathValue('${encodePathValue(item.id)}'))"><div class="claude-resume-title"><span>${escapeHtml((questions[0] || 'Codex session').slice(0, 120))}${item.current ? ' · current' : ''}</span><span>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '')}</span></div><div class="codex-resume-question-secondary">${escapeHtml((questions[1] || '').slice(0, 120))}</div></button>`;
|
|
2387
2431
|
}).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
|
|
2388
2432
|
} catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
|
|
2433
|
+
updateTerminalControlsHeight();
|
|
2389
2434
|
}
|
|
2390
2435
|
async function selectCodexResumeThread(threadId) {
|
|
2391
2436
|
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
|
|
@@ -2394,6 +2439,22 @@
|
|
|
2394
2439
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2395
2440
|
updateTerminalControlsHeight();
|
|
2396
2441
|
}
|
|
2442
|
+
async function selectCodexForkThread(threadId) {
|
|
2443
|
+
const panel = document.getElementById('codex-fork-panel');
|
|
2444
|
+
panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Forking and switching this conversation...</div>';
|
|
2445
|
+
updateTerminalControlsHeight();
|
|
2446
|
+
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-fork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 60000);
|
|
2447
|
+
const data = await res.json();
|
|
2448
|
+
if (!res.ok || !data.success) {
|
|
2449
|
+
panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(data.error || 'Unable to fork Codex thread')}</div>`;
|
|
2450
|
+
updateTerminalControlsHeight();
|
|
2451
|
+
return;
|
|
2452
|
+
}
|
|
2453
|
+
codexForkPanelOpen = false;
|
|
2454
|
+
panel.classList.remove('active');
|
|
2455
|
+
panel.innerHTML = '';
|
|
2456
|
+
updateTerminalControlsHeight();
|
|
2457
|
+
}
|
|
2397
2458
|
async function toggleCodexPresentation() {
|
|
2398
2459
|
const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
|
|
2399
2460
|
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
|
|
@@ -2528,8 +2589,11 @@
|
|
|
2528
2589
|
codexModelPanelOpen = false;
|
|
2529
2590
|
codexModelCandidate = null;
|
|
2530
2591
|
codexResumePanelOpen = false;
|
|
2592
|
+
codexForkPanelOpen = false;
|
|
2531
2593
|
document.getElementById('codex-model-panel').classList.remove('active');
|
|
2532
2594
|
document.getElementById('codex-resume-panel').classList.remove('active');
|
|
2595
|
+
document.getElementById('codex-fork-panel').classList.remove('active');
|
|
2596
|
+
document.getElementById('codex-control-rail').scrollLeft = 0;
|
|
2533
2597
|
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 };
|
|
2534
2598
|
setClaudeModeEnabled(false);
|
|
2535
2599
|
applyCodexState(codexState);
|