glad-web 1.0.21 → 1.0.23

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.
@@ -10,7 +10,7 @@
10
10
  <link rel="apple-touch-icon" href="logo.svg">
11
11
  <link rel="stylesheet" href="https://unpkg.com/xterm@5.3.0/css/xterm.css" />
12
12
  <style>
13
- :root { --primary: #007aff; --bg: #000; --card-bg: #1c1c1e; --text: #fff; --text-dim: #8e8e93; --terminal-controls-rest-height: 108px; }
13
+ :root { --primary: #007aff; --bg: #000; --card-bg: #1c1c1e; --text: #fff; --text-dim: #8e8e93; --terminal-controls-rest-height: 108px; --chat-content-max: 900px; --control-content-max: 1100px; }
14
14
  body, html { margin: 0; padding: 0; height: 100dvh; width: 100vw; background: var(--bg); color: var(--text); overflow: hidden; overflow-anchor: none; font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Helvetica, Arial, sans-serif; }
15
15
  .view { display: none; height: 100%; width: 100%; flex-direction: column; }
16
16
  .view.active { display: flex; }
@@ -48,7 +48,7 @@
48
48
  #session-title { flex: 1; text-align: center; font-weight: 600; font-size: 16px; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; padding: 0 10px; min-width: 0; }
49
49
  #terminal-controls { position: absolute; top: 100%; left: 0; right: 0; z-index: 2200; background: linear-gradient(180deg, rgba(18,18,18,0.98) 0%, rgba(18,18,18,0.92) 78%, rgba(18,18,18,0) 100%); border-bottom: 1px solid rgba(255,255,255,0.06); }
50
50
  #terminal-controls-spacer { flex: 0 0 var(--terminal-controls-rest-height); height: var(--terminal-controls-rest-height); background: #000; }
51
- #input-row { display: flex; align-items: flex-end; padding: 10px 14px 8px 14px; }
51
+ #input-row { display: flex; align-items: flex-end; width: min(100%, var(--control-content-max)); margin: 0 auto; padding: 10px 14px 8px 14px; box-sizing: border-box; }
52
52
  #cmd-input { flex: 1; min-height: 38px; max-height: 150px; background: rgba(255,255,255,0.08); border: 1px solid rgba(255,255,255,0.1); border-radius: 19px; color: #fff; padding: 9px 16px; font-size: 16px; outline: none; resize: none; overflow-y: auto; line-height: 20px; box-sizing: border-box; transition: background 0.18s ease, border-color 0.18s ease; }
53
53
  #cmd-input::placeholder { color: rgba(255,255,255,0.45); }
54
54
  #cmd-input:focus { background: rgba(255,255,255,0.1); border-color: rgba(0,122,255,0.42); color: #fff; }
@@ -78,6 +78,63 @@
78
78
  #terminal-container { flex: 1; min-height: 0; width: 100%; background: #000; position: relative; overflow: hidden; overflow-anchor: none; }
79
79
  #terminal { height: 100%; width: 100%; }
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
+ #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
+ .codex-conversation { width: min(100%, var(--chat-content-max)); margin: 0 auto; }
83
+ .codex-message { max-width: 100%; margin: 0 0 12px; color: #f5f5f7; font-size: 14px; line-height: 1.45; overflow-wrap: anywhere; }
84
+ .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
+ .codex-message.event { color: var(--text-dim); text-align: center; font-size: 12px; }
86
+ .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
+ .codex-tool summary { list-style: none; cursor: pointer; }
88
+ .codex-tool summary::-webkit-details-marker { display: none; }
89
+ .codex-tool-header { display: flex; align-items: center; min-width: 0; gap: 8px; min-height: 34px; padding: 6px 9px; }
90
+ .codex-tool-icon { width: 19px; flex: 0 0 19px; color: #c7c7cc; font: 700 11px/1 ui-monospace, SFMono-Regular, Menlo, monospace; text-align: center; }
91
+ .codex-tool-title { flex: 0 1 auto; min-width: 0; color: #f5f5f7; font-size: 13px; font-weight: 700; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
92
+ .codex-tool-command { flex: 1; min-width: 0; color: #a9a9b0; font: 12px/1.3 ui-monospace, SFMono-Regular, Menlo, monospace; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
93
+ .codex-tool-state { flex: 0 0 auto; color: #8e8e93; font-size: 11px; }
94
+ .codex-tool-state.running::before { content: ''; display: inline-block; width: 9px; height: 9px; margin-right: 5px; border: 1.5px solid #8e8e93; border-top-color: transparent; border-radius: 50%; animation: codex-spin .8s linear infinite; vertical-align: -1px; }
95
+ .codex-tool.error { border-color: rgba(255,59,48,.38); background: rgba(255,59,48,.07); }
96
+ .codex-tool-body { border-top: 1px solid rgba(255,255,255,.08); padding: 9px 10px; }
97
+ .codex-tool-code { margin: 0; max-height: 320px; overflow: auto; color: #c7c7cc; white-space: pre-wrap; overflow-wrap: anywhere; font: 12px/1.42 ui-monospace, SFMono-Regular, Menlo, monospace; }
98
+ .codex-work-group { margin: 8px 0; border-left: 1px solid rgba(255,255,255,.12); padding-left: 8px; }
99
+ .codex-work-group > summary { list-style: none; display: flex; align-items: center; gap: 7px; min-height: 30px; color: #a9a9b0; cursor: pointer; font-size: 12px; }
100
+ .codex-work-group > summary::-webkit-details-marker { display: none; }
101
+ .codex-work-group > summary::before { content: '›'; width: 12px; color: #8e8e93; font-size: 17px; transform-origin: center; }
102
+ .codex-work-group[open] > summary::before { transform: rotate(90deg); }
103
+ .codex-work-group-body { padding-left: 2px; }
104
+ .codex-patch-file { border-top: 1px solid rgba(255,255,255,.08); }
105
+ .codex-patch-file:first-child { border-top: 0; }
106
+ .codex-patch-file > summary { display: flex; align-items: center; gap: 8px; padding: 8px 10px; color: #d1d5db; font: 12px/1.35 ui-monospace, SFMono-Regular, Menlo, monospace; }
107
+ .codex-patch-file .path { flex: 1; min-width: 0; overflow: hidden; text-overflow: ellipsis; white-space: nowrap; }
108
+ .codex-patch-kind { color: #8e8e93; font-family: system-ui, sans-serif; }
109
+ .codex-diff { overflow-x: auto; border-top: 1px solid rgba(255,255,255,.08); background: rgba(0,0,0,.24); font: 11px/1.45 ui-monospace, SFMono-Regular, Menlo, monospace; }
110
+ .codex-diff-line { min-width: max-content; padding: 0 10px; white-space: pre; color: #c7c7cc; }
111
+ .codex-diff-line.add { color: #b7e4c7; background: rgba(52,199,89,.1); }
112
+ .codex-diff-line.del { color: #ffb3ad; background: rgba(255,59,48,.1); }
113
+ .codex-diff-line.hunk { color: #64b5f6; background: rgba(0,122,255,.08); }
114
+ .codex-permission-result { color: #a9a9b0; font-size: 12px; margin-top: 7px; }
115
+ .codex-inline-permission { border-top: 1px solid rgba(255,204,0,.28); padding: 9px 10px 10px; background: rgba(255,204,0,.055); }
116
+ .codex-inline-permission-title { color: #f5f5f7; font-size: 12px; font-weight: 700; }
117
+ .codex-inline-permission .claude-permission-actions { margin-top: 8px; }
118
+ @keyframes codex-spin { to { transform: rotate(360deg); } }
119
+ #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(5, minmax(0, 116px)); justify-content: center; gap: 7px; align-items: center; }
121
+ .codex-control-row > * { width: 100%; }
122
+ .codex-select-control { position: relative; display: block; min-width: 0; height: 32px; }
123
+ .codex-select-label { display: flex; width: 100%; height: 100%; align-items: center; justify-content: center; box-sizing: border-box; border: 1px solid rgba(255,255,255,.1); border-radius: 16px; background: rgba(255,255,255,.08); color: #f5f5f7; font-size: 11px; font-weight: 800; white-space: nowrap; }
124
+ .codex-select-control:focus-within .codex-select-label { background: rgba(255,255,255,.14); border-color: rgba(0,122,255,.45); }
125
+ .codex-select { appearance: none; position: absolute; inset: 0; width: 100%; min-width: 0; height: 100%; opacity: 0; cursor: pointer; }
126
+ .codex-select option { background: #1c1c1e; color: #fff; }
127
+ #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; }
128
+ #codex-model-panel.active, #codex-resume-panel.active { display: block; }
129
+ #codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
130
+ #codex-model-panel.active { display: grid; }
131
+ .codex-picker-column { min-width: 0; overflow-y: auto; }
132
+ .codex-picker-column + .codex-picker-column { border-left: 1px solid rgba(255,255,255,.08); }
133
+ .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; }
134
+ .codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
135
+ #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; }
136
+ #codex-state-bar::-webkit-scrollbar { display: none; }
137
+ #codex-resume-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
81
138
  .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); }
82
139
  .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; }
83
140
  .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; }
@@ -226,6 +283,37 @@
226
283
  .step-grid { grid-template-columns: 1fr; }
227
284
  .timed-row { grid-template-columns: 1fr; }
228
285
  }
286
+ @media (max-width: 480px) {
287
+ :root { --chat-content-max: 100%; --control-content-max: 100%; }
288
+ #nav-bar { padding: 8px 10px; }
289
+ #back-btn { font-size: 15px; }
290
+ #back-btn svg { width: 22px; height: 22px; }
291
+ #session-title { font-size: 15px; padding: 0 7px; }
292
+ #nav-bar > div:last-child { gap: 5px !important; }
293
+ #nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
294
+ #input-row { padding-left: 10px; padding-right: 10px; }
295
+ #codex-control-panel { padding-left: 8px; padding-right: 8px; }
296
+ .codex-control-row { grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 5px; }
297
+ .codex-control-row .claude-ctrl-btn { height: 36px; padding-left: 4px; padding-right: 4px; }
298
+ .codex-select-control { height: 36px; }
299
+ #codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
300
+ .codex-message.user { max-width: 94%; }
301
+ .claude-permission-actions { justify-content: flex-start; }
302
+ }
303
+ @media (max-width: 430px) {
304
+ #nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) { width: 34px; font-size: 0 !important; gap: 0 !important; }
305
+ #nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) svg { width: 16px; height: 16px; }
306
+ #codex-terminal-switch { padding-left: 8px !important; padding-right: 8px !important; }
307
+ }
308
+ @media (min-width: 768px) and (max-width: 1199px) {
309
+ :root { --chat-content-max: 780px; --control-content-max: 920px; }
310
+ #codex-chat-container { padding: 16px 18px 30px; }
311
+ #input-row, #codex-control-panel { padding-left: 18px; padding-right: 18px; }
312
+ }
313
+ @media (min-width: 1200px) {
314
+ :root { --chat-content-max: 960px; --control-content-max: 1100px; }
315
+ #codex-chat-container { padding: 18px 22px 34px; }
316
+ }
229
317
  </style>
230
318
  </head>
231
319
  <body>
@@ -264,6 +352,7 @@
264
352
  </button>
265
353
  <div id="session-title">Terminal</div>
266
354
  <div style="display: flex; align-items: center; gap: 8px;">
355
+ <button id="codex-terminal-switch" class="icon-btn" onclick="toggleCodexPresentation()" title="Switch Codex interface" style="display:none; color: var(--primary); font-size: 12px; font-weight: 700; background: rgba(0, 122, 255, 0.1); padding: 5px 9px; border-radius: 15px; gap: 4px;">TERM</button>
267
356
  <button class="icon-btn" onclick="showHistory()" title="History" style="color: var(--primary); font-size: 12px; font-weight: 700; background: rgba(0, 122, 255, 0.1); padding: 5px 9px; border-radius: 15px; gap: 4px;">
268
357
  <svg width="14" height="14" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2.3" stroke-linecap="round" stroke-linejoin="round"><path d="M3 12a9 9 0 1 0 3-6.7"></path><polyline points="3 3 3 9 9 9"></polyline><path d="M12 7v5l3 2"></path></svg>
269
358
  HIS
@@ -321,6 +410,28 @@
321
410
  <div id="claude-usage-panel"></div>
322
411
  <div id="claude-resume-panel"></div>
323
412
  </div>
413
+ <div id="codex-control-panel">
414
+ <div class="codex-control-row">
415
+ <label class="codex-select-control" title="Sandbox mode">
416
+ <span class="codex-select-label" aria-hidden="true">Sandbox</span>
417
+ <select id="codex-sandbox-select" class="codex-select" aria-label="Sandbox mode" onchange="updateCodexSettingsFromControls()">
418
+ <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>
419
+ </select>
420
+ </label>
421
+ <label class="codex-select-control" title="Approval policy">
422
+ <span class="codex-select-label" aria-hidden="true">Ask</span>
423
+ <select id="codex-permission-select" class="codex-select" aria-label="Approval policy" onchange="updateCodexSettingsFromControls()">
424
+ <option value="default">Default</option><option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
425
+ </select>
426
+ </label>
427
+ <button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
428
+ <button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
429
+ <button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
430
+ </div>
431
+ <div id="codex-state-bar"></div>
432
+ <div id="codex-model-panel"></div>
433
+ <div id="codex-resume-panel"></div>
434
+ </div>
324
435
  <div id="timed-send-panel">
325
436
  <div class="timed-row">
326
437
  <select id="timed-hours" onchange="updateTimedSendPreview()" title="Hours"></select>
@@ -360,6 +471,7 @@
360
471
  <div id="terminal-controls-spacer"></div>
361
472
  <div id="terminal-container"><div id="terminal"></div></div>
362
473
  <div id="claude-chat-container"></div>
474
+ <div id="codex-chat-container"></div>
363
475
  </div>
364
476
 
365
477
  <!-- Plain Text History View -->
@@ -509,6 +621,12 @@
509
621
  };
510
622
  let claudeResumePanelOpen = false;
511
623
  let claudeResumeItemsLoaded = false;
624
+ let codexMessages = [];
625
+ let codexPendingPermissions = [];
626
+ 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 };
627
+ let codexModelPanelOpen = false;
628
+ let codexModelCandidate = null;
629
+ let codexResumePanelOpen = false;
512
630
  const modifiers = { ctrl: false };
513
631
 
514
632
  function log(msg) {
@@ -800,25 +918,30 @@
800
918
  return activeToolKey === 'claude-code';
801
919
  }
802
920
 
921
+ function isCodexSession() {
922
+ return activeToolKey === 'codex';
923
+ }
924
+
803
925
  function setClaudeModeEnabled(enabled) {
804
- document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
926
+ const codexChat = isCodexSession() && codexState.presentation === 'structured';
927
+ const structured = enabled || codexChat;
928
+ document.getElementById('terminal-container').style.display = structured ? 'none' : '';
805
929
  document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
930
+ document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
806
931
  document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
932
+ document.getElementById('codex-control-panel').style.display = codexChat ? 'block' : 'none';
933
+ document.getElementById('codex-terminal-switch').style.display = isCodexSession() ? '' : 'none';
807
934
  document.getElementById('timer-btn').style.display = '';
808
- document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
809
- document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
810
- document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
935
+ document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
936
+ document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
937
+ document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
811
938
  updateTerminalControlsHeight();
812
939
  }
813
940
 
814
941
  function updateTerminalControlsHeight() {
815
- const timedOpen = document.getElementById('timed-send-panel')?.classList.contains('active');
816
- if (!isClaudeSession()) {
817
- document.documentElement.style.setProperty('--terminal-controls-rest-height', timedOpen ? '252px' : '108px');
818
- return;
819
- }
820
- const baseHeight = claudeResumePanelOpen ? 308 : ((claudePickerOpen || claudeUsagePanelOpen) ? 322 : 154);
821
- document.documentElement.style.setProperty('--terminal-controls-rest-height', `${baseHeight + (timedOpen ? 144 : 0)}px`);
942
+ const controls = document.getElementById('terminal-controls');
943
+ const measuredHeight = controls ? Math.ceil(controls.getBoundingClientRect().height) : 0;
944
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', `${Math.max(0, measuredHeight)}px`);
822
945
  }
823
946
 
824
947
  function shortModelLabel(value, resolved) {
@@ -1903,6 +2026,239 @@
1903
2026
  }));
1904
2027
  }
1905
2028
 
2029
+ function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
2030
+ function codexJson(value) {
2031
+ if (typeof value === 'string') return value;
2032
+ try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
2033
+ }
2034
+ function codexToolStatus(item) {
2035
+ const status = item.toolStatus || 'running';
2036
+ if (status === 'inProgress') return 'running';
2037
+ if (status === 'failed' || status === 'declined') return status;
2038
+ if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
2039
+ return status;
2040
+ }
2041
+ function renderCodexDiff(diff) {
2042
+ return `<div class="codex-diff">${String(diff || '').split('\n').map(line => {
2043
+ const type = line.startsWith('+++') || line.startsWith('---') ? 'hunk' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : line.startsWith('@@') ? 'hunk' : '';
2044
+ return `<div class="codex-diff-line ${type}">${escapeHtml(line || ' ')}</div>`;
2045
+ }).join('')}</div>`;
2046
+ }
2047
+ function normalizeCodexChanges(changes) {
2048
+ if (Array.isArray(changes)) return changes.filter(Boolean).map(change => [change.path || 'File', change]);
2049
+ if (changes && typeof changes === 'object') return Object.entries(changes);
2050
+ return [];
2051
+ }
2052
+ function codexChangeDiff(change = {}) {
2053
+ if (typeof change.diff === 'string') return change.diff;
2054
+ if (typeof change.unified_diff === 'string') return change.unified_diff;
2055
+ const kind = change.kind && typeof change.kind === 'object' ? change.kind : {};
2056
+ const type = change.type || kind.type || '';
2057
+ const oldText = change.modify?.old_content ?? change.old_content ?? change.oldContent ?? (type === 'delete' ? change.content : change.delete?.content) ?? '';
2058
+ const newText = change.modify?.new_content ?? change.new_content ?? change.newContent ?? (type === 'add' ? change.content : change.add?.content) ?? '';
2059
+ if (!oldText && !newText) return '';
2060
+ return `${String(oldText).split('\n').map(line => `-${line}`).join('\n')}\n${String(newText).split('\n').map(line => `+${line}`).join('\n')}`;
2061
+ }
2062
+ function renderCodexPatch(item) {
2063
+ const entries = normalizeCodexChanges(item.changes || item.input?.changes);
2064
+ if (!entries.length) return `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(codexJson(item.input))}</pre></div>`;
2065
+ return entries.map(([path, change]) => {
2066
+ const kind = change?.type || change?.kind?.type || 'edit';
2067
+ const move = change?.move_path || change?.kind?.move_path;
2068
+ const diff = codexChangeDiff(change);
2069
+ return `<details class="codex-patch-file"><summary><span class="path">${escapeHtml(path)}${move ? ` → ${escapeHtml(move)}` : ''}</span><span class="codex-patch-kind">${escapeHtml(kind)}</span></summary>${diff ? renderCodexDiff(diff) : ''}</details>`;
2070
+ }).join('');
2071
+ }
2072
+ function renderCodexPermission(request, inline = false) {
2073
+ if (!request) return '';
2074
+ const pending = request.status === 'pending';
2075
+ const labels = { approved: 'Allowed once', approved_for_session: 'Allowed for session', denied: 'Denied', abort: 'Stopped' };
2076
+ const detail = request.reason || (request.input && Object.keys(request.input).length ? codexJson(request.input) : '');
2077
+ const content = `${inline ? `<div class="codex-inline-permission-title">${escapeHtml(request.title || 'Permission required')}</div>` : ''}${detail ? `<div>${codexText(detail)}</div>` : ''}${pending ? `<div class="claude-permission-actions"><button class="small-btn primary" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved')">Yes</button><button class="small-btn" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'approved_for_session')">Yes, for session</button><button class="small-btn danger" onclick="respondCodexPermission('${escapeHtml(request.id)}', 'abort')">Stop and explain</button></div>` : `<div class="codex-permission-result">${escapeHtml(labels[request.decision] || request.status)}</div>`}`;
2078
+ if (inline) return `<div class="codex-inline-permission">${content}</div>`;
2079
+ return `<div class="claude-tool claude-permission"><div class="claude-tool-header"><strong>${escapeHtml(request.title || 'Permission required')}</strong></div><div class="claude-tool-body">${content}</div></div>`;
2080
+ }
2081
+ function renderCodexTool(item, permission = null) {
2082
+ const status = codexToolStatus(item);
2083
+ const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
2084
+ const runningClass = status === 'running' ? ' running' : '';
2085
+ if (item.name === 'CodexPatch') {
2086
+ return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
2087
+ }
2088
+ const command = item.name === 'CodexBash' ? item.command : item.title || item.tool || '';
2089
+ const icon = item.name === 'CodexBash' ? '>_' : item.name === 'McpTool' ? 'MCP' : item.name === 'Agent' ? 'A' : '•';
2090
+ const title = item.name === 'CodexBash' ? 'Command' : item.title || item.name || 'Tool';
2091
+ const result = item.result || (item.name === 'McpTool' || item.name === 'Agent' ? codexJson(item.input) : '');
2092
+ return `<details class="codex-tool${isError ? ' error' : ''}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
2093
+ }
2094
+ function renderCodexToolGroup(items, permissionById, usedPermissions) {
2095
+ const running = items.some(item => codexToolStatus(item) === 'running');
2096
+ const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
2097
+ const startedAt = Math.min(...items.map(item => Number(item.createdAt || Date.now())));
2098
+ const seconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
2099
+ const tools = items.map(item => {
2100
+ const permission = permissionById.get(String(item.providerId || ''));
2101
+ if (permission) usedPermissions.add(permission.id);
2102
+ return renderCodexTool(item, permission);
2103
+ }).join('');
2104
+ return `<details class="codex-work-group"${running ? ' open' : ''}><summary>${running ? 'Working' : failed ? 'Work finished with errors' : `Worked for ${seconds}s`} · ${items.length} tools</summary><div class="codex-work-group-body">${tools}</div></details>`;
2105
+ }
2106
+ function renderCodexChat() {
2107
+ const container = document.getElementById('codex-chat-container');
2108
+ if (!container) return;
2109
+ const parts = [];
2110
+ const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
2111
+ const usedPermissions = new Set();
2112
+ const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
2113
+ for (let i = 0; i < visible.length;) {
2114
+ const item = visible[i];
2115
+ if (item.kind === 'tool') {
2116
+ const tools = [];
2117
+ while (i < visible.length && visible[i].kind === 'tool') tools.push(visible[i++]);
2118
+ if (tools.length > 1) parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions));
2119
+ else {
2120
+ const permission = permissionById.get(String(tools[0].providerId || ''));
2121
+ if (permission) usedPermissions.add(permission.id);
2122
+ parts.push(renderCodexTool(tools[0], permission));
2123
+ }
2124
+ continue;
2125
+ }
2126
+ if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>`);
2127
+ else if (item.kind === 'user') parts.push(`<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>`);
2128
+ else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
2129
+ i += 1;
2130
+ }
2131
+ for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
2132
+ if (codexState.status === 'running') {
2133
+ parts.push('<div class="claude-status">Codex is working...</div>');
2134
+ }
2135
+ container.innerHTML = `<div class="codex-conversation">${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
2136
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
2137
+ }
2138
+
2139
+ function applyCodexState(state = {}) {
2140
+ const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
2141
+ codexState = { ...codexState, ...state };
2142
+ const permission = document.getElementById('codex-permission-select');
2143
+ if (permission) {
2144
+ const defaultOption = permission.querySelector('option[value="default"]');
2145
+ const labels = { untrusted: 'Untrusted', 'on-request': 'On request', never: 'Never ask' };
2146
+ if (defaultOption) defaultOption.textContent = codexState.effectivePermissionMode
2147
+ ? `Default (${labels[codexState.effectivePermissionMode] || codexState.effectivePermissionMode})`
2148
+ : 'Default';
2149
+ permission.value = codexState.permissionMode || 'default';
2150
+ }
2151
+ const sandbox = document.getElementById('codex-sandbox-select');
2152
+ if (sandbox) {
2153
+ const defaultOption = sandbox.querySelector('option[value="default"]');
2154
+ const labels = { 'read-only': 'Read only', 'workspace-write': 'Workspace write', 'danger-full-access': 'Full access' };
2155
+ if (defaultOption) defaultOption.textContent = codexState.effectiveSandboxMode
2156
+ ? `Default (${labels[codexState.effectiveSandboxMode] || codexState.effectiveSandboxMode})`
2157
+ : 'Default';
2158
+ sandbox.value = codexState.sandboxMode || 'default';
2159
+ }
2160
+ const modelButton = document.getElementById('codex-model-btn');
2161
+ if (modelButton) modelButton.textContent = 'Model';
2162
+ const abort = document.getElementById('codex-abort-btn');
2163
+ if (abort) abort.disabled = !codexState.canAbort;
2164
+ const terminal = document.getElementById('codex-terminal-switch');
2165
+ 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'; }
2166
+ renderCodexStateBar();
2167
+ renderCodexModelPanel();
2168
+ if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
2169
+ renderCodexChat();
2170
+ }
2171
+
2172
+ function renderCodexStateBar() {
2173
+ const el = document.getElementById('codex-state-bar');
2174
+ if (!el) return;
2175
+ const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
2176
+ const parts = [];
2177
+ if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
2178
+ else if (codexState.status === 'running') parts.push('<span class="claude-state-pill">Working</span>');
2179
+ el.innerHTML = parts.join('');
2180
+ el.style.display = parts.length ? 'flex' : 'none';
2181
+ }
2182
+
2183
+ function toggleCodexModelPanel() {
2184
+ codexModelPanelOpen = !codexModelPanelOpen;
2185
+ codexResumePanelOpen = false;
2186
+ codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
2187
+ document.getElementById('codex-resume-panel').classList.remove('active');
2188
+ renderCodexModelPanel();
2189
+ updateTerminalControlsHeight();
2190
+ }
2191
+
2192
+ function renderCodexModelPanel() {
2193
+ const panel = document.getElementById('codex-model-panel');
2194
+ if (!panel) return;
2195
+ panel.classList.toggle('active', codexModelPanelOpen);
2196
+ if (!codexModelPanelOpen) { panel.innerHTML = ''; return; }
2197
+ const models = codexState.models || [];
2198
+ const candidate = models.find(item => item.id === codexModelCandidate) || models.find(item => item.id === codexState.model) || models[0];
2199
+ if (!candidate) { panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading models...</div>'; return; }
2200
+ codexModelCandidate = candidate.id;
2201
+ const efforts = candidate.efforts?.length ? candidate.efforts : [candidate.defaultEffort || 'medium'];
2202
+ panel.innerHTML = `<div class="codex-picker-column">${models.map(item => `<button class="codex-picker-option${item.id === candidate.id ? ' selected' : ''}" onclick="selectCodexModelCandidate(decodePathValue('${encodePathValue(item.id)}'))">${escapeHtml(item.label || item.id)}</button>`).join('')}</div><div class="codex-picker-column">${efforts.map(effort => `<button class="codex-picker-option${candidate.id === codexState.model && effort === codexState.effort ? ' selected' : ''}" onclick="chooseCodexModelEffort(decodePathValue('${encodePathValue(candidate.id)}'), decodePathValue('${encodePathValue(effort)}'))">${escapeHtml(effort)}</button>`).join('')}</div>`;
2203
+ }
2204
+
2205
+ function selectCodexModelCandidate(model) { codexModelCandidate = model; renderCodexModelPanel(); }
2206
+ function chooseCodexModelEffort(model, effort) {
2207
+ codexModelPanelOpen = false;
2208
+ applyCodexState({ model, effort });
2209
+ sendCodexSettings({ model, effort });
2210
+ updateTerminalControlsHeight();
2211
+ }
2212
+
2213
+ function applyCodexEvent(event) {
2214
+ if (!event) return;
2215
+ if (event.type === 'message' && event.message) codexMessages.push(event.message);
2216
+ else if (event.type === 'message-updated' && event.message) { const i = codexMessages.findIndex(item => item.id === event.message.id); if (i >= 0) codexMessages[i] = event.message; else codexMessages.push(event.message); }
2217
+ else if (event.type === 'history-reset') codexMessages = event.messages || [];
2218
+ else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
2219
+ else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
2220
+ if (event.state) applyCodexState(event.state); else renderCodexChat();
2221
+ }
2222
+
2223
+ function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
2224
+ function updateCodexSettingsFromControls() {
2225
+ const permissionMode = document.getElementById('codex-permission-select').value;
2226
+ const sandboxMode = document.getElementById('codex-sandbox-select').value;
2227
+ applyCodexState({ permissionMode, sandboxMode });
2228
+ sendCodexSettings({ permissionMode, sandboxMode });
2229
+ }
2230
+ function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
2231
+ function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
2232
+ async function toggleCodexResumePanel() {
2233
+ codexResumePanelOpen = !codexResumePanelOpen;
2234
+ codexModelPanelOpen = false;
2235
+ document.getElementById('codex-model-panel').classList.remove('active');
2236
+ const panel = document.getElementById('codex-resume-panel');
2237
+ panel.classList.toggle('active', codexResumePanelOpen);
2238
+ updateTerminalControlsHeight();
2239
+ if (!codexResumePanelOpen) return;
2240
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
2241
+ try {
2242
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
2243
+ const data = await res.json();
2244
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Codex sessions');
2245
+ const items = data.items || [];
2246
+ panel.innerHTML = items.length ? items.map(item => `<button class="claude-resume-item" onclick="selectCodexResumeThread(decodePathValue('${encodePathValue(item.id)}'))"><div class="claude-resume-title"><span>${escapeHtml((item.preview || 'Codex session').slice(0, 48))}${item.current ? ' · current' : ''}</span><span>${escapeHtml(item.updatedAt ? new Date(item.updatedAt).toLocaleString() : '')}</span></div><div class="claude-resume-meta">${escapeHtml(item.id.slice(0, 12))}</div></button>`).join('') : '<div class="claude-resume-meta" style="padding:12px;">No Codex sessions found for this folder.</div>';
2247
+ } catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
2248
+ }
2249
+ async function selectCodexResumeThread(threadId) {
2250
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
2251
+ if (!res.ok) { alert((await res.json()).error || 'Unable to resume Codex thread'); return; }
2252
+ codexResumePanelOpen = false;
2253
+ document.getElementById('codex-resume-panel').classList.remove('active');
2254
+ updateTerminalControlsHeight();
2255
+ }
2256
+ async function toggleCodexPresentation() {
2257
+ const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
2258
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
2259
+ if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
2260
+ }
2261
+
1906
2262
  function joinSession(id, sessionName, toolKey = null) {
1907
2263
  stopTimedInputTimers();
1908
2264
  activeSessionId = id;
@@ -1926,6 +2282,10 @@
1926
2282
  initClaudeSession(sessionId);
1927
2283
  return;
1928
2284
  }
2285
+ if (isCodexSession() && codexState.presentation === 'structured') {
2286
+ initCodexSession(sessionId);
2287
+ return;
2288
+ }
1929
2289
  if (typeof Terminal === 'undefined') {
1930
2290
  alert('Terminal library not loaded yet.');
1931
2291
  showLobby();
@@ -1959,6 +2319,14 @@
1959
2319
  const msg = JSON.parse(e.data);
1960
2320
  if (msg.type === 'output') term.write(msg.data);
1961
2321
  if (msg.type === 'reset') term.reset();
2322
+ if (msg.type === 'codex-event' && isCodexSession()) {
2323
+ applyCodexEvent(msg.event);
2324
+ if (msg.event?.type === 'presentation' && msg.event.presentation === 'structured') {
2325
+ currentSocket.close();
2326
+ currentSocket = null;
2327
+ initCodexSession(sessionId);
2328
+ }
2329
+ }
1962
2330
  if (msg.type === 'exit') showLobby();
1963
2331
  };
1964
2332
  }
@@ -2010,6 +2378,38 @@
2010
2378
  };
2011
2379
  }
2012
2380
 
2381
+ function initCodexSession(sessionId) {
2382
+ codexMessages = [];
2383
+ codexPendingPermissions = [];
2384
+ codexModelPanelOpen = false;
2385
+ codexModelCandidate = null;
2386
+ codexResumePanelOpen = false;
2387
+ document.getElementById('codex-model-panel').classList.remove('active');
2388
+ document.getElementById('codex-resume-panel').classList.remove('active');
2389
+ 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 };
2390
+ setClaudeModeEnabled(false);
2391
+ applyCodexState(codexState);
2392
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
2393
+ currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
2394
+ currentSocket.onmessage = (e) => {
2395
+ const msg = JSON.parse(e.data);
2396
+ if (msg.type === 'codex-snapshot' && msg.snapshot) {
2397
+ codexMessages = msg.snapshot.messages || [];
2398
+ codexPendingPermissions = msg.snapshot.pendingPermissions || [];
2399
+ applyCodexState(msg.snapshot.state || {});
2400
+ }
2401
+ if (msg.type === 'codex-event') {
2402
+ applyCodexEvent(msg.event);
2403
+ if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
2404
+ currentSocket.close();
2405
+ currentSocket = null;
2406
+ initTerminal(sessionId);
2407
+ }
2408
+ }
2409
+ if (msg.type === 'exit') showLobby();
2410
+ };
2411
+ }
2412
+
2013
2413
  function sendWS(data) {
2014
2414
  if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
2015
2415
  }
@@ -2228,6 +2628,12 @@
2228
2628
  inputEl.style.height = '38px';
2229
2629
  return;
2230
2630
  }
2631
+ if (val && isCodexSession() && codexState.presentation === 'structured') {
2632
+ if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-input', text: val }));
2633
+ inputEl.value = '';
2634
+ inputEl.style.height = '38px';
2635
+ return;
2636
+ }
2231
2637
  if (val) {
2232
2638
  const formattedVal = val.replace(/\n/g, '\r');
2233
2639
  sendWS(formattedVal);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "glad-web",
3
- "version": "1.0.21",
3
+ "version": "1.0.23",
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": {
@@ -27,13 +27,13 @@
27
27
  ],
28
28
  "author": "Glad Team",
29
29
  "license": "MIT",
30
- "homepage": "https://github.com/Next2012/Glad",
30
+ "homepage": "https://github.com/Anonymous/Glad",
31
31
  "repository": {
32
32
  "type": "git",
33
- "url": "git+https://github.com/Next2012/Glad.git"
33
+ "url": "git+https://github.com/Anonymous/Glad.git"
34
34
  },
35
35
  "bugs": {
36
- "url": "https://github.com/Next2012/Glad/issues"
36
+ "url": "https://github.com/Anonymous/Glad/issues"
37
37
  },
38
38
  "engines": {
39
39
  "node": ">=18.0.0"
@@ -41,6 +41,7 @@
41
41
  "files": [
42
42
  "bin/",
43
43
  "lib/",
44
+ "!lib/**/*.test.js",
44
45
  "assets/logo.svg",
45
46
  "README.md"
46
47
  ],