glad-web 1.0.45 → 2.0.1

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 (68) 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 -58
  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 -1578
  19. package/lib/commands/config.js +0 -78
  20. package/lib/commands/tools.js +0 -128
  21. package/lib/commands/web.js +0 -586
  22. package/lib/config/constants.js +0 -17
  23. package/lib/config/manager.js +0 -89
  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/usage.js +0 -23
  37. package/lib/server/routes/workspace.js +0 -77
  38. package/lib/session/buffer.js +0 -102
  39. package/lib/session/file-attachment-store.js +0 -168
  40. package/lib/session/pty-manager.js +0 -255
  41. package/lib/session/rendered-history.js +0 -225
  42. package/lib/session/session-manager.js +0 -1001
  43. package/lib/session/text-history.js +0 -274
  44. package/lib/usage/ccusage-runner.js +0 -128
  45. package/lib/usage/source-catalog.js +0 -26
  46. package/lib/usage/usage-service.js +0 -226
  47. package/lib/utils/logger.js +0 -74
  48. package/lib/utils/pid.js +0 -67
  49. package/lib/utils/validation.js +0 -53
  50. package/lib/web/claude.js +0 -1129
  51. package/lib/web/codex.js +0 -1042
  52. package/lib/web/composer.js +0 -463
  53. package/lib/web/core.js +0 -373
  54. package/lib/web/git.js +0 -535
  55. package/lib/web/gitgraph.js +0 -293
  56. package/lib/web/index.html +0 -516
  57. package/lib/web/layout.js +0 -72
  58. package/lib/web/notifications.js +0 -163
  59. package/lib/web/schedules.js +0 -245
  60. package/lib/web/session.js +0 -360
  61. package/lib/web/shell.js +0 -59
  62. package/lib/web/styles.css +0 -905
  63. package/lib/web/terminal-scroll.js +0 -81
  64. package/lib/web/theme.js +0 -60
  65. package/lib/web/timed-inputs.js +0 -216
  66. package/lib/web/usage.js +0 -323
  67. package/lib/workspace/service.js +0 -77
  68. package/scripts/check-syntax.js +0 -26
@@ -1,463 +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
-
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);
89
-
90
- function isStructuredImageAttachmentAvailable() {
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);
99
- }
100
-
101
- function syncComposerButtonState() {
102
- const timerOpen = document.getElementById('timed-send-panel').classList.contains('active');
103
- document.getElementById('schedule-send-btn').classList.toggle('active', timerOpen);
104
- document.getElementById('attachment-btn').classList.toggle('active', selectedImageAttachments.length + selectedFileAttachments.length > 0);
105
- }
106
-
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);
114
- syncComposerButtonState();
115
- updateTerminalControlsHeight();
116
- }
117
-
118
- window.removeImageAttachment = async function(attachmentId) {
119
- const attachment = selectedImageAttachments.find(item => item.id === attachmentId);
120
- selectedImageAttachments = selectedImageAttachments.filter(item => item.id !== attachmentId);
121
- renderComposerAttachments();
122
- if (!attachment) return;
123
- clearInterval(attachment.indicatorTimer);
124
- attachment.abortUpload?.();
125
- if (attachment.uploading) return;
126
- try {
127
- await fetchWithTimeout(`/api/sessions/${attachment.sessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
128
- } catch (_) {
129
- // The server also removes all attachments when the session ends.
130
- }
131
- };
132
-
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() {
146
- const pending = selectedImageAttachments;
147
- const pendingFiles = selectedFileAttachments;
148
- selectedImageAttachments = [];
149
- selectedFileAttachments = [];
150
- renderComposerAttachments();
151
- for (const item of pending) item.abortUpload?.();
152
- for (const item of pendingFiles) item.abortUpload?.();
153
- await Promise.all(pending.filter(item => !item.uploading).map(item => fetchWithTimeout(
154
- `/api/sessions/${item.sessionId}/attachments/images/${encodeURIComponent(item.id)}`,
155
- { method: 'DELETE' }
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)));
161
- }
162
-
163
- const ATTACHMENT_UPLOAD_CHUNK_BYTES = 512 * 1024;
164
-
165
- function uploadImageInChunks(sessionId, file, onProgress) {
166
- let xhr = null;
167
- let cancelled = false;
168
- const uploadId = crypto.randomUUID?.() || `${Date.now()}-${Math.random().toString(16).slice(2)}`;
169
- const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
170
- return {
171
- abort: () => {
172
- cancelled = true;
173
- xhr?.abort();
174
- void fetch(`/api/sessions/${sessionId}/attachments/images/uploads/${encodeURIComponent(uploadId)}`, { method: 'DELETE' }).catch(() => null);
175
- },
176
- promise: (async () => {
177
- for (let chunkIndex = 0; chunkIndex < chunkTotal; chunkIndex++) {
178
- if (cancelled) throw new Error('Image upload cancelled');
179
- const start = chunkIndex * ATTACHMENT_UPLOAD_CHUNK_BYTES;
180
- const chunk = file.slice(start, Math.min(file.size, start + ATTACHMENT_UPLOAD_CHUNK_BYTES));
181
- const result = await new Promise((resolve, reject) => {
182
- xhr = new XMLHttpRequest();
183
- xhr.open('POST', `/api/sessions/${sessionId}/attachments/images/chunks`);
184
- xhr.timeout = 60_000;
185
- xhr.setRequestHeader('Content-Type', file.type || 'application/octet-stream');
186
- xhr.setRequestHeader('X-Glad-Upload-Id', uploadId);
187
- xhr.setRequestHeader('X-Glad-Chunk-Index', String(chunkIndex));
188
- xhr.setRequestHeader('X-Glad-Chunk-Total', String(chunkTotal));
189
- xhr.onerror = () => reject(new Error('Network error while uploading image'));
190
- xhr.ontimeout = () => reject(new Error(`Image upload timed out on chunk ${chunkIndex + 1}/${chunkTotal}`));
191
- xhr.onabort = () => reject(new Error('Image upload cancelled'));
192
- xhr.onload = () => {
193
- let data = {};
194
- try { data = JSON.parse(xhr.responseText || '{}'); } catch (_) {}
195
- if (xhr.status < 200 || xhr.status >= 300 || !data.success) {
196
- reject(new Error(data.error || `Upload failed (HTTP ${xhr.status})`));
197
- return;
198
- }
199
- resolve(data);
200
- };
201
- xhr.send(chunk);
202
- });
203
- const confirmedBytes = Math.min(file.size, start + chunk.size);
204
- onProgress(Math.round((confirmedBytes / file.size) * 100), chunkIndex + 1, chunkTotal);
205
- if (result.complete) return result.attachment;
206
- }
207
- throw new Error('Image upload did not complete');
208
- })()
209
- };
210
- }
211
-
212
- async function uploadImageFiles(files) {
213
- if (!isStructuredImageAttachmentAvailable()) {
214
- alert('Image attachments are available only in structured chat mode.');
215
- return;
216
- }
217
- const remaining = 5 - selectedImageAttachments.length;
218
- const batch = Array.from(files).slice(0, remaining);
219
- if (files.length > remaining) alert('You can attach up to 5 images at a time.');
220
- for (const file of batch) {
221
- if (file.size > 50 * 1024 * 1024) {
222
- alert(`${file.name} is larger than 50 MB.`);
223
- continue;
224
- }
225
- const chunkTotal = Math.ceil(file.size / ATTACHMENT_UPLOAD_CHUNK_BYTES);
226
- const pending = {
227
- id: `uploading-${crypto.randomUUID?.() || `${Date.now()}-${Math.random()}`}`,
228
- name: file.name || 'image',
229
- sessionId: activeSessionId,
230
- uploading: true,
231
- progress: 0,
232
- progressKnown: true,
233
- status: '0%',
234
- abortUpload: null
235
- };
236
- selectedImageAttachments.push(pending);
237
- renderComposerAttachments();
238
- try {
239
- const upload = uploadImageInChunks(activeSessionId, file, progress => {
240
- pending.progress = progress;
241
- pending.status = `${progress}%`;
242
- renderComposerAttachments();
243
- });
244
- pending.abortUpload = upload.abort;
245
- const attachment = await upload.promise;
246
- const index = selectedImageAttachments.indexOf(pending);
247
- if (index < 0) {
248
- await fetchWithTimeout(`/api/sessions/${activeSessionId}/attachments/images/${encodeURIComponent(attachment.id)}`, { method: 'DELETE' });
249
- continue;
250
- }
251
- selectedImageAttachments[index] = { ...attachment, name: file.name || attachment.name, sessionId: activeSessionId };
252
- renderComposerAttachments();
253
- } catch (e) {
254
- selectedImageAttachments = selectedImageAttachments.filter(item => item !== pending);
255
- renderComposerAttachments();
256
- if (e.message === 'Image upload cancelled') continue;
257
- alert(`Could not add ${file.name}: ${e.message}`);
258
- }
259
- }
260
- }
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
-
367
- function openTimedSendPanel() {
368
- if (isClaudeSession()) {
369
- closeClaudePicker();
370
- claudeResumePanelOpen = false;
371
- claudeForkPanelOpen = false;
372
- document.getElementById('claude-resume-panel').classList.remove('active');
373
- document.getElementById('claude-fork-panel').classList.remove('active');
374
- }
375
- initTimedDelaySelectors();
376
- resetTimedEditor({ keepInput: true });
377
- document.getElementById('timed-send-panel').classList.add('active');
378
- updateTimedSendPreview();
379
- loadTimedInputs();
380
- syncComposerButtonState();
381
- updateTerminalControlsHeight();
382
- }
383
-
384
- function markInputEditStart() {
385
- keepTerminalBottomForNextInput = keepTerminalBottomForNextInput || isTerminalAtBottom();
386
- }
387
-
388
- inputEl.addEventListener('beforeinput', markInputEditStart);
389
- inputEl.addEventListener('input', function() {
390
- const keepAtBottom = keepTerminalBottomForNextInput || isTerminalAtBottom();
391
- keepTerminalBottomForNextInput = false;
392
- this.style.height = 'auto';
393
- this.style.height = Math.min(this.scrollHeight, 150) + 'px';
394
- if (keepAtBottom) restoreTerminalBottomSoon();
395
- });
396
-
397
- function performSend() {
398
- const val = inputEl.value;
399
- const readyImageAttachments = selectedImageAttachments.filter(item => !item.uploading);
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.');
403
- return;
404
- }
405
- if ((val || readyImageAttachments.length || readyFileAttachments.length) && isClaudeSession()) {
406
- if (currentSocket && currentSocket.readyState === 1) {
407
- currentSocket.send(JSON.stringify({
408
- type: 'claude-input',
409
- text: val,
410
- attachmentIds: readyImageAttachments.map(item => item.id),
411
- ...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {})
412
- }));
413
- }
414
- inputEl.value = '';
415
- inputEl.style.height = '38px';
416
- selectedImageAttachments = [];
417
- selectedFileAttachments = [];
418
- renderComposerAttachments();
419
- return;
420
- }
421
- if ((val || readyImageAttachments.length || readyFileAttachments.length) && isCodexSession()) {
422
- if (!codexReadyForInput()) return;
423
- if (currentSocket && currentSocket.readyState === 1) {
424
- currentSocket.send(JSON.stringify({
425
- type: 'codex-input',
426
- text: val,
427
- attachmentIds: readyImageAttachments.map(item => item.id),
428
- ...(readyFileAttachments.length ? { fileAttachmentIds: readyFileAttachments.map(item => item.id) } : {}),
429
- skills: selectedCodexSkill ? [{ name: selectedCodexSkill.name, path: selectedCodexSkill.path }] : []
430
- }));
431
- }
432
- inputEl.value = '';
433
- inputEl.style.height = '38px';
434
- selectedImageAttachments = [];
435
- selectedFileAttachments = [];
436
- selectedCodexSkill = null;
437
- renderComposerAttachments();
438
- renderCodexChat();
439
- return;
440
- }
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
- }
456
- const formattedVal = val.replace(/\n/g, '\r');
457
- sendWS(formattedVal);
458
- inputEl.value = '';
459
- inputEl.style.height = '38px';
460
- restoreTerminalBottomSoon();
461
- setTimeout(() => { sendWS('\r'); }, 1000);
462
- }
463
- }