clay-server 2.18.0-beta.9 → 2.18.0

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.
@@ -0,0 +1,633 @@
1
+ import { mateAvatarUrl } from './avatar.js';
2
+ import { renderMarkdown, highlightCodeBlocks } from './markdown.js';
3
+ import { escapeHtml } from './utils.js';
4
+ import { iconHtml, refreshIcons } from './icons.js';
5
+
6
+ var ctx;
7
+
8
+ // --- State ---
9
+ var debateActive = false;
10
+ var debateTopic = "";
11
+ var debateRound = 0;
12
+ var debatePhase = "idle"; // idle | live | ended
13
+
14
+ // Current turn streaming state
15
+ var currentTurnEl = null;
16
+ var currentTurnMateId = null;
17
+ var turnFullText = "";
18
+ var turnStreamBuffer = "";
19
+ var turnDrainTimer = null;
20
+
21
+ // --- Init ---
22
+ export function initDebate(_ctx) {
23
+ ctx = _ctx;
24
+ }
25
+
26
+ function buildAvatarUrl(meta) {
27
+ return "https://api.dicebear.com/7.x/" + (meta.avatarStyle || "bottts") + "/svg?seed=" + encodeURIComponent(meta.avatarSeed || meta.mateId || "mate");
28
+ }
29
+
30
+ // --- Float info panel ---
31
+ function showDebateInfoFloat(msg) {
32
+ var floatEl = document.getElementById("debate-info-float");
33
+ if (!floatEl) return;
34
+
35
+ var html = '<div class="debate-info-float-inner">';
36
+ html += '<span class="debate-info-mod">' + iconHtml("mic") + ' ' + escapeHtml(msg.moderatorName || "Moderator") + '</span>';
37
+ html += '<span class="debate-info-sep">|</span>';
38
+ html += '<span class="debate-info-label">Panel:</span>';
39
+
40
+ if (msg.panelists) {
41
+ for (var i = 0; i < msg.panelists.length; i++) {
42
+ var p = msg.panelists[i];
43
+ if (i > 0) html += '<span class="debate-info-comma">,</span>';
44
+ html += '<span class="debate-info-chip">';
45
+ html += '<img class="debate-info-avatar" src="' + buildAvatarUrl(p) + '" width="14" height="14" />';
46
+ html += '<span>' + escapeHtml(p.name || "") + '</span>';
47
+ if (p.role) html += '<span class="debate-info-role">(' + escapeHtml(p.role) + ')</span>';
48
+ html += '</span>';
49
+ }
50
+ }
51
+
52
+ html += '</div>';
53
+ floatEl.innerHTML = html;
54
+ floatEl.classList.remove("hidden");
55
+ refreshIcons();
56
+ }
57
+
58
+ function hideDebateInfoFloat() {
59
+ var floatEl = document.getElementById("debate-info-float");
60
+ if (floatEl) {
61
+ floatEl.classList.add("hidden");
62
+ floatEl.innerHTML = "";
63
+ }
64
+ }
65
+
66
+ // --- Handlers ---
67
+
68
+ export function handleDebateResumed(msg) {
69
+ debateActive = true;
70
+ debatePhase = "live";
71
+ if (msg.topic) debateTopic = msg.topic;
72
+ if (msg.round) debateRound = msg.round;
73
+
74
+ // Show float info panel again if we have it
75
+ showDebateInfoFloat(msg);
76
+ }
77
+
78
+ export function handleDebateStarted(msg) {
79
+ debateActive = true;
80
+ debateTopic = msg.topic || "";
81
+ debateRound = 1;
82
+ debatePhase = "live";
83
+
84
+ // Show float info panel
85
+ showDebateInfoFloat(msg);
86
+
87
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
88
+ }
89
+
90
+ export function handleDebateTurn(msg) {
91
+ debateRound = msg.round || debateRound;
92
+
93
+ if (!ctx.messagesEl) return;
94
+
95
+ var turnEl = document.createElement("div");
96
+ turnEl.className = "debate-turn";
97
+
98
+ // Speaker header
99
+ var speakerRow = document.createElement("div");
100
+ speakerRow.className = "debate-speaker";
101
+
102
+ var avi = document.createElement("img");
103
+ avi.className = "debate-speaker-avatar";
104
+ avi.src = buildAvatarUrl(msg);
105
+ avi.width = 24;
106
+ avi.height = 24;
107
+ speakerRow.appendChild(avi);
108
+
109
+ var nameSpan = document.createElement("span");
110
+ nameSpan.className = "debate-speaker-name";
111
+ nameSpan.textContent = msg.mateName || "Speaker";
112
+ speakerRow.appendChild(nameSpan);
113
+
114
+ var roleSpan = document.createElement("span");
115
+ roleSpan.className = "debate-speaker-role";
116
+ roleSpan.textContent = msg.role || "";
117
+ speakerRow.appendChild(roleSpan);
118
+
119
+ turnEl.appendChild(speakerRow);
120
+
121
+ // Activity indicator
122
+ var activityDiv = document.createElement("div");
123
+ activityDiv.className = "activity-inline debate-activity-bar";
124
+ activityDiv.innerHTML =
125
+ '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
126
+ '<span class="activity-text">Thinking...</span>';
127
+ turnEl.appendChild(activityDiv);
128
+
129
+ // Content area
130
+ var contentDiv = document.createElement("div");
131
+ contentDiv.className = "md-content debate-turn-content";
132
+ contentDiv.dir = "auto";
133
+ turnEl.appendChild(contentDiv);
134
+
135
+ ctx.messagesEl.appendChild(turnEl);
136
+
137
+ // Set as current streaming target
138
+ currentTurnEl = turnEl;
139
+ currentTurnMateId = msg.mateId;
140
+ turnFullText = "";
141
+ turnStreamBuffer = "";
142
+
143
+ refreshIcons();
144
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
145
+ }
146
+
147
+ export function handleDebateActivity(msg) {
148
+ if (!currentTurnEl || msg.mateId !== currentTurnMateId) return;
149
+
150
+ var bar = currentTurnEl.querySelector(".debate-activity-bar");
151
+ if (msg.activity) {
152
+ if (!bar) {
153
+ bar = document.createElement("div");
154
+ bar.className = "activity-inline debate-activity-bar";
155
+ bar.innerHTML =
156
+ '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
157
+ '<span class="activity-text"></span>';
158
+ var contentEl = currentTurnEl.querySelector(".debate-turn-content");
159
+ if (contentEl) {
160
+ currentTurnEl.insertBefore(bar, contentEl);
161
+ } else {
162
+ currentTurnEl.appendChild(bar);
163
+ }
164
+ refreshIcons();
165
+ }
166
+ var textEl = bar.querySelector(".activity-text");
167
+ if (textEl) {
168
+ textEl.textContent = msg.activity === "thinking" ? "Thinking..." : msg.activity;
169
+ }
170
+ bar.style.display = "";
171
+ } else {
172
+ if (bar) bar.style.display = "none";
173
+ }
174
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
175
+ }
176
+
177
+ export function handleDebateStream(msg) {
178
+ if (!currentTurnEl || msg.mateId !== currentTurnMateId) return;
179
+
180
+ // Hide activity bar on first text
181
+ var bar = currentTurnEl.querySelector(".debate-activity-bar");
182
+ if (bar) bar.style.display = "none";
183
+
184
+ turnStreamBuffer += msg.delta;
185
+ if (!turnDrainTimer) {
186
+ turnDrainTimer = requestAnimationFrame(drainTurnStream);
187
+ }
188
+ }
189
+
190
+ function drainTurnStream() {
191
+ turnDrainTimer = null;
192
+ if (!currentTurnEl || turnStreamBuffer.length === 0) return;
193
+
194
+ var len = turnStreamBuffer.length;
195
+ var n;
196
+ if (len > 200) n = Math.ceil(len / 4);
197
+ else if (len > 80) n = 8;
198
+ else if (len > 30) n = 5;
199
+ else if (len > 10) n = 2;
200
+ else n = 1;
201
+
202
+ var chunk = turnStreamBuffer.slice(0, n);
203
+ turnStreamBuffer = turnStreamBuffer.slice(n);
204
+ turnFullText += chunk;
205
+
206
+ var contentEl = currentTurnEl.querySelector(".debate-turn-content");
207
+ if (contentEl) {
208
+ contentEl.innerHTML = renderMarkdown(turnFullText);
209
+ highlightCodeBlocks(contentEl);
210
+ }
211
+
212
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
213
+
214
+ if (turnStreamBuffer.length > 0) {
215
+ turnDrainTimer = requestAnimationFrame(drainTurnStream);
216
+ }
217
+ }
218
+
219
+ function flushTurnStream() {
220
+ if (turnDrainTimer) {
221
+ cancelAnimationFrame(turnDrainTimer);
222
+ turnDrainTimer = null;
223
+ }
224
+ if (turnStreamBuffer.length > 0) {
225
+ turnFullText += turnStreamBuffer;
226
+ turnStreamBuffer = "";
227
+ }
228
+ if (currentTurnEl) {
229
+ var contentEl = currentTurnEl.querySelector(".debate-turn-content");
230
+ if (contentEl) {
231
+ contentEl.innerHTML = renderMarkdown(turnFullText);
232
+ highlightCodeBlocks(contentEl);
233
+ }
234
+ }
235
+ }
236
+
237
+ export function handleDebateTurnDone(msg) {
238
+ flushTurnStream();
239
+
240
+ if (currentTurnEl) {
241
+ var bar = currentTurnEl.querySelector(".debate-activity-bar");
242
+ if (bar) bar.style.display = "none";
243
+ if (ctx.addCopyHandler && turnFullText) {
244
+ ctx.addCopyHandler(currentTurnEl, turnFullText);
245
+ }
246
+ }
247
+
248
+ currentTurnEl = null;
249
+ currentTurnMateId = null;
250
+ turnFullText = "";
251
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
252
+ }
253
+
254
+ export function handleDebateCommentQueued(msg) {
255
+ if (!ctx.messagesEl) return;
256
+
257
+ var commentEl = document.createElement("div");
258
+ commentEl.className = "debate-user-comment";
259
+
260
+ var label = document.createElement("span");
261
+ label.className = "debate-comment-label";
262
+ label.innerHTML = iconHtml("hand") + " You raised your hand:";
263
+
264
+ var textEl = document.createElement("div");
265
+ textEl.className = "debate-comment-text";
266
+ textEl.textContent = msg.text || "";
267
+
268
+ commentEl.appendChild(label);
269
+ commentEl.appendChild(textEl);
270
+ ctx.messagesEl.appendChild(commentEl);
271
+
272
+ refreshIcons();
273
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
274
+ }
275
+
276
+ export function handleDebateCommentInjected(msg) {
277
+ // Comment was delivered to moderator, no extra UI needed
278
+ }
279
+
280
+ export function handleDebateEnded(msg) {
281
+ debateActive = false;
282
+ debatePhase = "ended";
283
+
284
+ flushTurnStream();
285
+ currentTurnEl = null;
286
+ currentTurnMateId = null;
287
+
288
+ // Hide float info panel
289
+ hideDebateInfoFloat();
290
+
291
+ if (ctx.messagesEl) {
292
+ renderEndedBanner(msg);
293
+ }
294
+
295
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
296
+ }
297
+
298
+ function renderEndedBanner(entry) {
299
+ if (!ctx.messagesEl) return;
300
+
301
+ // Remove existing ended banner (prevent duplicates)
302
+ var existing = ctx.messagesEl.querySelector(".debate-ended-banner");
303
+ if (existing) existing.remove();
304
+
305
+ var endBanner = document.createElement("div");
306
+ endBanner.className = "debate-ended-banner";
307
+
308
+ var reasonText = entry.reason === "natural" ? "Debate concluded" :
309
+ entry.reason === "user_stopped" ? "Debate stopped by user" :
310
+ "Debate ended due to error";
311
+
312
+ var statusRow = document.createElement("div");
313
+ statusRow.className = "debate-ended-status";
314
+ statusRow.innerHTML = iconHtml("check-circle") + " " + escapeHtml(reasonText) + " (" + (entry.rounds || 0) + " rounds)";
315
+ endBanner.appendChild(statusRow);
316
+
317
+ // Resume row
318
+ var resumeRow = document.createElement("div");
319
+ resumeRow.className = "debate-ended-resume";
320
+
321
+ var resumeInput = document.createElement("textarea");
322
+ resumeInput.className = "debate-ended-resume-input";
323
+ resumeInput.rows = 1;
324
+ resumeInput.placeholder = "Continue with a new direction...";
325
+ resumeRow.appendChild(resumeInput);
326
+
327
+ var resumeBtn = document.createElement("button");
328
+ resumeBtn.className = "debate-ended-resume-btn";
329
+ resumeBtn.textContent = "Resume";
330
+ resumeBtn.addEventListener("click", function () {
331
+ var text = resumeInput.value.trim();
332
+ if (ctx.ws && ctx.ws.readyState === 1) {
333
+ ctx.ws.send(JSON.stringify({ type: "debate_conclude_response", action: "continue", text: text }));
334
+ }
335
+ endBanner.remove();
336
+ });
337
+ resumeRow.appendChild(resumeBtn);
338
+
339
+ endBanner.appendChild(resumeRow);
340
+
341
+ // Enter in textarea = resume
342
+ resumeInput.addEventListener("keydown", function (e) {
343
+ if (e.key === "Enter" && !e.shiftKey) {
344
+ e.preventDefault();
345
+ resumeBtn.click();
346
+ }
347
+ });
348
+
349
+ ctx.messagesEl.appendChild(endBanner);
350
+ refreshIcons();
351
+ }
352
+
353
+ export function handleDebateError(msg) {
354
+ if (ctx.messagesEl && debateActive) {
355
+ var errEl = document.createElement("div");
356
+ errEl.className = "debate-error";
357
+ errEl.textContent = "Error: " + (msg.error || "Unknown error");
358
+ ctx.messagesEl.appendChild(errEl);
359
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
360
+ }
361
+ }
362
+
363
+ // --- History replay ---
364
+ export function renderDebateStarted(entry) {
365
+ handleDebateStarted(entry);
366
+ }
367
+
368
+ export function renderDebateTurnDone(entry) {
369
+ if (!ctx.messagesEl) return;
370
+
371
+ var turnEl = document.createElement("div");
372
+ turnEl.className = "debate-turn";
373
+
374
+ var speakerRow = document.createElement("div");
375
+ speakerRow.className = "debate-speaker";
376
+
377
+ if (entry.avatarStyle || entry.avatarSeed || entry.mateId) {
378
+ var avi = document.createElement("img");
379
+ avi.className = "debate-speaker-avatar";
380
+ avi.src = buildAvatarUrl(entry);
381
+ avi.width = 24;
382
+ avi.height = 24;
383
+ speakerRow.appendChild(avi);
384
+ }
385
+
386
+ var nameSpan = document.createElement("span");
387
+ nameSpan.className = "debate-speaker-name";
388
+ nameSpan.textContent = entry.mateName || "Speaker";
389
+ speakerRow.appendChild(nameSpan);
390
+
391
+ var roleSpan = document.createElement("span");
392
+ roleSpan.className = "debate-speaker-role";
393
+ roleSpan.textContent = entry.role || "";
394
+ speakerRow.appendChild(roleSpan);
395
+
396
+ turnEl.appendChild(speakerRow);
397
+
398
+ var contentDiv = document.createElement("div");
399
+ contentDiv.className = "md-content debate-turn-content";
400
+ contentDiv.dir = "auto";
401
+ contentDiv.innerHTML = renderMarkdown(entry.text || "");
402
+ highlightCodeBlocks(contentDiv);
403
+ turnEl.appendChild(contentDiv);
404
+
405
+ ctx.messagesEl.appendChild(turnEl);
406
+ }
407
+
408
+ export function renderDebateUserResume(entry) {
409
+ if (!ctx.messagesEl) return;
410
+
411
+ // Remove the ended banner since we're resuming
412
+ var endedBanner = ctx.messagesEl.querySelector(".debate-ended-banner");
413
+ if (endedBanner) endedBanner.remove();
414
+
415
+ // Also remove conclude confirm if present
416
+ var confirmEl = document.getElementById("debate-conclude-confirm");
417
+ if (confirmEl) confirmEl.remove();
418
+
419
+ var el = document.createElement("div");
420
+ el.className = "debate-user-comment";
421
+
422
+ var label = document.createElement("span");
423
+ label.className = "debate-comment-label";
424
+ label.innerHTML = iconHtml("play") + " Debate resumed:";
425
+
426
+ var textEl = document.createElement("div");
427
+ textEl.className = "debate-comment-text";
428
+ textEl.textContent = entry.text || "";
429
+
430
+ el.appendChild(label);
431
+ el.appendChild(textEl);
432
+ ctx.messagesEl.appendChild(el);
433
+ refreshIcons();
434
+ if (ctx.scrollToBottom) ctx.scrollToBottom();
435
+ }
436
+
437
+ export function renderDebateEnded(entry) {
438
+ if (!ctx.messagesEl) return;
439
+
440
+ hideDebateInfoFloat();
441
+ renderEndedBanner(entry);
442
+ }
443
+
444
+ export function renderDebateCommentInjected(entry) {
445
+ if (!ctx.messagesEl) return;
446
+
447
+ var commentEl = document.createElement("div");
448
+ commentEl.className = "debate-user-comment";
449
+
450
+ var label = document.createElement("span");
451
+ label.className = "debate-comment-label";
452
+ label.innerHTML = iconHtml("hand") + " User comment:";
453
+
454
+ var textEl = document.createElement("div");
455
+ textEl.className = "debate-comment-text";
456
+ textEl.textContent = entry.text || "";
457
+
458
+ commentEl.appendChild(label);
459
+ commentEl.appendChild(textEl);
460
+ ctx.messagesEl.appendChild(commentEl);
461
+ refreshIcons();
462
+ }
463
+
464
+ export function isDebateActive() {
465
+ return debateActive;
466
+ }
467
+
468
+ // --- Debate modal ---
469
+ var modalEl = null;
470
+ var selectedPanelists = [];
471
+
472
+ export function openDebateModal() {
473
+ modalEl = document.getElementById("debate-modal");
474
+ if (!modalEl) return;
475
+
476
+ modalEl.classList.remove("hidden");
477
+
478
+ var topicInput = document.getElementById("debate-topic-input");
479
+ if (topicInput) {
480
+ topicInput.value = "";
481
+ topicInput.focus();
482
+ }
483
+
484
+ // Populate panelist list from mates (exclude current mate = moderator)
485
+ var panelList = document.getElementById("debate-panel-list");
486
+ if (panelList) {
487
+ panelList.innerHTML = "";
488
+ selectedPanelists = [];
489
+ var mates = ctx.matesList ? ctx.matesList() : [];
490
+ var currentMateId = ctx.currentMateId ? ctx.currentMateId() : null;
491
+ for (var i = 0; i < mates.length; i++) {
492
+ var m = mates[i];
493
+ if (m.status === "interviewing") continue;
494
+ if (m.id === currentMateId) continue; // moderator, skip
495
+ var item = createPanelItem(m);
496
+ panelList.appendChild(item);
497
+ }
498
+ }
499
+
500
+ // Close button
501
+ var closeBtn = document.getElementById("debate-modal-close");
502
+ if (closeBtn) {
503
+ closeBtn.onclick = closeDebateModal;
504
+ }
505
+ var cancelBtn = document.getElementById("debate-modal-cancel");
506
+ if (cancelBtn) {
507
+ cancelBtn.onclick = closeDebateModal;
508
+ }
509
+
510
+ // Backdrop click to close
511
+ var backdrop = modalEl.querySelector(".debate-modal-backdrop");
512
+ if (backdrop) {
513
+ backdrop.onclick = closeDebateModal;
514
+ }
515
+
516
+ // Start button
517
+ var startBtn = document.getElementById("debate-modal-start");
518
+ if (startBtn) {
519
+ startBtn.onclick = function () {
520
+ var topic = topicInput ? topicInput.value.trim() : "";
521
+ if (!topic) {
522
+ topicInput.focus();
523
+ return;
524
+ }
525
+ if (selectedPanelists.length === 0) return;
526
+
527
+ var currentMateId = ctx.currentMateId ? ctx.currentMateId() : null;
528
+ if (!currentMateId) return;
529
+
530
+ // Create a new session first, then send debate_start after switch
531
+ if (ctx.ws) {
532
+ var debatePayload = {
533
+ type: "debate_start",
534
+ moderatorId: currentMateId,
535
+ topic: topic,
536
+ panelists: selectedPanelists.map(function (id) {
537
+ return { mateId: id, role: "", brief: "" };
538
+ }),
539
+ };
540
+
541
+ // Listen for session_switched once, then send debate_start
542
+ var onMessage = function (evt) {
543
+ try {
544
+ var data = JSON.parse(evt.data);
545
+ if (data.type === "session_switched") {
546
+ ctx.ws.removeEventListener("message", onMessage);
547
+ ctx.ws.send(JSON.stringify(debatePayload));
548
+ }
549
+ } catch (e) {}
550
+ };
551
+ ctx.ws.addEventListener("message", onMessage);
552
+ ctx.ws.send(JSON.stringify({ type: "new_session" }));
553
+ }
554
+
555
+ closeDebateModal();
556
+ };
557
+ }
558
+
559
+ refreshIcons();
560
+ }
561
+
562
+ function createPanelItem(mate) {
563
+ var item = document.createElement("div");
564
+ item.className = "debate-panel-item";
565
+ item.dataset.mateId = mate.id;
566
+
567
+ var cb = document.createElement("input");
568
+ cb.type = "checkbox";
569
+ item.appendChild(cb);
570
+
571
+ var avatarSrc = "https://api.dicebear.com/7.x/" +
572
+ ((mate.profile && mate.profile.avatarStyle) || "bottts") +
573
+ "/svg?seed=" + encodeURIComponent((mate.profile && mate.profile.avatarSeed) || mate.id);
574
+ var avi = document.createElement("img");
575
+ avi.className = "debate-panel-item-avatar";
576
+ avi.src = avatarSrc;
577
+ item.appendChild(avi);
578
+
579
+ var info = document.createElement("div");
580
+ info.className = "debate-panel-item-info";
581
+
582
+ var nameSpan = document.createElement("div");
583
+ nameSpan.className = "debate-panel-item-name";
584
+ nameSpan.textContent = (mate.profile && mate.profile.displayName) || mate.name || "Mate";
585
+ info.appendChild(nameSpan);
586
+
587
+ if (mate.bio) {
588
+ var bioSpan = document.createElement("div");
589
+ bioSpan.className = "debate-panel-item-bio";
590
+ bioSpan.textContent = mate.bio;
591
+ info.appendChild(bioSpan);
592
+ }
593
+
594
+ item.appendChild(info);
595
+
596
+ // Toggle selection
597
+ function toggle() {
598
+ var idx = selectedPanelists.indexOf(mate.id);
599
+ if (idx === -1) {
600
+ selectedPanelists.push(mate.id);
601
+ item.classList.add("selected");
602
+ cb.checked = true;
603
+ } else {
604
+ selectedPanelists.splice(idx, 1);
605
+ item.classList.remove("selected");
606
+ cb.checked = false;
607
+ }
608
+ }
609
+
610
+ item.addEventListener("click", function (e) {
611
+ if (e.target === cb) return; // let checkbox handle itself
612
+ toggle();
613
+ });
614
+ cb.addEventListener("change", function () {
615
+ var idx = selectedPanelists.indexOf(mate.id);
616
+ if (cb.checked && idx === -1) {
617
+ selectedPanelists.push(mate.id);
618
+ item.classList.add("selected");
619
+ } else if (!cb.checked && idx !== -1) {
620
+ selectedPanelists.splice(idx, 1);
621
+ item.classList.remove("selected");
622
+ }
623
+ });
624
+
625
+ return item;
626
+ }
627
+
628
+ export function closeDebateModal() {
629
+ if (modalEl) {
630
+ modalEl.classList.add("hidden");
631
+ }
632
+ selectedPanelists = [];
633
+ }
@@ -14,6 +14,15 @@ var slashFiltered = [];
14
14
  var isComposing = false;
15
15
  var isRemoteInput = false;
16
16
 
17
+ export function hasSendableContent() {
18
+ return !!(
19
+ (ctx && ctx.inputEl && ctx.inputEl.value.trim()) ||
20
+ pendingPastes.length > 0 ||
21
+ pendingImages.length > 0 ||
22
+ pendingFiles.length > 0
23
+ );
24
+ }
25
+
17
26
  export var builtinCommands = [
18
27
  { name: "clear", desc: "Clear conversation" },
19
28
  { name: "context", desc: "Context window usage" },
@@ -145,7 +154,7 @@ export function autoResize() {
145
154
  ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
146
155
  // Defensive: sync send/stop button whenever input size changes
147
156
  if (ctx.processing && ctx.setSendBtnMode) {
148
- ctx.setSendBtnMode(ctx.inputEl.value.trim() ? "send" : "stop");
157
+ ctx.setSendBtnMode(hasSendableContent() ? "send" : "stop");
149
158
  }
150
159
  }
151
160
 
@@ -494,7 +503,7 @@ export function handleInputSync(text) {
494
503
  isRemoteInput = false;
495
504
  // Sync send/stop button state
496
505
  if (ctx.processing && ctx.setSendBtnMode) {
497
- ctx.setSendBtnMode(text.trim() ? "send" : "stop");
506
+ ctx.setSendBtnMode(hasSendableContent() ? "send" : "stop");
498
507
  }
499
508
  }
500
509
 
@@ -639,7 +648,7 @@ export function initInput(_ctx) {
639
648
  }
640
649
  // Toggle send/stop button based on input content during processing
641
650
  if (ctx.processing && ctx.setSendBtnMode) {
642
- ctx.setSendBtnMode(val.trim() ? "send" : "stop");
651
+ ctx.setSendBtnMode(hasSendableContent() ? "send" : "stop");
643
652
  }
644
653
  });
645
654
 
@@ -712,9 +721,9 @@ export function initInput(_ctx) {
712
721
  ctx.inputEl.setAttribute("enterkeyhint", "enter");
713
722
  }
714
723
 
715
- // Send/Stop button — if input has text, always send; otherwise stop
724
+ // Send/Stop button — if sendable content exists, always send; otherwise stop
716
725
  ctx.sendBtn.addEventListener("click", function () {
717
- if (ctx.inputEl.value.trim()) {
726
+ if (hasSendableContent()) {
718
727
  sendMessage();
719
728
  return;
720
729
  }