glad-web 1.0.26 → 1.0.27

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.
@@ -812,12 +812,17 @@ class CodexStructuredSession extends EventEmitter {
812
812
  this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
813
813
  this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
814
814
  const history = await this.request('thread/read', { threadId: this.threadId, includeTurns: true });
815
- this.model = history?.thread?.model || this.model;
816
- this.effort = history?.thread?.reasoningEffort || history?.thread?.reasoning_effort || this.effort;
817
- this.tokenUsage = history?.thread?.tokenUsage || history?.thread?.token_usage || this.tokenUsage;
815
+ this.restoreThreadHistory(history?.thread, `Resumed Codex thread ${this.threadId}`);
816
+ return true;
817
+ }
818
+
819
+ restoreThreadHistory(thread, eventText = '') {
820
+ this.model = thread?.model || this.model;
821
+ this.effort = thread?.reasoningEffort || thread?.reasoning_effort || this.effort;
822
+ this.tokenUsage = thread?.tokenUsage || thread?.token_usage || this.tokenUsage;
818
823
  this.messages = [];
819
824
  this.completedPermissions = [];
820
- for (const turn of history?.thread?.turns || []) {
825
+ for (const turn of thread?.turns || []) {
821
826
  const status = turn.status === 'failed' ? 'failed' : turn.status === 'interrupted' ? 'cancelled' : 'completed';
822
827
  const startedAt = Number(turn.startedAt || turn.createdAt || 0);
823
828
  const completedAt = Number(turn.completedAt || turn.updatedAt || 0);
@@ -831,10 +836,29 @@ class CodexStructuredSession extends EventEmitter {
831
836
  this.append({ kind: 'turn-end', turnId: turn.id, status, durationMs,
832
837
  ...(completedAtMs ? { createdAt: completedAtMs } : {}) });
833
838
  }
834
- this.append({ kind: 'event', level: 'info', text: `Resumed Codex thread ${this.threadId}` });
839
+ if (eventText) this.append({ kind: 'event', level: 'info', text: eventText });
835
840
  this.emitEvent({ type: 'history-reset', messages: this.messages });
836
841
  this.emitEvent({ type: 'state', state: this.getControlState() });
837
- return true;
842
+ }
843
+
844
+ async forkFrom(threadId) {
845
+ const sourceThreadId = String(threadId || '').trim();
846
+ if (!sourceThreadId || this.presentation !== 'structured' || this.status !== 'idle') return false;
847
+ await this.ensureProcess();
848
+ const params = { threadId: sourceThreadId, cwd: this.workingDir, ephemeral: false, threadSource: null };
849
+ if (this.hasModelOverride) params.model = this.model;
850
+ if (this.permissionMode) params.approvalPolicy = this.permissionMode;
851
+ if (this.sandboxMode) params.sandbox = this.sandboxMode;
852
+ const result = await this.request('thread/fork', params);
853
+ const forkedThread = result?.thread;
854
+ if (!forkedThread?.id) throw new Error('Codex did not return a forked thread');
855
+ this.threadId = forkedThread.id;
856
+ this.model = result.model || forkedThread.model || this.model;
857
+ this.effort = result.reasoningEffort || forkedThread.reasoningEffort || forkedThread.reasoning_effort || this.effort;
858
+ this.effectivePermissionMode = result.approvalPolicy || this.effectivePermissionMode;
859
+ this.effectiveSandboxMode = sandboxModeFromPolicy(result.sandbox) || this.effectiveSandboxMode;
860
+ this.restoreThreadHistory(forkedThread, `Forked from Codex thread ${sourceThreadId}`);
861
+ return { threadId: this.threadId };
838
862
  }
839
863
 
840
864
  async switchToTerminal() {
@@ -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,45 @@ 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 source = this.get(id);
443
+ if (!source || source.kind !== 'codex-structured') return null;
444
+ if (source.presentation !== 'structured' || source.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 || source.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
+
456
+ const target = this.createCodexStructuredSession({
457
+ tool: source.tool,
458
+ workingDirectory: this.getSessionWorkingDirectory(source),
459
+ name: `${source.name} Fork`,
460
+ codexOptions: {
461
+ ...(source.hasModelOverride && source.model ? { model: source.model } : {}),
462
+ ...(source.hasEffortOverride && source.effort ? { effort: source.effort } : {}),
463
+ ...(source.permissionMode ? { permissionMode: source.permissionMode } : {}),
464
+ ...(source.sandboxMode ? { sandboxMode: source.sandboxMode } : {})
465
+ }
466
+ });
467
+ target.parentSessionId = source.id;
468
+ target.forkedFromThreadId = sourceThreadId;
469
+ try {
470
+ const result = await target.forkFrom(sourceThreadId);
471
+ if (!result) throw new Error('Unable to fork the selected Codex thread');
472
+ source.append({ kind: 'event', level: 'info', text: `Forked a new session: ${target.name}` });
473
+ return target;
474
+ } catch (error) {
475
+ this.kill(target.id);
476
+ throw error;
477
+ }
478
+ }
479
+
441
480
  listCodexResumeThreads(id) {
442
481
  const session = this.get(id);
443
482
  if (!session || session.kind !== 'codex-structured') return null;
@@ -143,16 +143,18 @@
143
143
  .codex-inline-permission .claude-permission-actions { margin-top: 8px; }
144
144
  @keyframes codex-spin { to { transform: rotate(360deg); } }
145
145
  #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-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; }
147
- .codex-control-row > * { width: 100%; min-width: 0; overflow: hidden; }
148
- .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; }
146
+ .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; }
147
+ .codex-control-rail::-webkit-scrollbar { display: none; }
148
+ .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; }
149
+ .codex-control-page > * { width: 100%; min-width: 0; overflow: hidden; }
150
+ .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
151
  .codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
150
152
  .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
153
  .codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
152
154
  .codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
153
155
  .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; }
156
+ #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; }
157
+ #codex-model-panel.active, #codex-resume-panel.active, #codex-fork-panel.active { display: block; }
156
158
  #codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
157
159
  #codex-model-panel.active { display: grid; }
158
160
  .codex-picker-column { min-width: 0; overflow-y: auto; }
@@ -161,7 +163,7 @@
161
163
  .codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
162
164
  #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
165
  #codex-state-bar::-webkit-scrollbar { display: none; }
164
- #codex-resume-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
166
+ #codex-resume-panel, #codex-fork-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
165
167
  .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
168
  .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
169
  .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,8 +323,8 @@
321
323
  #nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
322
324
  #input-row { padding-left: 10px; padding-right: 10px; }
323
325
  #codex-control-panel { padding-left: 8px; padding-right: 8px; }
324
- .codex-control-row { grid-template-columns: repeat(6, minmax(0, 1fr)); }
325
- .codex-control-row .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
326
+ .codex-control-page { grid-template-columns: repeat(5, minmax(0, 1fr)); }
327
+ .codex-control-page .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
326
328
  .codex-select-control { height: 36px; }
327
329
  #codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
328
330
  .codex-message.user { max-width: 94%; }
@@ -443,27 +445,33 @@
443
445
  <div id="claude-resume-panel"></div>
444
446
  </div>
445
447
  <div id="codex-control-panel">
446
- <div class="codex-control-row">
447
- <label class="codex-select-control" title="Sandbox mode">
448
- <span class="codex-select-label" aria-hidden="true">Sandbox</span>
449
- <select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
450
- <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>
451
- </select>
452
- </label>
453
- <label class="codex-select-control" title="Approval policy">
454
- <span class="codex-select-label" aria-hidden="true">Ask</span>
455
- <select id="codex-permission-select" class="codex-select" aria-label="Approval policy" onchange="updateCodexSettingsFromControls()">
456
- <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
457
- </select>
458
- </label>
459
- <button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
460
- <button id="codex-status-btn" class="claude-ctrl-btn" onclick="requestCodexStatus()" title="Show Codex account, limits, and context status">Status</button>
461
- <button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
462
- <button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
448
+ <div id="codex-control-rail" class="codex-control-rail">
449
+ <div class="codex-control-page">
450
+ <button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
451
+ <button id="codex-status-btn" class="claude-ctrl-btn" onclick="requestCodexStatus()" title="Show Codex account, limits, and context status">Status</button>
452
+ <button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
453
+ <button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
454
+ <button id="codex-fork-btn" class="claude-ctrl-btn primary" onclick="toggleCodexForkPanel()" title="Fork a Codex thread into a new Glad session">Fork</button>
455
+ </div>
456
+ <div class="codex-control-page">
457
+ <label class="codex-select-control" title="Sandbox mode">
458
+ <span class="codex-select-label" aria-hidden="true">Sandbox</span>
459
+ <select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
460
+ <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>
461
+ </select>
462
+ </label>
463
+ <label class="codex-select-control" title="Approval policy">
464
+ <span class="codex-select-label" aria-hidden="true">Ask</span>
465
+ <select id="codex-permission-select" class="codex-select" aria-label="Approval policy" onchange="updateCodexSettingsFromControls()">
466
+ <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
467
+ </select>
468
+ </label>
469
+ </div>
463
470
  </div>
464
471
  <div id="codex-state-bar"></div>
465
472
  <div id="codex-model-panel"></div>
466
473
  <div id="codex-resume-panel"></div>
474
+ <div id="codex-fork-panel"></div>
467
475
  </div>
468
476
  <div id="timed-send-panel">
469
477
  <div class="timed-row">
@@ -660,6 +668,7 @@
660
668
  let codexModelPanelOpen = false;
661
669
  let codexModelCandidate = null;
662
670
  let codexResumePanelOpen = false;
671
+ let codexForkPanelOpen = false;
663
672
  let codexRenderFrame = null;
664
673
  const modifiers = { ctrl: false };
665
674
 
@@ -2297,6 +2306,8 @@
2297
2306
  if (modelButton) modelButton.textContent = 'Model';
2298
2307
  const abort = document.getElementById('codex-abort-btn');
2299
2308
  if (abort) abort.disabled = !codexState.canAbort;
2309
+ const fork = document.getElementById('codex-fork-btn');
2310
+ if (fork) fork.disabled = !(codexState.presentation === 'structured' && codexState.status === 'idle' && codexState.threadId);
2300
2311
  const terminal = document.getElementById('codex-terminal-switch');
2301
2312
  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
2313
  renderCodexStateBar();
@@ -2320,8 +2331,10 @@
2320
2331
  function toggleCodexModelPanel() {
2321
2332
  codexModelPanelOpen = !codexModelPanelOpen;
2322
2333
  codexResumePanelOpen = false;
2334
+ codexForkPanelOpen = false;
2323
2335
  codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
2324
2336
  document.getElementById('codex-resume-panel').classList.remove('active');
2337
+ document.getElementById('codex-fork-panel').classList.remove('active');
2325
2338
  renderCodexModelPanel();
2326
2339
  updateTerminalControlsHeight();
2327
2340
  }
@@ -2370,11 +2383,29 @@
2370
2383
  async function toggleCodexResumePanel() {
2371
2384
  codexResumePanelOpen = !codexResumePanelOpen;
2372
2385
  codexModelPanelOpen = false;
2386
+ codexForkPanelOpen = false;
2373
2387
  document.getElementById('codex-model-panel').classList.remove('active');
2388
+ document.getElementById('codex-fork-panel').classList.remove('active');
2374
2389
  const panel = document.getElementById('codex-resume-panel');
2375
2390
  panel.classList.toggle('active', codexResumePanelOpen);
2376
2391
  updateTerminalControlsHeight();
2377
2392
  if (!codexResumePanelOpen) return;
2393
+ await loadCodexThreadPanel(panel, 'resume');
2394
+ }
2395
+ async function toggleCodexForkPanel() {
2396
+ if (!(codexState.presentation === 'structured' && codexState.status === 'idle' && codexState.threadId)) return;
2397
+ codexForkPanelOpen = !codexForkPanelOpen;
2398
+ codexModelPanelOpen = false;
2399
+ codexResumePanelOpen = false;
2400
+ document.getElementById('codex-model-panel').classList.remove('active');
2401
+ document.getElementById('codex-resume-panel').classList.remove('active');
2402
+ const panel = document.getElementById('codex-fork-panel');
2403
+ panel.classList.toggle('active', codexForkPanelOpen);
2404
+ updateTerminalControlsHeight();
2405
+ if (!codexForkPanelOpen) return;
2406
+ await loadCodexThreadPanel(panel, 'fork');
2407
+ }
2408
+ async function loadCodexThreadPanel(panel, action) {
2378
2409
  panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
2379
2410
  try {
2380
2411
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
@@ -2383,9 +2414,11 @@
2383
2414
  const items = data.items || [];
2384
2415
  panel.innerHTML = items.length ? items.map(item => {
2385
2416
  const questions = Array.isArray(item.questions) ? item.questions : [];
2386
- return `<button class="claude-resume-item" onclick="selectCodexResumeThread(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>`;
2417
+ const handler = action === 'fork' ? 'selectCodexForkThread' : 'selectCodexResumeThread';
2418
+ 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
2419
  }).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
2388
2420
  } catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
2421
+ updateTerminalControlsHeight();
2389
2422
  }
2390
2423
  async function selectCodexResumeThread(threadId) {
2391
2424
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
@@ -2394,6 +2427,23 @@
2394
2427
  document.getElementById('codex-resume-panel').classList.remove('active');
2395
2428
  updateTerminalControlsHeight();
2396
2429
  }
2430
+ async function selectCodexForkThread(threadId) {
2431
+ const panel = document.getElementById('codex-fork-panel');
2432
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Forking into a new session...</div>';
2433
+ updateTerminalControlsHeight();
2434
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-fork`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 60000);
2435
+ const data = await res.json();
2436
+ if (!res.ok || !data.success) {
2437
+ panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(data.error || 'Unable to fork Codex thread')}</div>`;
2438
+ updateTerminalControlsHeight();
2439
+ return;
2440
+ }
2441
+ codexForkPanelOpen = false;
2442
+ panel.classList.remove('active');
2443
+ panel.innerHTML = '';
2444
+ updateTerminalControlsHeight();
2445
+ refreshSessionsNow();
2446
+ }
2397
2447
  async function toggleCodexPresentation() {
2398
2448
  const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
2399
2449
  const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
@@ -2528,8 +2578,11 @@
2528
2578
  codexModelPanelOpen = false;
2529
2579
  codexModelCandidate = null;
2530
2580
  codexResumePanelOpen = false;
2581
+ codexForkPanelOpen = false;
2531
2582
  document.getElementById('codex-model-panel').classList.remove('active');
2532
2583
  document.getElementById('codex-resume-panel').classList.remove('active');
2584
+ document.getElementById('codex-fork-panel').classList.remove('active');
2585
+ document.getElementById('codex-control-rail').scrollLeft = 0;
2533
2586
  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
2587
  setClaudeModeEnabled(false);
2535
2588
  applyCodexState(codexState);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.26",
3
+ "version": "1.0.27",
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": {