glad-web 1.0.20 → 1.0.22

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,59 @@
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(4, minmax(0, 1fr)); gap: 7px; align-items: center; }
121
+ .codex-select { appearance: none; max-width: 116px; min-width: 0; height: 32px; padding: 0 8px; border: 1px solid rgba(255,255,255,.12); border-radius: 7px; background: rgba(255,255,255,.08); color: #f5f5f7; font-weight: 700; font-size: 12px; }
122
+ .codex-select option { background: #1c1c1e; color: #fff; }
123
+ #codex-model-panel, #codex-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,.1); background: rgba(28,28,30,.99); border-radius: 8px; box-shadow: 0 16px 36px rgba(0,0,0,.34); overflow: hidden; }
124
+ #codex-model-panel.active, #codex-resume-panel.active { display: block; }
125
+ #codex-model-panel { grid-template-columns: minmax(0, 1fr) minmax(0, 1fr); max-height: min(300px, 42dvh); }
126
+ #codex-model-panel.active { display: grid; }
127
+ .codex-picker-column { min-width: 0; overflow-y: auto; }
128
+ .codex-picker-column + .codex-picker-column { border-left: 1px solid rgba(255,255,255,.08); }
129
+ .codex-picker-option { display: block; width: 100%; min-height: 38px; padding: 8px 10px; border: 0; border-bottom: 1px solid rgba(255,255,255,.06); background: transparent; color: #f5f5f7; text-align: left; font-size: 12px; font-weight: 700; overflow-wrap: anywhere; }
130
+ .codex-picker-option.selected { background: rgba(0,122,255,.18); color: #fff; }
131
+ #codex-state-bar { display: none; align-items: center; gap: 7px; min-height: 24px; margin-top: 8px; color: var(--text-dim); font-size: 11px; }
132
+ #codex-resume-panel { max-height: min(280px, 38dvh); overflow-y: auto; }
133
+ .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); }
81
134
  .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; }
82
135
  .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; }
83
136
  .claude-message.assistant { max-width: 100%; background: transparent; border: 0; color: #f5f5f7; padding: 0 4px; margin: 0 0 12px 0; }
@@ -127,11 +180,12 @@
127
180
  .claude-work-group-title { flex: 1; min-width: 0; font-size: 13px; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
128
181
  .claude-work-group-body { padding: 2px 8px 8px 8px; }
129
182
  .claude-permission { border-color: rgba(255,204,0,0.4); background: rgba(255,204,0,0.08); }
130
- .claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; }
183
+ .claude-permission-actions { display: flex; gap: 8px; justify-content: flex-end; margin-top: 10px; flex-wrap: wrap; }
184
+ .claude-permission-actions .small-btn { min-height: 30px; max-width: 100%; white-space: normal; line-height: 1.2; }
131
185
  .claude-status { color: var(--text-dim); font-size: 12px; text-align: center; padding: 6px 0 0 0; }
132
186
  #claude-control-panel { display: none; padding: 0 14px 10px 14px; box-sizing: border-box; }
133
- .claude-control-row { display: grid; grid-template-columns: repeat(5, minmax(0, 1fr)); gap: 7px; }
134
- .claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0 8px; outline: none; box-sizing: border-box; }
187
+ .claude-control-row { display: grid; grid-template-columns: repeat(6, minmax(0, 1fr)); gap: 7px; }
188
+ .claude-select, .claude-ctrl-btn { min-width: 0; height: 32px; border: 1px solid rgba(255,255,255,0.1); border-radius: 16px; background: rgba(255,255,255,0.08); color: #f5f5f7; font-size: 11px; font-weight: 800; letter-spacing: 0; padding: 0 10px; outline: none; box-sizing: border-box; }
135
189
  .claude-select { appearance: none; -webkit-appearance: none; text-align: center; text-align-last: center; }
136
190
  .claude-picker-wrap { min-width: 0; position: relative; }
137
191
  .claude-picker-wrap select { display: none; }
@@ -139,11 +193,19 @@
139
193
  .claude-picker-btn.active { background: rgba(0,122,255,0.22); border-color: rgba(0,122,255,0.5); }
140
194
  #claude-picker-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; max-height: min(280px, 42dvh); overflow-y: auto; box-shadow: 0 16px 36px rgba(0,0,0,0.34); }
141
195
  #claude-picker-panel.active { display: block; }
196
+ #claude-usage-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.99); border-radius: 12px; padding: 10px 11px; box-shadow: 0 16px 36px rgba(0,0,0,0.34); color: #f5f5f7; }
197
+ #claude-usage-panel.active { display: block; }
198
+ .claude-usage-title { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; margin-bottom: 9px; }
199
+ .claude-usage-grid { display: grid; grid-template-columns: repeat(3, minmax(0, 1fr)); gap: 8px; }
200
+ .claude-usage-item { border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 8px; padding: 8px; min-width: 0; }
201
+ .claude-usage-label { color: var(--text-dim); font-size: 10px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
202
+ .claude-usage-value { margin-top: 4px; color: #f5f5f7; font-size: 13px; font-weight: 800; white-space: nowrap; overflow: hidden; text-overflow: ellipsis; }
142
203
  .claude-picker-title { padding: 9px 11px 7px 11px; color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; letter-spacing: 0.04em; border-bottom: 1px solid rgba(255,255,255,0.06); }
143
204
  .claude-picker-option { width: 100%; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #f5f5f7; padding: 10px 11px; text-align: left; cursor: pointer; box-sizing: border-box; }
144
205
  .claude-picker-option:last-child { border-bottom: 0; }
145
206
  .claude-picker-option:active, .claude-picker-option.selected { background: rgba(0,122,255,0.18); }
146
207
  .claude-picker-option-main { display: flex; align-items: center; justify-content: space-between; gap: 10px; font-size: 13px; font-weight: 800; }
208
+ .claude-picker-option-main .selected-label { color: var(--text-dim); font-size: 11px; font-weight: 800; text-transform: uppercase; flex: 0 0 auto; }
147
209
  .claude-picker-option-value { min-width: 0; overflow-wrap: anywhere; font-family: ui-monospace, SFMono-Regular, Menlo, Consolas, monospace; color: #d1d5db; font-size: 12px; margin-top: 3px; line-height: 1.35; }
148
210
  .claude-ctrl-btn { cursor: pointer; display: inline-flex; align-items: center; justify-content: center; white-space: nowrap; }
149
211
  .claude-ctrl-btn:active, .claude-select:focus { background: rgba(255,255,255,0.14); border-color: rgba(0,122,255,0.45); }
@@ -154,6 +216,10 @@
154
216
  #claude-state-bar::-webkit-scrollbar { display: none; }
155
217
  .claude-state-pill { display: inline-flex; align-items: center; gap: 4px; border: 1px solid rgba(255,255,255,0.08); background: rgba(255,255,255,0.045); border-radius: 999px; padding: 3px 8px; flex: 0 0 auto; }
156
218
  .claude-state-pill.warn { color: #ffcc00; border-color: rgba(255,204,0,0.24); background: rgba(255,204,0,0.08); }
219
+ .claude-state-pill.perm { margin-left: auto; }
220
+ .claude-state-pill.perm.acceptEdits { color: #34c759; border-color: rgba(52,199,89,0.26); background: rgba(52,199,89,0.08); }
221
+ .claude-state-pill.perm.bypassPermissions { color: #ff9f0a; border-color: rgba(255,159,10,0.26); background: rgba(255,159,10,0.08); }
222
+ .claude-state-pill.perm.plan { color: #64d2ff; border-color: rgba(100,210,255,0.26); background: rgba(100,210,255,0.08); }
157
223
  #claude-resume-panel { display: none; margin-top: 8px; border: 1px solid rgba(255,255,255,0.1); background: rgba(28,28,30,0.98); border-radius: 12px; max-height: 180px; overflow-y: auto; }
158
224
  #claude-resume-panel.active { display: block; }
159
225
  .claude-resume-item { width: 100%; text-align: left; border: 0; border-bottom: 1px solid rgba(255,255,255,0.06); background: transparent; color: #fff; padding: 10px 11px; cursor: pointer; box-sizing: border-box; }
@@ -212,6 +278,35 @@
212
278
  .step-grid { grid-template-columns: 1fr; }
213
279
  .timed-row { grid-template-columns: 1fr; }
214
280
  }
281
+ @media (max-width: 480px) {
282
+ :root { --chat-content-max: 100%; --control-content-max: 100%; }
283
+ #nav-bar { padding: 8px 10px; }
284
+ #back-btn { font-size: 15px; }
285
+ #back-btn svg { width: 22px; height: 22px; }
286
+ #session-title { font-size: 15px; padding: 0 7px; }
287
+ #nav-bar > div:last-child { gap: 5px !important; }
288
+ #nav-bar > div:last-child .icon-btn { min-width: 34px; min-height: 30px; padding: 5px 7px !important; }
289
+ #input-row { padding-left: 10px; padding-right: 10px; }
290
+ #codex-control-panel { padding-left: 10px; padding-right: 10px; }
291
+ .codex-control-row { gap: 6px; }
292
+ #codex-chat-container { padding: 10px 10px calc(22px + env(safe-area-inset-bottom)); }
293
+ .codex-message.user { max-width: 94%; }
294
+ .claude-permission-actions { justify-content: flex-start; }
295
+ }
296
+ @media (max-width: 430px) {
297
+ #nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) { width: 34px; font-size: 0 !important; gap: 0 !important; }
298
+ #nav-bar > div:last-child .icon-btn:not(#codex-terminal-switch) svg { width: 16px; height: 16px; }
299
+ #codex-terminal-switch { padding-left: 8px !important; padding-right: 8px !important; }
300
+ }
301
+ @media (min-width: 768px) and (max-width: 1199px) {
302
+ :root { --chat-content-max: 780px; --control-content-max: 920px; }
303
+ #codex-chat-container { padding: 16px 18px 30px; }
304
+ #input-row, #codex-control-panel { padding-left: 18px; padding-right: 18px; }
305
+ }
306
+ @media (min-width: 1200px) {
307
+ :root { --chat-content-max: 960px; --control-content-max: 1100px; }
308
+ #codex-chat-container { padding: 18px 22px 34px; }
309
+ }
215
310
  </style>
216
311
  </head>
217
312
  <body>
@@ -250,6 +345,7 @@
250
345
  </button>
251
346
  <div id="session-title">Terminal</div>
252
347
  <div style="display: flex; align-items: center; gap: 8px;">
348
+ <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>
253
349
  <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;">
254
350
  <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>
255
351
  HIS
@@ -275,10 +371,10 @@
275
371
  <div class="claude-control-row">
276
372
  <div class="claude-picker-wrap">
277
373
  <select id="claude-permission-select" class="claude-select" title="Permission mode" onchange="updateClaudeSettingsFromControls()">
278
- <option value="default">P:Def</option>
279
- <option value="acceptEdits">P:Edit</option>
280
- <option value="plan">P:Plan</option>
281
- <option value="bypassPermissions">P:Byp</option>
374
+ <option value="default">Default</option>
375
+ <option value="acceptEdits">Accept edits</option>
376
+ <option value="plan">Plan mode</option>
377
+ <option value="bypassPermissions">Bypass</option>
282
378
  </select>
283
379
  <button id="claude-permission-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('permission')" title="Permission mode">P</button>
284
380
  </div>
@@ -298,13 +394,28 @@
298
394
  </select>
299
395
  <button id="claude-effort-picker-btn" class="claude-select claude-picker-btn" onclick="toggleClaudePicker('effort')" title="Effort">E</button>
300
396
  </div>
397
+ <button id="claude-usage-btn" class="claude-ctrl-btn" onclick="toggleClaudeUsagePanel()" title="Usage">Usage</button>
301
398
  <button id="claude-abort-btn" class="claude-ctrl-btn danger" onclick="abortClaudeSession()" title="Abort current Claude turn">Abort</button>
302
399
  <button id="claude-resume-btn" class="claude-ctrl-btn primary" onclick="toggleClaudeResumePanel()" title="Choose a Claude session to resume">Resume</button>
303
400
  </div>
304
401
  <div id="claude-state-bar"></div>
305
402
  <div id="claude-picker-panel"></div>
403
+ <div id="claude-usage-panel"></div>
306
404
  <div id="claude-resume-panel"></div>
307
405
  </div>
406
+ <div id="codex-control-panel">
407
+ <div class="codex-control-row">
408
+ <select id="codex-permission-select" class="codex-select" title="Permission mode" onchange="updateCodexSettingsFromControls()">
409
+ <option value="untrusted">Untrusted</option><option value="on-request">On request</option><option value="never">Never ask</option>
410
+ </select>
411
+ <button id="codex-model-btn" class="claude-ctrl-btn" onclick="toggleCodexModelPanel()" title="Model and effort">Model</button>
412
+ <button id="codex-abort-btn" class="claude-ctrl-btn danger" onclick="abortCodexSession()" title="Abort current Codex turn">Abort</button>
413
+ <button id="codex-resume-btn" class="claude-ctrl-btn primary" onclick="toggleCodexResumePanel()" title="Resume a Codex thread">Resume</button>
414
+ </div>
415
+ <div id="codex-state-bar"></div>
416
+ <div id="codex-model-panel"></div>
417
+ <div id="codex-resume-panel"></div>
418
+ </div>
308
419
  <div id="timed-send-panel">
309
420
  <div class="timed-row">
310
421
  <select id="timed-hours" onchange="updateTimedSendPreview()" title="Hours"></select>
@@ -344,6 +455,7 @@
344
455
  <div id="terminal-controls-spacer"></div>
345
456
  <div id="terminal-container"><div id="terminal"></div></div>
346
457
  <div id="claude-chat-container"></div>
458
+ <div id="codex-chat-container"></div>
347
459
  </div>
348
460
 
349
461
  <!-- Plain Text History View -->
@@ -478,11 +590,14 @@
478
590
  let claudeStatus = 'idle';
479
591
  let claudeRuntimeConfig = null;
480
592
  let claudePickerOpen = null;
593
+ let claudeUsagePanelOpen = false;
594
+ const CLAUDE_CONTEXT_SIZE = 190000;
481
595
  let claudeState = {
482
596
  permissionMode: 'default',
483
597
  model: 'default',
484
598
  effort: 'medium',
485
599
  contextRemaining: null,
600
+ latestUsage: null,
486
601
  claudeSessionId: null,
487
602
  resumeSessionId: null,
488
603
  canAbort: false,
@@ -490,6 +605,12 @@
490
605
  };
491
606
  let claudeResumePanelOpen = false;
492
607
  let claudeResumeItemsLoaded = false;
608
+ let codexMessages = [];
609
+ let codexPendingPermissions = [];
610
+ let codexState = { permissionMode: 'on-request', model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
611
+ let codexModelPanelOpen = false;
612
+ let codexModelCandidate = null;
613
+ let codexResumePanelOpen = false;
493
614
  const modifiers = { ctrl: false };
494
615
 
495
616
  function log(msg) {
@@ -781,25 +902,30 @@
781
902
  return activeToolKey === 'claude-code';
782
903
  }
783
904
 
905
+ function isCodexSession() {
906
+ return activeToolKey === 'codex';
907
+ }
908
+
784
909
  function setClaudeModeEnabled(enabled) {
785
- document.getElementById('terminal-container').style.display = enabled ? 'none' : '';
910
+ const codexChat = isCodexSession() && codexState.presentation === 'structured';
911
+ const structured = enabled || codexChat;
912
+ document.getElementById('terminal-container').style.display = structured ? 'none' : '';
786
913
  document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
914
+ document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
787
915
  document.getElementById('claude-control-panel').style.display = enabled ? 'block' : 'none';
788
- document.getElementById('timer-btn').style.display = enabled ? 'none' : '';
789
- if (enabled) closeTimedSendPanel();
790
- document.getElementById('shortcut-rail').style.display = enabled ? 'none' : '';
791
- document.getElementById('scroll-controls').style.display = enabled ? 'none' : '';
792
- document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : 'Type a message...';
916
+ document.getElementById('codex-control-panel').style.display = codexChat ? 'block' : 'none';
917
+ document.getElementById('codex-terminal-switch').style.display = isCodexSession() ? '' : 'none';
918
+ document.getElementById('timer-btn').style.display = '';
919
+ document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
920
+ document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
921
+ document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
793
922
  updateTerminalControlsHeight();
794
923
  }
795
924
 
796
925
  function updateTerminalControlsHeight() {
797
- if (!isClaudeSession()) {
798
- document.documentElement.style.setProperty('--terminal-controls-rest-height', '108px');
799
- return;
800
- }
801
- const restHeight = claudeResumePanelOpen ? '308px' : (claudePickerOpen ? '322px' : '154px');
802
- document.documentElement.style.setProperty('--terminal-controls-rest-height', restHeight);
926
+ const controls = document.getElementById('terminal-controls');
927
+ const measuredHeight = controls ? Math.ceil(controls.getBoundingClientRect().height) : 0;
928
+ document.documentElement.style.setProperty('--terminal-controls-rest-height', `${Math.max(0, measuredHeight)}px`);
803
929
  }
804
930
 
805
931
  function shortModelLabel(value, resolved) {
@@ -817,6 +943,35 @@
817
943
  return `M:${compact || source.slice(0, 9)}`;
818
944
  }
819
945
 
946
+ function fullModelLabel(value, resolved, label) {
947
+ const source = String(value || 'default');
948
+ if (source === 'default') return 'Default';
949
+ if (source === 'env') return resolved ? `Environment (${resolved})` : 'Environment';
950
+ if (label && label !== value) return label;
951
+ return String(resolved || value || 'Model');
952
+ }
953
+
954
+ function permissionModeLabel(value) {
955
+ const labels = {
956
+ default: 'Default',
957
+ acceptEdits: 'Accept edits',
958
+ plan: 'Plan mode',
959
+ bypassPermissions: 'Bypass'
960
+ };
961
+ return labels[value] || String(value || 'Default');
962
+ }
963
+
964
+ function effortLabel(value) {
965
+ const labels = {
966
+ low: 'Low',
967
+ medium: 'Medium',
968
+ high: 'High',
969
+ xhigh: 'Extra high',
970
+ max: 'Max'
971
+ };
972
+ return labels[value] || String(value || 'Medium');
973
+ }
974
+
820
975
  function pickerLabelFromOption(option) {
821
976
  return option ? (option.textContent || option.value || '') : '';
822
977
  }
@@ -828,19 +983,19 @@
828
983
 
829
984
  function syncClaudePickerButtons() {
830
985
  const mappings = [
831
- ['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'P'],
832
- ['model', 'claude-model-select', 'claude-model-picker-btn', 'M'],
833
- ['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'E']
986
+ ['permission', 'claude-permission-select', 'claude-permission-picker-btn', 'Perm'],
987
+ ['model', 'claude-model-select', 'claude-model-picker-btn', 'Model'],
988
+ ['effort', 'claude-effort-select', 'claude-effort-picker-btn', 'Effort']
834
989
  ];
835
- mappings.forEach(([type, selectId, buttonId, fixedLabel]) => {
990
+ mappings.forEach(([type, selectId, buttonId, prefix]) => {
836
991
  const select = document.getElementById(selectId);
837
992
  const button = document.getElementById(buttonId);
838
993
  if (!select || !button) return;
839
994
  const option = select.selectedOptions && select.selectedOptions[0];
840
995
  const label = pickerLabelFromOption(option);
841
996
  const fullValue = pickerFullValue(option);
842
- button.textContent = fixedLabel;
843
- button.title = fullValue || label || fixedLabel;
997
+ button.textContent = prefix;
998
+ button.title = fullValue || label || prefix;
844
999
  button.classList.toggle('active', claudePickerOpen === type);
845
1000
  });
846
1001
  }
@@ -856,6 +1011,13 @@
856
1011
  updateTerminalControlsHeight();
857
1012
  }
858
1013
 
1014
+ function closeClaudeUsagePanel() {
1015
+ claudeUsagePanelOpen = false;
1016
+ const panel = document.getElementById('claude-usage-panel');
1017
+ if (panel) panel.classList.remove('active');
1018
+ updateTerminalControlsHeight();
1019
+ }
1020
+
859
1021
  function pickerTitle(type) {
860
1022
  if (type === 'permission') return 'Permission mode';
861
1023
  if (type === 'model') return 'Model';
@@ -881,7 +1043,7 @@
881
1043
  return `<button class="claude-picker-option${selected ? ' selected' : ''}" onclick="chooseClaudePickerOption('${type}', decodePathValue('${encodePathValue(option.value)}'))">
882
1044
  <div class="claude-picker-option-main">
883
1045
  <span>${escapeHtml(option.textContent || option.value)}</span>
884
- ${selected ? '<span>Selected</span>' : ''}
1046
+ ${selected ? '<span class="selected-label">Selected</span>' : ''}
885
1047
  </div>
886
1048
  ${full ? `<div class="claude-picker-option-value">${escapeHtml(full)}</div>` : ''}
887
1049
  </button>`;
@@ -897,6 +1059,7 @@
897
1059
  return;
898
1060
  }
899
1061
  if (type === 'model') await refreshClaudeRuntimeConfig();
1062
+ closeClaudeUsagePanel();
900
1063
  claudeResumePanelOpen = false;
901
1064
  const resumePanel = document.getElementById('claude-resume-panel');
902
1065
  if (resumePanel) resumePanel.classList.remove('active');
@@ -920,9 +1083,10 @@
920
1083
  const current = modelEl.value || claudeState.model || config.defaultModel || 'default';
921
1084
  modelEl.innerHTML = '';
922
1085
  config.models.forEach(item => {
923
- const option = new Option(shortModelLabel(item.value, item.resolved), item.value);
1086
+ const option = new Option(fullModelLabel(item.value, item.resolved, item.label), item.value);
924
1087
  option.title = item.resolved ? `${item.label}: ${item.resolved}` : item.label;
925
1088
  option.dataset.resolved = item.resolved || '';
1089
+ option.dataset.shortLabel = shortModelLabel(item.value, item.resolved);
926
1090
  modelEl.add(option);
927
1091
  });
928
1092
  const next = Array.from(modelEl.options).some(option => option.value === current)
@@ -977,6 +1141,8 @@
977
1141
  const abortBtn = document.getElementById('claude-abort-btn');
978
1142
  if (abortBtn) abortBtn.disabled = !(claudeState.canAbort || claudeStatus === 'thinking');
979
1143
  renderClaudeStateBar();
1144
+ renderClaudeUsagePanel();
1145
+ renderClaudeChat();
980
1146
  }
981
1147
 
982
1148
  function shortValue(value, fallback = 'N/A') {
@@ -984,26 +1150,90 @@
984
1150
  return text.length > 16 ? text.slice(0, 13) + '...' : text;
985
1151
  }
986
1152
 
1153
+ function formatTokenCount(value) {
1154
+ const number = Number(value || 0);
1155
+ if (number >= 1000000) return `${(number / 1000000).toFixed(2)}M`;
1156
+ if (number >= 1000) return `${(number / 1000).toFixed(1)}K`;
1157
+ return String(Math.round(number));
1158
+ }
1159
+
1160
+ function formatContextK(value) {
1161
+ return `${(Number(value || 0) / 1000).toFixed(1)}K`;
1162
+ }
1163
+
1164
+ function contextRemainingPercent(usage) {
1165
+ if (!usage || typeof usage.contextSize !== 'number') return null;
1166
+ const used = Math.max(0, usage.contextSize);
1167
+ return Math.max(0, Math.min(100, 100 - (used / CLAUDE_CONTEXT_SIZE) * 100));
1168
+ }
1169
+
1170
+ function renderClaudeContextSizeBadge() {
1171
+ const usage = claudeState.latestUsage;
1172
+ if (!usage || typeof usage.contextSize !== 'number') return '';
1173
+ return `<div class="claude-context-size-badge">Context ${escapeHtml(formatContextK(usage.contextSize))}</div>`;
1174
+ }
1175
+
987
1176
  function renderClaudeStateBar() {
988
1177
  const el = document.getElementById('claude-state-bar');
989
1178
  if (!el) return;
990
- const modelEl = document.getElementById('claude-model-select');
991
- const modelOption = modelEl && modelEl.selectedOptions && modelEl.selectedOptions[0];
992
- const modelLabel = modelOption && modelOption.dataset.resolved
993
- ? modelOption.dataset.resolved
994
- : (claudeState.model || 'default');
995
- const context = typeof claudeState.contextRemaining === 'number'
996
- ? `Ctx ${claudeState.contextRemaining}`
997
- : 'Ctx N/A';
998
1179
  const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
999
- const status = claudeStatus === 'thinking' ? 'Working' : shortValue(claudeStatus || 'Idle');
1000
- el.innerHTML = [
1001
- `<span class="claude-state-pill">${escapeHtml(shortValue(claudeState.permissionMode || 'default'))}</span>`,
1002
- `<span class="claude-state-pill">${escapeHtml(shortValue(modelLabel))}</span>`,
1003
- `<span class="claude-state-pill">Eff ${escapeHtml(shortValue(claudeState.effort || 'medium'))}</span>`,
1004
- `<span class="claude-state-pill">${escapeHtml(context)}</span>`,
1005
- `<span class="claude-state-pill${pending ? ' warn' : ''}">${pending ? `${pending} approval` : escapeHtml(status)}</span>`
1006
- ].join('');
1180
+ const mode = claudeState.permissionMode || 'default';
1181
+ const parts = [];
1182
+ if (pending) {
1183
+ parts.push(`<span class="claude-state-pill warn">${pending} approval${pending > 1 ? 's' : ''}</span>`);
1184
+ } else if (claudeStatus === 'thinking') {
1185
+ parts.push('<span class="claude-state-pill">Working</span>');
1186
+ } else if (claudeStatus && !['idle', 'stopped'].includes(claudeStatus)) {
1187
+ parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
1188
+ }
1189
+ const remainingPercent = contextRemainingPercent(claudeState.latestUsage);
1190
+ if (remainingPercent !== null && remainingPercent <= 10) {
1191
+ parts.push(`<span class="claude-state-pill warn">${Math.round(remainingPercent)}% left</span>`);
1192
+ }
1193
+ if (mode !== 'default') {
1194
+ parts.push(`<span class="claude-state-pill perm ${escapeHtml(mode)}">${escapeHtml(permissionModeLabel(mode))}</span>`);
1195
+ }
1196
+ el.innerHTML = parts.join('');
1197
+ el.style.display = parts.length ? 'flex' : 'none';
1198
+ }
1199
+
1200
+ function usageItem(label, value) {
1201
+ return `<div class="claude-usage-item">
1202
+ <div class="claude-usage-label">${escapeHtml(label)}</div>
1203
+ <div class="claude-usage-value">${escapeHtml(value)}</div>
1204
+ </div>`;
1205
+ }
1206
+
1207
+ function renderClaudeUsagePanel() {
1208
+ const panel = document.getElementById('claude-usage-panel');
1209
+ if (!panel) return;
1210
+ const usage = claudeState.latestUsage;
1211
+ if (!usage) {
1212
+ panel.innerHTML = '<div class="claude-usage-title">Usage</div><div class="claude-resume-meta">No usage yet.</div>';
1213
+ } else {
1214
+ panel.innerHTML = `<div class="claude-usage-title">Usage</div>
1215
+ <div class="claude-usage-grid">
1216
+ ${usageItem('Total', formatTokenCount(usage.totalTokens))}
1217
+ ${usageItem('Context', formatContextK(usage.contextSize))}
1218
+ ${usageItem('Input', formatTokenCount(usage.inputTokens))}
1219
+ ${usageItem('Output', formatTokenCount(usage.outputTokens))}
1220
+ ${usageItem('Cache write', formatTokenCount(usage.cacheCreation))}
1221
+ ${usageItem('Cache read', formatTokenCount(usage.cacheRead))}
1222
+ </div>`;
1223
+ }
1224
+ panel.classList.toggle('active', claudeUsagePanelOpen);
1225
+ }
1226
+
1227
+ function toggleClaudeUsagePanel() {
1228
+ claudeUsagePanelOpen = !claudeUsagePanelOpen;
1229
+ if (claudeUsagePanelOpen) {
1230
+ closeClaudePicker();
1231
+ claudeResumePanelOpen = false;
1232
+ const resumePanel = document.getElementById('claude-resume-panel');
1233
+ if (resumePanel) resumePanel.classList.remove('active');
1234
+ }
1235
+ renderClaudeUsagePanel();
1236
+ updateTerminalControlsHeight();
1007
1237
  }
1008
1238
 
1009
1239
  async function updateClaudeSettingsFromControls() {
@@ -1027,6 +1257,7 @@
1027
1257
  async function toggleClaudeResumePanel() {
1028
1258
  claudeResumePanelOpen = !claudeResumePanelOpen;
1029
1259
  if (claudeResumePanelOpen) closeClaudePicker();
1260
+ if (claudeResumePanelOpen) closeClaudeUsagePanel();
1030
1261
  const panel = document.getElementById('claude-resume-panel');
1031
1262
  panel.classList.toggle('active', claudeResumePanelOpen);
1032
1263
  updateTerminalControlsHeight();
@@ -1360,9 +1591,41 @@
1360
1591
  return '';
1361
1592
  }
1362
1593
 
1594
+ function isClaudeEditTool(name) {
1595
+ return ['Edit', 'MultiEdit', 'Write', 'NotebookEdit'].includes(name || '');
1596
+ }
1597
+
1598
+ function isClaudeExitPlanTool(name) {
1599
+ return name === 'exit_plan_mode' || name === 'ExitPlanMode';
1600
+ }
1601
+
1602
+ function claudeAllowToolLabel(req) {
1603
+ if (req.toolName === 'Bash' && req.input && typeof req.input.command === 'string') return 'Allow command';
1604
+ return 'Allow tool';
1605
+ }
1606
+
1607
+ function renderClaudePermissionActions(req) {
1608
+ const id = escapeHtml(req.id);
1609
+ const toolName = req.toolName || '';
1610
+ const parts = [
1611
+ `<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-once')">Yes</button>`
1612
+ ];
1613
+ if (isClaudeEditTool(toolName) || isClaudeExitPlanTool(toolName) || req.canAllowEdits) {
1614
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-edits')">Allow edits</button>`);
1615
+ }
1616
+ if (isClaudeExitPlanTool(toolName) || req.canBypass) {
1617
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'bypass')">Allow all</button>`);
1618
+ }
1619
+ if (toolName && !isClaudeEditTool(toolName) && !isClaudeExitPlanTool(toolName) && req.canAllowTool !== false) {
1620
+ parts.push(`<button class="small-btn primary" onclick="respondClaudePermission('${id}', 'allow-tool')">${escapeHtml(claudeAllowToolLabel(req))}</button>`);
1621
+ }
1622
+ parts.push(`<button class="small-btn danger" onclick="respondClaudePermission('${id}', 'deny')">Deny</button>`);
1623
+ return parts.join('');
1624
+ }
1625
+
1363
1626
  function renderClaudeChat() {
1364
1627
  const container = document.getElementById('claude-chat-container');
1365
- const parts = buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem);
1628
+ const parts = [renderClaudeContextSizeBadge(), ...buildClaudeDisplayItems(claudeMessages).map(renderDisplayItem)].filter(Boolean);
1366
1629
  for (const req of claudePendingPermissions.filter(item => item.status === 'pending')) {
1367
1630
  parts.push(`<div class="claude-tool claude-permission">
1368
1631
  <div class="claude-tool-header">
@@ -1371,10 +1634,10 @@
1371
1634
  </div>
1372
1635
  <div class="claude-tool-body">
1373
1636
  ${req.reason ? `<div>${escapeHtml(req.reason)}</div>` : ''}
1637
+ ${req.blockedPath ? `<div class="claude-resume-meta">${escapeHtml(req.blockedPath)}</div>` : ''}
1374
1638
  ${renderToolSection('Input', req.input || {})}
1375
1639
  <div class="claude-permission-actions">
1376
- <button class="small-btn danger" onclick="respondClaudePermission('${req.id}', false)">Deny</button>
1377
- <button class="small-btn primary" onclick="respondClaudePermission('${req.id}', true)">Allow</button>
1640
+ ${renderClaudePermissionActions(req)}
1378
1641
  </div>
1379
1642
  </div>
1380
1643
  </div>`);
@@ -1411,9 +1674,17 @@
1411
1674
  renderClaudeChat();
1412
1675
  }
1413
1676
 
1414
- function respondClaudePermission(id, approved) {
1677
+ function respondClaudePermission(id, actionOrApproved) {
1415
1678
  if (!currentSocket || currentSocket.readyState !== 1) return;
1416
- currentSocket.send(JSON.stringify({ type: 'claude-permission', id, approved }));
1679
+ const action = typeof actionOrApproved === 'string'
1680
+ ? actionOrApproved
1681
+ : (actionOrApproved ? 'allow-once' : 'deny');
1682
+ currentSocket.send(JSON.stringify({
1683
+ type: 'claude-permission',
1684
+ id,
1685
+ action,
1686
+ approved: action !== 'deny'
1687
+ }));
1417
1688
  }
1418
1689
 
1419
1690
  async function createSession(toolKey, sessionName) {
@@ -1739,6 +2010,214 @@
1739
2010
  }));
1740
2011
  }
1741
2012
 
2013
+ function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
2014
+ function codexJson(value) {
2015
+ if (typeof value === 'string') return value;
2016
+ try { return JSON.stringify(value || {}, null, 2); } catch (_) { return String(value || ''); }
2017
+ }
2018
+ function codexToolStatus(item) {
2019
+ const status = item.toolStatus || 'running';
2020
+ if (status === 'inProgress') return 'running';
2021
+ if (status === 'failed' || status === 'declined') return status;
2022
+ if (status === 'completed') return item.exitCode && item.exitCode !== 0 ? 'failed' : 'completed';
2023
+ return status;
2024
+ }
2025
+ function renderCodexDiff(diff) {
2026
+ return `<div class="codex-diff">${String(diff || '').split('\n').map(line => {
2027
+ const type = line.startsWith('+++') || line.startsWith('---') ? 'hunk' : line.startsWith('+') ? 'add' : line.startsWith('-') ? 'del' : line.startsWith('@@') ? 'hunk' : '';
2028
+ return `<div class="codex-diff-line ${type}">${escapeHtml(line || ' ')}</div>`;
2029
+ }).join('')}</div>`;
2030
+ }
2031
+ function normalizeCodexChanges(changes) {
2032
+ if (Array.isArray(changes)) return changes.filter(Boolean).map(change => [change.path || 'File', change]);
2033
+ if (changes && typeof changes === 'object') return Object.entries(changes);
2034
+ return [];
2035
+ }
2036
+ function codexChangeDiff(change = {}) {
2037
+ if (typeof change.diff === 'string') return change.diff;
2038
+ if (typeof change.unified_diff === 'string') return change.unified_diff;
2039
+ const kind = change.kind && typeof change.kind === 'object' ? change.kind : {};
2040
+ const type = change.type || kind.type || '';
2041
+ const oldText = change.modify?.old_content ?? change.old_content ?? change.oldContent ?? (type === 'delete' ? change.content : change.delete?.content) ?? '';
2042
+ const newText = change.modify?.new_content ?? change.new_content ?? change.newContent ?? (type === 'add' ? change.content : change.add?.content) ?? '';
2043
+ if (!oldText && !newText) return '';
2044
+ return `${String(oldText).split('\n').map(line => `-${line}`).join('\n')}\n${String(newText).split('\n').map(line => `+${line}`).join('\n')}`;
2045
+ }
2046
+ function renderCodexPatch(item) {
2047
+ const entries = normalizeCodexChanges(item.changes || item.input?.changes);
2048
+ if (!entries.length) return `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(codexJson(item.input))}</pre></div>`;
2049
+ return entries.map(([path, change]) => {
2050
+ const kind = change?.type || change?.kind?.type || 'edit';
2051
+ const move = change?.move_path || change?.kind?.move_path;
2052
+ const diff = codexChangeDiff(change);
2053
+ 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>`;
2054
+ }).join('');
2055
+ }
2056
+ function renderCodexPermission(request, inline = false) {
2057
+ if (!request) return '';
2058
+ const pending = request.status === 'pending';
2059
+ const labels = { approved: 'Allowed once', approved_for_session: 'Allowed for session', denied: 'Denied', abort: 'Stopped' };
2060
+ const detail = request.reason || (request.input && Object.keys(request.input).length ? codexJson(request.input) : '');
2061
+ 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>`}`;
2062
+ if (inline) return `<div class="codex-inline-permission">${content}</div>`;
2063
+ return `<div class="claude-tool claude-permission"><div class="claude-tool-header"><strong>${escapeHtml(request.title || 'Permission required')}</strong></div><div class="claude-tool-body">${content}</div></div>`;
2064
+ }
2065
+ function renderCodexTool(item, permission = null) {
2066
+ const status = codexToolStatus(item);
2067
+ const isError = status === 'failed' || Boolean(item.error) || (item.exitCode != null && item.exitCode !== 0);
2068
+ const runningClass = status === 'running' ? ' running' : '';
2069
+ if (item.name === 'CodexPatch') {
2070
+ return `<div class="codex-tool${isError ? ' error' : ''}">${renderCodexPatch(item)}${permission ? renderCodexPermission(permission, true) : ''}</div>`;
2071
+ }
2072
+ const command = item.name === 'CodexBash' ? item.command : item.title || item.tool || '';
2073
+ const icon = item.name === 'CodexBash' ? '>_' : item.name === 'McpTool' ? 'MCP' : item.name === 'Agent' ? 'A' : '•';
2074
+ const title = item.name === 'CodexBash' ? 'Command' : item.title || item.name || 'Tool';
2075
+ const result = item.result || (item.name === 'McpTool' || item.name === 'Agent' ? codexJson(item.input) : '');
2076
+ return `<details class="codex-tool${isError ? ' error' : ''}"><summary class="codex-tool-header"><span class="codex-tool-icon">${escapeHtml(icon)}</span><span class="codex-tool-title">${escapeHtml(title)}</span>${command && command !== title ? `<span class="codex-tool-command">${escapeHtml(command)}</span>` : '<span class="codex-tool-command"></span>'}<span class="codex-tool-state${runningClass}">${escapeHtml(status === 'completed' ? '' : status)}</span></summary>${result ? `<div class="codex-tool-body"><pre class="codex-tool-code">${escapeHtml(result)}</pre></div>` : ''}${permission ? renderCodexPermission(permission, true) : ''}</details>`;
2077
+ }
2078
+ function renderCodexToolGroup(items, permissionById, usedPermissions) {
2079
+ const running = items.some(item => codexToolStatus(item) === 'running');
2080
+ const failed = items.some(item => codexToolStatus(item) === 'failed' || item.error);
2081
+ const startedAt = Math.min(...items.map(item => Number(item.createdAt || Date.now())));
2082
+ const seconds = Math.max(1, Math.round((Date.now() - startedAt) / 1000));
2083
+ const tools = items.map(item => {
2084
+ const permission = permissionById.get(String(item.providerId || ''));
2085
+ if (permission) usedPermissions.add(permission.id);
2086
+ return renderCodexTool(item, permission);
2087
+ }).join('');
2088
+ 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>`;
2089
+ }
2090
+ function renderCodexChat() {
2091
+ const container = document.getElementById('codex-chat-container');
2092
+ if (!container) return;
2093
+ const parts = [];
2094
+ const permissionById = new Map(codexPendingPermissions.map(item => [String(item.id), item]));
2095
+ const usedPermissions = new Set();
2096
+ const visible = codexMessages.filter(item => !['reasoning', 'turn-start', 'turn-end'].includes(item.kind));
2097
+ for (let i = 0; i < visible.length;) {
2098
+ const item = visible[i];
2099
+ if (item.kind === 'tool') {
2100
+ const tools = [];
2101
+ while (i < visible.length && visible[i].kind === 'tool') tools.push(visible[i++]);
2102
+ if (tools.length > 1) parts.push(renderCodexToolGroup(tools, permissionById, usedPermissions));
2103
+ else {
2104
+ const permission = permissionById.get(String(tools[0].providerId || ''));
2105
+ if (permission) usedPermissions.add(permission.id);
2106
+ parts.push(renderCodexTool(tools[0], permission));
2107
+ }
2108
+ continue;
2109
+ }
2110
+ if (item.kind === 'assistant') parts.push(`<div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>`);
2111
+ else if (item.kind === 'user') parts.push(`<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}</div>`);
2112
+ else parts.push(`<div class="codex-message ${escapeHtml(item.level === 'error' ? 'event error' : item.kind || 'event')}">${codexText(item.text)}</div>`);
2113
+ i += 1;
2114
+ }
2115
+ for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
2116
+ container.innerHTML = `<div class="codex-conversation">${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
2117
+ requestAnimationFrame(() => { container.scrollTop = container.scrollHeight; });
2118
+ }
2119
+
2120
+ function applyCodexState(state = {}) {
2121
+ codexState = { ...codexState, ...state };
2122
+ const permission = document.getElementById('codex-permission-select');
2123
+ if (permission) permission.value = codexState.permissionMode || 'on-request';
2124
+ const modelButton = document.getElementById('codex-model-btn');
2125
+ if (modelButton) modelButton.textContent = codexState.model ? codexState.model.replace(/^gpt-/, '').slice(0, 10) : 'Model';
2126
+ const abort = document.getElementById('codex-abort-btn');
2127
+ if (abort) abort.disabled = !codexState.canAbort;
2128
+ const terminal = document.getElementById('codex-terminal-switch');
2129
+ if (terminal) { terminal.textContent = codexState.presentation === 'terminal' ? 'CHAT' : 'TERM'; terminal.disabled = codexState.presentation === 'structured' && !codexState.canSwitchToTerminal; terminal.title = codexState.presentation === 'terminal' ? 'Return to Codex chat' : 'Switch to Codex terminal'; }
2130
+ renderCodexStateBar();
2131
+ renderCodexModelPanel();
2132
+ setClaudeModeEnabled(isClaudeSession());
2133
+ renderCodexChat();
2134
+ }
2135
+
2136
+ function renderCodexStateBar() {
2137
+ const el = document.getElementById('codex-state-bar');
2138
+ if (!el) return;
2139
+ const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
2140
+ const parts = [];
2141
+ if (pending || codexState.status === 'waiting_approval') parts.push(`<span class="claude-state-pill warn">${pending || 1} approval${pending === 1 ? '' : 's'}</span>`);
2142
+ else if (codexState.status === 'running') parts.push('<span class="claude-state-pill">Working</span>');
2143
+ el.innerHTML = parts.join('');
2144
+ el.style.display = parts.length ? 'flex' : 'none';
2145
+ }
2146
+
2147
+ function toggleCodexModelPanel() {
2148
+ codexModelPanelOpen = !codexModelPanelOpen;
2149
+ codexResumePanelOpen = false;
2150
+ codexModelCandidate = codexState.model || codexState.models?.[0]?.id || null;
2151
+ document.getElementById('codex-resume-panel').classList.remove('active');
2152
+ renderCodexModelPanel();
2153
+ updateTerminalControlsHeight();
2154
+ }
2155
+
2156
+ function renderCodexModelPanel() {
2157
+ const panel = document.getElementById('codex-model-panel');
2158
+ if (!panel) return;
2159
+ panel.classList.toggle('active', codexModelPanelOpen);
2160
+ if (!codexModelPanelOpen) { panel.innerHTML = ''; return; }
2161
+ const models = codexState.models || [];
2162
+ const candidate = models.find(item => item.id === codexModelCandidate) || models.find(item => item.id === codexState.model) || models[0];
2163
+ if (!candidate) { panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading models...</div>'; return; }
2164
+ codexModelCandidate = candidate.id;
2165
+ const efforts = candidate.efforts?.length ? candidate.efforts : [candidate.defaultEffort || 'medium'];
2166
+ 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>`;
2167
+ }
2168
+
2169
+ function selectCodexModelCandidate(model) { codexModelCandidate = model; renderCodexModelPanel(); }
2170
+ function chooseCodexModelEffort(model, effort) {
2171
+ codexModelPanelOpen = false;
2172
+ applyCodexState({ model, effort });
2173
+ sendCodexSettings({ model, effort });
2174
+ updateTerminalControlsHeight();
2175
+ }
2176
+
2177
+ function applyCodexEvent(event) {
2178
+ if (!event) return;
2179
+ if (event.type === 'message' && event.message) codexMessages.push(event.message);
2180
+ 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); }
2181
+ else if (event.type === 'history-reset') codexMessages = event.messages || [];
2182
+ else if (event.type === 'permission-request' && event.request) { codexPendingPermissions = [...codexPendingPermissions.filter(item => item.id !== event.request.id), event.request]; }
2183
+ else if (event.type === 'permission-updated' && event.request) codexPendingPermissions = codexPendingPermissions.map(item => item.id === event.request.id ? event.request : item);
2184
+ if (event.state) applyCodexState(event.state); else renderCodexChat();
2185
+ }
2186
+
2187
+ function sendCodexSettings(settings) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-settings', settings })); }
2188
+ function updateCodexSettingsFromControls() { const permissionMode = document.getElementById('codex-permission-select').value; applyCodexState({ permissionMode }); sendCodexSettings({ permissionMode }); }
2189
+ function abortCodexSession() { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-abort' })); }
2190
+ function respondCodexPermission(id, decision) { if (currentSocket?.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-permission', id, decision })); }
2191
+ async function toggleCodexResumePanel() {
2192
+ codexResumePanelOpen = !codexResumePanelOpen;
2193
+ codexModelPanelOpen = false;
2194
+ document.getElementById('codex-model-panel').classList.remove('active');
2195
+ const panel = document.getElementById('codex-resume-panel');
2196
+ panel.classList.toggle('active', codexResumePanelOpen);
2197
+ updateTerminalControlsHeight();
2198
+ if (!codexResumePanelOpen) return;
2199
+ panel.innerHTML = '<div class="claude-resume-meta" style="padding:12px;">Loading sessions...</div>';
2200
+ try {
2201
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume-threads`, {}, 30000);
2202
+ const data = await res.json();
2203
+ if (!res.ok || !data.success) throw new Error(data.error || 'Failed to load Codex sessions');
2204
+ const items = data.items || [];
2205
+ 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>';
2206
+ } catch (e) { panel.innerHTML = `<div class="claude-resume-meta" style="padding:12px;color:#ff6b61;">${escapeHtml(e.message)}</div>`; }
2207
+ }
2208
+ async function selectCodexResumeThread(threadId) {
2209
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-resume`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ threadId }) }, 30000);
2210
+ if (!res.ok) { alert((await res.json()).error || 'Unable to resume Codex thread'); return; }
2211
+ codexResumePanelOpen = false;
2212
+ document.getElementById('codex-resume-panel').classList.remove('active');
2213
+ updateTerminalControlsHeight();
2214
+ }
2215
+ async function toggleCodexPresentation() {
2216
+ const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
2217
+ const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
2218
+ if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
2219
+ }
2220
+
1742
2221
  function joinSession(id, sessionName, toolKey = null) {
1743
2222
  stopTimedInputTimers();
1744
2223
  activeSessionId = id;
@@ -1762,6 +2241,10 @@
1762
2241
  initClaudeSession(sessionId);
1763
2242
  return;
1764
2243
  }
2244
+ if (isCodexSession() && codexState.presentation === 'structured') {
2245
+ initCodexSession(sessionId);
2246
+ return;
2247
+ }
1765
2248
  if (typeof Terminal === 'undefined') {
1766
2249
  alert('Terminal library not loaded yet.');
1767
2250
  showLobby();
@@ -1795,6 +2278,14 @@
1795
2278
  const msg = JSON.parse(e.data);
1796
2279
  if (msg.type === 'output') term.write(msg.data);
1797
2280
  if (msg.type === 'reset') term.reset();
2281
+ if (msg.type === 'codex-event' && isCodexSession()) {
2282
+ applyCodexEvent(msg.event);
2283
+ if (msg.event?.type === 'presentation' && msg.event.presentation === 'structured') {
2284
+ currentSocket.close();
2285
+ currentSocket = null;
2286
+ initCodexSession(sessionId);
2287
+ }
2288
+ }
1798
2289
  if (msg.type === 'exit') showLobby();
1799
2290
  };
1800
2291
  }
@@ -1809,6 +2300,7 @@
1809
2300
  model: (runtimeConfig && runtimeConfig.defaultModel) || 'default',
1810
2301
  effort: (runtimeConfig && runtimeConfig.defaultEffort) || 'medium',
1811
2302
  contextRemaining: null,
2303
+ latestUsage: null,
1812
2304
  claudeSessionId: null,
1813
2305
  resumeSessionId: null,
1814
2306
  canAbort: false,
@@ -1845,6 +2337,38 @@
1845
2337
  };
1846
2338
  }
1847
2339
 
2340
+ function initCodexSession(sessionId) {
2341
+ codexMessages = [];
2342
+ codexPendingPermissions = [];
2343
+ codexModelPanelOpen = false;
2344
+ codexModelCandidate = null;
2345
+ codexResumePanelOpen = false;
2346
+ document.getElementById('codex-model-panel').classList.remove('active');
2347
+ document.getElementById('codex-resume-panel').classList.remove('active');
2348
+ codexState = { permissionMode: 'on-request', model: null, effort: null, status: 'idle', threadId: null, presentation: 'structured', models: [], canAbort: false, canSwitchToTerminal: false, canSwitchToStructured: false };
2349
+ setClaudeModeEnabled(false);
2350
+ applyCodexState(codexState);
2351
+ const protocol = window.location.protocol === 'https:' ? 'wss:' : 'ws:';
2352
+ currentSocket = new WebSocket(protocol + '//' + window.location.host + '?sessionId=' + sessionId);
2353
+ currentSocket.onmessage = (e) => {
2354
+ const msg = JSON.parse(e.data);
2355
+ if (msg.type === 'codex-snapshot' && msg.snapshot) {
2356
+ codexMessages = msg.snapshot.messages || [];
2357
+ codexPendingPermissions = msg.snapshot.pendingPermissions || [];
2358
+ applyCodexState(msg.snapshot.state || {});
2359
+ }
2360
+ if (msg.type === 'codex-event') {
2361
+ applyCodexEvent(msg.event);
2362
+ if (msg.event?.type === 'presentation' && msg.event.presentation === 'terminal') {
2363
+ currentSocket.close();
2364
+ currentSocket = null;
2365
+ initTerminal(sessionId);
2366
+ }
2367
+ }
2368
+ if (msg.type === 'exit') showLobby();
2369
+ };
2370
+ }
2371
+
1848
2372
  function sendWS(data) {
1849
2373
  if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'input', data }));
1850
2374
  }
@@ -2063,6 +2587,12 @@
2063
2587
  inputEl.style.height = '38px';
2064
2588
  return;
2065
2589
  }
2590
+ if (val && isCodexSession() && codexState.presentation === 'structured') {
2591
+ if (currentSocket && currentSocket.readyState === 1) currentSocket.send(JSON.stringify({ type: 'codex-input', text: val }));
2592
+ inputEl.value = '';
2593
+ inputEl.style.height = '38px';
2594
+ return;
2595
+ }
2066
2596
  if (val) {
2067
2597
  const formattedVal = val.replace(/\n/g, '\r');
2068
2598
  sendWS(formattedVal);
@@ -2142,6 +2672,7 @@
2142
2672
  document.getElementById('timer-btn').classList.remove('active');
2143
2673
  editingTimedInputId = null;
2144
2674
  renderTimedTags();
2675
+ updateTerminalControlsHeight();
2145
2676
  }
2146
2677
 
2147
2678
  function stopTimedInputTimers() {
@@ -2202,6 +2733,7 @@
2202
2733
  document.getElementById('timer-btn').classList.add('active');
2203
2734
  updateTimedSendPreview();
2204
2735
  renderTimedTags();
2736
+ updateTerminalControlsHeight();
2205
2737
  }
2206
2738
 
2207
2739
  async function saveTimedSend() {
@@ -2250,12 +2782,19 @@
2250
2782
  const isOpen = panel.classList.toggle('active');
2251
2783
  document.getElementById('timer-btn').classList.toggle('active', isOpen);
2252
2784
  if (isOpen) {
2785
+ if (isClaudeSession()) {
2786
+ closeClaudePicker();
2787
+ closeClaudeUsagePanel();
2788
+ claudeResumePanelOpen = false;
2789
+ document.getElementById('claude-resume-panel').classList.remove('active');
2790
+ }
2253
2791
  initTimedDelaySelectors();
2254
2792
  resetTimedEditor({ keepInput: true });
2255
2793
  updateTimedSendPreview();
2256
2794
  loadTimedInputs();
2257
2795
  }
2258
2796
  else closeTimedSendPanel();
2797
+ updateTerminalControlsHeight();
2259
2798
  });
2260
2799
  inputEl.addEventListener('keydown', (e) => {
2261
2800
  if (e.key === 'Enter' && e.shiftKey) { e.preventDefault(); performSend(); }