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
package/lib/web/composer.js
CHANGED
|
@@ -1,24 +1,116 @@
|
|
|
1
1
|
const inputEl = document.getElementById('cmd-input');
|
|
2
|
-
const
|
|
2
|
+
const attachmentFileInput = document.getElementById('attachment-file-input');
|
|
3
3
|
const attachmentStrip = document.getElementById('attachment-strip');
|
|
4
4
|
let selectedImageAttachments = [];
|
|
5
|
+
let selectedFileAttachments = [];
|
|
5
6
|
let keepTerminalBottomForNextInput = false;
|
|
7
|
+
const composerActionTooltip = document.getElementById('composer-action-tooltip');
|
|
8
|
+
let composerActionTooltipTimer = null;
|
|
9
|
+
let composerTouchGesture = null;
|
|
10
|
+
let suppressTouchFocusUntil = 0;
|
|
11
|
+
|
|
12
|
+
function composerActionControl(target) {
|
|
13
|
+
const control = target instanceof Element ? target.closest('button, label') : null;
|
|
14
|
+
if (!control || !document.getElementById('terminal-controls').contains(control)) return null;
|
|
15
|
+
return control.querySelector('.action-icon') ? control : null;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function composerActionName(control) {
|
|
19
|
+
return control?.querySelector('.action-label')?.textContent?.trim()
|
|
20
|
+
|| control?.getAttribute('aria-label')
|
|
21
|
+
|| control?.getAttribute('title')
|
|
22
|
+
|| '';
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
function hideComposerActionTooltip() {
|
|
26
|
+
clearTimeout(composerActionTooltipTimer);
|
|
27
|
+
composerActionTooltipTimer = null;
|
|
28
|
+
composerActionTooltip.classList.remove('visible');
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
function showComposerActionTooltip(control, autoHide = false) {
|
|
32
|
+
const label = composerActionName(control);
|
|
33
|
+
if (!label) return;
|
|
34
|
+
clearTimeout(composerActionTooltipTimer);
|
|
35
|
+
composerActionTooltip.textContent = label;
|
|
36
|
+
composerActionTooltip.classList.add('visible');
|
|
37
|
+
const controlBox = control.getBoundingClientRect();
|
|
38
|
+
const tooltipBox = composerActionTooltip.getBoundingClientRect();
|
|
39
|
+
const left = Math.max(8, Math.min(window.innerWidth - tooltipBox.width - 8,
|
|
40
|
+
controlBox.left + controlBox.width / 2 - tooltipBox.width / 2));
|
|
41
|
+
const above = controlBox.top - tooltipBox.height - 8;
|
|
42
|
+
composerActionTooltip.style.left = `${left}px`;
|
|
43
|
+
composerActionTooltip.style.top = `${above >= 8 ? above : controlBox.bottom + 8}px`;
|
|
44
|
+
if (autoHide) composerActionTooltipTimer = setTimeout(hideComposerActionTooltip, 1400);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const terminalControls = document.getElementById('terminal-controls');
|
|
48
|
+
terminalControls.addEventListener('focusin', event => {
|
|
49
|
+
if (performance.now() < suppressTouchFocusUntil) return;
|
|
50
|
+
const control = composerActionControl(event.target);
|
|
51
|
+
if (control) showComposerActionTooltip(control);
|
|
52
|
+
});
|
|
53
|
+
terminalControls.addEventListener('focusout', hideComposerActionTooltip);
|
|
54
|
+
terminalControls.addEventListener('pointerover', event => {
|
|
55
|
+
const control = composerActionControl(event.target);
|
|
56
|
+
if (control && event.pointerType !== 'touch') showComposerActionTooltip(control);
|
|
57
|
+
});
|
|
58
|
+
terminalControls.addEventListener('pointerout', event => {
|
|
59
|
+
const control = composerActionControl(event.target);
|
|
60
|
+
if (control && !control.contains(event.relatedTarget)) hideComposerActionTooltip();
|
|
61
|
+
});
|
|
62
|
+
terminalControls.addEventListener('pointerdown', event => {
|
|
63
|
+
const control = composerActionControl(event.target);
|
|
64
|
+
if (control && event.pointerType === 'touch') {
|
|
65
|
+
composerTouchGesture = { pointerId: event.pointerId, control, x: event.clientX, y: event.clientY, moved: false };
|
|
66
|
+
suppressTouchFocusUntil = performance.now() + 800;
|
|
67
|
+
hideComposerActionTooltip();
|
|
68
|
+
}
|
|
69
|
+
});
|
|
70
|
+
terminalControls.addEventListener('pointermove', event => {
|
|
71
|
+
if (!composerTouchGesture || event.pointerId !== composerTouchGesture.pointerId) return;
|
|
72
|
+
if (Math.hypot(event.clientX - composerTouchGesture.x, event.clientY - composerTouchGesture.y) > 8) {
|
|
73
|
+
composerTouchGesture.moved = true;
|
|
74
|
+
hideComposerActionTooltip();
|
|
75
|
+
}
|
|
76
|
+
});
|
|
77
|
+
terminalControls.addEventListener('pointerup', event => {
|
|
78
|
+
if (!composerTouchGesture || event.pointerId !== composerTouchGesture.pointerId) return;
|
|
79
|
+
const gesture = composerTouchGesture;
|
|
80
|
+
composerTouchGesture = null;
|
|
81
|
+
suppressTouchFocusUntil = performance.now() + 800;
|
|
82
|
+
if (!gesture.moved) showComposerActionTooltip(gesture.control, true);
|
|
83
|
+
});
|
|
84
|
+
terminalControls.addEventListener('pointercancel', () => {
|
|
85
|
+
composerTouchGesture = null;
|
|
86
|
+
hideComposerActionTooltip();
|
|
87
|
+
});
|
|
88
|
+
terminalControls.addEventListener('scroll', hideComposerActionTooltip, true);
|
|
6
89
|
|
|
7
90
|
function isStructuredImageAttachmentAvailable() {
|
|
8
|
-
return isClaudeSession() ||
|
|
91
|
+
return isClaudeSession() || isCodexSession();
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function isSupportedImageFile(file) {
|
|
95
|
+
const type = String(file?.type || '').toLowerCase();
|
|
96
|
+
const name = String(file?.name || '').toLowerCase();
|
|
97
|
+
return ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(type)
|
|
98
|
+
|| /\.(png|jpe?g|gif|webp)$/.test(name);
|
|
9
99
|
}
|
|
10
100
|
|
|
11
101
|
function syncComposerButtonState() {
|
|
12
102
|
const timerOpen = document.getElementById('timed-send-panel').classList.contains('active');
|
|
13
103
|
document.getElementById('schedule-send-btn').classList.toggle('active', timerOpen);
|
|
14
|
-
document.getElementById('
|
|
104
|
+
document.getElementById('attachment-btn').classList.toggle('active', selectedImageAttachments.length + selectedFileAttachments.length > 0);
|
|
15
105
|
}
|
|
16
106
|
|
|
17
|
-
function
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
107
|
+
function renderComposerAttachments() {
|
|
108
|
+
const chip = (item, kind) => `<div class="attachment-chip ${kind}${item.uploading ? ' uploading' : ''}"><svg class="attachment-chip-icon action-icon" aria-hidden="true"><use href="#icon-${kind === 'image' ? 'image' : 'file'}"></use></svg><span class="attachment-chip-content"><span class="attachment-chip-name" title="${escapeHtml(item.name)}">${escapeHtml(item.name)}</span>${item.uploading ? `<span class="attachment-progress${item.progressKnown ? '' : ' estimated'}"><span style="width:${Math.max(0, Math.min(100, item.progress || 0))}%"></span></span><span class="attachment-status">${escapeHtml(item.status || `Uploading ${kind}…`)}</span>` : ''}</span><button class="attachment-remove" type="button" title="Remove ${kind}" aria-label="Remove ${escapeHtml(item.name)}" onclick="${kind === 'image' ? 'removeImageAttachment' : 'removeFileAttachment'}('${item.id}')">×</button></div>`;
|
|
109
|
+
attachmentStrip.innerHTML = [
|
|
110
|
+
...selectedImageAttachments.map(item => chip(item, 'image')),
|
|
111
|
+
...selectedFileAttachments.map(item => chip(item, 'file'))
|
|
112
|
+
].join('');
|
|
113
|
+
attachmentStrip.classList.toggle('active', selectedImageAttachments.length + selectedFileAttachments.length > 0);
|
|
22
114
|
syncComposerButtonState();
|
|
23
115
|
updateTerminalControlsHeight();
|
|
24
116
|
}
|
|
@@ -26,7 +118,7 @@
|
|
|
26
118
|
window.removeImageAttachment = async function(attachmentId) {
|
|
27
119
|
const attachment = selectedImageAttachments.find(item => item.id === attachmentId);
|
|
28
120
|
selectedImageAttachments = selectedImageAttachments.filter(item => item.id !== attachmentId);
|
|
29
|
-
|
|
121
|
+
renderComposerAttachments();
|
|
30
122
|
if (!attachment) return;
|
|
31
123
|
clearInterval(attachment.indicatorTimer);
|
|
32
124
|
attachment.abortUpload?.();
|
|
@@ -38,24 +130,43 @@
|
|
|
38
130
|
}
|
|
39
131
|
};
|
|
40
132
|
|
|
41
|
-
async function
|
|
133
|
+
window.removeFileAttachment = async function(attachmentId) {
|
|
134
|
+
const attachment = selectedFileAttachments.find(item => item.id === attachmentId);
|
|
135
|
+
selectedFileAttachments = selectedFileAttachments.filter(item => item.id !== attachmentId);
|
|
136
|
+
renderComposerAttachments();
|
|
137
|
+
if (!attachment) return;
|
|
138
|
+
attachment.abortUpload?.();
|
|
139
|
+
if (attachment.uploading) return;
|
|
140
|
+
try {
|
|
141
|
+
await fetchWithTimeout(`/api/sessions/${attachment.sessionId}/attachments/files/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
|
|
142
|
+
} catch (_) {}
|
|
143
|
+
};
|
|
144
|
+
|
|
145
|
+
async function clearComposerAttachments() {
|
|
42
146
|
const pending = selectedImageAttachments;
|
|
147
|
+
const pendingFiles = selectedFileAttachments;
|
|
43
148
|
selectedImageAttachments = [];
|
|
44
|
-
|
|
149
|
+
selectedFileAttachments = [];
|
|
150
|
+
renderComposerAttachments();
|
|
45
151
|
for (const item of pending) item.abortUpload?.();
|
|
152
|
+
for (const item of pendingFiles) item.abortUpload?.();
|
|
46
153
|
await Promise.all(pending.filter(item => !item.uploading).map(item => fetchWithTimeout(
|
|
47
154
|
`/api/sessions/${item.sessionId}/attachments/images/${encodeURIComponent(item.id)}`,
|
|
48
155
|
{ method: 'DELETE' }
|
|
49
156
|
).catch(() => null)));
|
|
157
|
+
await Promise.all(pendingFiles.filter(item => !item.uploading).map(item => fetchWithTimeout(
|
|
158
|
+
`/api/sessions/${item.sessionId}/attachments/files/${encodeURIComponent(item.id)}`,
|
|
159
|
+
{ method: 'DELETE' }
|
|
160
|
+
).catch(() => null)));
|
|
50
161
|
}
|
|
51
162
|
|
|
52
|
-
const
|
|
163
|
+
const ATTACHMENT_UPLOAD_CHUNK_BYTES = 512 * 1024;
|
|
53
164
|
|
|
54
165
|
function uploadImageInChunks(sessionId, file, onProgress) {
|
|
55
166
|
let xhr = null;
|
|
56
167
|
let cancelled = false;
|
|
57
168
|
const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
58
|
-
const chunkTotal = Math.ceil(file.size /
|
|
169
|
+
const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
|
|
59
170
|
return {
|
|
60
171
|
abort: () => {
|
|
61
172
|
cancelled = true;
|
|
@@ -65,8 +176,8 @@
|
|
|
65
176
|
promise: (async () => {
|
|
66
177
|
for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
|
|
67
178
|
if (cancelled) throw new Error('Image upload cancelled');
|
|
68
|
-
const start = chunkIndex *
|
|
69
|
-
const chunk = file.slice(start, Math.min(file.size, start +
|
|
179
|
+
const start = chunkIndex * ATTACHMENT_UPLOAD_CHUNK_BYTES;
|
|
180
|
+
const chunk = file.slice(start, Math.min(file.size, start + ATTACHMENT_UPLOAD_CHUNK_BYTES));
|
|
70
181
|
const result = await new Promise((resolve, reject) => {
|
|
71
182
|
xhr = new XMLHttpRequest();
|
|
72
183
|
xhr.open('POST', `/api/sessions/${sessionId}/attachments/images/chunks`);
|
|
@@ -111,7 +222,7 @@
|
|
|
111
222
|
alert(`${file.name} is larger than 50 MB.`);
|
|
112
223
|
continue;
|
|
113
224
|
}
|
|
114
|
-
const chunkTotal = Math.ceil(file.size /
|
|
225
|
+
const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
|
|
115
226
|
const pending = {
|
|
116
227
|
id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
|
|
117
228
|
name: file.name || 'image',
|
|
@@ -123,12 +234,12 @@
|
|
|
123
234
|
abortUpload: null
|
|
124
235
|
};
|
|
125
236
|
selectedImageAttachments.push(pending);
|
|
126
|
-
|
|
237
|
+
renderComposerAttachments();
|
|
127
238
|
try {
|
|
128
239
|
const upload = uploadImageInChunks(activeSessionId, file, progress => {
|
|
129
240
|
pending.progress = progress;
|
|
130
241
|
pending.status = `${progress}%`;
|
|
131
|
-
|
|
242
|
+
renderComposerAttachments();
|
|
132
243
|
});
|
|
133
244
|
pending.abortUpload = upload.abort;
|
|
134
245
|
const attachment = await upload.promise;
|
|
@@ -138,16 +249,121 @@
|
|
|
138
249
|
continue;
|
|
139
250
|
}
|
|
140
251
|
selectedImageAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
|
|
141
|
-
|
|
252
|
+
renderComposerAttachments();
|
|
142
253
|
} catch (e) {
|
|
143
254
|
selectedImageAttachments = selectedImageAttachments.filter(item => item !== pending);
|
|
144
|
-
|
|
255
|
+
renderComposerAttachments();
|
|
145
256
|
if (e.message === 'Image upload cancelled') continue;
|
|
146
257
|
alert(`Could not add ${file.name}: ${e.message}`);
|
|
147
258
|
}
|
|
148
259
|
}
|
|
149
260
|
}
|
|
150
261
|
|
|
262
|
+
function uploadFileInChunks(sessionId, file, onProgress) {
|
|
263
|
+
let xhr = null;
|
|
264
|
+
let cancelled = false;
|
|
265
|
+
const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
|
|
266
|
+
const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
|
|
267
|
+
return {
|
|
268
|
+
abort: () => {
|
|
269
|
+
cancelled = true;
|
|
270
|
+
xhr?.abort();
|
|
271
|
+
void fetch(`/api/sessions/${sessionId}/attachments/files/uploads/${encodeURIComponent(uploadId)}`, { method: 'DELETE' }).catch(() => null);
|
|
272
|
+
},
|
|
273
|
+
promise: (async () => {
|
|
274
|
+
for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
|
|
275
|
+
if (cancelled) throw new Error('File upload cancelled');
|
|
276
|
+
const start = chunkIndex * ATTACHMENT_UPLOAD_CHUNK_BYTES;
|
|
277
|
+
const chunk = file.slice(start, Math.min(file.size, start + ATTACHMENT_UPLOAD_CHUNK_BYTES));
|
|
278
|
+
const result = await new Promise((resolve, reject) => {
|
|
279
|
+
xhr = new XMLHttpRequest();
|
|
280
|
+
xhr.open('POST', `/api/sessions/${sessionId}/attachments/files/chunks`);
|
|
281
|
+
xhr.timeout = 60_000;
|
|
282
|
+
xhr.setRequestHeader('Content-Type', 'application/octet-stream');
|
|
283
|
+
xhr.setRequestHeader('X-Glad-Upload-Id', uploadId);
|
|
284
|
+
xhr.setRequestHeader('X-Glad-Chunk-Index', String(chunkIndex));
|
|
285
|
+
xhr.setRequestHeader('X-Glad-Chunk-Total', String(chunkTotal));
|
|
286
|
+
xhr.setRequestHeader('X-Glad-File-Name', encodeURIComponent(file.name || 'attachment.bin'));
|
|
287
|
+
xhr.onerror = () => reject(new Error('Network error while uploading file'));
|
|
288
|
+
xhr.ontimeout = () => reject(new Error(`File upload timed out on chunk ${chunkIndex + 1}/${chunkTotal}`));
|
|
289
|
+
xhr.onabort = () => reject(new Error('File upload cancelled'));
|
|
290
|
+
xhr.onload = () => {
|
|
291
|
+
let data = {};
|
|
292
|
+
try { data = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
|
|
293
|
+
if (xhr.status < 200 || xhr.status >= 300 || !data.success) {
|
|
294
|
+
reject(new Error(data.error || `Upload failed (HTTP ${xhr.status})`));
|
|
295
|
+
return;
|
|
296
|
+
}
|
|
297
|
+
resolve(data);
|
|
298
|
+
};
|
|
299
|
+
xhr.send(chunk);
|
|
300
|
+
});
|
|
301
|
+
onProgress(Math.round((Math.min(file.size, start + chunk.size) / file.size) * 100));
|
|
302
|
+
if (result.complete) return result.attachment;
|
|
303
|
+
}
|
|
304
|
+
throw new Error('File upload did not complete');
|
|
305
|
+
})()
|
|
306
|
+
};
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
async function uploadAttachmentFiles(files) {
|
|
310
|
+
const remaining = 8 - selectedFileAttachments.length;
|
|
311
|
+
const batch = Array.from(files).slice(0, remaining);
|
|
312
|
+
if (files.length > remaining) alert('You can attach up to 8 files at a time.');
|
|
313
|
+
for (const file of batch) {
|
|
314
|
+
if (!file.size) {
|
|
315
|
+
alert(`${file.name || 'File'} is empty.`);
|
|
316
|
+
continue;
|
|
317
|
+
}
|
|
318
|
+
if (file.size > 50 * 1024 * 1024) {
|
|
319
|
+
alert(`${file.name} is larger than 50 MB.`);
|
|
320
|
+
continue;
|
|
321
|
+
}
|
|
322
|
+
const pending = {
|
|
323
|
+
id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
|
|
324
|
+
name: file.name || 'attachment.bin',
|
|
325
|
+
sessionId: activeSessionId,
|
|
326
|
+
uploading: true,
|
|
327
|
+
progress: 0,
|
|
328
|
+
progressKnown: true,
|
|
329
|
+
status: '0%',
|
|
330
|
+
abortUpload: null
|
|
331
|
+
};
|
|
332
|
+
selectedFileAttachments.push(pending);
|
|
333
|
+
renderComposerAttachments();
|
|
334
|
+
try {
|
|
335
|
+
const upload = uploadFileInChunks(activeSessionId, file, progress => {
|
|
336
|
+
pending.progress = progress;
|
|
337
|
+
pending.status = `${progress}%`;
|
|
338
|
+
renderComposerAttachments();
|
|
339
|
+
});
|
|
340
|
+
pending.abortUpload = upload.abort;
|
|
341
|
+
const attachment = await upload.promise;
|
|
342
|
+
const index = selectedFileAttachments.indexOf(pending);
|
|
343
|
+
if (index < 0) {
|
|
344
|
+
await fetchWithTimeout(`/api/sessions/${activeSessionId}/attachments/files/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
|
|
345
|
+
continue;
|
|
346
|
+
}
|
|
347
|
+
selectedFileAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
|
|
348
|
+
renderComposerAttachments();
|
|
349
|
+
} catch (e) {
|
|
350
|
+
selectedFileAttachments = selectedFileAttachments.filter(item => item !== pending);
|
|
351
|
+
renderComposerAttachments();
|
|
352
|
+
if (e.message === 'File upload cancelled') continue;
|
|
353
|
+
alert(`Could not add ${file.name}: ${e.message}`);
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
async function uploadSelectedAttachments(files) {
|
|
359
|
+
const selected = Array.from(files || []);
|
|
360
|
+
const images = isStructuredImageAttachmentAvailable()
|
|
361
|
+
? selected.filter(isSupportedImageFile) : [];
|
|
362
|
+
const regularFiles = selected.filter(file => !images.includes(file));
|
|
363
|
+
if (images.length) await uploadImageFiles(images);
|
|
364
|
+
if (regularFiles.length) await uploadAttachmentFiles(regularFiles);
|
|
365
|
+
}
|
|
366
|
+
|
|
151
367
|
function openTimedSendPanel() {
|
|
152
368
|
if (isClaudeSession()) {
|
|
153
369
|
closeClaudePicker();
|
|
@@ -181,43 +397,62 @@
|
|
|
181
397
|
function performSend() {
|
|
182
398
|
const val = inputEl.value;
|
|
183
399
|
const readyImageAttachments = selectedImageAttachments.filter(item => !item.uploading);
|
|
184
|
-
|
|
185
|
-
|
|
400
|
+
const readyFileAttachments = selectedFileAttachments.filter(item => !item.uploading);
|
|
401
|
+
if (selectedImageAttachments.some(item => item.uploading) || selectedFileAttachments.some(item => item.uploading)) {
|
|
402
|
+
alert('Wait for attachments to finish uploading before sending.');
|
|
186
403
|
return;
|
|
187
404
|
}
|
|
188
|
-
if ((val || readyImageAttachments.length) && isClaudeSession()) {
|
|
405
|
+
if ((val || readyImageAttachments.length || readyFileAttachments.length) && isClaudeSession()) {
|
|
189
406
|
if (currentSocket && currentSocket.readyState === 1) {
|
|
190
407
|
currentSocket.send(JSON.stringify({
|
|
191
408
|
type: 'claude-input',
|
|
192
409
|
text: val,
|
|
193
|
-
attachmentIds: readyImageAttachments.map(item => item.id)
|
|
410
|
+
attachmentIds: readyImageAttachments.map(item => item.id),
|
|
411
|
+
...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {})
|
|
194
412
|
}));
|
|
195
413
|
}
|
|
196
414
|
inputEl.value = '';
|
|
197
415
|
inputEl.style.height = '38px';
|
|
198
416
|
selectedImageAttachments = [];
|
|
199
|
-
|
|
417
|
+
selectedFileAttachments = [];
|
|
418
|
+
renderComposerAttachments();
|
|
200
419
|
return;
|
|
201
420
|
}
|
|
202
|
-
if ((val || readyImageAttachments.length) && isCodexSession()
|
|
421
|
+
if ((val || readyImageAttachments.length || readyFileAttachments.length) && isCodexSession()) {
|
|
203
422
|
if (!codexReadyForInput()) return;
|
|
204
423
|
if (currentSocket && currentSocket.readyState === 1) {
|
|
205
424
|
currentSocket.send(JSON.stringify({
|
|
206
425
|
type: 'codex-input',
|
|
207
426
|
text: val,
|
|
208
427
|
attachmentIds: readyImageAttachments.map(item => item.id),
|
|
428
|
+
...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {}),
|
|
209
429
|
skills: selectedCodexSkill ? [{ name: selectedCodexSkill.name, path: selectedCodexSkill.path }] : []
|
|
210
430
|
}));
|
|
211
431
|
}
|
|
212
432
|
inputEl.value = '';
|
|
213
433
|
inputEl.style.height = '38px';
|
|
214
434
|
selectedImageAttachments = [];
|
|
435
|
+
selectedFileAttachments = [];
|
|
215
436
|
selectedCodexSkill = null;
|
|
216
|
-
|
|
437
|
+
renderComposerAttachments();
|
|
217
438
|
renderCodexChat();
|
|
218
439
|
return;
|
|
219
440
|
}
|
|
220
|
-
if (val) {
|
|
441
|
+
if (val || readyFileAttachments.length) {
|
|
442
|
+
if (readyFileAttachments.length) {
|
|
443
|
+
if (currentSocket && currentSocket.readyState === 1) {
|
|
444
|
+
currentSocket.send(JSON.stringify({
|
|
445
|
+
type: 'file-input',
|
|
446
|
+
text: val,
|
|
447
|
+
fileAttachmentIds: readyFileAttachments.map(item => item.id)
|
|
448
|
+
}));
|
|
449
|
+
}
|
|
450
|
+
inputEl.value = '';
|
|
451
|
+
inputEl.style.height = '38px';
|
|
452
|
+
selectedFileAttachments = [];
|
|
453
|
+
renderComposerAttachments();
|
|
454
|
+
return;
|
|
455
|
+
}
|
|
221
456
|
const formattedVal = val.replace(/\n/g, '\r');
|
|
222
457
|
sendWS(formattedVal);
|
|
223
458
|
inputEl.value = '';
|
package/lib/web/core.js
CHANGED
|
@@ -18,6 +18,13 @@
|
|
|
18
18
|
}
|
|
19
19
|
button.textContent = label;
|
|
20
20
|
}
|
|
21
|
+
function renderSessionAttention(parts = []) {
|
|
22
|
+
const strip = document.getElementById('session-status-strip');
|
|
23
|
+
const rail = document.getElementById('session-attention-rail');
|
|
24
|
+
if (!strip || !rail) return;
|
|
25
|
+
rail.innerHTML = parts.join('');
|
|
26
|
+
strip.style.display = parts.length ? 'block' : 'none';
|
|
27
|
+
}
|
|
21
28
|
let claudeMessages = [];
|
|
22
29
|
let claudePendingPermissions = [];
|
|
23
30
|
let claudeStatus = 'idle';
|
|
@@ -44,9 +51,8 @@
|
|
|
44
51
|
permissionMode: 'default', sandboxMode: 'default',
|
|
45
52
|
effectivePermissionMode: null, effectiveSandboxMode: null,
|
|
46
53
|
model: null, effort: null, status: 'idle', threadId: null,
|
|
47
|
-
|
|
48
|
-
canAbort: false, canCompact: false, compacting: false
|
|
49
|
-
canSwitchToTerminal: false, canSwitchToStructured: false
|
|
54
|
+
models: [], aborting: false, resuming: false,
|
|
55
|
+
canAbort: false, canCompact: false, compacting: false
|
|
50
56
|
};
|
|
51
57
|
}
|
|
52
58
|
let codexMessages = [];
|
|
@@ -325,7 +331,7 @@
|
|
|
325
331
|
}
|
|
326
332
|
|
|
327
333
|
function closeToolModal(e) {
|
|
328
|
-
if (e.target.id === 'modal-overlay') document.getElementById('modal-overlay').style.display = 'none';
|
|
334
|
+
if (!e || e.target.id === 'modal-overlay') document.getElementById('modal-overlay').style.display = 'none';
|
|
329
335
|
}
|
|
330
336
|
|
|
331
337
|
function isLobbyVisible() {
|