glad-web 1.0.43 → 1.0.45
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -0
- package/README.zh-CN.md +2 -0
- package/lib/claude/structured-session.js +10 -5
- package/lib/codex/image-store.js +1 -2
- package/lib/codex/structured-session.js +24 -73
- package/lib/commands/web.js +43 -5
- package/lib/server/routes/providers.js +0 -12
- package/lib/session/file-attachment-store.js +168 -0
- package/lib/session/session-manager.js +75 -23
- package/lib/web/claude.js +28 -11
- package/lib/web/codex.js +49 -23
- package/lib/web/composer.js +263 -34
- package/lib/web/core.js +22 -6
- package/lib/web/git.js +53 -51
- package/lib/web/gitgraph.js +8 -8
- package/lib/web/index.html +94 -36
- package/lib/web/layout.js +72 -0
- package/lib/web/notifications.js +1 -0
- package/lib/web/session.js +6 -20
- package/lib/web/shell.js +3 -0
- package/lib/web/styles.css +375 -11
- package/lib/web/theme.js +60 -0
- package/lib/web/timed-inputs.js +9 -16
- package/package.json +1 -1
|
@@ -11,6 +11,7 @@ const ClaudeStructuredSession = require('../claude/structured-session');
|
|
|
11
11
|
const CodexStructuredSession = require('../codex/structured-session');
|
|
12
12
|
const ClaudeTranscriptRepository = require('../claude/transcript-repository');
|
|
13
13
|
const CodexImageStore = require('../codex/image-store');
|
|
14
|
+
const FileAttachmentStore = require('./file-attachment-store');
|
|
14
15
|
|
|
15
16
|
function previewText(text, maxChars = 320) {
|
|
16
17
|
if (!text) return '';
|
|
@@ -29,8 +30,16 @@ function imageMediaType(name) {
|
|
|
29
30
|
: extension === '.gif' ? 'image/gif' : 'image/webp';
|
|
30
31
|
}
|
|
31
32
|
|
|
33
|
+
function promptWithFileReferences(text, attachments) {
|
|
34
|
+
const prompt = String(text || '').trim();
|
|
35
|
+
if (!attachments.length) return prompt;
|
|
36
|
+
const references = attachments.map(item => `- ${item.name}: ${item.path}`).join('\n');
|
|
37
|
+
const note = `The user attached the following local files. Read them if relevant to the request:\n${references}`;
|
|
38
|
+
return prompt ? `${prompt}\n\n${note}` : note;
|
|
39
|
+
}
|
|
40
|
+
|
|
32
41
|
class SessionManager extends EventEmitter {
|
|
33
|
-
constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, claudeForkSession } = {}) {
|
|
42
|
+
constructor({ baseDir, renderHistoryTools, debugHistoryEnabled = false, logger, hasConnectedSessionClient, claudeTranscriptRepository, codexImageStore, fileAttachmentStore, claudeForkSession } = {}) {
|
|
34
43
|
super();
|
|
35
44
|
this.baseDir = baseDir || process.cwd();
|
|
36
45
|
this.renderHistoryTools = renderHistoryTools || new Set();
|
|
@@ -39,6 +48,7 @@ class SessionManager extends EventEmitter {
|
|
|
39
48
|
this.hasConnectedSessionClient = hasConnectedSessionClient || (() => false);
|
|
40
49
|
this.sessions = new Map();
|
|
41
50
|
this.codexImages = codexImageStore || new CodexImageStore({ logger: this.logger });
|
|
51
|
+
this.fileAttachmentStore = fileAttachmentStore || new FileAttachmentStore({ logger: this.logger });
|
|
42
52
|
Object.defineProperties(this, {
|
|
43
53
|
codexImageRoot: {
|
|
44
54
|
get: () => this.codexImages.root,
|
|
@@ -67,7 +77,7 @@ class SessionManager extends EventEmitter {
|
|
|
67
77
|
startTime: session.startTime,
|
|
68
78
|
toolKey: session.tool.key,
|
|
69
79
|
workingDirectory: this.getSessionWorkingDirectory(session),
|
|
70
|
-
mode: ['claude-structured', 'codex-structured'].includes(session.kind) ?
|
|
80
|
+
mode: ['claude-structured', 'codex-structured'].includes(session.kind) ? 'structured' : 'terminal',
|
|
71
81
|
hasUnreadCompletion: Boolean(session.hasUnreadCompletion),
|
|
72
82
|
serverChanNotificationEnabled: Boolean(session.serverChanNotificationEnabled),
|
|
73
83
|
timedInputCount: session.timedInputs
|
|
@@ -142,6 +152,8 @@ class SessionManager extends EventEmitter {
|
|
|
142
152
|
hasConnectedWebClient: false,
|
|
143
153
|
hasUnreadCompletion: false,
|
|
144
154
|
timedInputs: new Map(),
|
|
155
|
+
fileAttachments: new Map(),
|
|
156
|
+
fileUploads: new Map(),
|
|
145
157
|
write: data => this.write(id, data),
|
|
146
158
|
isRunning: () => this.has(id) && ptyManager.isRunning(),
|
|
147
159
|
kill: () => this.kill(id)
|
|
@@ -187,6 +199,8 @@ class SessionManager extends EventEmitter {
|
|
|
187
199
|
});
|
|
188
200
|
session.imageAttachments = new Map();
|
|
189
201
|
session.imageUploads = new Map();
|
|
202
|
+
session.fileAttachments = new Map();
|
|
203
|
+
session.fileUploads = new Map();
|
|
190
204
|
|
|
191
205
|
this.sessions.set(id, session);
|
|
192
206
|
session.on('event', event => this.emit('claude-event', { sessionId: id, event, session }));
|
|
@@ -209,9 +223,10 @@ class SessionManager extends EventEmitter {
|
|
|
209
223
|
const session = new CodexStructuredSession({ id, tool, workingDir: sessionDir, name: name || tool.displayName, logger: this.logger, options: codexOptions });
|
|
210
224
|
session.imageAttachments = new Map();
|
|
211
225
|
session.imageUploads = new Map();
|
|
226
|
+
session.fileAttachments = new Map();
|
|
227
|
+
session.fileUploads = new Map();
|
|
212
228
|
this.sessions.set(id, session);
|
|
213
229
|
session.on('event', event => this.emit('codex-event', { sessionId: id, event, session }));
|
|
214
|
-
session.on('output', data => this.emit('output', { sessionId: id, data, session }));
|
|
215
230
|
session.on('exit', () => this.handleExit(session));
|
|
216
231
|
session.ensureProcess().catch(error => {
|
|
217
232
|
session.append({ kind: 'event', level: 'error', text: `Unable to start Codex app-server: ${error.message}` });
|
|
@@ -228,12 +243,26 @@ class SessionManager extends EventEmitter {
|
|
|
228
243
|
return session.ptyManager.write(data);
|
|
229
244
|
}
|
|
230
245
|
|
|
231
|
-
|
|
246
|
+
sendTerminalFileInput(id, text, fileAttachmentIds = []) {
|
|
247
|
+
const session = this.get(id);
|
|
248
|
+
if (!session || ['claude-structured', 'codex-structured'].includes(session.kind)) return false;
|
|
249
|
+
const files = this.getFileAttachments(id, fileAttachmentIds);
|
|
250
|
+
const prompt = promptWithFileReferences(text, files);
|
|
251
|
+
if (!prompt) return false;
|
|
252
|
+
this.write(id, prompt.replace(/\n/g, '\r'));
|
|
253
|
+
const enterTimer = setTimeout(() => this.write(id, '\r'), 1000);
|
|
254
|
+
enterTimer.unref?.();
|
|
255
|
+
if (files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
|
|
256
|
+
return true;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
async sendClaudeInput(id, text, attachmentIds = [], fileAttachmentIds = []) {
|
|
232
260
|
const session = this.get(id);
|
|
233
261
|
if (!session || session.kind !== 'claude-structured') return false;
|
|
234
262
|
const attachments = this.getImageAttachments(id, attachmentIds);
|
|
263
|
+
const files = this.getFileAttachments(id, fileAttachmentIds);
|
|
235
264
|
const prompt = String(text || '');
|
|
236
|
-
if (!prompt.trim() && attachments.length === 0) return false;
|
|
265
|
+
if (!prompt.trim() && attachments.length === 0 && files.length === 0) return false;
|
|
237
266
|
const prepared = await Promise.all(attachments.map(async attachment => ({
|
|
238
267
|
id: attachment.id,
|
|
239
268
|
name: attachment.name,
|
|
@@ -241,12 +270,37 @@ class SessionManager extends EventEmitter {
|
|
|
241
270
|
mediaType: imageMediaType(attachment.name),
|
|
242
271
|
data: (await fs.promises.readFile(attachment.path)).toString('base64')
|
|
243
272
|
})));
|
|
244
|
-
|
|
245
|
-
|
|
273
|
+
const agentPrompt = promptWithFileReferences(prompt, files);
|
|
274
|
+
this.markSessionInput(session, prompt || (files.length ? '[file attachment]' : '[image attachment]'));
|
|
275
|
+
const sent = session.sendUserMessage(prompt, prepared, {
|
|
276
|
+
agentText: agentPrompt,
|
|
277
|
+
displayAttachments: files.map(item => ({ id: item.id, name: item.name, size: item.size, kind: 'file' }))
|
|
278
|
+
});
|
|
246
279
|
if (sent && attachments.length) this.scheduleImageCleanup(id, attachments.map(item => item.id));
|
|
280
|
+
if (sent && files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
|
|
247
281
|
return sent;
|
|
248
282
|
}
|
|
249
283
|
|
|
284
|
+
appendFileChunk(id, input = {}, bytes) {
|
|
285
|
+
return this.fileAttachmentStore.appendChunk(this.get(id), input, bytes);
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
discardFileUpload(id, uploadId) {
|
|
289
|
+
return this.fileAttachmentStore.discardUpload(this.get(id), uploadId);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
discardFileAttachment(id, attachmentId) {
|
|
293
|
+
return this.fileAttachmentStore.discardAttachment(this.get(id), attachmentId);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
getFileAttachments(id, attachmentIds = []) {
|
|
297
|
+
return this.fileAttachmentStore.resolve(this.get(id), attachmentIds);
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
scheduleFileCleanup(id, attachmentIds) {
|
|
301
|
+
this.fileAttachmentStore.scheduleCleanup(this.get(id), attachmentIds);
|
|
302
|
+
}
|
|
303
|
+
|
|
250
304
|
storeImageAttachment(id, bytes) {
|
|
251
305
|
return this.codexImages.store(this.get(id), bytes);
|
|
252
306
|
}
|
|
@@ -295,15 +349,21 @@ class SessionManager extends EventEmitter {
|
|
|
295
349
|
this.scheduleImageCleanup(id, attachmentIds);
|
|
296
350
|
}
|
|
297
351
|
|
|
298
|
-
async sendCodexInput(id, text, attachmentIds = [], skills = []) {
|
|
352
|
+
async sendCodexInput(id, text, attachmentIds = [], skills = [], fileAttachmentIds = []) {
|
|
299
353
|
const session = this.get(id);
|
|
300
354
|
if (!session || session.kind !== 'codex-structured') return false;
|
|
301
355
|
const attachments = this.getCodexImageAttachments(id, attachmentIds);
|
|
356
|
+
const files = this.getFileAttachments(id, fileAttachmentIds);
|
|
302
357
|
const prompt = String(text || '');
|
|
303
|
-
if (!prompt.trim() && attachments.length === 0) return false;
|
|
304
|
-
|
|
305
|
-
|
|
358
|
+
if (!prompt.trim() && attachments.length === 0 && files.length === 0) return false;
|
|
359
|
+
const agentPrompt = promptWithFileReferences(prompt, files);
|
|
360
|
+
this.markSessionInput(session, prompt || (files.length ? '[file attachment]' : '[image attachment]'));
|
|
361
|
+
const sent = await session.sendUserMessage(prompt, attachments, skills, {
|
|
362
|
+
agentText: agentPrompt,
|
|
363
|
+
displayAttachments: files.map(item => ({ id: item.id, name: item.name, size: item.size, kind: 'file' }))
|
|
364
|
+
});
|
|
306
365
|
if (sent && attachments.length) this.scheduleCodexImageCleanup(id, attachments.map(item => item.id));
|
|
366
|
+
if (sent && files.length) this.scheduleFileCleanup(id, files.map(item => item.id));
|
|
307
367
|
return sent;
|
|
308
368
|
}
|
|
309
369
|
|
|
@@ -409,8 +469,8 @@ class SessionManager extends EventEmitter {
|
|
|
409
469
|
async forkCodex(id, threadId) {
|
|
410
470
|
const session = this.get(id);
|
|
411
471
|
if (!session || session.kind !== 'codex-structured') return null;
|
|
412
|
-
if (session.
|
|
413
|
-
const error = new Error('Codex must be idle
|
|
472
|
+
if (session.status !== 'idle' || session.resuming) {
|
|
473
|
+
const error = new Error('Codex must be idle before forking');
|
|
414
474
|
error.statusCode = 409;
|
|
415
475
|
throw error;
|
|
416
476
|
}
|
|
@@ -444,20 +504,11 @@ class SessionManager extends EventEmitter {
|
|
|
444
504
|
return session.listSkills(forceReload);
|
|
445
505
|
}
|
|
446
506
|
|
|
447
|
-
switchCodexPresentation(id, presentation) {
|
|
448
|
-
const session = this.get(id);
|
|
449
|
-
if (!session || session.kind !== 'codex-structured') return Promise.resolve(false);
|
|
450
|
-
return presentation === 'terminal' ? session.switchToTerminal() : session.switchToStructured();
|
|
451
|
-
}
|
|
452
|
-
|
|
453
507
|
resize(id, cols, rows) {
|
|
454
508
|
const session = this.get(id);
|
|
455
509
|
if (!session) return false;
|
|
456
510
|
if (session.kind === 'claude-structured') return true;
|
|
457
|
-
if (session.kind === 'codex-structured')
|
|
458
|
-
if (session.presentation === 'terminal') session.ptyManager.resize(cols, rows);
|
|
459
|
-
return true;
|
|
460
|
-
}
|
|
511
|
+
if (session.kind === 'codex-structured') return true;
|
|
461
512
|
if (session.renderedHistory) {
|
|
462
513
|
session.renderedHistory.resize(cols, rows);
|
|
463
514
|
}
|
|
@@ -804,6 +855,7 @@ class SessionManager extends EventEmitter {
|
|
|
804
855
|
this.clearTimedInputs(session);
|
|
805
856
|
this.clearCodexImageUploads(session);
|
|
806
857
|
this.clearCodexImageAttachments(session);
|
|
858
|
+
this.fileAttachmentStore.clear(session);
|
|
807
859
|
this.disposeSessionHistory(session);
|
|
808
860
|
this.sessions.delete(session.id);
|
|
809
861
|
this.emit('exit', { sessionId: session.id, session });
|
package/lib/web/claude.js
CHANGED
|
@@ -7,19 +7,34 @@
|
|
|
7
7
|
}
|
|
8
8
|
|
|
9
9
|
function setClaudeModeEnabled(enabled) {
|
|
10
|
-
const codexChat = isCodexSession()
|
|
10
|
+
const codexChat = isCodexSession();
|
|
11
11
|
const structured = enabled || codexChat;
|
|
12
|
+
const actionRail = enabled
|
|
13
|
+
? document.querySelector('.claude-control-rail')
|
|
14
|
+
: codexChat
|
|
15
|
+
? document.getElementById('codex-control-rail')
|
|
16
|
+
: document.getElementById('shortcut-rail');
|
|
17
|
+
const attachmentButton = document.getElementById('attachment-btn');
|
|
18
|
+
const scheduleButton = document.getElementById('schedule-send-btn');
|
|
19
|
+
if (actionRail && attachmentButton && scheduleButton) {
|
|
20
|
+
actionRail.prepend(scheduleButton);
|
|
21
|
+
actionRail.prepend(attachmentButton);
|
|
22
|
+
}
|
|
12
23
|
document.getElementById('terminal-container').style.display = structured ? 'none' : '';
|
|
13
24
|
document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
|
|
14
25
|
document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
|
|
15
|
-
document.getElementById('claude-control-panel').style.display = enabled ? '
|
|
16
|
-
document.getElementById('codex-control-panel').style.display = codexChat ? '
|
|
17
|
-
document.getElementById('
|
|
18
|
-
document.getElementById('timer-btn').style.display = '';
|
|
19
|
-
document.getElementById('attach-image-btn').style.display = structured ? '' : 'none';
|
|
26
|
+
document.getElementById('claude-control-panel').style.display = enabled ? 'flex' : 'none';
|
|
27
|
+
document.getElementById('codex-control-panel').style.display = codexChat ? 'flex' : 'none';
|
|
28
|
+
document.getElementById('attachment-btn').style.display = '';
|
|
20
29
|
document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
|
|
21
30
|
document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
|
|
22
31
|
document.getElementById('cmd-input').placeholder = enabled ? 'Message Claude...' : codexChat ? 'Message Codex...' : 'Type a message...';
|
|
32
|
+
if (!codexChat) {
|
|
33
|
+
const skillPrefix = document.getElementById('composer-skill-prefix');
|
|
34
|
+
skillPrefix.classList.remove('active');
|
|
35
|
+
skillPrefix.innerHTML = '';
|
|
36
|
+
}
|
|
37
|
+
if (!structured) renderSessionAttention();
|
|
23
38
|
updateTerminalControlsHeight();
|
|
24
39
|
}
|
|
25
40
|
|
|
@@ -94,7 +109,7 @@
|
|
|
94
109
|
const option = select.selectedOptions && select.selectedOptions[0];
|
|
95
110
|
const label = pickerLabelFromOption(option);
|
|
96
111
|
const fullValue = pickerFullValue(option);
|
|
97
|
-
button
|
|
112
|
+
setActionButtonLabel(button, prefix);
|
|
98
113
|
button.title = type === 'model'
|
|
99
114
|
? `${fullValue || label || prefix} · ${effortLabel(claudeState.effort)}`
|
|
100
115
|
: (fullValue || label || prefix);
|
|
@@ -252,12 +267,12 @@
|
|
|
252
267
|
const usageBtn = document.getElementById('claude-usage-btn');
|
|
253
268
|
if (usageBtn) {
|
|
254
269
|
usageBtn.disabled = claudeUsagePending || claudeStatus === 'thinking';
|
|
255
|
-
usageBtn
|
|
270
|
+
setActionButtonLabel(usageBtn, claudeUsagePending ? 'Loading' : 'Usage');
|
|
256
271
|
}
|
|
257
272
|
const contextBtn = document.getElementById('claude-context-btn');
|
|
258
273
|
if (contextBtn) {
|
|
259
274
|
contextBtn.disabled = claudeContextPending || claudeStatus === 'thinking';
|
|
260
|
-
contextBtn
|
|
275
|
+
setActionButtonLabel(contextBtn, claudeContextPending ? 'Loading' : 'Context');
|
|
261
276
|
}
|
|
262
277
|
renderClaudeStateBar();
|
|
263
278
|
renderClaudeChat();
|
|
@@ -304,8 +319,9 @@
|
|
|
304
319
|
const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
305
320
|
const mode = claudeState.permissionMode || 'default';
|
|
306
321
|
const parts = [];
|
|
322
|
+
const attention = [];
|
|
307
323
|
if (pending) {
|
|
308
|
-
|
|
324
|
+
attention.push(`<button type="button" class="session-attention-pill approval claude-approval-jump" onclick="jumpToClaudeApproval()" title="Jump to pending approval" aria-label="Jump to pending Claude approval"><span aria-hidden="true">!</span>${pending} approval${pending > 1 ? 's' : ''}<span aria-hidden="true">↓</span></button>`);
|
|
309
325
|
} else if (claudeStatus && !['idle', 'stopped', 'thinking'].includes(claudeStatus)) {
|
|
310
326
|
parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
|
|
311
327
|
}
|
|
@@ -314,6 +330,7 @@
|
|
|
314
330
|
}
|
|
315
331
|
el.innerHTML = parts.join('');
|
|
316
332
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
333
|
+
renderSessionAttention(attention);
|
|
317
334
|
}
|
|
318
335
|
|
|
319
336
|
function jumpToClaudeApproval() {
|
|
@@ -831,7 +848,7 @@
|
|
|
831
848
|
const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
|
|
832
849
|
if (parsed.kind === 'hidden') return '';
|
|
833
850
|
const attachments = Array.isArray(message.attachments) && message.attachments.length
|
|
834
|
-
? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || '
|
|
851
|
+
? `<div class="claude-message-attachments">${message.attachments.map(item => `<span class="claude-message-attachment" title="${escapeHtml(item.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${item.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(item.name || 'Attachment')}</span>`).join('')}</div>` : '';
|
|
835
852
|
if (parsed.kind === 'command') {
|
|
836
853
|
const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
|
|
837
854
|
return `${args}<div class="claude-message user"><span class="claude-command-chip">/${escapeHtml(parsed.commandName)}</span>${attachments}</div>`;
|
package/lib/web/codex.js
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
function codexText(text) { return escapeHtml(text || '').replace(/\n/g, '<br>'); }
|
|
2
2
|
function codexReadyForInput() {
|
|
3
|
-
return codexState.
|
|
3
|
+
return codexState.status === 'idle'
|
|
4
4
|
&& !codexState.aborting && !codexState.resuming && !codexResumeInFlight;
|
|
5
5
|
}
|
|
6
6
|
function codexJson(value) {
|
|
@@ -219,13 +219,30 @@
|
|
|
219
219
|
}
|
|
220
220
|
function renderCodexMessageSkills(item) {
|
|
221
221
|
const skills = Array.isArray(item?.skills) ? item.skills : [];
|
|
222
|
-
|
|
222
|
+
if (!skills.length) return '';
|
|
223
|
+
return `<div class="codex-message-skills">${skills.map(skill => `<span class="codex-message-skill" title="${escapeHtml(skill.path || skill.name || '')}">Skill · ${escapeHtml(skill.name || 'unknown')}</span>`).join('')}</div>`;
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
function renderComposerSkillPrefix() {
|
|
227
|
+
const container = document.getElementById('composer-skill-prefix');
|
|
228
|
+
if (!container) return;
|
|
229
|
+
const visible = selectedCodexSkill && codexReadyForInput();
|
|
230
|
+
const wasVisible = container.classList.contains('active');
|
|
231
|
+
container.classList.toggle('active', Boolean(visible));
|
|
232
|
+
container.innerHTML = visible
|
|
233
|
+
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
234
|
+
: '';
|
|
235
|
+
if (wasVisible !== Boolean(visible)) updateTerminalControlsHeight();
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function renderCodexMessageAttachments(item) {
|
|
239
|
+
if (!Array.isArray(item.attachments) || !item.attachments.length) return '';
|
|
240
|
+
return `<div class="claude-message-attachments">${item.attachments.map(attachment => `<span class="claude-message-attachment" title="${escapeHtml(attachment.name || 'Attachment')}"><svg class="message-attachment-icon action-icon" aria-hidden="true"><use href="#icon-${attachment.kind === 'file' ? 'file' : 'image'}"></use></svg>${escapeHtml(attachment.name || 'Attachment')}</span>`).join('')}</div>`;
|
|
223
241
|
}
|
|
224
242
|
function renderCodexMessageMeta(item, finalOnly = false, context = null) {
|
|
225
243
|
const time = renderCodexMessageTime(item, finalOnly);
|
|
226
|
-
const skills = renderCodexMessageSkills(item);
|
|
227
244
|
const meter = renderCodexContextMeter(context);
|
|
228
|
-
return time ||
|
|
245
|
+
return time || meter ? `<div class="codex-message-meta">${time}${meter}</div>` : '';
|
|
229
246
|
}
|
|
230
247
|
function syncCodexDom(current, next) {
|
|
231
248
|
if (!current || !next) return;
|
|
@@ -320,7 +337,7 @@
|
|
|
320
337
|
const context = lastAssistantByTurn.get(turnId) === item.id ? turnEnd?.context : null;
|
|
321
338
|
parts.push(`<div class="codex-message-block assistant" data-codex-key="message-${escapeHtml(item.id || '')}"><div class="codex-message assistant claude-md">${renderMarkdown(item.text || '')}</div>${renderCodexMessageMeta(item, true, context)}</div>`);
|
|
322
339
|
}
|
|
323
|
-
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}"
|
|
340
|
+
else if (item.kind === 'user') parts.push(`<div class="codex-message-block user" data-codex-key="message-${escapeHtml(item.id || '')}">${renderCodexMessageSkills(item)}<div class="codex-message user claude-md">${renderMarkdown(item.text || '')}${renderCodexMessageAttachments(item)}</div>${renderCodexMessageMeta(item)}</div>`);
|
|
324
341
|
else if (item.kind === 'compaction') parts.push(renderCodexCompaction(item));
|
|
325
342
|
else if (item.kind === 'status') parts.push(renderCodexStatus(item));
|
|
326
343
|
else if (item.kind === 'event' && item.level === 'warning') parts.push(renderCodexWarning(item));
|
|
@@ -328,16 +345,14 @@
|
|
|
328
345
|
i += 1;
|
|
329
346
|
}
|
|
330
347
|
for (const request of codexPendingPermissions) if (!usedPermissions.has(request.id)) parts.push(renderCodexPermission(request));
|
|
331
|
-
const skillBubble = selectedCodexSkill && codexReadyForInput()
|
|
332
|
-
? `<div class="codex-skill-bubble" role="status" aria-label="Selected skill: ${escapeHtml(selectedCodexSkill.name)}"><span class="codex-skill-bubble-name">Skill · ${escapeHtml(selectedCodexSkill.name)}</span><button type="button" class="codex-skill-bubble-close" onclick="clearCodexSkillSelection()" title="Remove selected skill" aria-label="Remove selected skill">×</button></div>`
|
|
333
|
-
: '';
|
|
334
348
|
const working = `<div class="codex-working-indicator" role="status" aria-label="Codex is working" title="Codex is working"${codexState.status === 'running' ? '' : ' style="display:none"'}></div>`;
|
|
335
349
|
const template = document.createElement('template');
|
|
336
|
-
template.innerHTML = `<div class="codex-conversation">${
|
|
350
|
+
template.innerHTML = `<div class="codex-conversation">${working}${parts.join('') || '<div class="codex-message event">Send a message to start Codex.</div>'}</div>`;
|
|
337
351
|
const next = template.content.firstElementChild;
|
|
338
352
|
const current = container.firstElementChild;
|
|
339
353
|
if (!current) container.appendChild(next);
|
|
340
354
|
else syncCodexDom(current, next);
|
|
355
|
+
renderComposerSkillPrefix();
|
|
341
356
|
container.scrollTop = shouldStickToBottom ? container.scrollHeight : previousScrollTop;
|
|
342
357
|
}
|
|
343
358
|
|
|
@@ -424,7 +439,6 @@
|
|
|
424
439
|
}
|
|
425
440
|
|
|
426
441
|
function applyCodexState(state = {}) {
|
|
427
|
-
const presentationChanged = state.presentation !== undefined && state.presentation !== codexState.presentation;
|
|
428
442
|
codexState = { ...codexState, ...state };
|
|
429
443
|
const permission = document.getElementById('codex-permission-select');
|
|
430
444
|
if (permission) {
|
|
@@ -445,25 +459,22 @@
|
|
|
445
459
|
sandbox.value = codexState.sandboxMode || 'default';
|
|
446
460
|
}
|
|
447
461
|
const modelButton = document.getElementById('codex-model-btn');
|
|
448
|
-
if (modelButton) modelButton
|
|
462
|
+
if (modelButton) setActionButtonLabel(modelButton, 'Model');
|
|
449
463
|
const abort = document.getElementById('codex-abort-btn');
|
|
450
464
|
if (abort) {
|
|
451
465
|
abort.disabled = !codexState.canAbort;
|
|
452
|
-
abort
|
|
466
|
+
setActionButtonLabel(abort, codexState.aborting ? 'Aborting…' : 'Abort');
|
|
453
467
|
}
|
|
454
468
|
const compact = document.getElementById('codex-compact-btn');
|
|
455
|
-
if (compact) { compact.disabled = !codexState.canCompact; compact
|
|
469
|
+
if (compact) { compact.disabled = !codexState.canCompact; setActionButtonLabel(compact, codexState.compacting ? 'Compacting' : 'Compact'); }
|
|
456
470
|
const skills = document.getElementById('codex-skills-btn');
|
|
457
471
|
if (skills) { skills.disabled = !codexReadyForInput(); skills.classList.toggle('primary', Boolean(selectedCodexSkill)); }
|
|
458
472
|
const resume = document.getElementById('codex-resume-btn');
|
|
459
473
|
if (resume) resume.disabled = !codexReadyForInput();
|
|
460
474
|
const fork = document.getElementById('codex-fork-btn');
|
|
461
475
|
if (fork) fork.disabled = !codexReadyForInput();
|
|
462
|
-
const terminal = document.getElementById('codex-terminal-switch');
|
|
463
|
-
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'; }
|
|
464
476
|
renderCodexStateBar();
|
|
465
477
|
renderCodexModelPanel();
|
|
466
|
-
if (presentationChanged) setClaudeModeEnabled(isClaudeSession());
|
|
467
478
|
renderCodexChat();
|
|
468
479
|
}
|
|
469
480
|
|
|
@@ -473,12 +484,32 @@
|
|
|
473
484
|
const pending = Number(codexState.pendingPermissionCount || codexPendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
474
485
|
const subagents = Number(codexState.activeSubagentCount || 0) || 0;
|
|
475
486
|
const parts = [];
|
|
487
|
+
const attention = [];
|
|
476
488
|
if (codexState.aborting) parts.push('<span class="claude-state-pill warn">Stopping Codex…</span>');
|
|
477
489
|
else if (codexState.resuming || codexResumeInFlight) parts.push('<span class="claude-state-pill">Resuming conversation…</span>');
|
|
478
|
-
if (pending || codexState.status === 'waiting_approval')
|
|
479
|
-
if (subagents)
|
|
490
|
+
if (pending || codexState.status === 'waiting_approval') attention.push(`<button type="button" class="session-attention-pill approval codex-approval-jump" onclick="jumpToCodexApproval()" title="Jump to pending approval" aria-label="Jump to pending approval"><span aria-hidden="true">!</span>${pending || 1} approval${pending === 1 ? '' : 's'}<span aria-hidden="true">↓</span></button>`);
|
|
491
|
+
if (subagents) attention.push(`<button type="button" class="session-attention-pill subagent codex-subagent-jump" onclick="jumpToActiveCodexSubagent()" title="Jump to active subagent" aria-label="Jump to active subagent"><span aria-hidden="true">↳</span>${subagents} subagent${subagents === 1 ? '' : 's'} running</button>`);
|
|
480
492
|
el.innerHTML = parts.join('');
|
|
481
493
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
494
|
+
renderSessionAttention(attention);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
function jumpToActiveCodexSubagent(retry = true) {
|
|
498
|
+
const groups = Array.from(document.querySelectorAll('.codex-subagent-group'));
|
|
499
|
+
const target = groups[groups.length - 1];
|
|
500
|
+
if (!target) {
|
|
501
|
+
if (!retry) return false;
|
|
502
|
+
commitCodexChatRender();
|
|
503
|
+
requestAnimationFrame(() => jumpToActiveCodexSubagent(false));
|
|
504
|
+
return false;
|
|
505
|
+
}
|
|
506
|
+
target.open = true;
|
|
507
|
+
target.scrollIntoView({ behavior: 'smooth', block: 'center', inline: 'nearest' });
|
|
508
|
+
target.classList.remove('codex-approval-focus');
|
|
509
|
+
void target.offsetWidth;
|
|
510
|
+
target.classList.add('codex-approval-focus');
|
|
511
|
+
setTimeout(() => target.classList.remove('codex-approval-focus'), 1800);
|
|
512
|
+
return true;
|
|
482
513
|
}
|
|
483
514
|
|
|
484
515
|
function jumpToCodexApproval() {
|
|
@@ -1009,8 +1040,3 @@
|
|
|
1009
1040
|
panel.innerHTML = '';
|
|
1010
1041
|
updateTerminalControlsHeight();
|
|
1011
1042
|
}
|
|
1012
|
-
async function toggleCodexPresentation() {
|
|
1013
|
-
const presentation = codexState.presentation === 'terminal' ? 'structured' : 'terminal';
|
|
1014
|
-
const res = await fetchWithTimeout(`/api/sessions/${activeSessionId}/codex-presentation`, { method: 'POST', headers: { 'Content-Type': 'application/json' }, body: JSON.stringify({ presentation }) }, 30000);
|
|
1015
|
-
if (!res.ok) alert((await res.json()).error || 'Unable to switch Codex interface');
|
|
1016
|
-
}
|