@luckydraw/cumulus 0.28.2 → 0.28.4

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.
@@ -1226,6 +1226,10 @@
1226
1226
  // WeakMap: DOM element -> BlockHandle[] for lifecycle management
1227
1227
  var blexHandles = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
1228
1228
 
1229
+ // Per-thread store for blex interaction values (poll answers, confirm clicks, etc.)
1230
+ // Key: "threadName" → Map<"msgTimestamp:blockIdx", interactionValue>
1231
+ var blexInteractionStore = {};
1232
+
1229
1233
  // Extract ~~~blex:TYPE\n{json}\n~~~ fences from text
1230
1234
  // Returns { text: string (with placeholders), blocks: Array<{type, json, idx}> }
1231
1235
  function extractBlexBlocks(text) {
@@ -1240,18 +1244,23 @@
1240
1244
  }
1241
1245
 
1242
1246
  // Render blex blocks into placeholder divs within a message element
1243
- function renderBlexBlocks(el, blexBlocks, isStreaming) {
1247
+ function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
1244
1248
  if (!blexBlocks || blexBlocks.length === 0) return;
1245
1249
  if (typeof Blex === 'undefined') {
1246
1250
  console.warn('[blex] Blex library not loaded — blex.min.js may have failed to fetch');
1247
1251
  return;
1248
1252
  }
1249
1253
  var placeholders = el.querySelectorAll('.blex-block-container');
1254
+ // Get the interaction store for this thread
1255
+ var store = threadName
1256
+ ? blexInteractionStore[threadName] || (blexInteractionStore[threadName] = {})
1257
+ : null;
1250
1258
 
1251
1259
  placeholders.forEach(function (container) {
1252
1260
  var idx = parseInt(container.getAttribute('data-blex-idx'), 10);
1253
1261
  if (isNaN(idx) || idx >= blexBlocks.length) return;
1254
1262
  var block = blexBlocks[idx];
1263
+ var storeKey = msgKey ? msgKey + ':' + idx : null;
1255
1264
 
1256
1265
  try {
1257
1266
  var data = JSON.parse(block.json);
@@ -1261,8 +1270,14 @@
1261
1270
  // During streaming, show placeholder skeleton
1262
1271
  Blex.renderPlaceholder(block.type, container);
1263
1272
  } else {
1273
+ // Check for previously stored interaction value
1274
+ var renderOpts = {};
1275
+ if (store && storeKey && store[storeKey]) {
1276
+ renderOpts.previousValue = store[storeKey];
1277
+ }
1278
+
1264
1279
  // Final render — full interactive block
1265
- Blex.renderBlock(blockObj, container)
1280
+ Blex.renderBlock(blockObj, container, renderOpts)
1266
1281
  .then(function (handle) {
1267
1282
  // Store handle immediately (inside async callback, not after sync loop)
1268
1283
  if (blexHandles) {
@@ -1273,6 +1288,11 @@
1273
1288
  if (handle && handle.onInteraction) {
1274
1289
  // Wire interaction handler
1275
1290
  handle.onInteraction(function (interaction) {
1291
+ // Store the interaction value for persistence across re-renders
1292
+ if (store && storeKey && interaction.value !== undefined) {
1293
+ store[storeKey] = interaction.value;
1294
+ }
1295
+
1276
1296
  // Find the panel — try el.closest first, fall back to document query
1277
1297
  // (el may be detached from DOM if renderPanelMessages rebuilt the message list)
1278
1298
  var panel = el.closest('.cumulus-thread-panel') || el.closest('.cumulus-panel');
@@ -1287,8 +1307,11 @@
1287
1307
 
1288
1308
  if (inputEl && interaction.serialized) {
1289
1309
  if (interaction.immediate) {
1290
- // Immediate: inject into textarea and auto-send
1291
- inputEl.value = interaction.serialized;
1310
+ // Immediate: prepend any existing input text, then auto-send
1311
+ var existingText = inputEl.value.trim();
1312
+ inputEl.value = existingText
1313
+ ? existingText + '\n\n' + interaction.serialized
1314
+ : interaction.serialized;
1292
1315
  inputEl.dispatchEvent(new Event('input', { bubbles: true }));
1293
1316
  // Find and click the send button
1294
1317
  var sendBtn = panel ? panel.querySelector('[data-testid*="send"]') : null;
@@ -1389,14 +1412,25 @@
1389
1412
  // ── Inline Annotations (Google Docs-style highlight + comment) ──────────
1390
1413
 
1391
1414
  // Add an annotation chip to the chip tray
1392
- function addAnnotationChip(panel, quote, comment) {
1415
+ function addAnnotationChip(panel, quote, comment, isCodeBlock, codeLang) {
1393
1416
  if (!panel) return;
1394
1417
  var tray = panel.querySelector('.cumulus-chip-tray');
1395
1418
  if (!tray) return;
1396
1419
 
1397
1420
  var truncatedQuote = quote.length > 30 ? quote.substring(0, 30) + '\u2026' : quote;
1398
1421
  var truncatedComment = comment.length > 30 ? comment.substring(0, 30) + '\u2026' : comment;
1399
- var serialized = '> ' + quote.replace(/\n/g, '\n> ') + '\n' + comment;
1422
+ var serialized;
1423
+ if (isCodeBlock) {
1424
+ serialized =
1425
+ '```' +
1426
+ (codeLang && codeLang !== 'text' ? codeLang : '') +
1427
+ '\n' +
1428
+ quote +
1429
+ '\n```\n' +
1430
+ comment;
1431
+ } else {
1432
+ serialized = '> ' + quote.replace(/\n/g, '\n> ') + '\n' + comment;
1433
+ }
1400
1434
 
1401
1435
  var chip = document.createElement('span');
1402
1436
  chip.className = 'cumulus-annotation-chip';
@@ -1420,7 +1454,7 @@
1420
1454
  }
1421
1455
 
1422
1456
  // Show the annotation popover near a text selection
1423
- function showAnnotationPopover(panel, selectedText, anchorRect) {
1457
+ function showAnnotationPopover(panel, selectedText, anchorRect, isCodeBlock, codeLang) {
1424
1458
  // Remove any existing popover
1425
1459
  dismissAnnotationPopover(panel);
1426
1460
 
@@ -1469,7 +1503,7 @@
1469
1503
  submitBtn.addEventListener('click', function () {
1470
1504
  var comment = textarea.value.trim();
1471
1505
  if (comment) {
1472
- addAnnotationChip(panel, selectedText, comment);
1506
+ addAnnotationChip(panel, selectedText, comment, isCodeBlock, codeLang);
1473
1507
  }
1474
1508
  dismissAnnotationPopover(panel);
1475
1509
  window.getSelection().removeAllRanges();
@@ -1491,10 +1525,8 @@
1491
1525
 
1492
1526
  panel.appendChild(popover);
1493
1527
 
1494
- // Focus the textarea after a tick (so the popover is in the DOM)
1495
- setTimeout(function () {
1496
- textarea.focus();
1497
- }, 0);
1528
+ // Don't auto-focus the textarea it clears the text selection.
1529
+ // User can click into it when ready to type.
1498
1530
  }
1499
1531
 
1500
1532
  function dismissAnnotationPopover(panel) {
@@ -1521,10 +1553,22 @@
1521
1553
  var msgEl = container.closest ? container.closest('.cumulus-msg.assistant') : null;
1522
1554
  if (!msgEl) return;
1523
1555
 
1556
+ // Detect if selection is inside a code block
1557
+ var codeAncestor = container.closest ? container.closest('pre, code') : null;
1558
+ var isCodeBlock = !!codeAncestor;
1559
+ var codeLang = '';
1560
+ if (isCodeBlock) {
1561
+ var wrapper = container.closest ? container.closest('.code-block-wrapper') : null;
1562
+ if (wrapper) {
1563
+ var langEl = wrapper.querySelector('.code-block-language');
1564
+ if (langEl) codeLang = langEl.textContent.trim();
1565
+ }
1566
+ }
1567
+
1524
1568
  // Get anchor position for the popover
1525
1569
  var rect = range.getBoundingClientRect();
1526
1570
 
1527
- showAnnotationPopover(panel, selectedText, rect);
1571
+ showAnnotationPopover(panel, selectedText, rect, isCodeBlock, codeLang);
1528
1572
  }, 10);
1529
1573
  });
1530
1574
 
@@ -2170,7 +2214,8 @@
2170
2214
  function buildUserMsgEl(msg) {
2171
2215
  var el = document.createElement('div');
2172
2216
  el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
2173
- el.textContent = msg.content;
2217
+ var mdResult = renderMarkdown(msg.content);
2218
+ el.innerHTML = mdResult.html;
2174
2219
  if (msg.attachments && msg.attachments.length > 0) {
2175
2220
  var attRow = document.createElement('div');
2176
2221
  attRow.className = 'cumulus-msg-attachments';
@@ -2195,7 +2240,7 @@
2195
2240
  return el;
2196
2241
  }
2197
2242
 
2198
- function buildAssistantMsgEl(content, isStreaming) {
2243
+ function buildAssistantMsgEl(content, isStreaming, msgKey) {
2199
2244
  var el = document.createElement('div');
2200
2245
  el.className = 'cumulus-msg assistant';
2201
2246
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
@@ -2207,7 +2252,7 @@
2207
2252
  }
2208
2253
  // Render blex blocks — skip during streaming to avoid destroy/recreate churn
2209
2254
  if (!isStreaming) {
2210
- renderBlexBlocks(el, mdResult.blexBlocks, false);
2255
+ renderBlexBlocks(el, mdResult.blexBlocks, false, msgKey, threadName);
2211
2256
  }
2212
2257
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
2213
2258
  btn.addEventListener('click', function () {
@@ -2298,7 +2343,7 @@
2298
2343
  row.appendChild(buildUserMsgEl(msg));
2299
2344
  }
2300
2345
  } else {
2301
- row.appendChild(buildAssistantMsgEl(msg.content, false));
2346
+ row.appendChild(buildAssistantMsgEl(msg.content, false, String(msg.timestamp)));
2302
2347
  }
2303
2348
  appendTimestamp(row, msg.timestamp);
2304
2349
  messagesEl.appendChild(row);
@@ -3735,7 +3780,8 @@
3735
3780
  function buildUserMsgEl(msg) {
3736
3781
  var el = document.createElement('div');
3737
3782
  el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
3738
- el.textContent = msg.content;
3783
+ var mdResult = renderMarkdown(msg.content);
3784
+ el.innerHTML = mdResult.html;
3739
3785
  if (msg.attachments && msg.attachments.length > 0) {
3740
3786
  var attRow = document.createElement('div');
3741
3787
  attRow.className = 'cumulus-msg-attachments';
@@ -3760,7 +3806,7 @@
3760
3806
  return el;
3761
3807
  }
3762
3808
 
3763
- function buildAssistantMsgEl(content, isStreaming) {
3809
+ function buildAssistantMsgEl(content, isStreaming, msgKey) {
3764
3810
  var el = document.createElement('div');
3765
3811
  el.className = 'cumulus-msg assistant';
3766
3812
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
@@ -3772,7 +3818,7 @@
3772
3818
  }
3773
3819
  // Render blex blocks — skip during streaming to avoid destroy/recreate churn
3774
3820
  if (!isStreaming) {
3775
- renderBlexBlocks(el, mdResult.blexBlocks, false);
3821
+ renderBlexBlocks(el, mdResult.blexBlocks, false, msgKey, threadName);
3776
3822
  }
3777
3823
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
3778
3824
  btn.addEventListener('click', function () {
@@ -3855,7 +3901,7 @@
3855
3901
  row.appendChild(buildUserMsgEl(msg));
3856
3902
  }
3857
3903
  } else {
3858
- row.appendChild(buildAssistantMsgEl(msg.content, false));
3904
+ row.appendChild(buildAssistantMsgEl(msg.content, false, String(msg.timestamp)));
3859
3905
  }
3860
3906
  appendTimestamp(row, msg.timestamp);
3861
3907
  messagesEl.appendChild(row);
@@ -4131,13 +4177,15 @@
4131
4177
  return;
4132
4178
  }
4133
4179
 
4134
- // Freeze the partial assistant response as a message
4180
+ // Freeze the partial assistant response as a message so the interjection
4181
+ // appears AFTER the (partial) LLM response, not next to the prior user message
4135
4182
  if (state.streamBuffer) {
4136
4183
  state.messages.push({ role: 'assistant', content: state.streamBuffer });
4137
4184
  }
4138
4185
  // Reset streaming state — the server will send 'interjected' for the old stream
4139
4186
  state.streamBuffer = '';
4140
4187
  state.interjecting = true;
4188
+ state.interjectionStreamId = (state.interjectionStreamId || 0) + 1;
4141
4189
  // Don't set streaming=false — the new message will keep streaming
4142
4190
  }
4143
4191
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydraw/cumulus",
3
- "version": "0.28.2",
3
+ "version": "0.28.4",
4
4
  "description": "RLM-based CLI chat wrapper for Claude with external history context management",
5
5
  "type": "module",
6
6
  "main": "dist/index.js",