@luckydraw/cumulus 0.27.19 → 0.27.23

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 (51) 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/blex-chart.min.js +23 -0
  19. package/dist/gateway/static/blex.min.js +105 -0
  20. package/dist/gateway/static/chat.html +1 -0
  21. package/dist/gateway/static/widget.js +1055 -46
  22. package/dist/index.d.ts +4 -0
  23. package/dist/index.d.ts.map +1 -1
  24. package/dist/index.js +6 -0
  25. package/dist/index.js.map +1 -1
  26. package/dist/lib/config.d.ts +8 -0
  27. package/dist/lib/config.d.ts.map +1 -1
  28. package/dist/lib/config.js +61 -1
  29. package/dist/lib/config.js.map +1 -1
  30. package/dist/lib/content-detector.d.ts.map +1 -1
  31. package/dist/lib/content-detector.js +9 -2
  32. package/dist/lib/content-detector.js.map +1 -1
  33. package/dist/lib/executor.d.ts +168 -0
  34. package/dist/lib/executor.d.ts.map +1 -0
  35. package/dist/lib/executor.js +284 -0
  36. package/dist/lib/executor.js.map +1 -0
  37. package/dist/lib/gateway.d.ts +14 -2
  38. package/dist/lib/gateway.d.ts.map +1 -1
  39. package/dist/lib/gateway.js +234 -103
  40. package/dist/lib/gateway.js.map +1 -1
  41. package/dist/lib/summaries.d.ts.map +1 -1
  42. package/dist/lib/summaries.js +9 -2
  43. package/dist/lib/summaries.js.map +1 -1
  44. package/dist/mcp/index.js +0 -0
  45. package/dist/mcp/tool-handler.d.ts.map +1 -1
  46. package/dist/mcp/tool-handler.js +10 -3
  47. package/dist/mcp/tool-handler.js.map +1 -1
  48. package/dist/tui/hooks/useClaudeProcess.d.ts.map +1 -1
  49. package/dist/tui/hooks/useClaudeProcess.js +18 -23
  50. package/dist/tui/hooks/useClaudeProcess.js.map +1 -1
  51. package/package.json +3 -2
@@ -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; }',
@@ -165,6 +198,17 @@
165
198
  '.cumulus-msg em { font-style: italic; }',
166
199
  '.cumulus-msg del { text-decoration: line-through; color: #888; }',
167
200
 
201
+ /* Blex block containers */
202
+ '.blex-block-container {',
203
+ ' margin: 0.6em 0;',
204
+ ' border-radius: 8px;',
205
+ ' overflow: hidden;',
206
+ ' min-height: 2em;',
207
+ '}',
208
+ '.blex-block-container[data-blex-ready="true"] {',
209
+ ' min-height: auto;',
210
+ '}',
211
+
168
212
  /* Headings */
169
213
  '.cumulus-msg h1,.cumulus-msg h2,.cumulus-msg h3,.cumulus-msg h4,.cumulus-msg h5,.cumulus-msg h6 {',
170
214
  ' font-weight: 600; margin: 0.7em 0 0.3em; line-height: 1.3; color: #e8e8e8;',
@@ -685,6 +729,97 @@
685
729
  ' border-radius: 3px; font-family: inherit; white-space: nowrap;',
686
730
  '}',
687
731
  '.cumulus-panel-action-btn:hover { color: #ddd; border-color: #555; }',
732
+ '.cumulus-panel-action-btn.active { color: #e0e0e0; border-color: #0066cc; background: rgba(0,102,204,0.12); }',
733
+
734
+ /* ── Context menu (right-click on thread) ── */
735
+ '.cumulus-context-menu {',
736
+ ' position: fixed; z-index: 300; background: #2a2a2a; border: 1px solid #444;',
737
+ ' border-radius: 6px; padding: 4px 0; min-width: 140px;',
738
+ ' box-shadow: 0 4px 12px rgba(0,0,0,0.5); font-size: 13px;',
739
+ '}',
740
+ '.cumulus-context-menu-item {',
741
+ ' padding: 6px 14px; color: #ccc; cursor: pointer; display: block;',
742
+ ' width: 100%; border: none; background: none; text-align: left;',
743
+ ' font-family: inherit; font-size: inherit;',
744
+ '}',
745
+ '.cumulus-context-menu-item:hover { background: #3a3a3a; color: #fff; }',
746
+ '.cumulus-context-menu-item.danger { color: #ef4444; }',
747
+ '.cumulus-context-menu-item.danger:hover { background: rgba(239,68,68,0.15); color: #ff6b6b; }',
748
+
749
+ /* ── Unread dot (glowing) ── */
750
+ '.cumulus-thread-unread {',
751
+ ' width: 8px; height: 8px; border-radius: 50%; flex-shrink: 0;',
752
+ ' background: #3b82f6;',
753
+ ' box-shadow: 0 0 4px #3b82f6, 0 0 8px rgba(59,130,246,0.4);',
754
+ ' animation: cumulus-unread-pulse 2s ease-in-out infinite;',
755
+ '}',
756
+ '@keyframes cumulus-unread-pulse {',
757
+ ' 0%, 100% { box-shadow: 0 0 4px #3b82f6, 0 0 8px rgba(59,130,246,0.4); }',
758
+ ' 50% { box-shadow: 0 0 6px #3b82f6, 0 0 14px rgba(59,130,246,0.6); }',
759
+ '}',
760
+
761
+ /* ── Message checkbox (selection mode) ── */
762
+ '.cumulus-msg-checkbox {',
763
+ ' display: none; flex-shrink: 0; align-self: flex-start; margin-top: 0.4em;',
764
+ ' width: 16px; height: 16px; cursor: pointer; accent-color: #0066cc;',
765
+ '}',
766
+ '.cumulus-selection-mode .cumulus-msg-checkbox { display: block; }',
767
+ '.cumulus-selection-mode .cumulus-msg-row {',
768
+ ' flex-direction: row; align-items: flex-start; gap: 8px;',
769
+ '}',
770
+ '.cumulus-selection-mode .cumulus-msg-row > *:not(.cumulus-msg-checkbox) { flex: 1; min-width: 0; }',
771
+
772
+ /* ── Selection action bar (floats at bottom of messages area) ── */
773
+ '.cumulus-selection-bar {',
774
+ ' display: flex; align-items: center; gap: 8px;',
775
+ ' padding: 8px 12px;',
776
+ ' background: #2d2d2d; border-top: 1px solid #3a3a3a;',
777
+ ' flex-shrink: 0;',
778
+ '}',
779
+ '.cumulus-selection-count { font-size: 13px; color: #aaa; flex: 1; }',
780
+ '.cumulus-selection-select-all-btn {',
781
+ ' background: none; border: 1px solid #555; color: #ccc;',
782
+ ' padding: 4px 10px; border-radius: 4px; font-size: 12px;',
783
+ ' cursor: pointer; font-family: inherit;',
784
+ '}',
785
+ '.cumulus-selection-select-all-btn:hover { border-color: #888; color: #e0e0e0; }',
786
+ '.cumulus-selection-delete-btn {',
787
+ ' background: #8b2222; border: none; color: white;',
788
+ ' padding: 4px 12px; border-radius: 4px; font-size: 12px;',
789
+ ' cursor: pointer; font-family: inherit; font-weight: 600;',
790
+ '}',
791
+ '.cumulus-selection-delete-btn:hover:not(:disabled) { background: #a33; }',
792
+ '.cumulus-selection-delete-btn:disabled { background: #3a2a2a; color: #666; cursor: not-allowed; }',
793
+
794
+ /* ── Confirm dialog overlay ── */
795
+ '.cumulus-confirm-dialog {',
796
+ ' position: absolute; top: 0; left: 0; right: 0; bottom: 0;',
797
+ ' background: rgba(0,0,0,0.6); z-index: 20;',
798
+ ' display: flex; align-items: center; justify-content: center;',
799
+ '}',
800
+ '.cumulus-confirm-dialog-box {',
801
+ ' background: #2d2d2d; border: 1px solid #4a4a4a; border-radius: 8px;',
802
+ ' padding: 1.5em; max-width: 320px; width: 90%;',
803
+ ' display: flex; flex-direction: column; gap: 1em; text-align: center;',
804
+ ' box-shadow: 0 8px 32px rgba(0,0,0,0.5);',
805
+ '}',
806
+ '.cumulus-confirm-dialog-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
807
+ '.cumulus-confirm-dialog-text { font-size: 13px; color: #aaa; }',
808
+ '.cumulus-confirm-dialog-warn { font-size: 12px; color: #f59e0b; }',
809
+ '.cumulus-confirm-dialog-actions { display: flex; gap: 8px; justify-content: center; }',
810
+ '.cumulus-confirm-dialog-cancel {',
811
+ ' padding: 0.4em 1.2em; border-radius: 4px; cursor: pointer;',
812
+ ' font-size: 13px; border: 1px solid #3a3a3a; background: #2a2a2a; color: #ccc;',
813
+ ' font-family: inherit;',
814
+ '}',
815
+ '.cumulus-confirm-dialog-cancel:hover { background: #3a3a3a; }',
816
+ '.cumulus-confirm-dialog-confirm {',
817
+ ' padding: 0.4em 1.2em; border-radius: 4px; cursor: pointer;',
818
+ ' font-size: 13px; border: none; background: #8b2222; color: white;',
819
+ ' font-family: inherit; font-weight: 600;',
820
+ '}',
821
+ '.cumulus-confirm-dialog-confirm:hover { background: #a33; }',
822
+ '.cumulus-confirm-dialog-confirm:disabled { background: #3a2a2a; color: #666; cursor: not-allowed; }',
688
823
 
689
824
  /* ── Overlay (covers the thread panel) ── */
690
825
  '.cumulus-overlay {',
@@ -920,7 +1055,173 @@
920
1055
  // else, then reinsert code blocks. This prevents markdown patterns inside
921
1056
  // code from being processed.
922
1057
 
1058
+ // ── Agent message detection + timestamps ──────────────────────────────────
1059
+ var AGENT_MSG_RE = /^\[([^\]]+)\s*\u2192\s*([^\]]+)\]:\s*/; // [sender → recipient]:
1060
+ var AGENT_COLORS = ['#9966cc', '#2aa198', '#e69500', '#d33682', '#85c025', '#ff6b6b'];
1061
+
1062
+ function getAgentColor(name) {
1063
+ var hash = 0;
1064
+ for (var i = 0; i < name.length; i++) hash = ((hash << 5) - hash + name.charCodeAt(i)) | 0;
1065
+ return AGENT_COLORS[Math.abs(hash) % AGENT_COLORS.length];
1066
+ }
1067
+
1068
+ function parseAgentMessage(content) {
1069
+ var match = content.match(AGENT_MSG_RE);
1070
+ if (!match) return null;
1071
+ var body = content
1072
+ .slice(match[0].length)
1073
+ .replace(/\n\n\(Reply using send_to_agent\([^)]*\)[^)]*\)\s*$/, '')
1074
+ .trim();
1075
+ // Also strip CC/broadcast reply hints
1076
+ body = body.replace(/\n\n\(You are CC'd on this message[^)]*\)\s*$/, '').trim();
1077
+ body = body.replace(/\n\n\(Review the above messages[^)]*\)\s*$/, '').trim();
1078
+ return { sender: match[1].trim(), recipient: match[2].trim(), body: body };
1079
+ }
1080
+
1081
+ function formatTime(timestamp) {
1082
+ if (!timestamp) return null;
1083
+ var d = new Date(timestamp);
1084
+ return d.toLocaleTimeString([], { hour: '2-digit', minute: '2-digit' });
1085
+ }
1086
+
1087
+ function appendTimestamp(parent, timestamp) {
1088
+ var time = formatTime(timestamp);
1089
+ if (time) {
1090
+ var timeEl = document.createElement('div');
1091
+ timeEl.className = 'cumulus-msg-time';
1092
+ timeEl.textContent = time;
1093
+ parent.appendChild(timeEl);
1094
+ }
1095
+ }
1096
+
1097
+ function buildAgentMsgEl(agent) {
1098
+ var el = document.createElement('div');
1099
+ el.className = 'cumulus-msg agent';
1100
+ el.style.setProperty('--agent-color', getAgentColor(agent.sender));
1101
+
1102
+ var header = document.createElement('div');
1103
+ header.className = 'cumulus-agent-header';
1104
+ var nameEl = document.createElement('span');
1105
+ nameEl.className = 'cumulus-agent-name';
1106
+ nameEl.textContent = agent.sender;
1107
+ header.appendChild(nameEl);
1108
+ var ctx = document.createElement('span');
1109
+ ctx.className = 'cumulus-agent-context';
1110
+ ctx.textContent = '\u2192 ' + agent.recipient;
1111
+ header.appendChild(ctx);
1112
+ el.appendChild(header);
1113
+
1114
+ var body = document.createElement('div');
1115
+ var mdResult = renderMarkdown(agent.body);
1116
+ body.innerHTML = mdResult.html;
1117
+ renderBlexBlocks(body, mdResult.blexBlocks, null);
1118
+ el.appendChild(body);
1119
+ return el;
1120
+ }
1121
+
1122
+ // ── Markdown ──────────────────────────────────────────────────────────────
1123
+
1124
+ // ─── Blex block handle tracking ─────────────────────────────────────────────
1125
+ // Map: msgEl → Array<BlockHandle> — for cleanup on message delete/revert
1126
+ var blexHandles = new WeakMap();
1127
+
1128
+ /**
1129
+ * Parse blex fenced blocks from markdown text.
1130
+ * Returns { text: processedText, blocks: [{type, json, id}] }
1131
+ * Fences: ~~~blex:TYPE\n{json}\n~~~ or ```blex:TYPE\n{json}\n```
1132
+ */
1133
+ function extractBlexBlocks(text) {
1134
+ var blocks = [];
1135
+ var BLEX_PREFIX = '\x00BLEX_';
1136
+ var processed = text.replace(
1137
+ /(?:~~~|```)blex:(\w[\w-]*)\n([\s\S]*?)(?:~~~|```)/g,
1138
+ function (_, type, json) {
1139
+ var idx = blocks.length;
1140
+ var id = 'blex-' + idx + '-' + Date.now();
1141
+ blocks.push({ type: type, json: json.trim(), id: id });
1142
+ return BLEX_PREFIX + idx + '\x00';
1143
+ }
1144
+ );
1145
+ return { text: processed, blocks: blocks };
1146
+ }
1147
+
1148
+ /**
1149
+ * After innerHTML is set, find blex placeholder divs and render blocks.
1150
+ * Returns array of BlockHandles for lifecycle management.
1151
+ */
1152
+ function renderBlexBlocks(container, blocks, inputEl) {
1153
+ if (!blocks || !blocks.length || typeof window.Blex === 'undefined') return;
1154
+ var handles = [];
1155
+ var placeholders = container.querySelectorAll('[data-blex-idx]');
1156
+ placeholders.forEach(function (ph) {
1157
+ var idx = parseInt(ph.getAttribute('data-blex-idx'), 10);
1158
+ var block = blocks[idx];
1159
+ if (!block) return;
1160
+
1161
+ // Try to parse JSON
1162
+ var data;
1163
+ try {
1164
+ data = JSON.parse(block.json);
1165
+ } catch (e) {
1166
+ // Show parse error in a fallback
1167
+ ph.innerHTML =
1168
+ '<pre style="color:#f44;font-size:0.85em;">blex:' +
1169
+ escapeHtml(block.type) +
1170
+ ' — invalid JSON: ' +
1171
+ escapeHtml(e.message) +
1172
+ '</pre>';
1173
+ return;
1174
+ }
1175
+
1176
+ var contentBlock = { type: block.type, id: block.id, data: data };
1177
+
1178
+ // Render the block
1179
+ window.Blex.renderBlock(contentBlock, ph)
1180
+ .then(function (handle) {
1181
+ handles.push(handle);
1182
+ // Wire interaction → inject into input
1183
+ if (handle && handle.onInteraction && inputEl) {
1184
+ handle.onInteraction(function (interaction) {
1185
+ if (interaction && interaction.serialized) {
1186
+ inputEl.value =
1187
+ (inputEl.value ? inputEl.value + '\n' : '') + interaction.serialized;
1188
+ inputEl.focus();
1189
+ // Trigger input event so send button updates
1190
+ inputEl.dispatchEvent(new Event('input', { bubbles: true }));
1191
+ }
1192
+ });
1193
+ }
1194
+ })
1195
+ .catch(function (err) {
1196
+ ph.innerHTML =
1197
+ '<pre style="color:#f44;font-size:0.85em;">blex:' +
1198
+ escapeHtml(block.type) +
1199
+ ' render error: ' +
1200
+ escapeHtml(err.message) +
1201
+ '</pre>';
1202
+ });
1203
+ });
1204
+ blexHandles.set(container, handles);
1205
+ }
1206
+
1207
+ /**
1208
+ * Destroy all blex block handles for a message element.
1209
+ */
1210
+ function destroyBlexBlocks(container) {
1211
+ var handles = blexHandles.get(container);
1212
+ if (handles) {
1213
+ handles.forEach(function (h) {
1214
+ if (h && h.destroy) h.destroy();
1215
+ });
1216
+ blexHandles.delete(container);
1217
+ }
1218
+ }
1219
+
923
1220
  function renderMarkdown(text) {
1221
+ // Phase 0.5: Extract blex fenced blocks — replace with unique tokens
1222
+ var blexResult = extractBlexBlocks(text);
1223
+ text = blexResult.text;
1224
+
924
1225
  // Phase 1: Extract fenced code blocks — replace with unique tokens
925
1226
  var codeBlocks = [];
926
1227
  var TOKEN_PREFIX = '\x00CODEBLOCK_';
@@ -993,7 +1294,20 @@
993
1294
  return codeBlocks[parseInt(idx, 10)];
994
1295
  });
995
1296
 
996
- return html;
1297
+ // Phase 7.5: Reinsert blex block placeholders
1298
+ if (blexResult.blocks.length > 0) {
1299
+ html = html.replace(/\x00BLEX_(\d+)\x00/g, function (_, idx) {
1300
+ return (
1301
+ '<div class="blex-block-container" data-blex-idx="' +
1302
+ idx +
1303
+ '" data-testid="blex-block-' +
1304
+ idx +
1305
+ '"></div>'
1306
+ );
1307
+ });
1308
+ }
1309
+
1310
+ return { html: html, blexBlocks: blexResult.blocks };
997
1311
  }
998
1312
 
999
1313
  function buildCodeBlock(lang, escapedCode, tokenId) {
@@ -1333,8 +1647,12 @@
1333
1647
  var streamBuffer = '';
1334
1648
  var connection = null;
1335
1649
  var connectionStatus = 'disconnected';
1650
+ var embeddedStreamTimer = null;
1651
+ var EMBEDDED_STREAM_TIMEOUT = 120000;
1336
1652
  var authenticated = false;
1337
1653
  var pendingAttachments = [];
1654
+ var selectionMode = false;
1655
+ var selectedIds = {};
1338
1656
 
1339
1657
  // ── Panel ──
1340
1658
  var panel = document.createElement('div');
@@ -1351,6 +1669,9 @@
1351
1669
  '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1352
1670
  '<span data-testid="webchat-status-text">Disconnected</span>' +
1353
1671
  '</span>' +
1672
+ '<div class="cumulus-panel-actions">' +
1673
+ '<button class="cumulus-panel-action-btn" data-testid="webchat-select-btn" title="Select messages to delete">Select</button>' +
1674
+ '</div>' +
1354
1675
  '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>';
1355
1676
  panel.appendChild(header);
1356
1677
 
@@ -1449,7 +1770,9 @@
1449
1770
 
1450
1771
  // ── Rendering ──
1451
1772
  function scrollToBottom() {
1452
- messagesEl.scrollTop = messagesEl.scrollHeight;
1773
+ requestAnimationFrame(function () {
1774
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1775
+ });
1453
1776
  }
1454
1777
 
1455
1778
  function isWideUserMessage(content) {
@@ -1491,10 +1814,29 @@
1491
1814
  el.className = 'cumulus-msg assistant';
1492
1815
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
1493
1816
  if (content) {
1494
- el.innerHTML = renderMarkdown(content);
1817
+ var mdResult = renderMarkdown(content);
1818
+ el.innerHTML = mdResult.html;
1495
1819
  if (isStreaming) {
1496
1820
  el.innerHTML += '<span class="cumulus-cursor"></span>';
1497
1821
  }
1822
+ // Render blex blocks
1823
+ if (mdResult.blexBlocks.length > 0) {
1824
+ if (isStreaming && typeof window.Blex !== 'undefined') {
1825
+ // During streaming: show placeholders
1826
+ el.querySelectorAll('[data-blex-idx]').forEach(function (ph) {
1827
+ var idx = parseInt(ph.getAttribute('data-blex-idx'), 10);
1828
+ var block = mdResult.blexBlocks[idx];
1829
+ if (block) window.Blex.renderPlaceholder(block.type, ph);
1830
+ });
1831
+ } else if (!isStreaming) {
1832
+ // Final render: full blex blocks with interaction wiring
1833
+ var inputEl = el.closest('.cumulus-panel, .cumulus-widget');
1834
+ var blexInputEl = inputEl
1835
+ ? inputEl.querySelector('.cumulus-input, .cumulus-standalone-input')
1836
+ : null;
1837
+ renderBlexBlocks(el, mdResult.blexBlocks, blexInputEl);
1838
+ }
1839
+ }
1498
1840
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
1499
1841
  btn.addEventListener('click', function () {
1500
1842
  var targetId = btn.getAttribute('data-copy-target');
@@ -1517,21 +1859,56 @@
1517
1859
  }
1518
1860
 
1519
1861
  function renderMessages() {
1862
+ // Destroy existing blex block handles before clearing DOM
1863
+ messagesEl.querySelectorAll('.cumulus-msg.assistant').forEach(function (m) {
1864
+ destroyBlexBlocks(m);
1865
+ });
1520
1866
  messagesEl.innerHTML = '';
1867
+ // Apply selection mode class
1868
+ if (selectionMode) {
1869
+ messagesEl.classList.add('cumulus-selection-mode');
1870
+ } else {
1871
+ messagesEl.classList.remove('cumulus-selection-mode');
1872
+ }
1521
1873
  if (messages.length === 0 && !streaming) {
1522
1874
  messagesEl.innerHTML =
1523
1875
  '<div class="cumulus-empty">Send a message to start chatting</div>';
1876
+ updateEmbeddedSelectionBar();
1524
1877
  return;
1525
1878
  }
1526
1879
  for (var i = 0; i < messages.length; i++) {
1527
1880
  var msg = messages[i];
1528
1881
  var row = document.createElement('div');
1529
1882
  row.className = 'cumulus-msg-row';
1883
+ // Add checkbox in selection mode
1884
+ if (selectionMode && msg.id) {
1885
+ var cb = document.createElement('input');
1886
+ cb.type = 'checkbox';
1887
+ cb.className = 'cumulus-msg-checkbox';
1888
+ cb.checked = !!selectedIds[msg.id];
1889
+ (function (msgId) {
1890
+ cb.addEventListener('change', function () {
1891
+ if (cb.checked) {
1892
+ selectedIds[msgId] = true;
1893
+ } else {
1894
+ delete selectedIds[msgId];
1895
+ }
1896
+ updateEmbeddedSelectionBar();
1897
+ });
1898
+ })(msg.id);
1899
+ row.appendChild(cb);
1900
+ }
1530
1901
  if (msg.role === 'user') {
1531
- row.appendChild(buildUserMsgEl(msg));
1902
+ var agent = parseAgentMessage(msg.content);
1903
+ if (agent) {
1904
+ row.appendChild(buildAgentMsgEl(agent));
1905
+ } else {
1906
+ row.appendChild(buildUserMsgEl(msg));
1907
+ }
1532
1908
  } else {
1533
1909
  row.appendChild(buildAssistantMsgEl(msg.content, false));
1534
1910
  }
1911
+ appendTimestamp(row, msg.timestamp);
1535
1912
  messagesEl.appendChild(row);
1536
1913
  }
1537
1914
  if (streaming) {
@@ -1541,6 +1918,115 @@
1541
1918
  messagesEl.appendChild(row);
1542
1919
  }
1543
1920
  scrollToBottom();
1921
+ updateEmbeddedSelectionBar();
1922
+ }
1923
+
1924
+ function updateEmbeddedSelectionBar() {
1925
+ var existingBar = panel.querySelector('.cumulus-selection-bar');
1926
+ if (existingBar) existingBar.remove();
1927
+ if (!selectionMode) return;
1928
+
1929
+ var selectedCount = Object.keys(selectedIds).length;
1930
+ var selectableCount = messages.filter(function (m) {
1931
+ return !!m.id;
1932
+ }).length;
1933
+
1934
+ var bar = document.createElement('div');
1935
+ bar.className = 'cumulus-selection-bar';
1936
+
1937
+ var countEl = document.createElement('span');
1938
+ countEl.className = 'cumulus-selection-count';
1939
+ countEl.textContent = selectedCount + ' selected';
1940
+ bar.appendChild(countEl);
1941
+
1942
+ var selectAllBtn = document.createElement('button');
1943
+ selectAllBtn.className = 'cumulus-selection-select-all-btn';
1944
+ selectAllBtn.textContent =
1945
+ selectedCount === selectableCount && selectableCount > 0 ? 'Deselect All' : 'Select All';
1946
+ selectAllBtn.addEventListener('click', function () {
1947
+ if (selectedCount === selectableCount && selectableCount > 0) {
1948
+ selectedIds = {};
1949
+ } else {
1950
+ messages.forEach(function (m) {
1951
+ if (m.id) selectedIds[m.id] = true;
1952
+ });
1953
+ }
1954
+ renderMessages();
1955
+ });
1956
+ bar.appendChild(selectAllBtn);
1957
+
1958
+ var deleteBtn = document.createElement('button');
1959
+ deleteBtn.className = 'cumulus-selection-delete-btn';
1960
+ deleteBtn.textContent = 'Delete';
1961
+ deleteBtn.disabled = selectedCount === 0;
1962
+ deleteBtn.addEventListener('click', function () {
1963
+ if (selectedCount === 0) return;
1964
+ showEmbeddedDeleteMessagesConfirm(Object.keys(selectedIds));
1965
+ });
1966
+ bar.appendChild(deleteBtn);
1967
+
1968
+ var inputAreaEl = panel.querySelector('.cumulus-input-area');
1969
+ if (inputAreaEl) {
1970
+ panel.insertBefore(bar, inputAreaEl);
1971
+ } else {
1972
+ panel.appendChild(bar);
1973
+ }
1974
+ }
1975
+
1976
+ function showEmbeddedDeleteMessagesConfirm(ids) {
1977
+ var existing = panel.querySelector('.cumulus-confirm-dialog');
1978
+ if (existing) existing.remove();
1979
+
1980
+ var dialog = document.createElement('div');
1981
+ dialog.className = 'cumulus-confirm-dialog';
1982
+
1983
+ var box = document.createElement('div');
1984
+ box.className = 'cumulus-confirm-dialog-box';
1985
+
1986
+ var title = document.createElement('div');
1987
+ title.className = 'cumulus-confirm-dialog-title';
1988
+ title.textContent = 'Delete Messages';
1989
+ box.appendChild(title);
1990
+
1991
+ var text = document.createElement('div');
1992
+ text.className = 'cumulus-confirm-dialog-text';
1993
+ text.textContent =
1994
+ ids.length + ' message' + (ids.length === 1 ? '' : 's') + ' will be permanently deleted.';
1995
+ box.appendChild(text);
1996
+
1997
+ var warn = document.createElement('div');
1998
+ warn.className = 'cumulus-confirm-dialog-warn';
1999
+ warn.textContent = 'This cannot be undone.';
2000
+ box.appendChild(warn);
2001
+
2002
+ var actions = document.createElement('div');
2003
+ actions.className = 'cumulus-confirm-dialog-actions';
2004
+
2005
+ var cancelBtn = document.createElement('button');
2006
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
2007
+ cancelBtn.textContent = 'Cancel';
2008
+ cancelBtn.addEventListener('click', function () {
2009
+ dialog.remove();
2010
+ });
2011
+ actions.appendChild(cancelBtn);
2012
+
2013
+ var confirmBtn = document.createElement('button');
2014
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
2015
+ confirmBtn.setAttribute('data-testid', 'webchat-delete-messages-confirm');
2016
+ confirmBtn.textContent = 'Delete';
2017
+ confirmBtn.addEventListener('click', function () {
2018
+ confirmBtn.disabled = true;
2019
+ confirmBtn.textContent = 'Deleting\u2026';
2020
+ if (connection) {
2021
+ connection.send({ type: 'delete_messages', threadName: sessionId, messageIds: ids });
2022
+ }
2023
+ dialog.remove();
2024
+ });
2025
+ actions.appendChild(confirmBtn);
2026
+
2027
+ box.appendChild(actions);
2028
+ dialog.appendChild(box);
2029
+ panel.appendChild(dialog);
1544
2030
  }
1545
2031
 
1546
2032
  function updateSendBtn() {
@@ -1629,6 +2115,20 @@
1629
2115
  function handleServerMessage(data) {
1630
2116
  switch (data.type) {
1631
2117
  case 'auth_ok':
2118
+ // Reset streaming state on reconnect
2119
+ if (streaming) {
2120
+ if (streamBuffer) {
2121
+ messages.push({ role: 'assistant', content: streamBuffer });
2122
+ streamBuffer = '';
2123
+ }
2124
+ streaming = false;
2125
+ stopRequested = false;
2126
+ if (embeddedStreamTimer) {
2127
+ clearTimeout(embeddedStreamTimer);
2128
+ embeddedStreamTimer = null;
2129
+ }
2130
+ updateSendBtn();
2131
+ }
1632
2132
  showChatView();
1633
2133
  break;
1634
2134
  case 'auth_error':
@@ -1637,7 +2137,7 @@
1637
2137
  case 'history':
1638
2138
  if (data.messages && data.messages.length > 0) {
1639
2139
  messages = data.messages.map(function (m) {
1640
- return { role: m.role, content: m.content };
2140
+ return { id: m.id, role: m.role, content: m.content, timestamp: m.timestamp };
1641
2141
  });
1642
2142
  renderMessages();
1643
2143
  }
@@ -1650,11 +2150,36 @@
1650
2150
  updateSendBtn();
1651
2151
  }
1652
2152
  streamBuffer += data.text;
2153
+ // Reset streaming timeout on each token
2154
+ if (embeddedStreamTimer) clearTimeout(embeddedStreamTimer);
2155
+ embeddedStreamTimer = setTimeout(function () {
2156
+ if (streaming) {
2157
+ console.warn('[Cumulus] Streaming timeout — resetting');
2158
+ if (streamBuffer) {
2159
+ messages.push({
2160
+ role: 'assistant',
2161
+ content: streamBuffer + '\n\n*(response may be incomplete)*',
2162
+ });
2163
+ }
2164
+ streaming = false;
2165
+ streamBuffer = '';
2166
+ updateSendBtn();
2167
+ renderMessages();
2168
+ // Reload history
2169
+ if (connection)
2170
+ connection.send({ type: 'history', threadName: sessionId, limit: 50 });
2171
+ }
2172
+ embeddedStreamTimer = null;
2173
+ }, EMBEDDED_STREAM_TIMEOUT);
1653
2174
  renderMessages();
1654
2175
  break;
1655
2176
  case 'segment':
1656
2177
  break;
1657
2178
  case 'done':
2179
+ if (embeddedStreamTimer) {
2180
+ clearTimeout(embeddedStreamTimer);
2181
+ embeddedStreamTimer = null;
2182
+ }
1658
2183
  if (stopRequested) {
1659
2184
  stopRequested = false;
1660
2185
  streaming = false;
@@ -1674,8 +2199,16 @@
1674
2199
  break;
1675
2200
  case 'interjected':
1676
2201
  // Server confirms old stream was aborted. Partial already frozen client-side.
2202
+ if (embeddedStreamTimer) {
2203
+ clearTimeout(embeddedStreamTimer);
2204
+ embeddedStreamTimer = null;
2205
+ }
1677
2206
  break;
1678
2207
  case 'error':
2208
+ if (embeddedStreamTimer) {
2209
+ clearTimeout(embeddedStreamTimer);
2210
+ embeddedStreamTimer = null;
2211
+ }
1679
2212
  stopRequested = false;
1680
2213
  streaming = false;
1681
2214
  if (streamBuffer) {
@@ -1693,6 +2226,30 @@
1693
2226
  updateSendBtn();
1694
2227
  renderMessages();
1695
2228
  break;
2229
+ case 'new_messages':
2230
+ // Agent inject completed — append new messages and re-render
2231
+ if (data.messages && data.messages.length > 0) {
2232
+ for (var nm = 0; nm < data.messages.length; nm++) {
2233
+ messages.push({ role: data.messages[nm].role, content: data.messages[nm].content });
2234
+ }
2235
+ renderMessages();
2236
+ }
2237
+ break;
2238
+ case 'messages_deleted':
2239
+ // Server confirmed deletion — reload history and exit selection mode
2240
+ selectionMode = false;
2241
+ selectedIds = {};
2242
+ var embSelectBtn = panel.querySelector('[data-testid="webchat-select-btn"]');
2243
+ if (embSelectBtn) {
2244
+ embSelectBtn.textContent = 'Select';
2245
+ embSelectBtn.classList.remove('active');
2246
+ }
2247
+ messages = [];
2248
+ // Re-request history
2249
+ if (connection) {
2250
+ connection.send({ type: 'history', threadName: sessionId, limit: 200 });
2251
+ }
2252
+ break;
1696
2253
  }
1697
2254
  }
1698
2255
 
@@ -1808,6 +2365,22 @@
1808
2365
  });
1809
2366
  }
1810
2367
 
2368
+ // Select button for embedded mode
2369
+ var embSelectBtn = panel.querySelector('[data-testid="webchat-select-btn"]');
2370
+ if (embSelectBtn) {
2371
+ embSelectBtn.addEventListener('click', function () {
2372
+ selectionMode = !selectionMode;
2373
+ selectedIds = {};
2374
+ embSelectBtn.textContent = selectionMode ? 'Done' : 'Select';
2375
+ if (selectionMode) {
2376
+ embSelectBtn.classList.add('active');
2377
+ } else {
2378
+ embSelectBtn.classList.remove('active');
2379
+ }
2380
+ renderMessages();
2381
+ });
2382
+ }
2383
+
1811
2384
  // Embedded mode: no auth panel handling needed (API key from attribute)
1812
2385
  function startConnection() {
1813
2386
  if (connection) {
@@ -1847,10 +2420,49 @@
1847
2420
 
1848
2421
  // Thread registry: threadName -> { messages, streaming, streamBuffer, stopRequested, pendingAttachments }
1849
2422
  var threadStates = {};
2423
+ var streamingTimeouts = {}; // threadName -> timeout ID
2424
+ var STREAMING_TIMEOUT_MS = 120000; // 2 minutes with no token = assume dead
2425
+
2426
+ function resetStreamingTimeout(threadName) {
2427
+ if (streamingTimeouts[threadName]) clearTimeout(streamingTimeouts[threadName]);
2428
+ streamingTimeouts[threadName] = setTimeout(function () {
2429
+ var s = threadStates[threadName];
2430
+ if (s && s.streaming) {
2431
+ console.warn('[CumulusChat] Streaming timeout for thread:', threadName);
2432
+ if (s.streamBuffer) {
2433
+ s.messages.push({
2434
+ role: 'assistant',
2435
+ content: s.streamBuffer + '\n\n*(response may be incomplete — connection issue)*',
2436
+ });
2437
+ }
2438
+ s.streaming = false;
2439
+ s.streamBuffer = '';
2440
+ s.stopRequested = false;
2441
+ s.interjecting = false;
2442
+ // Reload history to get the real response if server completed
2443
+ s.historyLoaded = false;
2444
+ if (connection) {
2445
+ connection.send({ type: 'history', threadName: threadName, limit: 50 });
2446
+ }
2447
+ refreshThreadPanel(threadName);
2448
+ }
2449
+ delete streamingTimeouts[threadName];
2450
+ }, STREAMING_TIMEOUT_MS);
2451
+ }
2452
+
2453
+ function clearStreamingTimeout(threadName) {
2454
+ if (streamingTimeouts[threadName]) {
2455
+ clearTimeout(streamingTimeouts[threadName]);
2456
+ delete streamingTimeouts[threadName];
2457
+ }
2458
+ }
1850
2459
 
1851
2460
  // Thread list from server: [{ name, messageCount, lastActivity }]
1852
2461
  var allThreads = [];
1853
2462
 
2463
+ // Unread threads: threadName -> true
2464
+ var unreadThreads = {};
2465
+
1854
2466
  // Currently visible panel names (ordered, max 3)
1855
2467
  var visibleThreads = [];
1856
2468
 
@@ -1868,6 +2480,8 @@
1868
2480
  interjecting: false,
1869
2481
  pendingAttachments: [],
1870
2482
  historyLoaded: false,
2483
+ selectionMode: false,
2484
+ selectedIds: {},
1871
2485
  };
1872
2486
  }
1873
2487
  return threadStates[threadName];
@@ -2132,7 +2746,9 @@
2132
2746
  item.setAttribute('data-thread-name', name);
2133
2747
 
2134
2748
  var dot = document.createElement('span');
2135
- dot.className = 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
2749
+ dot.className = unreadThreads[name]
2750
+ ? 'cumulus-thread-unread'
2751
+ : 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
2136
2752
  item.appendChild(dot);
2137
2753
 
2138
2754
  var nameEl = document.createElement('span');
@@ -2155,6 +2771,13 @@
2155
2771
  item.appendChild(countEl);
2156
2772
  }
2157
2773
 
2774
+ // Right-click context menu
2775
+ item.addEventListener('contextmenu', function (e) {
2776
+ e.preventDefault();
2777
+ e.stopPropagation();
2778
+ showThreadContextMenu(name, e.clientX, e.clientY);
2779
+ });
2780
+
2158
2781
  item.addEventListener('click', function (e) {
2159
2782
  var multiSelect = e.metaKey || e.ctrlKey;
2160
2783
  if (multiSelect) {
@@ -2167,8 +2790,123 @@
2167
2790
  return item;
2168
2791
  }
2169
2792
 
2793
+ // ── Thread context menu (right-click) ──
2794
+ function dismissContextMenu() {
2795
+ var existing = document.querySelector('.cumulus-context-menu');
2796
+ if (existing) existing.remove();
2797
+ document.removeEventListener('click', dismissContextMenu);
2798
+ document.removeEventListener('contextmenu', dismissContextMenu);
2799
+ }
2800
+
2801
+ function showThreadContextMenu(threadName, x, y) {
2802
+ dismissContextMenu();
2803
+
2804
+ var menu = document.createElement('div');
2805
+ menu.className = 'cumulus-context-menu';
2806
+ menu.style.left = x + 'px';
2807
+ menu.style.top = y + 'px';
2808
+
2809
+ var deleteItem = document.createElement('button');
2810
+ deleteItem.className = 'cumulus-context-menu-item danger';
2811
+ deleteItem.textContent = 'Delete thread';
2812
+ deleteItem.setAttribute('data-testid', 'webchat-thread-delete');
2813
+ deleteItem.addEventListener('click', function () {
2814
+ dismissContextMenu();
2815
+ showThreadDeleteConfirm(threadName);
2816
+ });
2817
+ menu.appendChild(deleteItem);
2818
+
2819
+ document.body.appendChild(menu);
2820
+
2821
+ // Clamp to viewport
2822
+ var rect = menu.getBoundingClientRect();
2823
+ if (rect.right > window.innerWidth)
2824
+ menu.style.left = window.innerWidth - rect.width - 4 + 'px';
2825
+ if (rect.bottom > window.innerHeight)
2826
+ menu.style.top = window.innerHeight - rect.height - 4 + 'px';
2827
+
2828
+ // Dismiss on next click or right-click anywhere
2829
+ setTimeout(function () {
2830
+ document.addEventListener('click', dismissContextMenu);
2831
+ document.addEventListener('contextmenu', dismissContextMenu);
2832
+ }, 0);
2833
+ }
2834
+
2835
+ // ── Thread delete confirmation ──
2836
+ function showThreadDeleteConfirm(threadName) {
2837
+ // Remove any existing confirm dialogs
2838
+ var existing = document.querySelector(
2839
+ '.cumulus-confirm-dialog[data-context="thread-delete"]'
2840
+ );
2841
+ if (existing) {
2842
+ existing.remove();
2843
+ return;
2844
+ }
2845
+
2846
+ // Build inline confirm dialog attached to the sidebar scroll area
2847
+ var dialog = document.createElement('div');
2848
+ dialog.className = 'cumulus-confirm-dialog';
2849
+ dialog.setAttribute('data-context', 'thread-delete');
2850
+ dialog.style.cssText =
2851
+ '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;';
2852
+
2853
+ var box = document.createElement('div');
2854
+ box.className = 'cumulus-confirm-dialog-box';
2855
+
2856
+ var title = document.createElement('div');
2857
+ title.className = 'cumulus-confirm-dialog-title';
2858
+ title.textContent = 'Delete Thread';
2859
+ box.appendChild(title);
2860
+
2861
+ var text = document.createElement('div');
2862
+ text.className = 'cumulus-confirm-dialog-text';
2863
+ text.textContent = 'Delete "' + threadName + '"?';
2864
+ box.appendChild(text);
2865
+
2866
+ var warn = document.createElement('div');
2867
+ warn.className = 'cumulus-confirm-dialog-warn';
2868
+ warn.textContent = 'All messages and history will be permanently removed.';
2869
+ box.appendChild(warn);
2870
+
2871
+ var actions = document.createElement('div');
2872
+ actions.className = 'cumulus-confirm-dialog-actions';
2873
+
2874
+ var cancelBtn = document.createElement('button');
2875
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
2876
+ cancelBtn.textContent = 'Cancel';
2877
+ cancelBtn.addEventListener('click', function () {
2878
+ dialog.remove();
2879
+ });
2880
+ actions.appendChild(cancelBtn);
2881
+
2882
+ var confirmBtn = document.createElement('button');
2883
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
2884
+ confirmBtn.setAttribute('data-testid', 'webchat-thread-delete-confirm');
2885
+ confirmBtn.textContent = 'Delete';
2886
+ confirmBtn.addEventListener('click', function () {
2887
+ confirmBtn.disabled = true;
2888
+ confirmBtn.textContent = 'Deleting\u2026';
2889
+ if (connection) {
2890
+ connection.send({ type: 'delete_thread', threadName: threadName });
2891
+ }
2892
+ dialog.remove();
2893
+ });
2894
+ actions.appendChild(confirmBtn);
2895
+
2896
+ box.appendChild(actions);
2897
+ dialog.appendChild(box);
2898
+
2899
+ // Dismiss on backdrop click
2900
+ dialog.addEventListener('click', function (e) {
2901
+ if (e.target === dialog) dialog.remove();
2902
+ });
2903
+
2904
+ document.body.appendChild(dialog);
2905
+ }
2906
+
2170
2907
  // ── Panel management ──
2171
2908
  function soloThread(name) {
2909
+ delete unreadThreads[name];
2172
2910
  visibleThreads = [name];
2173
2911
  renderSidebar();
2174
2912
  renderContentArea();
@@ -2177,6 +2915,7 @@
2177
2915
  }
2178
2916
 
2179
2917
  function togglePanelThread(name) {
2918
+ delete unreadThreads[name];
2180
2919
  // On mobile, always solo (no side-by-side)
2181
2920
  if (isMobile()) {
2182
2921
  soloThread(name);
@@ -2307,6 +3046,16 @@
2307
3046
  });
2308
3047
  actions.appendChild(revertBtn);
2309
3048
 
3049
+ var selectBtn = document.createElement('button');
3050
+ selectBtn.className = 'cumulus-panel-action-btn';
3051
+ selectBtn.setAttribute('data-testid', 'webchat-select-btn');
3052
+ selectBtn.setAttribute('title', 'Select messages to delete');
3053
+ selectBtn.textContent = 'Select';
3054
+ selectBtn.addEventListener('click', function () {
3055
+ toggleSelectionMode();
3056
+ });
3057
+ actions.appendChild(selectBtn);
3058
+
2310
3059
  panelHeader.appendChild(actions);
2311
3060
 
2312
3061
  var closeBtn = document.createElement('button');
@@ -2375,7 +3124,11 @@
2375
3124
 
2376
3125
  // ── Panel-local render functions ──
2377
3126
  function scrollToBottom() {
2378
- messagesEl.scrollTop = messagesEl.scrollHeight;
3127
+ requestAnimationFrame(function () {
3128
+ requestAnimationFrame(function () {
3129
+ messagesEl.scrollTop = messagesEl.scrollHeight;
3130
+ });
3131
+ });
2379
3132
  }
2380
3133
 
2381
3134
  function isWideUserMessage(content) {
@@ -2417,10 +3170,29 @@
2417
3170
  el.className = 'cumulus-msg assistant';
2418
3171
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
2419
3172
  if (content) {
2420
- el.innerHTML = renderMarkdown(content);
3173
+ var mdResult = renderMarkdown(content);
3174
+ el.innerHTML = mdResult.html;
2421
3175
  if (isStreaming) {
2422
3176
  el.innerHTML += '<span class="cumulus-cursor"></span>';
2423
3177
  }
3178
+ // Render blex blocks
3179
+ if (mdResult.blexBlocks.length > 0) {
3180
+ if (isStreaming && typeof window.Blex !== 'undefined') {
3181
+ // During streaming: show placeholders
3182
+ el.querySelectorAll('[data-blex-idx]').forEach(function (ph) {
3183
+ var idx = parseInt(ph.getAttribute('data-blex-idx'), 10);
3184
+ var block = mdResult.blexBlocks[idx];
3185
+ if (block) window.Blex.renderPlaceholder(block.type, ph);
3186
+ });
3187
+ } else if (!isStreaming) {
3188
+ // Final render: full blex blocks with interaction wiring
3189
+ var inputEl = el.closest('.cumulus-panel, .cumulus-widget');
3190
+ var blexInputEl = inputEl
3191
+ ? inputEl.querySelector('.cumulus-input, .cumulus-standalone-input')
3192
+ : null;
3193
+ renderBlexBlocks(el, mdResult.blexBlocks, blexInputEl);
3194
+ }
3195
+ }
2424
3196
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
2425
3197
  btn.addEventListener('click', function () {
2426
3198
  var targetId = btn.getAttribute('data-copy-target');
@@ -2443,21 +3215,56 @@
2443
3215
  }
2444
3216
 
2445
3217
  function renderPanelMessages() {
3218
+ // Destroy existing blex block handles before clearing DOM
3219
+ messagesEl.querySelectorAll('.cumulus-msg.assistant').forEach(function (m) {
3220
+ destroyBlexBlocks(m);
3221
+ });
2446
3222
  messagesEl.innerHTML = '';
3223
+ // Apply selection mode class to messages container
3224
+ if (state.selectionMode) {
3225
+ messagesEl.classList.add('cumulus-selection-mode');
3226
+ } else {
3227
+ messagesEl.classList.remove('cumulus-selection-mode');
3228
+ }
2447
3229
  if (state.messages.length === 0 && !state.streaming) {
2448
3230
  messagesEl.innerHTML =
2449
3231
  '<div class="cumulus-empty">Send a message to start chatting</div>';
3232
+ updateSelectionBar();
2450
3233
  return;
2451
3234
  }
2452
3235
  for (var i = 0; i < state.messages.length; i++) {
2453
3236
  var msg = state.messages[i];
2454
3237
  var row = document.createElement('div');
2455
3238
  row.className = 'cumulus-msg-row';
3239
+ // Add checkbox if in selection mode and message has an id
3240
+ if (state.selectionMode && msg.id) {
3241
+ var cb = document.createElement('input');
3242
+ cb.type = 'checkbox';
3243
+ cb.className = 'cumulus-msg-checkbox';
3244
+ cb.checked = !!state.selectedIds[msg.id];
3245
+ (function (msgId) {
3246
+ cb.addEventListener('change', function () {
3247
+ if (cb.checked) {
3248
+ state.selectedIds[msgId] = true;
3249
+ } else {
3250
+ delete state.selectedIds[msgId];
3251
+ }
3252
+ updateSelectionBar();
3253
+ });
3254
+ })(msg.id);
3255
+ row.appendChild(cb);
3256
+ }
2456
3257
  if (msg.role === 'user') {
2457
- row.appendChild(buildUserMsgEl(msg));
3258
+ var agent = parseAgentMessage(msg.content);
3259
+ if (agent) {
3260
+ row.appendChild(buildAgentMsgEl(agent));
3261
+ } else {
3262
+ row.appendChild(buildUserMsgEl(msg));
3263
+ }
2458
3264
  } else {
2459
3265
  row.appendChild(buildAssistantMsgEl(msg.content, false));
2460
3266
  }
3267
+ appendTimestamp(row, msg.timestamp);
2461
3268
  messagesEl.appendChild(row);
2462
3269
  }
2463
3270
  if (state.streaming) {
@@ -2467,6 +3274,132 @@
2467
3274
  messagesEl.appendChild(row);
2468
3275
  }
2469
3276
  scrollToBottom();
3277
+ updateSelectionBar();
3278
+ }
3279
+
3280
+ function updateSelectionBar() {
3281
+ // Remove existing bar
3282
+ var existingBar = panel.querySelector('.cumulus-selection-bar');
3283
+ if (existingBar) existingBar.remove();
3284
+ if (!state.selectionMode) return;
3285
+
3286
+ var selectedCount = Object.keys(state.selectedIds).length;
3287
+ var selectableCount = state.messages.filter(function (m) {
3288
+ return !!m.id;
3289
+ }).length;
3290
+
3291
+ var bar = document.createElement('div');
3292
+ bar.className = 'cumulus-selection-bar';
3293
+
3294
+ var countEl = document.createElement('span');
3295
+ countEl.className = 'cumulus-selection-count';
3296
+ countEl.textContent = selectedCount + ' selected';
3297
+ bar.appendChild(countEl);
3298
+
3299
+ var selectAllBtn = document.createElement('button');
3300
+ selectAllBtn.className = 'cumulus-selection-select-all-btn';
3301
+ selectAllBtn.textContent =
3302
+ selectedCount === selectableCount && selectableCount > 0 ? 'Deselect All' : 'Select All';
3303
+ selectAllBtn.addEventListener('click', function () {
3304
+ if (selectedCount === selectableCount && selectableCount > 0) {
3305
+ // Deselect all
3306
+ state.selectedIds = {};
3307
+ } else {
3308
+ // Select all with IDs
3309
+ state.messages.forEach(function (m) {
3310
+ if (m.id) state.selectedIds[m.id] = true;
3311
+ });
3312
+ }
3313
+ renderPanelMessages();
3314
+ });
3315
+ bar.appendChild(selectAllBtn);
3316
+
3317
+ var deleteBtn = document.createElement('button');
3318
+ deleteBtn.className = 'cumulus-selection-delete-btn';
3319
+ deleteBtn.textContent = 'Delete';
3320
+ deleteBtn.disabled = selectedCount === 0;
3321
+ deleteBtn.addEventListener('click', function () {
3322
+ if (selectedCount === 0) return;
3323
+ showDeleteMessagesConfirm(threadName, panel, Object.keys(state.selectedIds));
3324
+ });
3325
+ bar.appendChild(deleteBtn);
3326
+
3327
+ // Insert before input area
3328
+ var inputAreaEl = panel.querySelector('.cumulus-input-area');
3329
+ if (inputAreaEl) {
3330
+ panel.insertBefore(bar, inputAreaEl);
3331
+ } else {
3332
+ panel.appendChild(bar);
3333
+ }
3334
+ }
3335
+
3336
+ function toggleSelectionMode() {
3337
+ state.selectionMode = !state.selectionMode;
3338
+ state.selectedIds = {};
3339
+ selectBtn.textContent = state.selectionMode ? 'Done' : 'Select';
3340
+ if (state.selectionMode) {
3341
+ selectBtn.classList.add('active');
3342
+ } else {
3343
+ selectBtn.classList.remove('active');
3344
+ }
3345
+ renderPanelMessages();
3346
+ }
3347
+
3348
+ function showDeleteMessagesConfirm(tName, panelEl, ids) {
3349
+ // Remove any existing confirm dialog
3350
+ var existing = panelEl.querySelector('.cumulus-confirm-dialog');
3351
+ if (existing) existing.remove();
3352
+
3353
+ var dialog = document.createElement('div');
3354
+ dialog.className = 'cumulus-confirm-dialog';
3355
+
3356
+ var box = document.createElement('div');
3357
+ box.className = 'cumulus-confirm-dialog-box';
3358
+
3359
+ var title = document.createElement('div');
3360
+ title.className = 'cumulus-confirm-dialog-title';
3361
+ title.textContent = 'Delete Messages';
3362
+ box.appendChild(title);
3363
+
3364
+ var text = document.createElement('div');
3365
+ text.className = 'cumulus-confirm-dialog-text';
3366
+ text.textContent =
3367
+ ids.length + ' message' + (ids.length === 1 ? '' : 's') + ' will be permanently deleted.';
3368
+ box.appendChild(text);
3369
+
3370
+ var warn = document.createElement('div');
3371
+ warn.className = 'cumulus-confirm-dialog-warn';
3372
+ warn.textContent = 'This cannot be undone.';
3373
+ box.appendChild(warn);
3374
+
3375
+ var actions = document.createElement('div');
3376
+ actions.className = 'cumulus-confirm-dialog-actions';
3377
+
3378
+ var cancelBtn = document.createElement('button');
3379
+ cancelBtn.className = 'cumulus-confirm-dialog-cancel';
3380
+ cancelBtn.textContent = 'Cancel';
3381
+ cancelBtn.addEventListener('click', function () {
3382
+ dialog.remove();
3383
+ });
3384
+ actions.appendChild(cancelBtn);
3385
+
3386
+ var confirmBtn = document.createElement('button');
3387
+ confirmBtn.className = 'cumulus-confirm-dialog-confirm';
3388
+ confirmBtn.setAttribute('data-testid', 'webchat-delete-messages-confirm');
3389
+ confirmBtn.textContent = 'Delete';
3390
+ confirmBtn.addEventListener('click', function () {
3391
+ confirmBtn.disabled = true;
3392
+ confirmBtn.textContent = 'Deleting\u2026';
3393
+ if (connection) {
3394
+ connection.send({ type: 'delete_messages', threadName: tName, messageIds: ids });
3395
+ }
3396
+ dialog.remove();
3397
+ });
3398
+ actions.appendChild(confirmBtn);
3399
+
3400
+ box.appendChild(actions);
3401
+ dialog.appendChild(box);
3402
+ panelEl.appendChild(dialog);
2470
3403
  }
2471
3404
 
2472
3405
  function updatePanelSendBtn() {
@@ -2557,6 +3490,9 @@
2557
3490
  state._renderMessages = renderPanelMessages;
2558
3491
  state._updateSendBtn = updatePanelSendBtn;
2559
3492
 
3493
+ // Initial render (messages may already be loaded from a previous view)
3494
+ renderPanelMessages();
3495
+
2560
3496
  // ── Send message for this panel ──
2561
3497
  function sendPanelMessage() {
2562
3498
  var text = inputEl.value.trim();
@@ -2568,6 +3504,7 @@
2568
3504
  if (!text && state.pendingAttachments.length === 0) {
2569
3505
  state.stopRequested = true;
2570
3506
  state.streaming = false;
3507
+ clearStreamingTimeout(threadName);
2571
3508
  updatePanelSendBtn();
2572
3509
  if (state.streamBuffer) {
2573
3510
  state.messages.push({ role: 'assistant', content: state.streamBuffer });
@@ -2601,6 +3538,7 @@
2601
3538
  state.stopRequested = false;
2602
3539
  state.streamBuffer = '';
2603
3540
  state.interjecting = false;
3541
+ resetStreamingTimeout(threadName);
2604
3542
  updatePanelSendBtn();
2605
3543
  renderPanelMessages();
2606
3544
 
@@ -2749,14 +3687,12 @@
2749
3687
  var filePath = addInput.value.trim();
2750
3688
  if (!filePath) return;
2751
3689
  connection &&
2752
- connection.send(
2753
- JSON.stringify({
2754
- type: 'include_add',
2755
- threadName: threadName,
2756
- filePath: filePath,
2757
- scope: currentScope,
2758
- })
2759
- );
3690
+ connection.send({
3691
+ type: 'include_add',
3692
+ threadName: threadName,
3693
+ filePath: filePath,
3694
+ scope: currentScope,
3695
+ });
2760
3696
  addInput.value = '';
2761
3697
  });
2762
3698
 
@@ -2779,8 +3715,7 @@
2779
3715
  panel.appendChild(overlay);
2780
3716
 
2781
3717
  // Request the list
2782
- connection &&
2783
- connection.send(JSON.stringify({ type: 'include_list', threadName: threadName }));
3718
+ connection && connection.send({ type: 'include_list', threadName: threadName });
2784
3719
  }
2785
3720
 
2786
3721
  function renderIncludeList(panel, threadName, files) {
@@ -2817,13 +3752,11 @@
2817
3752
  removeBtn.setAttribute('title', 'Remove');
2818
3753
  removeBtn.addEventListener('click', function () {
2819
3754
  connection &&
2820
- connection.send(
2821
- JSON.stringify({
2822
- type: 'include_remove',
2823
- threadName: threadName,
2824
- filePath: f.path,
2825
- })
2826
- );
3755
+ connection.send({
3756
+ type: 'include_remove',
3757
+ threadName: threadName,
3758
+ filePath: f.path,
3759
+ });
2827
3760
  });
2828
3761
  item.appendChild(removeBtn);
2829
3762
 
@@ -2868,8 +3801,7 @@
2868
3801
  panel.appendChild(overlay);
2869
3802
 
2870
3803
  // Request the turn list
2871
- connection &&
2872
- connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
3804
+ connection && connection.send({ type: 'revert_list', threadName: threadName });
2873
3805
  }
2874
3806
 
2875
3807
  function renderRevertList(panel, threadName, turns) {
@@ -2948,8 +3880,7 @@
2948
3880
  cancelBtn.textContent = 'Cancel';
2949
3881
  cancelBtn.addEventListener('click', function () {
2950
3882
  // Re-request list
2951
- connection &&
2952
- connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
3883
+ connection && connection.send({ type: 'revert_list', threadName: threadName });
2953
3884
  });
2954
3885
  actions.appendChild(cancelBtn);
2955
3886
 
@@ -2961,13 +3892,11 @@
2961
3892
  revertBtn.disabled = true;
2962
3893
  revertBtn.textContent = 'Reverting\u2026';
2963
3894
  connection &&
2964
- connection.send(
2965
- JSON.stringify({
2966
- type: 'revert_execute',
2967
- threadName: threadName,
2968
- targetMessageId: turn.userMessageId,
2969
- })
2970
- );
3895
+ connection.send({
3896
+ type: 'revert_execute',
3897
+ threadName: threadName,
3898
+ targetMessageId: turn.userMessageId,
3899
+ });
2971
3900
  });
2972
3901
  actions.appendChild(revertBtn);
2973
3902
 
@@ -3072,8 +4001,26 @@
3072
4001
 
3073
4002
  // ── Message router ──
3074
4003
  function handleServerMessage(data) {
4004
+ if (data.type !== 'token') {
4005
+ console.log('[CumulusChat] WS recv:', data.type, data.threadName || '', data);
4006
+ }
3075
4007
  switch (data.type) {
3076
4008
  case 'auth_ok':
4009
+ // Reset streaming state on reconnect — server may have finished while we were disconnected
4010
+ Object.keys(threadStates).forEach(function (tn) {
4011
+ var s = threadStates[tn];
4012
+ if (s.streaming) {
4013
+ if (s.streamBuffer) {
4014
+ s.messages.push({ role: 'assistant', content: s.streamBuffer });
4015
+ s.streamBuffer = '';
4016
+ }
4017
+ s.streaming = false;
4018
+ s.stopRequested = false;
4019
+ s.interjecting = false;
4020
+ }
4021
+ // Reload history from server to pick up any responses we missed
4022
+ s.historyLoaded = false;
4023
+ });
3077
4024
  showAppView();
3078
4025
  registerPushNotifications();
3079
4026
  break;
@@ -3117,12 +4064,50 @@
3117
4064
  }
3118
4065
  break;
3119
4066
 
4067
+ case 'thread_deleted':
4068
+ // Server confirms thread deleted; remove from sidebar and panels
4069
+ if (data.threadName) {
4070
+ allThreads = allThreads.filter(function (t) {
4071
+ return t.name !== data.threadName;
4072
+ });
4073
+ delete threadStates[data.threadName];
4074
+ removeThreadFromView(data.threadName);
4075
+ renderSidebar();
4076
+ }
4077
+ break;
4078
+
4079
+ case 'messages_deleted':
4080
+ // Server confirms messages deleted; reload history for that thread
4081
+ if (data.threadName) {
4082
+ var dState = getThreadState(data.threadName);
4083
+ dState.messages = [];
4084
+ dState.historyLoaded = false;
4085
+ dState.selectionMode = false;
4086
+ dState.selectedIds = {};
4087
+ // Exit selection mode on visible panel
4088
+ var dPanel = document.querySelector('[data-thread-panel="' + data.threadName + '"]');
4089
+ if (dPanel) {
4090
+ var dMsgs = dPanel.querySelector('.cumulus-messages');
4091
+ if (dMsgs) dMsgs.classList.remove('cumulus-selection-mode');
4092
+ var dBar = dPanel.querySelector('.cumulus-selection-bar');
4093
+ if (dBar) dBar.remove();
4094
+ var dSelectBtn = dPanel.querySelector('[data-testid="webchat-select-btn"]');
4095
+ if (dSelectBtn) {
4096
+ dSelectBtn.textContent = 'Select';
4097
+ dSelectBtn.classList.remove('active');
4098
+ }
4099
+ }
4100
+ connection &&
4101
+ connection.send({ type: 'history', threadName: data.threadName, limit: 200 });
4102
+ }
4103
+ break;
4104
+
3120
4105
  case 'history':
3121
4106
  if (data.threadName) {
3122
4107
  var state = getThreadState(data.threadName);
3123
4108
  if (data.messages && data.messages.length > 0) {
3124
4109
  state.messages = data.messages.map(function (m) {
3125
- return { id: m.id, role: m.role, content: m.content };
4110
+ return { id: m.id, role: m.role, content: m.content, timestamp: m.timestamp };
3126
4111
  });
3127
4112
  }
3128
4113
  refreshThreadPanel(data.threadName);
@@ -3138,6 +4123,8 @@
3138
4123
  state.streamBuffer = '';
3139
4124
  }
3140
4125
  state.streamBuffer += data.text;
4126
+ // Reset streaming timeout on each token
4127
+ resetStreamingTimeout(data.threadName);
3141
4128
  // Update lastActivity for this thread in allThreads
3142
4129
  updateThreadActivity(data.threadName);
3143
4130
  refreshThreadPanel(data.threadName);
@@ -3150,6 +4137,7 @@
3150
4137
 
3151
4138
  case 'done':
3152
4139
  if (data.threadName) {
4140
+ clearStreamingTimeout(data.threadName);
3153
4141
  var state = getThreadState(data.threadName);
3154
4142
  if (state.stopRequested) {
3155
4143
  state.stopRequested = false;
@@ -3176,11 +4164,12 @@
3176
4164
  case 'interjected':
3177
4165
  // Server confirms the old stream was aborted due to interjection.
3178
4166
  // The partial response was already frozen client-side in sendPanelMessage.
3179
- // Nothing to do — the new stream's tokens are already arriving.
4167
+ if (data.threadName) clearStreamingTimeout(data.threadName);
3180
4168
  break;
3181
4169
 
3182
4170
  case 'error':
3183
4171
  if (data.threadName) {
4172
+ clearStreamingTimeout(data.threadName);
3184
4173
  var state = getThreadState(data.threadName);
3185
4174
  state.stopRequested = false;
3186
4175
  state.streaming = false;
@@ -3201,6 +4190,22 @@
3201
4190
  }
3202
4191
  break;
3203
4192
 
4193
+ case 'new_messages':
4194
+ // Agent inject completed — append new messages to the thread and re-render
4195
+ if (data.threadName && data.messages && data.messages.length > 0) {
4196
+ var state = getThreadState(data.threadName);
4197
+ for (var nm = 0; nm < data.messages.length; nm++) {
4198
+ state.messages.push({
4199
+ id: data.messages[nm].id,
4200
+ role: data.messages[nm].role,
4201
+ content: data.messages[nm].content,
4202
+ });
4203
+ }
4204
+ updateThreadActivity(data.threadName);
4205
+ refreshThreadPanel(data.threadName);
4206
+ }
4207
+ break;
4208
+
3204
4209
  // ── Include/Revert responses ──
3205
4210
  case 'include_list':
3206
4211
  if (data.threadName && data.files) {
@@ -3233,13 +4238,11 @@
3233
4238
  tState.historyLoaded = false;
3234
4239
  // Re-request history
3235
4240
  connection &&
3236
- connection.send(
3237
- JSON.stringify({
3238
- type: 'history',
3239
- threadName: data.threadName,
3240
- limit: 200,
3241
- })
3242
- );
4241
+ connection.send({
4242
+ type: 'history',
4243
+ threadName: data.threadName,
4244
+ limit: 200,
4245
+ });
3243
4246
  if (overlay) overlay.remove();
3244
4247
  } else {
3245
4248
  // Show error in overlay
@@ -3263,6 +4266,10 @@
3263
4266
  break;
3264
4267
  }
3265
4268
  }
4269
+ // Mark unread if thread is not currently visible
4270
+ if (visibleThreads.indexOf(threadName) === -1) {
4271
+ unreadThreads[threadName] = true;
4272
+ }
3266
4273
  // Re-render sidebar to reflect new sort order / move thread up
3267
4274
  renderSidebar();
3268
4275
  }
@@ -3415,5 +4422,7 @@
3415
4422
  }
3416
4423
 
3417
4424
  window.CumulusChat = widget;
4425
+ window.CumulusChat.version = '0.27.15';
4426
+ console.log('[CumulusChat] Widget loaded, version ' + window.CumulusChat.version);
3418
4427
  }
3419
4428
  })();