glad-web 1.0.44 → 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 +41 -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 +15 -8
- package/lib/web/codex.js +46 -20
- package/lib/web/composer.js +263 -28
- package/lib/web/core.js +10 -4
- package/lib/web/git.js +53 -51
- package/lib/web/gitgraph.js +8 -8
- package/lib/web/index.html +19 -15
- package/lib/web/session.js +4 -19
- package/lib/web/styles.css +103 -8
- package/lib/web/timed-inputs.js +6 -6
- 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,29 +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
12
|
const actionRail = enabled
|
|
13
13
|
? document.querySelector('.claude-control-rail')
|
|
14
14
|
: codexChat
|
|
15
15
|
? document.getElementById('codex-control-rail')
|
|
16
16
|
: document.getElementById('shortcut-rail');
|
|
17
|
-
const
|
|
17
|
+
const attachmentButton = document.getElementById('attachment-btn');
|
|
18
18
|
const scheduleButton = document.getElementById('schedule-send-btn');
|
|
19
|
-
if (actionRail &&
|
|
19
|
+
if (actionRail && attachmentButton && scheduleButton) {
|
|
20
20
|
actionRail.prepend(scheduleButton);
|
|
21
|
-
actionRail.prepend(
|
|
21
|
+
actionRail.prepend(attachmentButton);
|
|
22
22
|
}
|
|
23
23
|
document.getElementById('terminal-container').style.display = structured ? 'none' : '';
|
|
24
24
|
document.getElementById('claude-chat-container').style.display = enabled ? 'block' : 'none';
|
|
25
25
|
document.getElementById('codex-chat-container').style.display = codexChat ? 'block' : 'none';
|
|
26
26
|
document.getElementById('claude-control-panel').style.display = enabled ? 'flex' : 'none';
|
|
27
27
|
document.getElementById('codex-control-panel').style.display = codexChat ? 'flex' : 'none';
|
|
28
|
-
document.getElementById('
|
|
29
|
-
document.getElementById('attach-image-btn').style.display = structured ? '' : 'none';
|
|
28
|
+
document.getElementById('attachment-btn').style.display = '';
|
|
30
29
|
document.getElementById('shortcut-rail').style.display = structured ? 'none' : '';
|
|
31
30
|
document.getElementById('scroll-controls').style.display = structured ? 'none' : '';
|
|
32
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();
|
|
33
38
|
updateTerminalControlsHeight();
|
|
34
39
|
}
|
|
35
40
|
|
|
@@ -314,8 +319,9 @@
|
|
|
314
319
|
const pending = Number(claudeState.pendingPermissionCount || claudePendingPermissions.filter(item => item.status === 'pending').length) || 0;
|
|
315
320
|
const mode = claudeState.permissionMode || 'default';
|
|
316
321
|
const parts = [];
|
|
322
|
+
const attention = [];
|
|
317
323
|
if (pending) {
|
|
318
|
-
|
|
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>`);
|
|
319
325
|
} else if (claudeStatus && !['idle', 'stopped', 'thinking'].includes(claudeStatus)) {
|
|
320
326
|
parts.push(`<span class="claude-state-pill warn">${escapeHtml(shortValue(claudeStatus))}</span>`);
|
|
321
327
|
}
|
|
@@ -324,6 +330,7 @@
|
|
|
324
330
|
}
|
|
325
331
|
el.innerHTML = parts.join('');
|
|
326
332
|
el.style.display = parts.length ? 'flex' : 'none';
|
|
333
|
+
renderSessionAttention(attention);
|
|
327
334
|
}
|
|
328
335
|
|
|
329
336
|
function jumpToClaudeApproval() {
|
|
@@ -841,7 +848,7 @@
|
|
|
841
848
|
const parsed = parseLocalCommandMessage(textFromClaudeMessage(message));
|
|
842
849
|
if (parsed.kind === 'hidden') return '';
|
|
843
850
|
const attachments = Array.isArray(message.attachments) && message.attachments.length
|
|
844
|
-
? `<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>` : '';
|
|
845
852
|
if (parsed.kind === 'command') {
|
|
846
853
|
const args = parsed.args ? `<div class="claude-message user">${renderMarkdown(parsed.args)}</div>` : '';
|
|
847
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) {
|
|
@@ -459,11 +473,8 @@
|
|
|
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
|
-
}
|