glad-web 1.0.46 → 2.0.2

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.
Files changed (74) hide show
  1. package/README.md +4 -192
  2. package/THIRD_PARTY_NOTICES.md +27 -0
  3. package/bin/glad.cjs +56 -0
  4. package/package.json +19 -61
  5. package/README.zh-CN.md +0 -198
  6. package/assets/logo.svg +0 -43
  7. package/bin/cli.js +0 -65
  8. package/lib/ai-tools/demo/enhanced-demo.js +0 -625
  9. package/lib/ai-tools/demo/index.js +0 -24
  10. package/lib/ai-tools/demo/responses.js +0 -88
  11. package/lib/ai-tools/detector.js +0 -76
  12. package/lib/ai-tools/registry.js +0 -300
  13. package/lib/claude/cli-usage.js +0 -95
  14. package/lib/claude/config.js +0 -82
  15. package/lib/claude/structured-session.js +0 -884
  16. package/lib/claude/transcript-repository.js +0 -216
  17. package/lib/codex/image-store.js +0 -174
  18. package/lib/codex/structured-session.js +0 -1590
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -605
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -108
  24. package/lib/git/service.js +0 -83
  25. package/lib/notifications/message-formatter.js +0 -94
  26. package/lib/notifications/notification-service.js +0 -143
  27. package/lib/notifications/serverchan-client.js +0 -58
  28. package/lib/notifications/serverchan-settings-store.js +0 -115
  29. package/lib/schedule/job-runner.js +0 -162
  30. package/lib/schedule/job-store.js +0 -167
  31. package/lib/schedule/key-sequences.js +0 -49
  32. package/lib/schedule/scheduler-service.js +0 -39
  33. package/lib/server/routes/notifications.js +0 -52
  34. package/lib/server/routes/providers.js +0 -114
  35. package/lib/server/routes/schedules.js +0 -54
  36. package/lib/server/routes/skillhub.js +0 -104
  37. package/lib/server/routes/usage.js +0 -23
  38. package/lib/server/routes/workspace.js +0 -77
  39. package/lib/session/buffer.js +0 -102
  40. package/lib/session/file-attachment-store.js +0 -168
  41. package/lib/session/pty-manager.js +0 -255
  42. package/lib/session/rendered-history.js +0 -225
  43. package/lib/session/session-manager.js +0 -1032
  44. package/lib/session/text-history.js +0 -274
  45. package/lib/skillhub/client.js +0 -121
  46. package/lib/skillhub/settings-store.js +0 -168
  47. package/lib/skillhub/skill-installer.js +0 -320
  48. package/lib/usage/ccusage-runner.js +0 -128
  49. package/lib/usage/source-catalog.js +0 -26
  50. package/lib/usage/usage-service.js +0 -226
  51. package/lib/utils/logger.js +0 -74
  52. package/lib/utils/pid.js +0 -67
  53. package/lib/utils/validation.js +0 -53
  54. package/lib/web/bootstrap.js +0 -34
  55. package/lib/web/claude.js +0 -1150
  56. package/lib/web/codex.js +0 -1045
  57. package/lib/web/composer.js +0 -493
  58. package/lib/web/core.js +0 -385
  59. package/lib/web/git.js +0 -535
  60. package/lib/web/gitgraph.js +0 -293
  61. package/lib/web/index.html +0 -547
  62. package/lib/web/layout.js +0 -69
  63. package/lib/web/notifications.js +0 -164
  64. package/lib/web/schedules.js +0 -245
  65. package/lib/web/session.js +0 -361
  66. package/lib/web/shell.js +0 -74
  67. package/lib/web/skillhub.js +0 -197
  68. package/lib/web/styles.css +0 -932
  69. package/lib/web/terminal-scroll.js +0 -81
  70. package/lib/web/theme.js +0 -60
  71. package/lib/web/timed-inputs.js +0 -216
  72. package/lib/web/usage.js +0 -323
  73. package/lib/workspace/service.js +0 -77
  74. package/scripts/check-syntax.js +0 -26
@@ -1,493 +0,0 @@
1
- const inputEl = document.getElementById('cmd-input');
2
- const attachmentFileInput = document.getElementById('attachment-file-input');
3
- const attachmentStrip = document.getElementById('attachment-strip');
4
- let selectedImageAttachments = [];
5
- let selectedFileAttachments = [];
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
- let composerSendPending = false;
12
-
13
- function composerExecutionInProgress() {
14
- if (composerSendPending) return true;
15
- if (isClaudeSession()) return claudeStatus === 'thinking';
16
- if (isCodexSession()) return !codexReadyForInput();
17
- return false;
18
- }
19
-
20
- function syncComposerSendState({ acknowledgeProviderState = false } = {}) {
21
- if (acknowledgeProviderState) composerSendPending = false;
22
- const sendButton = document.getElementById('send-btn');
23
- const executing = composerExecutionInProgress();
24
- sendButton.disabled = executing;
25
- sendButton.title = executing ? 'Wait for the current run to finish' : 'Send';
26
- sendButton.setAttribute('aria-label', executing ? 'Send disabled while running' : 'Send');
27
- }
28
-
29
- function markComposerSendPending() {
30
- composerSendPending = true;
31
- syncComposerSendState();
32
- }
33
-
34
- function resetComposerSendState() {
35
- composerSendPending = false;
36
- syncComposerSendState();
37
- }
38
-
39
- function composerActionControl(target) {
40
- const control = target instanceof Element ? target.closest('button, label') : null;
41
- if (!control || !document.getElementById('terminal-controls').contains(control)) return null;
42
- return control.querySelector('.action-icon') ? control : null;
43
- }
44
-
45
- function composerActionName(control) {
46
- return control?.querySelector('.action-label')?.textContent?.trim()
47
- || control?.getAttribute('aria-label')
48
- || control?.getAttribute('title')
49
- || '';
50
- }
51
-
52
- function hideComposerActionTooltip() {
53
- clearTimeout(composerActionTooltipTimer);
54
- composerActionTooltipTimer = null;
55
- composerActionTooltip.classList.remove('visible');
56
- }
57
-
58
- function showComposerActionTooltip(control, autoHide = false) {
59
- const label = composerActionName(control);
60
- if (!label) return;
61
- clearTimeout(composerActionTooltipTimer);
62
- composerActionTooltip.textContent = label;
63
- composerActionTooltip.classList.add('visible');
64
- const controlBox = control.getBoundingClientRect();
65
- const tooltipBox = composerActionTooltip.getBoundingClientRect();
66
- const left = Math.max(8, Math.min(window.innerWidth - tooltipBox.width - 8,
67
- controlBox.left + controlBox.width / 2 - tooltipBox.width / 2));
68
- const above = controlBox.top - tooltipBox.height - 8;
69
- composerActionTooltip.style.left = `${left}px`;
70
- composerActionTooltip.style.top = `${above >= 8 ? above : controlBox.bottom + 8}px`;
71
- if (autoHide) composerActionTooltipTimer = setTimeout(hideComposerActionTooltip, 1400);
72
- }
73
-
74
- const terminalControls = document.getElementById('terminal-controls');
75
- terminalControls.addEventListener('focusin', event => {
76
- if (performance.now() < suppressTouchFocusUntil) return;
77
- const control = composerActionControl(event.target);
78
- if (control) showComposerActionTooltip(control);
79
- });
80
- terminalControls.addEventListener('focusout', hideComposerActionTooltip);
81
- terminalControls.addEventListener('pointerover', event => {
82
- const control = composerActionControl(event.target);
83
- if (control && event.pointerType !== 'touch') showComposerActionTooltip(control);
84
- });
85
- terminalControls.addEventListener('pointerout', event => {
86
- const control = composerActionControl(event.target);
87
- if (control && !control.contains(event.relatedTarget)) hideComposerActionTooltip();
88
- });
89
- terminalControls.addEventListener('pointerdown', event => {
90
- const control = composerActionControl(event.target);
91
- if (control && event.pointerType === 'touch') {
92
- composerTouchGesture = { pointerId: event.pointerId, control, x: event.clientX, y: event.clientY, moved: false };
93
- suppressTouchFocusUntil = performance.now() + 800;
94
- hideComposerActionTooltip();
95
- }
96
- });
97
- terminalControls.addEventListener('pointermove', event => {
98
- if (!composerTouchGesture || event.pointerId !== composerTouchGesture.pointerId) return;
99
- if (Math.hypot(event.clientX - composerTouchGesture.x, event.clientY - composerTouchGesture.y) > 8) {
100
- composerTouchGesture.moved = true;
101
- hideComposerActionTooltip();
102
- }
103
- });
104
- terminalControls.addEventListener('pointerup', event => {
105
- if (!composerTouchGesture || event.pointerId !== composerTouchGesture.pointerId) return;
106
- const gesture = composerTouchGesture;
107
- composerTouchGesture = null;
108
- suppressTouchFocusUntil = performance.now() + 800;
109
- if (!gesture.moved) showComposerActionTooltip(gesture.control, true);
110
- });
111
- terminalControls.addEventListener('pointercancel', () => {
112
- composerTouchGesture = null;
113
- hideComposerActionTooltip();
114
- });
115
- terminalControls.addEventListener('scroll', hideComposerActionTooltip, true);
116
-
117
- function isStructuredImageAttachmentAvailable() {
118
- return isClaudeSession() || isCodexSession();
119
- }
120
-
121
- function isSupportedImageFile(file) {
122
- const type = String(file?.type || '').toLowerCase();
123
- const name = String(file?.name || '').toLowerCase();
124
- return ['image/png', 'image/jpeg', 'image/gif', 'image/webp'].includes(type)
125
- || /\.(png|jpe?g|gif|webp)$/.test(name);
126
- }
127
-
128
- function syncComposerButtonState() {
129
- const timerOpen = document.getElementById('timed-send-panel').classList.contains('active');
130
- document.getElementById('schedule-send-btn').classList.toggle('active', timerOpen);
131
- document.getElementById('attachment-btn').classList.toggle('active', selectedImageAttachments.length + selectedFileAttachments.length > 0);
132
- }
133
-
134
- function renderComposerAttachments() {
135
- 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>`;
136
- attachmentStrip.innerHTML = [
137
- ...selectedImageAttachments.map(item => chip(item, 'image')),
138
- ...selectedFileAttachments.map(item => chip(item, 'file'))
139
- ].join('');
140
- attachmentStrip.classList.toggle('active', selectedImageAttachments.length + selectedFileAttachments.length > 0);
141
- syncComposerButtonState();
142
- updateTerminalControlsHeight();
143
- }
144
-
145
- window.removeImageAttachment = async function(attachmentId) {
146
- const attachment = selectedImageAttachments.find(item => item.id === attachmentId);
147
- selectedImageAttachments = selectedImageAttachments.filter(item => item.id !== attachmentId);
148
- renderComposerAttachments();
149
- if (!attachment) return;
150
- clearInterval(attachment.indicatorTimer);
151
- attachment.abortUpload?.();
152
- if (attachment.uploading) return;
153
- try {
154
- await fetchWithTimeout(`/api/sessions/${attachment.sessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
155
- } catch (_) {
156
- // The server also removes all attachments when the session ends.
157
- }
158
- };
159
-
160
- window.removeFileAttachment = async function(attachmentId) {
161
- const attachment = selectedFileAttachments.find(item => item.id === attachmentId);
162
- selectedFileAttachments = selectedFileAttachments.filter(item => item.id !== attachmentId);
163
- renderComposerAttachments();
164
- if (!attachment) return;
165
- attachment.abortUpload?.();
166
- if (attachment.uploading) return;
167
- try {
168
- await fetchWithTimeout(`/api/sessions/${attachment.sessionId}/attachments/files/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
169
- } catch (_) {}
170
- };
171
-
172
- async function clearComposerAttachments() {
173
- const pending = selectedImageAttachments;
174
- const pendingFiles = selectedFileAttachments;
175
- selectedImageAttachments = [];
176
- selectedFileAttachments = [];
177
- renderComposerAttachments();
178
- for (const item of pending) item.abortUpload?.();
179
- for (const item of pendingFiles) item.abortUpload?.();
180
- await Promise.all(pending.filter(item => !item.uploading).map(item => fetchWithTimeout(
181
- `/api/sessions/${item.sessionId}/attachments/images/${encodeURIComponent(item.id)}`,
182
- { method: 'DELETE' }
183
- ).catch(() => null)));
184
- await Promise.all(pendingFiles.filter(item => !item.uploading).map(item => fetchWithTimeout(
185
- `/api/sessions/${item.sessionId}/attachments/files/${encodeURIComponent(item.id)}`,
186
- { method: 'DELETE' }
187
- ).catch(() => null)));
188
- }
189
-
190
- const ATTACHMENT_UPLOAD_CHUNK_BYTES = 512 * 1024;
191
-
192
- function uploadImageInChunks(sessionId, file, onProgress) {
193
- let xhr = null;
194
- let cancelled = false;
195
- const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
196
- const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
197
- return {
198
- abort: () => {
199
- cancelled = true;
200
- xhr?.abort();
201
- void fetch(`/api/sessions/${sessionId}/attachments/images/uploads/${encodeURIComponent(uploadId)}`, { method: 'DELETE' }).catch(() => null);
202
- },
203
- promise: (async () => {
204
- for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
205
- if (cancelled) throw new Error('Image upload cancelled');
206
- const start = chunkIndex * ATTACHMENT_UPLOAD_CHUNK_BYTES;
207
- const chunk = file.slice(start, Math.min(file.size, start + ATTACHMENT_UPLOAD_CHUNK_BYTES));
208
- const result = await new Promise((resolve, reject) => {
209
- xhr = new XMLHttpRequest();
210
- xhr.open('POST', `/api/sessions/${sessionId}/attachments/images/chunks`);
211
- xhr.timeout = 60_000;
212
- xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
213
- xhr.setRequestHeader('X-Glad-Upload-Id', uploadId);
214
- xhr.setRequestHeader('X-Glad-Chunk-Index', String(chunkIndex));
215
- xhr.setRequestHeader('X-Glad-Chunk-Total', String(chunkTotal));
216
- xhr.onerror = () => reject(new Error('Network error while uploading image'));
217
- xhr.ontimeout = () => reject(new Error(`Image upload timed out on chunk ${chunkIndex + 1}/${chunkTotal}`));
218
- xhr.onabort = () => reject(new Error('Image upload cancelled'));
219
- xhr.onload = () => {
220
- let data = {};
221
- try { data = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
222
- if (xhr.status < 200 || xhr.status >= 300 || !data.success) {
223
- reject(new Error(data.error || `Upload failed (HTTP ${xhr.status})`));
224
- return;
225
- }
226
- resolve(data);
227
- };
228
- xhr.send(chunk);
229
- });
230
- const confirmedBytes = Math.min(file.size, start + chunk.size);
231
- onProgress(Math.round((confirmedBytes / file.size) * 100), chunkIndex + 1, chunkTotal);
232
- if (result.complete) return result.attachment;
233
- }
234
- throw new Error('Image upload did not complete');
235
- })()
236
- };
237
- }
238
-
239
- async function uploadImageFiles(files) {
240
- if (!isStructuredImageAttachmentAvailable()) {
241
- alert('Image attachments are available only in structured chat mode.');
242
- return;
243
- }
244
- const remaining = 5 - selectedImageAttachments.length;
245
- const batch = Array.from(files).slice(0, remaining);
246
- if (files.length > remaining) alert('You can attach up to 5 images at a time.');
247
- for (const file of batch) {
248
- if (file.size > 50 * 1024 * 1024) {
249
- alert(`${file.name} is larger than 50 MB.`);
250
- continue;
251
- }
252
- const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
253
- const pending = {
254
- id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
255
- name: file.name || 'image',
256
- sessionId: activeSessionId,
257
- uploading: true,
258
- progress: 0,
259
- progressKnown: true,
260
- status: '0%',
261
- abortUpload: null
262
- };
263
- selectedImageAttachments.push(pending);
264
- renderComposerAttachments();
265
- try {
266
- const upload = uploadImageInChunks(activeSessionId, file, progress => {
267
- pending.progress = progress;
268
- pending.status = `${progress}%`;
269
- renderComposerAttachments();
270
- });
271
- pending.abortUpload = upload.abort;
272
- const attachment = await upload.promise;
273
- const index = selectedImageAttachments.indexOf(pending);
274
- if (index < 0) {
275
- await fetchWithTimeout(`/api/sessions/${activeSessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
276
- continue;
277
- }
278
- selectedImageAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
279
- renderComposerAttachments();
280
- } catch (e) {
281
- selectedImageAttachments = selectedImageAttachments.filter(item => item !== pending);
282
- renderComposerAttachments();
283
- if (e.message === 'Image upload cancelled') continue;
284
- alert(`Could not add ${file.name}: ${e.message}`);
285
- }
286
- }
287
- }
288
-
289
- function uploadFileInChunks(sessionId, file, onProgress) {
290
- let xhr = null;
291
- let cancelled = false;
292
- const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
293
- const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
294
- return {
295
- abort: () => {
296
- cancelled = true;
297
- xhr?.abort();
298
- void fetch(`/api/sessions/${sessionId}/attachments/files/uploads/${encodeURIComponent(uploadId)}`, { method: 'DELETE' }).catch(() => null);
299
- },
300
- promise: (async () => {
301
- for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
302
- if (cancelled) throw new Error('File upload cancelled');
303
- const start = chunkIndex * ATTACHMENT_UPLOAD_CHUNK_BYTES;
304
- const chunk = file.slice(start, Math.min(file.size, start + ATTACHMENT_UPLOAD_CHUNK_BYTES));
305
- const result = await new Promise((resolve, reject) => {
306
- xhr = new XMLHttpRequest();
307
- xhr.open('POST', `/api/sessions/${sessionId}/attachments/files/chunks`);
308
- xhr.timeout = 60_000;
309
- xhr.setRequestHeader('Content-Type', 'application/octet-stream');
310
- xhr.setRequestHeader('X-Glad-Upload-Id', uploadId);
311
- xhr.setRequestHeader('X-Glad-Chunk-Index', String(chunkIndex));
312
- xhr.setRequestHeader('X-Glad-Chunk-Total', String(chunkTotal));
313
- xhr.setRequestHeader('X-Glad-File-Name', encodeURIComponent(file.name || 'attachment.bin'));
314
- xhr.onerror = () => reject(new Error('Network error while uploading file'));
315
- xhr.ontimeout = () => reject(new Error(`File upload timed out on chunk ${chunkIndex + 1}/${chunkTotal}`));
316
- xhr.onabort = () => reject(new Error('File upload cancelled'));
317
- xhr.onload = () => {
318
- let data = {};
319
- try { data = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
320
- if (xhr.status < 200 || xhr.status >= 300 || !data.success) {
321
- reject(new Error(data.error || `Upload failed (HTTP ${xhr.status})`));
322
- return;
323
- }
324
- resolve(data);
325
- };
326
- xhr.send(chunk);
327
- });
328
- onProgress(Math.round((Math.min(file.size, start + chunk.size) / file.size) * 100));
329
- if (result.complete) return result.attachment;
330
- }
331
- throw new Error('File upload did not complete');
332
- })()
333
- };
334
- }
335
-
336
- async function uploadAttachmentFiles(files) {
337
- const remaining = 8 - selectedFileAttachments.length;
338
- const batch = Array.from(files).slice(0, remaining);
339
- if (files.length > remaining) alert('You can attach up to 8 files at a time.');
340
- for (const file of batch) {
341
- if (!file.size) {
342
- alert(`${file.name || 'File'} is empty.`);
343
- continue;
344
- }
345
- if (file.size > 50 * 1024 * 1024) {
346
- alert(`${file.name} is larger than 50 MB.`);
347
- continue;
348
- }
349
- const pending = {
350
- id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
351
- name: file.name || 'attachment.bin',
352
- sessionId: activeSessionId,
353
- uploading: true,
354
- progress: 0,
355
- progressKnown: true,
356
- status: '0%',
357
- abortUpload: null
358
- };
359
- selectedFileAttachments.push(pending);
360
- renderComposerAttachments();
361
- try {
362
- const upload = uploadFileInChunks(activeSessionId, file, progress => {
363
- pending.progress = progress;
364
- pending.status = `${progress}%`;
365
- renderComposerAttachments();
366
- });
367
- pending.abortUpload = upload.abort;
368
- const attachment = await upload.promise;
369
- const index = selectedFileAttachments.indexOf(pending);
370
- if (index < 0) {
371
- await fetchWithTimeout(`/api/sessions/${activeSessionId}/attachments/files/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
372
- continue;
373
- }
374
- selectedFileAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
375
- renderComposerAttachments();
376
- } catch (e) {
377
- selectedFileAttachments = selectedFileAttachments.filter(item => item !== pending);
378
- renderComposerAttachments();
379
- if (e.message === 'File upload cancelled') continue;
380
- alert(`Could not add ${file.name}: ${e.message}`);
381
- }
382
- }
383
- }
384
-
385
- async function uploadSelectedAttachments(files) {
386
- const selected = Array.from(files || []);
387
- const images = isStructuredImageAttachmentAvailable()
388
- ? selected.filter(isSupportedImageFile) : [];
389
- const regularFiles = selected.filter(file => !images.includes(file));
390
- if (images.length) await uploadImageFiles(images);
391
- if (regularFiles.length) await uploadAttachmentFiles(regularFiles);
392
- }
393
-
394
- function openTimedSendPanel() {
395
- if (isClaudeSession()) {
396
- closeClaudePicker();
397
- claudeResumePanelOpen = false;
398
- claudeForkPanelOpen = false;
399
- document.getElementById('claude-resume-panel').classList.remove('active');
400
- document.getElementById('claude-fork-panel').classList.remove('active');
401
- }
402
- initTimedDelaySelectors();
403
- resetTimedEditor({ keepInput: true });
404
- document.getElementById('timed-send-panel').classList.add('active');
405
- updateTimedSendPreview();
406
- loadTimedInputs();
407
- syncComposerButtonState();
408
- updateTerminalControlsHeight();
409
- }
410
-
411
- function markInputEditStart() {
412
- keepTerminalBottomForNextInput = keepTerminalBottomForNextInput || isTerminalAtBottom();
413
- }
414
-
415
- inputEl.addEventListener('beforeinput', markInputEditStart);
416
- inputEl.addEventListener('input', function() {
417
- const keepAtBottom = keepTerminalBottomForNextInput || isTerminalAtBottom();
418
- keepTerminalBottomForNextInput = false;
419
- this.style.height = 'auto';
420
- this.style.height = Math.min(this.scrollHeight, 150) + 'px';
421
- if (keepAtBottom) restoreTerminalBottomSoon();
422
- });
423
-
424
- function performSend() {
425
- if (document.getElementById('send-btn').disabled) return;
426
- const val = inputEl.value;
427
- const readyImageAttachments = selectedImageAttachments.filter(item => !item.uploading);
428
- const readyFileAttachments = selectedFileAttachments.filter(item => !item.uploading);
429
- if (selectedImageAttachments.some(item => item.uploading) || selectedFileAttachments.some(item => item.uploading)) {
430
- alert('Wait for attachments to finish uploading before sending.');
431
- return;
432
- }
433
- if ((val || readyImageAttachments.length || readyFileAttachments.length) && isClaudeSession()) {
434
- if (currentSocket && currentSocket.readyState === 1) {
435
- currentSocket.send(JSON.stringify({
436
- type: 'claude-input',
437
- text: val,
438
- attachmentIds: readyImageAttachments.map(item => item.id),
439
- ...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {})
440
- }));
441
- markComposerSendPending();
442
- }
443
- inputEl.value = '';
444
- inputEl.style.height = '38px';
445
- selectedImageAttachments = [];
446
- selectedFileAttachments = [];
447
- renderComposerAttachments();
448
- return;
449
- }
450
- if ((val || readyImageAttachments.length || readyFileAttachments.length) && isCodexSession()) {
451
- if (!codexReadyForInput()) return;
452
- if (currentSocket && currentSocket.readyState === 1) {
453
- currentSocket.send(JSON.stringify({
454
- type: 'codex-input',
455
- text: val,
456
- attachmentIds: readyImageAttachments.map(item => item.id),
457
- ...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {}),
458
- skills: selectedCodexSkill ? [{ name: selectedCodexSkill.name, path: selectedCodexSkill.path }] : []
459
- }));
460
- markComposerSendPending();
461
- }
462
- inputEl.value = '';
463
- inputEl.style.height = '38px';
464
- selectedImageAttachments = [];
465
- selectedFileAttachments = [];
466
- selectedCodexSkill = null;
467
- renderComposerAttachments();
468
- renderCodexChat();
469
- return;
470
- }
471
- if (val || readyFileAttachments.length) {
472
- if (readyFileAttachments.length) {
473
- if (currentSocket && currentSocket.readyState === 1) {
474
- currentSocket.send(JSON.stringify({
475
- type: 'file-input',
476
- text: val,
477
- fileAttachmentIds: readyFileAttachments.map(item => item.id)
478
- }));
479
- }
480
- inputEl.value = '';
481
- inputEl.style.height = '38px';
482
- selectedFileAttachments = [];
483
- renderComposerAttachments();
484
- return;
485
- }
486
- const formattedVal = val.replace(/\n/g, '\r');
487
- sendWS(formattedVal);
488
- inputEl.value = '';
489
- inputEl.style.height = '38px';
490
- restoreTerminalBottomSoon();
491
- setTimeout(() => { sendWS('\r'); }, 1000);
492
- }
493
- }