@luckydraw/cumulus 0.27.19 → 0.27.21

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 (48) hide show
  1. package/dist/cli/cumulus.js +0 -0
  2. package/dist/gateway/adapters/webchat.d.ts +3 -1
  3. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  4. package/dist/gateway/adapters/webchat.js +168 -4
  5. package/dist/gateway/adapters/webchat.js.map +1 -1
  6. package/dist/gateway/daemon.js +8 -0
  7. package/dist/gateway/daemon.js.map +1 -1
  8. package/dist/gateway/device-mcp.d.ts +21 -0
  9. package/dist/gateway/device-mcp.d.ts.map +1 -0
  10. package/dist/gateway/device-mcp.js +165 -0
  11. package/dist/gateway/device-mcp.js.map +1 -0
  12. package/dist/gateway/gateway-agents-mcp.js +1 -1
  13. package/dist/gateway/gateway-agents-mcp.js.map +1 -1
  14. package/dist/gateway/server.d.ts +9 -0
  15. package/dist/gateway/server.d.ts.map +1 -1
  16. package/dist/gateway/server.js +243 -6
  17. package/dist/gateway/server.js.map +1 -1
  18. package/dist/gateway/static/widget.js +861 -43
  19. package/dist/index.d.ts +4 -0
  20. package/dist/index.d.ts.map +1 -1
  21. package/dist/index.js +6 -0
  22. package/dist/index.js.map +1 -1
  23. package/dist/lib/config.d.ts +8 -0
  24. package/dist/lib/config.d.ts.map +1 -1
  25. package/dist/lib/config.js +61 -1
  26. package/dist/lib/config.js.map +1 -1
  27. package/dist/lib/content-detector.d.ts.map +1 -1
  28. package/dist/lib/content-detector.js +9 -2
  29. package/dist/lib/content-detector.js.map +1 -1
  30. package/dist/lib/executor.d.ts +168 -0
  31. package/dist/lib/executor.d.ts.map +1 -0
  32. package/dist/lib/executor.js +284 -0
  33. package/dist/lib/executor.js.map +1 -0
  34. package/dist/lib/gateway.d.ts +14 -2
  35. package/dist/lib/gateway.d.ts.map +1 -1
  36. package/dist/lib/gateway.js +234 -103
  37. package/dist/lib/gateway.js.map +1 -1
  38. package/dist/lib/summaries.d.ts.map +1 -1
  39. package/dist/lib/summaries.js +9 -2
  40. package/dist/lib/summaries.js.map +1 -1
  41. package/dist/mcp/index.js +0 -0
  42. package/dist/mcp/tool-handler.d.ts.map +1 -1
  43. package/dist/mcp/tool-handler.js +10 -3
  44. package/dist/mcp/tool-handler.js.map +1 -1
  45. package/dist/tui/hooks/useClaudeProcess.d.ts.map +1 -1
  46. package/dist/tui/hooks/useClaudeProcess.js +18 -23
  47. package/dist/tui/hooks/useClaudeProcess.js.map +1 -1
  48. package/package.json +1 -1
@@ -2,6 +2,7 @@
2
2
  * Cumulus Web Chat Widget — self-contained, no external dependencies.
3
3
  * Embeddable via <script src="/widget.js" data-api-key="..."></script>
4
4
  * or usable standalone at /chat.
5
+ * @version 0.27.15
5
6
  *
6
7
  * < 80KB unminified.
7
8
  */
@@ -154,6 +155,38 @@
154
155
  ' word-wrap: break-word;',
155
156
  '}',
156
157
 
158
+ /* ── Timestamps ── */
159
+ '.cumulus-msg-time {',
160
+ ' font-size: 0.75em; color: #555; margin-top: 0.3em; padding: 0 0.3em;',
161
+ '}',
162
+ '.cumulus-msg-row:has(.user) .cumulus-msg-time { align-self: flex-end; }',
163
+ '.cumulus-msg-row:has(.assistant) .cumulus-msg-time { align-self: flex-start; }',
164
+
165
+ /* ── Agent messages ── */
166
+ '.cumulus-msg.agent {',
167
+ ' align-self: flex-start;',
168
+ ' background: transparent;',
169
+ ' color: #e0e0e0;',
170
+ ' padding: 0.4em 0 0.4em 0.75em;',
171
+ ' width: 100%;',
172
+ ' word-wrap: break-word;',
173
+ ' border-left: 2px solid var(--agent-color, #9966cc);',
174
+ '}',
175
+ '.cumulus-agent-header {',
176
+ ' display: flex; align-items: center; gap: 0.4em;',
177
+ ' margin-bottom: 0.3em; font-size: 0.82em;',
178
+ '}',
179
+ '.cumulus-agent-name {',
180
+ ' font-family: "SF Mono", "Fira Code", Consolas, monospace;',
181
+ ' font-weight: 600; color: var(--agent-color, #9966cc);',
182
+ '}',
183
+ '.cumulus-agent-name::before { content: "["; opacity: 0.5; }',
184
+ '.cumulus-agent-name::after { content: "]"; opacity: 0.5; }',
185
+ '.cumulus-agent-context {',
186
+ ' font-size: 0.9em; color: #666;',
187
+ ' font-family: "SF Mono", "Fira Code", Consolas, monospace;',
188
+ '}',
189
+
157
190
  /* ── Markdown rendered inside assistant messages ── */
158
191
  '.cumulus-msg.assistant p { margin: 0.4em 0; }',
159
192
  '.cumulus-msg.assistant p:first-child { margin-top: 0; }',
@@ -685,6 +718,97 @@
685
718
  ' border-radius: 3px; font-family: inherit; white-space: nowrap;',
686
719
  '}',
687
720
  '.cumulus-panel-action-btn:hover { color: #ddd; border-color: #555; }',
721
+ '.cumulus-panel-action-btn.active { color: #e0e0e0; border-color: #0066cc; background: rgba(0,102,204,0.12); }',
722
+
723
+ /* ── Context menu (right-click on thread) ── */
724
+ '.cumulus-context-menu {',
725
+ ' position: fixed; z-index: 300; background: #2a2a2a; border: 1px solid #444;',
726
+ ' border-radius: 6px; padding: 4px 0; min-width: 140px;',
727
+ ' box-shadow: 0 4px 12px rgba(0,0,0,0.5); font-size: 13px;',
728
+ '}',
729
+ '.cumulus-context-menu-item {',
730
+ ' padding: 6px 14px; color: #ccc; cursor: pointer; display: block;',
731
+ ' width: 100%; border: none; background: none; text-align: left;',
732
+ ' font-family: inherit; font-size: inherit;',
733
+ '}',
734
+ '.cumulus-context-menu-item:hover { background: #3a3a3a; color: #fff; }',
735
+ '.cumulus-context-menu-item.danger { color: #ef4444; }',
736
+ '.cumulus-context-menu-item.danger:hover { background: rgba(239,68,68,0.15); color: #ff6b6b; }',
737
+
738
+ /* ── Unread dot (glowing) ── */
739
+ '.cumulus-thread-unread {',
740
+ ' width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;',
741
+ ' background: #3b82f6;',
742
+ ' box-shadow: 0 0 4px #3b82f6, 0 0 8px rgba(59,130,246,0.4);',
743
+ ' animation: cumulus-unread-pulse 2s ease-in-out infinite;',
744
+ '}',
745
+ '@keyframes cumulus-unread-pulse {',
746
+ ' 0%, 100% { box-shadow: 0 0 4px #3b82f6, 0 0 8px rgba(59,130,246,0.4); }',
747
+ ' 50% { box-shadow: 0 0 6px #3b82f6, 0 0 14px rgba(59,130,246,0.6); }',
748
+ '}',
749
+
750
+ /* ── Message checkbox (selection mode) ── */
751
+ '.cumulus-msg-checkbox {',
752
+ ' display: none; flex-shrink: 0; align-self: flex-start; margin-top: 0.4em;',
753
+ ' width: 16px; height: 16px; cursor: pointer; accent-color: #0066cc;',
754
+ '}',
755
+ '.cumulus-selection-mode .cumulus-msg-checkbox { display: block; }',
756
+ '.cumulus-selection-mode .cumulus-msg-row {',
757
+ ' flex-direction: row; align-items: flex-start; gap: 8px;',
758
+ '}',
759
+ '.cumulus-selection-mode .cumulus-msg-row > *:not(.cumulus-msg-checkbox) { flex: 1; min-width: 0; }',
760
+
761
+ /* ── Selection action bar (floats at bottom of messages area) ── */
762
+ '.cumulus-selection-bar {',
763
+ ' display: flex; align-items: center; gap: 8px;',
764
+ ' padding: 8px 12px;',
765
+ ' background: #2d2d2d; border-top: 1px solid #3a3a3a;',
766
+ ' flex-shrink: 0;',
767
+ '}',
768
+ '.cumulus-selection-count { font-size: 13px; color: #aaa; flex: 1; }',
769
+ '.cumulus-selection-select-all-btn {',
770
+ ' background: none; border: 1px solid #555; color: #ccc;',
771
+ ' padding: 4px 10px; border-radius: 4px; font-size: 12px;',
772
+ ' cursor: pointer; font-family: inherit;',
773
+ '}',
774
+ '.cumulus-selection-select-all-btn:hover { border-color: #888; color: #e0e0e0; }',
775
+ '.cumulus-selection-delete-btn {',
776
+ ' background: #8b2222; border: none; color: white;',
777
+ ' padding: 4px 12px; border-radius: 4px; font-size: 12px;',
778
+ ' cursor: pointer; font-family: inherit; font-weight: 600;',
779
+ '}',
780
+ '.cumulus-selection-delete-btn:hover:not(:disabled) { background: #a33; }',
781
+ '.cumulus-selection-delete-btn:disabled { background: #3a2a2a; color: #666; cursor: not-allowed; }',
782
+
783
+ /* ── Confirm dialog overlay ── */
784
+ '.cumulus-confirm-dialog {',
785
+ ' position: absolute; top: 0; left: 0; right: 0; bottom: 0;',
786
+ ' background: rgba(0,0,0,0.6); z-index: 20;',
787
+ ' display: flex; align-items: center; justify-content: center;',
788
+ '}',
789
+ '.cumulus-confirm-dialog-box {',
790
+ ' background: #2d2d2d; border: 1px solid #4a4a4a; border-radius: 8px;',
791
+ ' padding: 1.5em; max-width: 320px; width: 90%;',
792
+ ' display: flex; flex-direction: column; gap: 1em; text-align: center;',
793
+ ' box-shadow: 0 8px 32px rgba(0,0,0,0.5);',
794
+ '}',
795
+ '.cumulus-confirm-dialog-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
796
+ '.cumulus-confirm-dialog-text { font-size: 13px; color: #aaa; }',
797
+ '.cumulus-confirm-dialog-warn { font-size: 12px; color: #f59e0b; }',
798
+ '.cumulus-confirm-dialog-actions { display: flex; gap: 8px; justify-content: center; }',
799
+ '.cumulus-confirm-dialog-cancel {',
800
+ ' padding: 0.4em 1.2em; border-radius: 4px; cursor: pointer;',
801
+ ' font-size: 13px; border: 1px solid #3a3a3a; background: #2a2a2a; color: #ccc;',
802
+ ' font-family: inherit;',
803
+ '}',
804
+ '.cumulus-confirm-dialog-cancel:hover { background: #3a3a3a; }',
805
+ '.cumulus-confirm-dialog-confirm {',
806
+ ' padding: 0.4em 1.2em; border-radius: 4px; cursor: pointer;',
807
+ ' font-size: 13px; border: none; background: #8b2222; color: white;',
808
+ ' font-family: inherit; font-weight: 600;',
809
+ '}',
810
+ '.cumulus-confirm-dialog-confirm:hover { background: #a33; }',
811
+ '.cumulus-confirm-dialog-confirm:disabled { background: #3a2a2a; color: #666; cursor: not-allowed; }',
688
812
 
689
813
  /* ── Overlay (covers the thread panel) ── */
690
814
  '.cumulus-overlay {',
@@ -920,6 +1044,70 @@
920
1044
  // else, then reinsert code blocks. This prevents markdown patterns inside
921
1045
  // code from being processed.
922
1046
 
1047
+ // ── Agent message detection + timestamps ──────────────────────────────────
1048
+ var AGENT_MSG_RE = /^\[([^\]]+)\s*\u2192\s*([^\]]+)\]:\s*/; // [sender → recipient]:
1049
+ var AGENT_COLORS = ['#9966cc', '#2aa198', '#e69500', '#d33682', '#85c025', '#ff6b6b'];
1050
+
1051
+ function getAgentColor(name) {
1052
+ var hash = 0;
1053
+ for (var i = 0; i < name.length; i++) hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
1054
+ return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length];
1055
+ }
1056
+
1057
+ function parseAgentMessage(content) {
1058
+ var match = content.match(AGENT_MSG_RE);
1059
+ if (!match) return null;
1060
+ var body = content
1061
+ .slice(match[0].length)
1062
+ .replace(/\n\n\(Reply using send_to_agent\([^)]*\)[^)]*\)\s*$/, '')
1063
+ .trim();
1064
+ // Also strip CC/broadcast reply hints
1065
+ body = body.replace(/\n\n\(You are CC'd on this message[^)]*\)\s*$/, '').trim();
1066
+ body = body.replace(/\n\n\(Review the above messages[^)]*\)\s*$/, '').trim();
1067
+ return { sender: match[1].trim(), recipient: match[2].trim(), body: body };
1068
+ }
1069
+
1070
+ function formatTime(timestamp) {
1071
+ if (!timestamp) return null;
1072
+ var d = new Date(timestamp);
1073
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1074
+ }
1075
+
1076
+ function appendTimestamp(parent, timestamp) {
1077
+ var time = formatTime(timestamp);
1078
+ if (time) {
1079
+ var timeEl = document.createElement('div');
1080
+ timeEl.className = 'cumulus-msg-time';
1081
+ timeEl.textContent = time;
1082
+ parent.appendChild(timeEl);
1083
+ }
1084
+ }
1085
+
1086
+ function buildAgentMsgEl(agent) {
1087
+ var el = document.createElement('div');
1088
+ el.className = 'cumulus-msg agent';
1089
+ el.style.setProperty('--agent-color', getAgentColor(agent.sender));
1090
+
1091
+ var header = document.createElement('div');
1092
+ header.className = 'cumulus-agent-header';
1093
+ var nameEl = document.createElement('span');
1094
+ nameEl.className = 'cumulus-agent-name';
1095
+ nameEl.textContent = agent.sender;
1096
+ header.appendChild(nameEl);
1097
+ var ctx = document.createElement('span');
1098
+ ctx.className = 'cumulus-agent-context';
1099
+ ctx.textContent = '\u2192 ' + agent.recipient;
1100
+ header.appendChild(ctx);
1101
+ el.appendChild(header);
1102
+
1103
+ var body = document.createElement('div');
1104
+ body.innerHTML = renderMarkdown(agent.body);
1105
+ el.appendChild(body);
1106
+ return el;
1107
+ }
1108
+
1109
+ // ── Markdown ──────────────────────────────────────────────────────────────
1110
+
923
1111
  function renderMarkdown(text) {
924
1112
  // Phase 1: Extract fenced code blocks — replace with unique tokens
925
1113
  var codeBlocks = [];
@@ -1333,8 +1521,12 @@
1333
1521
  var streamBuffer = '';
1334
1522
  var connection = null;
1335
1523
  var connectionStatus = 'disconnected';
1524
+ var embeddedStreamTimer = null;
1525
+ var EMBEDDED_STREAM_TIMEOUT = 120000;
1336
1526
  var authenticated = false;
1337
1527
  var pendingAttachments = [];
1528
+ var selectionMode = false;
1529
+ var selectedIds = {};
1338
1530
 
1339
1531
  // ── Panel ──
1340
1532
  var panel = document.createElement('div');
@@ -1351,6 +1543,9 @@
1351
1543
  '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1352
1544
  '<span data-testid="webchat-status-text">Disconnected</span>' +
1353
1545
  '</span>' +
1546
+ '<div class="cumulus-panel-actions">' +
1547
+ '<button class="cumulus-panel-action-btn" data-testid="webchat-select-btn" title="Select messages to delete">Select</button>' +
1548
+ '</div>' +
1354
1549
  '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>';
1355
1550
  panel.appendChild(header);
1356
1551
 
@@ -1449,7 +1644,9 @@
1449
1644
 
1450
1645
  // ── Rendering ──
1451
1646
  function scrollToBottom() {
1452
- messagesEl.scrollTop = messagesEl.scrollHeight;
1647
+ requestAnimationFrame(function () {
1648
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1649
+ });
1453
1650
  }
1454
1651
 
1455
1652
  function isWideUserMessage(content) {
@@ -1518,20 +1715,51 @@
1518
1715
 
1519
1716
  function renderMessages() {
1520
1717
  messagesEl.innerHTML = '';
1718
+ // Apply selection mode class
1719
+ if (selectionMode) {
1720
+ messagesEl.classList.add('cumulus-selection-mode');
1721
+ } else {
1722
+ messagesEl.classList.remove('cumulus-selection-mode');
1723
+ }
1521
1724
  if (messages.length === 0 && !streaming) {
1522
1725
  messagesEl.innerHTML =
1523
1726
  '<div class="cumulus-empty">Send a message to start chatting</div>';
1727
+ updateEmbeddedSelectionBar();
1524
1728
  return;
1525
1729
  }
1526
1730
  for (var i = 0; i < messages.length; i++) {
1527
1731
  var msg = messages[i];
1528
1732
  var row = document.createElement('div');
1529
1733
  row.className = 'cumulus-msg-row';
1734
+ // Add checkbox in selection mode
1735
+ if (selectionMode && msg.id) {
1736
+ var cb = document.createElement('input');
1737
+ cb.type = 'checkbox';
1738
+ cb.className = 'cumulus-msg-checkbox';
1739
+ cb.checked = !!selectedIds[msg.id];
1740
+ (function (msgId) {
1741
+ cb.addEventListener('change', function () {
1742
+ if (cb.checked) {
1743
+ selectedIds[msgId] = true;
1744
+ } else {
1745
+ delete selectedIds[msgId];
1746
+ }
1747
+ updateEmbeddedSelectionBar();
1748
+ });
1749
+ })(msg.id);
1750
+ row.appendChild(cb);
1751
+ }
1530
1752
  if (msg.role === 'user') {
1531
- row.appendChild(buildUserMsgEl(msg));
1753
+ var agent = parseAgentMessage(msg.content);
1754
+ if (agent) {
1755
+ row.appendChild(buildAgentMsgEl(agent));
1756
+ } else {
1757
+ row.appendChild(buildUserMsgEl(msg));
1758
+ }
1532
1759
  } else {
1533
1760
  row.appendChild(buildAssistantMsgEl(msg.content, false));
1534
1761
  }
1762
+ appendTimestamp(row, msg.timestamp);
1535
1763
  messagesEl.appendChild(row);
1536
1764
  }
1537
1765
  if (streaming) {
@@ -1541,6 +1769,115 @@
1541
1769
  messagesEl.appendChild(row);
1542
1770
  }
1543
1771
  scrollToBottom();
1772
+ updateEmbeddedSelectionBar();
1773
+ }
1774
+
1775
+ function updateEmbeddedSelectionBar() {
1776
+ var existingBar = panel.querySelector('.cumulus-selection-bar');
1777
+ if (existingBar) existingBar.remove();
1778
+ if (!selectionMode) return;
1779
+
1780
+ var selectedCount = Object.keys(selectedIds).length;
1781
+ var selectableCount = messages.filter(function (m) {
1782
+ return !!m.id;
1783
+ }).length;
1784
+
1785
+ var bar = document.createElement('div');
1786
+ bar.className = 'cumulus-selection-bar';
1787
+
1788
+ var countEl = document.createElement('span');
1789
+ countEl.className = 'cumulus-selection-count';
1790
+ countEl.textContent = selectedCount + ' selected';
1791
+ bar.appendChild(countEl);
1792
+
1793
+ var selectAllBtn = document.createElement('button');
1794
+ selectAllBtn.className = 'cumulus-selection-select-all-btn';
1795
+ selectAllBtn.textContent =
1796
+ selectedCount === selectableCount && selectableCount > 0 ? 'Deselect All' : 'Select All';
1797
+ selectAllBtn.addEventListener('click', function () {
1798
+ if (selectedCount === selectableCount && selectableCount > 0) {
1799
+ selectedIds = {};
1800
+ } else {
1801
+ messages.forEach(function (m) {
1802
+ if (m.id) selectedIds[m.id] = true;
1803
+ });
1804
+ }
1805
+ renderMessages();
1806
+ });
1807
+ bar.appendChild(selectAllBtn);
1808
+
1809
+ var deleteBtn = document.createElement('button');
1810
+ deleteBtn.className = 'cumulus-selection-delete-btn';
1811
+ deleteBtn.textContent = 'Delete';
1812
+ deleteBtn.disabled = selectedCount === 0;
1813
+ deleteBtn.addEventListener('click', function () {
1814
+ if (selectedCount === 0) return;
1815
+ showEmbeddedDeleteMessagesConfirm(Object.keys(selectedIds));
1816
+ });
1817
+ bar.appendChild(deleteBtn);
1818
+
1819
+ var inputAreaEl = panel.querySelector('.cumulus-input-area');
1820
+ if (inputAreaEl) {
1821
+ panel.insertBefore(bar, inputAreaEl);
1822
+ } else {
1823
+ panel.appendChild(bar);
1824
+ }
1825
+ }
1826
+
1827
+ function showEmbeddedDeleteMessagesConfirm(ids) {
1828
+ var existing = panel.querySelector('.cumulus-confirm-dialog');
1829
+ if (existing) existing.remove();
1830
+
1831
+ var dialog = document.createElement('div');
1832
+ dialog.className = 'cumulus-confirm-dialog';
1833
+
1834
+ var box = document.createElement('div');
1835
+ box.className = 'cumulus-confirm-dialog-box';
1836
+
1837
+ var title = document.createElement('div');
1838
+ title.className = 'cumulus-confirm-dialog-title';
1839
+ title.textContent = 'Delete Messages';
1840
+ box.appendChild(title);
1841
+
1842
+ var text = document.createElement('div');
1843
+ text.className = 'cumulus-confirm-dialog-text';
1844
+ text.textContent =
1845
+ ids.length + ' message' + (ids.length === 1 ? '' : 's') + ' will be permanently deleted.';
1846
+ box.appendChild(text);
1847
+
1848
+ var warn = document.createElement('div');
1849
+ warn.className = 'cumulus-confirm-dialog-warn';
1850
+ warn.textContent = 'This cannot be undone.';
1851
+ box.appendChild(warn);
1852
+
1853
+ var actions = document.createElement('div');
1854
+ actions.className = 'cumulus-confirm-dialog-actions';
1855
+
1856
+ var cancelBtn = document.createElement('button');
1857
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
1858
+ cancelBtn.textContent = 'Cancel';
1859
+ cancelBtn.addEventListener('click', function () {
1860
+ dialog.remove();
1861
+ });
1862
+ actions.appendChild(cancelBtn);
1863
+
1864
+ var confirmBtn = document.createElement('button');
1865
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
1866
+ confirmBtn.setAttribute('data-testid', 'webchat-delete-messages-confirm');
1867
+ confirmBtn.textContent = 'Delete';
1868
+ confirmBtn.addEventListener('click', function () {
1869
+ confirmBtn.disabled = true;
1870
+ confirmBtn.textContent = 'Deleting\u2026';
1871
+ if (connection) {
1872
+ connection.send({ type: 'delete_messages', threadName: sessionId, messageIds: ids });
1873
+ }
1874
+ dialog.remove();
1875
+ });
1876
+ actions.appendChild(confirmBtn);
1877
+
1878
+ box.appendChild(actions);
1879
+ dialog.appendChild(box);
1880
+ panel.appendChild(dialog);
1544
1881
  }
1545
1882
 
1546
1883
  function updateSendBtn() {
@@ -1629,6 +1966,17 @@
1629
1966
  function handleServerMessage(data) {
1630
1967
  switch (data.type) {
1631
1968
  case 'auth_ok':
1969
+ // Reset streaming state on reconnect
1970
+ if (streaming) {
1971
+ if (streamBuffer) {
1972
+ messages.push({ role: 'assistant', content: streamBuffer });
1973
+ streamBuffer = '';
1974
+ }
1975
+ streaming = false;
1976
+ stopRequested = false;
1977
+ if (embeddedStreamTimer) { clearTimeout(embeddedStreamTimer); embeddedStreamTimer = null; }
1978
+ updateSendBtn();
1979
+ }
1632
1980
  showChatView();
1633
1981
  break;
1634
1982
  case 'auth_error':
@@ -1637,7 +1985,7 @@
1637
1985
  case 'history':
1638
1986
  if (data.messages && data.messages.length > 0) {
1639
1987
  messages = data.messages.map(function (m) {
1640
- return { role: m.role, content: m.content };
1988
+ return { id: m.id, role: m.role, content: m.content, timestamp: m.timestamp };
1641
1989
  });
1642
1990
  renderMessages();
1643
1991
  }
@@ -1650,11 +1998,29 @@
1650
1998
  updateSendBtn();
1651
1999
  }
1652
2000
  streamBuffer += data.text;
2001
+ // Reset streaming timeout on each token
2002
+ if (embeddedStreamTimer) clearTimeout(embeddedStreamTimer);
2003
+ embeddedStreamTimer = setTimeout(function () {
2004
+ if (streaming) {
2005
+ console.warn('[Cumulus] Streaming timeout — resetting');
2006
+ if (streamBuffer) {
2007
+ messages.push({ role: 'assistant', content: streamBuffer + '\n\n*(response may be incomplete)*' });
2008
+ }
2009
+ streaming = false;
2010
+ streamBuffer = '';
2011
+ updateSendBtn();
2012
+ renderMessages();
2013
+ // Reload history
2014
+ if (connection) connection.send({ type: 'history', threadName: sessionId, limit: 50 });
2015
+ }
2016
+ embeddedStreamTimer = null;
2017
+ }, EMBEDDED_STREAM_TIMEOUT);
1653
2018
  renderMessages();
1654
2019
  break;
1655
2020
  case 'segment':
1656
2021
  break;
1657
2022
  case 'done':
2023
+ if (embeddedStreamTimer) { clearTimeout(embeddedStreamTimer); embeddedStreamTimer = null; }
1658
2024
  if (stopRequested) {
1659
2025
  stopRequested = false;
1660
2026
  streaming = false;
@@ -1674,8 +2040,10 @@
1674
2040
  break;
1675
2041
  case 'interjected':
1676
2042
  // Server confirms old stream was aborted. Partial already frozen client-side.
2043
+ if (embeddedStreamTimer) { clearTimeout(embeddedStreamTimer); embeddedStreamTimer = null; }
1677
2044
  break;
1678
2045
  case 'error':
2046
+ if (embeddedStreamTimer) { clearTimeout(embeddedStreamTimer); embeddedStreamTimer = null; }
1679
2047
  stopRequested = false;
1680
2048
  streaming = false;
1681
2049
  if (streamBuffer) {
@@ -1693,6 +2061,30 @@
1693
2061
  updateSendBtn();
1694
2062
  renderMessages();
1695
2063
  break;
2064
+ case 'new_messages':
2065
+ // Agent inject completed — append new messages and re-render
2066
+ if (data.messages && data.messages.length > 0) {
2067
+ for (var nm = 0; nm < data.messages.length; nm++) {
2068
+ messages.push({ role: data.messages[nm].role, content: data.messages[nm].content });
2069
+ }
2070
+ renderMessages();
2071
+ }
2072
+ break;
2073
+ case 'messages_deleted':
2074
+ // Server confirmed deletion — reload history and exit selection mode
2075
+ selectionMode = false;
2076
+ selectedIds = {};
2077
+ var embSelectBtn = panel.querySelector('[data-testid="webchat-select-btn"]');
2078
+ if (embSelectBtn) {
2079
+ embSelectBtn.textContent = 'Select';
2080
+ embSelectBtn.classList.remove('active');
2081
+ }
2082
+ messages = [];
2083
+ // Re-request history
2084
+ if (connection) {
2085
+ connection.send({ type: 'history', threadName: sessionId, limit: 200 });
2086
+ }
2087
+ break;
1696
2088
  }
1697
2089
  }
1698
2090
 
@@ -1808,6 +2200,22 @@
1808
2200
  });
1809
2201
  }
1810
2202
 
2203
+ // Select button for embedded mode
2204
+ var embSelectBtn = panel.querySelector('[data-testid="webchat-select-btn"]');
2205
+ if (embSelectBtn) {
2206
+ embSelectBtn.addEventListener('click', function () {
2207
+ selectionMode = !selectionMode;
2208
+ selectedIds = {};
2209
+ embSelectBtn.textContent = selectionMode ? 'Done' : 'Select';
2210
+ if (selectionMode) {
2211
+ embSelectBtn.classList.add('active');
2212
+ } else {
2213
+ embSelectBtn.classList.remove('active');
2214
+ }
2215
+ renderMessages();
2216
+ });
2217
+ }
2218
+
1811
2219
  // Embedded mode: no auth panel handling needed (API key from attribute)
1812
2220
  function startConnection() {
1813
2221
  if (connection) {
@@ -1847,10 +2255,46 @@
1847
2255
 
1848
2256
  // Thread registry: threadName -> { messages, streaming, streamBuffer, stopRequested, pendingAttachments }
1849
2257
  var threadStates = {};
2258
+ var streamingTimeouts = {}; // threadName -> timeout ID
2259
+ var STREAMING_TIMEOUT_MS = 120000; // 2 minutes with no token = assume dead
2260
+
2261
+ function resetStreamingTimeout(threadName) {
2262
+ if (streamingTimeouts[threadName]) clearTimeout(streamingTimeouts[threadName]);
2263
+ streamingTimeouts[threadName] = setTimeout(function () {
2264
+ var s = threadStates[threadName];
2265
+ if (s && s.streaming) {
2266
+ console.warn('[CumulusChat] Streaming timeout for thread:', threadName);
2267
+ if (s.streamBuffer) {
2268
+ s.messages.push({ role: 'assistant', content: s.streamBuffer + '\n\n*(response may be incomplete — connection issue)*' });
2269
+ }
2270
+ s.streaming = false;
2271
+ s.streamBuffer = '';
2272
+ s.stopRequested = false;
2273
+ s.interjecting = false;
2274
+ // Reload history to get the real response if server completed
2275
+ s.historyLoaded = false;
2276
+ if (connection) {
2277
+ connection.send({ type: 'history', threadName: threadName, limit: 50 });
2278
+ }
2279
+ refreshThreadPanel(threadName);
2280
+ }
2281
+ delete streamingTimeouts[threadName];
2282
+ }, STREAMING_TIMEOUT_MS);
2283
+ }
2284
+
2285
+ function clearStreamingTimeout(threadName) {
2286
+ if (streamingTimeouts[threadName]) {
2287
+ clearTimeout(streamingTimeouts[threadName]);
2288
+ delete streamingTimeouts[threadName];
2289
+ }
2290
+ }
1850
2291
 
1851
2292
  // Thread list from server: [{ name, messageCount, lastActivity }]
1852
2293
  var allThreads = [];
1853
2294
 
2295
+ // Unread threads: threadName -> true
2296
+ var unreadThreads = {};
2297
+
1854
2298
  // Currently visible panel names (ordered, max 3)
1855
2299
  var visibleThreads = [];
1856
2300
 
@@ -1868,6 +2312,8 @@
1868
2312
  interjecting: false,
1869
2313
  pendingAttachments: [],
1870
2314
  historyLoaded: false,
2315
+ selectionMode: false,
2316
+ selectedIds: {},
1871
2317
  };
1872
2318
  }
1873
2319
  return threadStates[threadName];
@@ -2132,7 +2578,9 @@
2132
2578
  item.setAttribute('data-thread-name', name);
2133
2579
 
2134
2580
  var dot = document.createElement('span');
2135
- dot.className = 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
2581
+ dot.className = unreadThreads[name]
2582
+ ? 'cumulus-thread-unread'
2583
+ : 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
2136
2584
  item.appendChild(dot);
2137
2585
 
2138
2586
  var nameEl = document.createElement('span');
@@ -2155,6 +2603,13 @@
2155
2603
  item.appendChild(countEl);
2156
2604
  }
2157
2605
 
2606
+ // Right-click context menu
2607
+ item.addEventListener('contextmenu', function (e) {
2608
+ e.preventDefault();
2609
+ e.stopPropagation();
2610
+ showThreadContextMenu(name, e.clientX, e.clientY);
2611
+ });
2612
+
2158
2613
  item.addEventListener('click', function (e) {
2159
2614
  var multiSelect = e.metaKey || e.ctrlKey;
2160
2615
  if (multiSelect) {
@@ -2167,8 +2622,123 @@
2167
2622
  return item;
2168
2623
  }
2169
2624
 
2625
+ // ── Thread context menu (right-click) ──
2626
+ function dismissContextMenu() {
2627
+ var existing = document.querySelector('.cumulus-context-menu');
2628
+ if (existing) existing.remove();
2629
+ document.removeEventListener('click', dismissContextMenu);
2630
+ document.removeEventListener('contextmenu', dismissContextMenu);
2631
+ }
2632
+
2633
+ function showThreadContextMenu(threadName, x, y) {
2634
+ dismissContextMenu();
2635
+
2636
+ var menu = document.createElement('div');
2637
+ menu.className = 'cumulus-context-menu';
2638
+ menu.style.left = x + 'px';
2639
+ menu.style.top = y + 'px';
2640
+
2641
+ var deleteItem = document.createElement('button');
2642
+ deleteItem.className = 'cumulus-context-menu-item danger';
2643
+ deleteItem.textContent = 'Delete thread';
2644
+ deleteItem.setAttribute('data-testid', 'webchat-thread-delete');
2645
+ deleteItem.addEventListener('click', function () {
2646
+ dismissContextMenu();
2647
+ showThreadDeleteConfirm(threadName);
2648
+ });
2649
+ menu.appendChild(deleteItem);
2650
+
2651
+ document.body.appendChild(menu);
2652
+
2653
+ // Clamp to viewport
2654
+ var rect = menu.getBoundingClientRect();
2655
+ if (rect.right > window.innerWidth)
2656
+ menu.style.left = window.innerWidth - rect.width - 4 + 'px';
2657
+ if (rect.bottom > window.innerHeight)
2658
+ menu.style.top = window.innerHeight - rect.height - 4 + 'px';
2659
+
2660
+ // Dismiss on next click or right-click anywhere
2661
+ setTimeout(function () {
2662
+ document.addEventListener('click', dismissContextMenu);
2663
+ document.addEventListener('contextmenu', dismissContextMenu);
2664
+ }, 0);
2665
+ }
2666
+
2667
+ // ── Thread delete confirmation ──
2668
+ function showThreadDeleteConfirm(threadName) {
2669
+ // Remove any existing confirm dialogs
2670
+ var existing = document.querySelector(
2671
+ '.cumulus-confirm-dialog[data-context="thread-delete"]'
2672
+ );
2673
+ if (existing) {
2674
+ existing.remove();
2675
+ return;
2676
+ }
2677
+
2678
+ // Build inline confirm dialog attached to the sidebar scroll area
2679
+ var dialog = document.createElement('div');
2680
+ dialog.className = 'cumulus-confirm-dialog';
2681
+ dialog.setAttribute('data-context', 'thread-delete');
2682
+ dialog.style.cssText =
2683
+ 'position: fixed; top: 0; left: 0; right: 0; bottom: 0; background: rgba(0,0,0,0.6); z-index: 200; display: flex; align-items: center; justify-content: center;';
2684
+
2685
+ var box = document.createElement('div');
2686
+ box.className = 'cumulus-confirm-dialog-box';
2687
+
2688
+ var title = document.createElement('div');
2689
+ title.className = 'cumulus-confirm-dialog-title';
2690
+ title.textContent = 'Delete Thread';
2691
+ box.appendChild(title);
2692
+
2693
+ var text = document.createElement('div');
2694
+ text.className = 'cumulus-confirm-dialog-text';
2695
+ text.textContent = 'Delete "' + threadName + '"?';
2696
+ box.appendChild(text);
2697
+
2698
+ var warn = document.createElement('div');
2699
+ warn.className = 'cumulus-confirm-dialog-warn';
2700
+ warn.textContent = 'All messages and history will be permanently removed.';
2701
+ box.appendChild(warn);
2702
+
2703
+ var actions = document.createElement('div');
2704
+ actions.className = 'cumulus-confirm-dialog-actions';
2705
+
2706
+ var cancelBtn = document.createElement('button');
2707
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
2708
+ cancelBtn.textContent = 'Cancel';
2709
+ cancelBtn.addEventListener('click', function () {
2710
+ dialog.remove();
2711
+ });
2712
+ actions.appendChild(cancelBtn);
2713
+
2714
+ var confirmBtn = document.createElement('button');
2715
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
2716
+ confirmBtn.setAttribute('data-testid', 'webchat-thread-delete-confirm');
2717
+ confirmBtn.textContent = 'Delete';
2718
+ confirmBtn.addEventListener('click', function () {
2719
+ confirmBtn.disabled = true;
2720
+ confirmBtn.textContent = 'Deleting\u2026';
2721
+ if (connection) {
2722
+ connection.send({ type: 'delete_thread', threadName: threadName });
2723
+ }
2724
+ dialog.remove();
2725
+ });
2726
+ actions.appendChild(confirmBtn);
2727
+
2728
+ box.appendChild(actions);
2729
+ dialog.appendChild(box);
2730
+
2731
+ // Dismiss on backdrop click
2732
+ dialog.addEventListener('click', function (e) {
2733
+ if (e.target === dialog) dialog.remove();
2734
+ });
2735
+
2736
+ document.body.appendChild(dialog);
2737
+ }
2738
+
2170
2739
  // ── Panel management ──
2171
2740
  function soloThread(name) {
2741
+ delete unreadThreads[name];
2172
2742
  visibleThreads = [name];
2173
2743
  renderSidebar();
2174
2744
  renderContentArea();
@@ -2177,6 +2747,7 @@
2177
2747
  }
2178
2748
 
2179
2749
  function togglePanelThread(name) {
2750
+ delete unreadThreads[name];
2180
2751
  // On mobile, always solo (no side-by-side)
2181
2752
  if (isMobile()) {
2182
2753
  soloThread(name);
@@ -2307,6 +2878,16 @@
2307
2878
  });
2308
2879
  actions.appendChild(revertBtn);
2309
2880
 
2881
+ var selectBtn = document.createElement('button');
2882
+ selectBtn.className = 'cumulus-panel-action-btn';
2883
+ selectBtn.setAttribute('data-testid', 'webchat-select-btn');
2884
+ selectBtn.setAttribute('title', 'Select messages to delete');
2885
+ selectBtn.textContent = 'Select';
2886
+ selectBtn.addEventListener('click', function () {
2887
+ toggleSelectionMode();
2888
+ });
2889
+ actions.appendChild(selectBtn);
2890
+
2310
2891
  panelHeader.appendChild(actions);
2311
2892
 
2312
2893
  var closeBtn = document.createElement('button');
@@ -2375,7 +2956,11 @@
2375
2956
 
2376
2957
  // ── Panel-local render functions ──
2377
2958
  function scrollToBottom() {
2378
- messagesEl.scrollTop = messagesEl.scrollHeight;
2959
+ requestAnimationFrame(function () {
2960
+ requestAnimationFrame(function () {
2961
+ messagesEl.scrollTop = messagesEl.scrollHeight;
2962
+ });
2963
+ });
2379
2964
  }
2380
2965
 
2381
2966
  function isWideUserMessage(content) {
@@ -2444,20 +3029,51 @@
2444
3029
 
2445
3030
  function renderPanelMessages() {
2446
3031
  messagesEl.innerHTML = '';
3032
+ // Apply selection mode class to messages container
3033
+ if (state.selectionMode) {
3034
+ messagesEl.classList.add('cumulus-selection-mode');
3035
+ } else {
3036
+ messagesEl.classList.remove('cumulus-selection-mode');
3037
+ }
2447
3038
  if (state.messages.length === 0 && !state.streaming) {
2448
3039
  messagesEl.innerHTML =
2449
3040
  '<div class="cumulus-empty">Send a message to start chatting</div>';
3041
+ updateSelectionBar();
2450
3042
  return;
2451
3043
  }
2452
3044
  for (var i = 0; i < state.messages.length; i++) {
2453
3045
  var msg = state.messages[i];
2454
3046
  var row = document.createElement('div');
2455
3047
  row.className = 'cumulus-msg-row';
3048
+ // Add checkbox if in selection mode and message has an id
3049
+ if (state.selectionMode && msg.id) {
3050
+ var cb = document.createElement('input');
3051
+ cb.type = 'checkbox';
3052
+ cb.className = 'cumulus-msg-checkbox';
3053
+ cb.checked = !!state.selectedIds[msg.id];
3054
+ (function (msgId) {
3055
+ cb.addEventListener('change', function () {
3056
+ if (cb.checked) {
3057
+ state.selectedIds[msgId] = true;
3058
+ } else {
3059
+ delete state.selectedIds[msgId];
3060
+ }
3061
+ updateSelectionBar();
3062
+ });
3063
+ })(msg.id);
3064
+ row.appendChild(cb);
3065
+ }
2456
3066
  if (msg.role === 'user') {
2457
- row.appendChild(buildUserMsgEl(msg));
3067
+ var agent = parseAgentMessage(msg.content);
3068
+ if (agent) {
3069
+ row.appendChild(buildAgentMsgEl(agent));
3070
+ } else {
3071
+ row.appendChild(buildUserMsgEl(msg));
3072
+ }
2458
3073
  } else {
2459
3074
  row.appendChild(buildAssistantMsgEl(msg.content, false));
2460
3075
  }
3076
+ appendTimestamp(row, msg.timestamp);
2461
3077
  messagesEl.appendChild(row);
2462
3078
  }
2463
3079
  if (state.streaming) {
@@ -2467,6 +3083,132 @@
2467
3083
  messagesEl.appendChild(row);
2468
3084
  }
2469
3085
  scrollToBottom();
3086
+ updateSelectionBar();
3087
+ }
3088
+
3089
+ function updateSelectionBar() {
3090
+ // Remove existing bar
3091
+ var existingBar = panel.querySelector('.cumulus-selection-bar');
3092
+ if (existingBar) existingBar.remove();
3093
+ if (!state.selectionMode) return;
3094
+
3095
+ var selectedCount = Object.keys(state.selectedIds).length;
3096
+ var selectableCount = state.messages.filter(function (m) {
3097
+ return !!m.id;
3098
+ }).length;
3099
+
3100
+ var bar = document.createElement('div');
3101
+ bar.className = 'cumulus-selection-bar';
3102
+
3103
+ var countEl = document.createElement('span');
3104
+ countEl.className = 'cumulus-selection-count';
3105
+ countEl.textContent = selectedCount + ' selected';
3106
+ bar.appendChild(countEl);
3107
+
3108
+ var selectAllBtn = document.createElement('button');
3109
+ selectAllBtn.className = 'cumulus-selection-select-all-btn';
3110
+ selectAllBtn.textContent =
3111
+ selectedCount === selectableCount && selectableCount > 0 ? 'Deselect All' : 'Select All';
3112
+ selectAllBtn.addEventListener('click', function () {
3113
+ if (selectedCount === selectableCount && selectableCount > 0) {
3114
+ // Deselect all
3115
+ state.selectedIds = {};
3116
+ } else {
3117
+ // Select all with IDs
3118
+ state.messages.forEach(function (m) {
3119
+ if (m.id) state.selectedIds[m.id] = true;
3120
+ });
3121
+ }
3122
+ renderPanelMessages();
3123
+ });
3124
+ bar.appendChild(selectAllBtn);
3125
+
3126
+ var deleteBtn = document.createElement('button');
3127
+ deleteBtn.className = 'cumulus-selection-delete-btn';
3128
+ deleteBtn.textContent = 'Delete';
3129
+ deleteBtn.disabled = selectedCount === 0;
3130
+ deleteBtn.addEventListener('click', function () {
3131
+ if (selectedCount === 0) return;
3132
+ showDeleteMessagesConfirm(threadName, panel, Object.keys(state.selectedIds));
3133
+ });
3134
+ bar.appendChild(deleteBtn);
3135
+
3136
+ // Insert before input area
3137
+ var inputAreaEl = panel.querySelector('.cumulus-input-area');
3138
+ if (inputAreaEl) {
3139
+ panel.insertBefore(bar, inputAreaEl);
3140
+ } else {
3141
+ panel.appendChild(bar);
3142
+ }
3143
+ }
3144
+
3145
+ function toggleSelectionMode() {
3146
+ state.selectionMode = !state.selectionMode;
3147
+ state.selectedIds = {};
3148
+ selectBtn.textContent = state.selectionMode ? 'Done' : 'Select';
3149
+ if (state.selectionMode) {
3150
+ selectBtn.classList.add('active');
3151
+ } else {
3152
+ selectBtn.classList.remove('active');
3153
+ }
3154
+ renderPanelMessages();
3155
+ }
3156
+
3157
+ function showDeleteMessagesConfirm(tName, panelEl, ids) {
3158
+ // Remove any existing confirm dialog
3159
+ var existing = panelEl.querySelector('.cumulus-confirm-dialog');
3160
+ if (existing) existing.remove();
3161
+
3162
+ var dialog = document.createElement('div');
3163
+ dialog.className = 'cumulus-confirm-dialog';
3164
+
3165
+ var box = document.createElement('div');
3166
+ box.className = 'cumulus-confirm-dialog-box';
3167
+
3168
+ var title = document.createElement('div');
3169
+ title.className = 'cumulus-confirm-dialog-title';
3170
+ title.textContent = 'Delete Messages';
3171
+ box.appendChild(title);
3172
+
3173
+ var text = document.createElement('div');
3174
+ text.className = 'cumulus-confirm-dialog-text';
3175
+ text.textContent =
3176
+ ids.length + ' message' + (ids.length === 1 ? '' : 's') + ' will be permanently deleted.';
3177
+ box.appendChild(text);
3178
+
3179
+ var warn = document.createElement('div');
3180
+ warn.className = 'cumulus-confirm-dialog-warn';
3181
+ warn.textContent = 'This cannot be undone.';
3182
+ box.appendChild(warn);
3183
+
3184
+ var actions = document.createElement('div');
3185
+ actions.className = 'cumulus-confirm-dialog-actions';
3186
+
3187
+ var cancelBtn = document.createElement('button');
3188
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
3189
+ cancelBtn.textContent = 'Cancel';
3190
+ cancelBtn.addEventListener('click', function () {
3191
+ dialog.remove();
3192
+ });
3193
+ actions.appendChild(cancelBtn);
3194
+
3195
+ var confirmBtn = document.createElement('button');
3196
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
3197
+ confirmBtn.setAttribute('data-testid', 'webchat-delete-messages-confirm');
3198
+ confirmBtn.textContent = 'Delete';
3199
+ confirmBtn.addEventListener('click', function () {
3200
+ confirmBtn.disabled = true;
3201
+ confirmBtn.textContent = 'Deleting\u2026';
3202
+ if (connection) {
3203
+ connection.send({ type: 'delete_messages', threadName: tName, messageIds: ids });
3204
+ }
3205
+ dialog.remove();
3206
+ });
3207
+ actions.appendChild(confirmBtn);
3208
+
3209
+ box.appendChild(actions);
3210
+ dialog.appendChild(box);
3211
+ panelEl.appendChild(dialog);
2470
3212
  }
2471
3213
 
2472
3214
  function updatePanelSendBtn() {
@@ -2557,6 +3299,9 @@
2557
3299
  state._renderMessages = renderPanelMessages;
2558
3300
  state._updateSendBtn = updatePanelSendBtn;
2559
3301
 
3302
+ // Initial render (messages may already be loaded from a previous view)
3303
+ renderPanelMessages();
3304
+
2560
3305
  // ── Send message for this panel ──
2561
3306
  function sendPanelMessage() {
2562
3307
  var text = inputEl.value.trim();
@@ -2568,6 +3313,7 @@
2568
3313
  if (!text && state.pendingAttachments.length === 0) {
2569
3314
  state.stopRequested = true;
2570
3315
  state.streaming = false;
3316
+ clearStreamingTimeout(threadName);
2571
3317
  updatePanelSendBtn();
2572
3318
  if (state.streamBuffer) {
2573
3319
  state.messages.push({ role: 'assistant', content: state.streamBuffer });
@@ -2601,6 +3347,7 @@
2601
3347
  state.stopRequested = false;
2602
3348
  state.streamBuffer = '';
2603
3349
  state.interjecting = false;
3350
+ resetStreamingTimeout(threadName);
2604
3351
  updatePanelSendBtn();
2605
3352
  renderPanelMessages();
2606
3353
 
@@ -2749,14 +3496,12 @@
2749
3496
  var filePath = addInput.value.trim();
2750
3497
  if (!filePath) return;
2751
3498
  connection &&
2752
- connection.send(
2753
- JSON.stringify({
2754
- type: 'include_add',
2755
- threadName: threadName,
2756
- filePath: filePath,
2757
- scope: currentScope,
2758
- })
2759
- );
3499
+ connection.send({
3500
+ type: 'include_add',
3501
+ threadName: threadName,
3502
+ filePath: filePath,
3503
+ scope: currentScope,
3504
+ });
2760
3505
  addInput.value = '';
2761
3506
  });
2762
3507
 
@@ -2779,8 +3524,7 @@
2779
3524
  panel.appendChild(overlay);
2780
3525
 
2781
3526
  // Request the list
2782
- connection &&
2783
- connection.send(JSON.stringify({ type: 'include_list', threadName: threadName }));
3527
+ connection && connection.send({ type: 'include_list', threadName: threadName });
2784
3528
  }
2785
3529
 
2786
3530
  function renderIncludeList(panel, threadName, files) {
@@ -2817,13 +3561,11 @@
2817
3561
  removeBtn.setAttribute('title', 'Remove');
2818
3562
  removeBtn.addEventListener('click', function () {
2819
3563
  connection &&
2820
- connection.send(
2821
- JSON.stringify({
2822
- type: 'include_remove',
2823
- threadName: threadName,
2824
- filePath: f.path,
2825
- })
2826
- );
3564
+ connection.send({
3565
+ type: 'include_remove',
3566
+ threadName: threadName,
3567
+ filePath: f.path,
3568
+ });
2827
3569
  });
2828
3570
  item.appendChild(removeBtn);
2829
3571
 
@@ -2868,8 +3610,7 @@
2868
3610
  panel.appendChild(overlay);
2869
3611
 
2870
3612
  // Request the turn list
2871
- connection &&
2872
- connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
3613
+ connection && connection.send({ type: 'revert_list', threadName: threadName });
2873
3614
  }
2874
3615
 
2875
3616
  function renderRevertList(panel, threadName, turns) {
@@ -2948,8 +3689,7 @@
2948
3689
  cancelBtn.textContent = 'Cancel';
2949
3690
  cancelBtn.addEventListener('click', function () {
2950
3691
  // Re-request list
2951
- connection &&
2952
- connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
3692
+ connection && connection.send({ type: 'revert_list', threadName: threadName });
2953
3693
  });
2954
3694
  actions.appendChild(cancelBtn);
2955
3695
 
@@ -2961,13 +3701,11 @@
2961
3701
  revertBtn.disabled = true;
2962
3702
  revertBtn.textContent = 'Reverting\u2026';
2963
3703
  connection &&
2964
- connection.send(
2965
- JSON.stringify({
2966
- type: 'revert_execute',
2967
- threadName: threadName,
2968
- targetMessageId: turn.userMessageId,
2969
- })
2970
- );
3704
+ connection.send({
3705
+ type: 'revert_execute',
3706
+ threadName: threadName,
3707
+ targetMessageId: turn.userMessageId,
3708
+ });
2971
3709
  });
2972
3710
  actions.appendChild(revertBtn);
2973
3711
 
@@ -3072,8 +3810,26 @@
3072
3810
 
3073
3811
  // ── Message router ──
3074
3812
  function handleServerMessage(data) {
3813
+ if (data.type !== 'token') {
3814
+ console.log('[CumulusChat] WS recv:', data.type, data.threadName || '', data);
3815
+ }
3075
3816
  switch (data.type) {
3076
3817
  case 'auth_ok':
3818
+ // Reset streaming state on reconnect — server may have finished while we were disconnected
3819
+ Object.keys(threadStates).forEach(function (tn) {
3820
+ var s = threadStates[tn];
3821
+ if (s.streaming) {
3822
+ if (s.streamBuffer) {
3823
+ s.messages.push({ role: 'assistant', content: s.streamBuffer });
3824
+ s.streamBuffer = '';
3825
+ }
3826
+ s.streaming = false;
3827
+ s.stopRequested = false;
3828
+ s.interjecting = false;
3829
+ }
3830
+ // Reload history from server to pick up any responses we missed
3831
+ s.historyLoaded = false;
3832
+ });
3077
3833
  showAppView();
3078
3834
  registerPushNotifications();
3079
3835
  break;
@@ -3117,12 +3873,50 @@
3117
3873
  }
3118
3874
  break;
3119
3875
 
3876
+ case 'thread_deleted':
3877
+ // Server confirms thread deleted; remove from sidebar and panels
3878
+ if (data.threadName) {
3879
+ allThreads = allThreads.filter(function (t) {
3880
+ return t.name !== data.threadName;
3881
+ });
3882
+ delete threadStates[data.threadName];
3883
+ removeThreadFromView(data.threadName);
3884
+ renderSidebar();
3885
+ }
3886
+ break;
3887
+
3888
+ case 'messages_deleted':
3889
+ // Server confirms messages deleted; reload history for that thread
3890
+ if (data.threadName) {
3891
+ var dState = getThreadState(data.threadName);
3892
+ dState.messages = [];
3893
+ dState.historyLoaded = false;
3894
+ dState.selectionMode = false;
3895
+ dState.selectedIds = {};
3896
+ // Exit selection mode on visible panel
3897
+ var dPanel = document.querySelector('[data-thread-panel="' + data.threadName + '"]');
3898
+ if (dPanel) {
3899
+ var dMsgs = dPanel.querySelector('.cumulus-messages');
3900
+ if (dMsgs) dMsgs.classList.remove('cumulus-selection-mode');
3901
+ var dBar = dPanel.querySelector('.cumulus-selection-bar');
3902
+ if (dBar) dBar.remove();
3903
+ var dSelectBtn = dPanel.querySelector('[data-testid="webchat-select-btn"]');
3904
+ if (dSelectBtn) {
3905
+ dSelectBtn.textContent = 'Select';
3906
+ dSelectBtn.classList.remove('active');
3907
+ }
3908
+ }
3909
+ connection &&
3910
+ connection.send({ type: 'history', threadName: data.threadName, limit: 200 });
3911
+ }
3912
+ break;
3913
+
3120
3914
  case 'history':
3121
3915
  if (data.threadName) {
3122
3916
  var state = getThreadState(data.threadName);
3123
3917
  if (data.messages && data.messages.length > 0) {
3124
3918
  state.messages = data.messages.map(function (m) {
3125
- return { id: m.id, role: m.role, content: m.content };
3919
+ return { id: m.id, role: m.role, content: m.content, timestamp: m.timestamp };
3126
3920
  });
3127
3921
  }
3128
3922
  refreshThreadPanel(data.threadName);
@@ -3138,6 +3932,8 @@
3138
3932
  state.streamBuffer = '';
3139
3933
  }
3140
3934
  state.streamBuffer += data.text;
3935
+ // Reset streaming timeout on each token
3936
+ resetStreamingTimeout(data.threadName);
3141
3937
  // Update lastActivity for this thread in allThreads
3142
3938
  updateThreadActivity(data.threadName);
3143
3939
  refreshThreadPanel(data.threadName);
@@ -3150,6 +3946,7 @@
3150
3946
 
3151
3947
  case 'done':
3152
3948
  if (data.threadName) {
3949
+ clearStreamingTimeout(data.threadName);
3153
3950
  var state = getThreadState(data.threadName);
3154
3951
  if (state.stopRequested) {
3155
3952
  state.stopRequested = false;
@@ -3176,11 +3973,12 @@
3176
3973
  case 'interjected':
3177
3974
  // Server confirms the old stream was aborted due to interjection.
3178
3975
  // The partial response was already frozen client-side in sendPanelMessage.
3179
- // Nothing to do — the new stream's tokens are already arriving.
3976
+ if (data.threadName) clearStreamingTimeout(data.threadName);
3180
3977
  break;
3181
3978
 
3182
3979
  case 'error':
3183
3980
  if (data.threadName) {
3981
+ clearStreamingTimeout(data.threadName);
3184
3982
  var state = getThreadState(data.threadName);
3185
3983
  state.stopRequested = false;
3186
3984
  state.streaming = false;
@@ -3201,6 +3999,22 @@
3201
3999
  }
3202
4000
  break;
3203
4001
 
4002
+ case 'new_messages':
4003
+ // Agent inject completed — append new messages to the thread and re-render
4004
+ if (data.threadName && data.messages && data.messages.length > 0) {
4005
+ var state = getThreadState(data.threadName);
4006
+ for (var nm = 0; nm < data.messages.length; nm++) {
4007
+ state.messages.push({
4008
+ id: data.messages[nm].id,
4009
+ role: data.messages[nm].role,
4010
+ content: data.messages[nm].content,
4011
+ });
4012
+ }
4013
+ updateThreadActivity(data.threadName);
4014
+ refreshThreadPanel(data.threadName);
4015
+ }
4016
+ break;
4017
+
3204
4018
  // ── Include/Revert responses ──
3205
4019
  case 'include_list':
3206
4020
  if (data.threadName && data.files) {
@@ -3233,13 +4047,11 @@
3233
4047
  tState.historyLoaded = false;
3234
4048
  // Re-request history
3235
4049
  connection &&
3236
- connection.send(
3237
- JSON.stringify({
3238
- type: 'history',
3239
- threadName: data.threadName,
3240
- limit: 200,
3241
- })
3242
- );
4050
+ connection.send({
4051
+ type: 'history',
4052
+ threadName: data.threadName,
4053
+ limit: 200,
4054
+ });
3243
4055
  if (overlay) overlay.remove();
3244
4056
  } else {
3245
4057
  // Show error in overlay
@@ -3263,6 +4075,10 @@
3263
4075
  break;
3264
4076
  }
3265
4077
  }
4078
+ // Mark unread if thread is not currently visible
4079
+ if (visibleThreads.indexOf(threadName) === -1) {
4080
+ unreadThreads[threadName] = true;
4081
+ }
3266
4082
  // Re-render sidebar to reflect new sort order / move thread up
3267
4083
  renderSidebar();
3268
4084
  }
@@ -3415,5 +4231,7 @@
3415
4231
  }
3416
4232
 
3417
4233
  window.CumulusChat = widget;
4234
+ window.CumulusChat.version = '0.27.15';
4235
+ console.log('[CumulusChat] Widget loaded, version ' + window.CumulusChat.version);
3418
4236
  }
3419
4237
  })();