@luckydraw/cumulus 0.28.3 → 0.28.5

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.
@@ -3,6 +3,11 @@
3
3
  <head>
4
4
  <meta charset="utf-8">
5
5
  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
6
+ <meta name="apple-mobile-web-app-capable" content="yes">
7
+ <meta name="apple-mobile-web-app-status-bar-style" content="black-translucent">
8
+ <meta name="apple-mobile-web-app-title" content="Cumulus">
9
+ <link rel="manifest" href="/manifest.json">
10
+ <link rel="apple-touch-icon" href="/icon-192.png">
6
11
  <title>Cumulus Chat</title>
7
12
  <style>
8
13
  *, *::before, *::after { box-sizing: border-box; }
Binary file
@@ -0,0 +1,4 @@
1
+ <svg xmlns="http://www.w3.org/2000/svg" width="192" height="192" viewBox="0 0 192 192">
2
+ <rect width="192" height="192" rx="32" fill="#2d2d2d"/>
3
+ <text x="96" y="120" font-family="system-ui, -apple-system, sans-serif" font-size="100" font-weight="700" fill="#e0e0e0" text-anchor="middle">C</text>
4
+ </svg>
@@ -0,0 +1,15 @@
1
+ {
2
+ "name": "Cumulus Chat",
3
+ "short_name": "Cumulus",
4
+ "start_url": "/chat",
5
+ "display": "standalone",
6
+ "background_color": "#1e1e1e",
7
+ "theme_color": "#1e1e1e",
8
+ "icons": [
9
+ {
10
+ "src": "/icon-192.png",
11
+ "sizes": "192x192",
12
+ "type": "image/png"
13
+ }
14
+ ]
15
+ }
@@ -234,10 +234,13 @@
234
234
  ' display: inline-flex; align-items: center; gap: 0.3em;',
235
235
  ' background: #2a3a4a; color: #d4e8f8; border: 1px solid #3a5060;',
236
236
  ' border-radius: 1em; padding: 0.15em 0.6em; font-size: 0.82em;',
237
- ' cursor: default; max-width: 32ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
237
+ ' cursor: default; max-width: 32ch;',
238
+ '}',
239
+ '.cumulus-interaction-chip .chip-label {',
240
+ ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;',
238
241
  '}',
239
242
  '.cumulus-interaction-chip .chip-dismiss {',
240
- ' cursor: pointer; opacity: 0.6; font-size: 0.9em; margin-left: 0.2em;',
243
+ ' cursor: pointer; opacity: 0.6; font-size: 0.9em; margin-left: 0.2em; flex-shrink: 0;',
241
244
  '}',
242
245
  '.cumulus-interaction-chip .chip-dismiss:hover { opacity: 1; }',
243
246
 
@@ -246,10 +249,13 @@
246
249
  ' display: inline-flex; align-items: center; gap: 0.3em;',
247
250
  ' background: #2a3a2a; color: #b8e6b8; border: 1px solid #3a6040;',
248
251
  ' border-radius: 1em; padding: 0.15em 0.6em; font-size: 0.82em;',
249
- ' cursor: default; max-width: 40ch; overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
252
+ ' cursor: default; max-width: 40ch;',
253
+ '}',
254
+ '.cumulus-annotation-chip .chip-label {',
255
+ ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap; min-width: 0;',
250
256
  '}',
251
257
  '.cumulus-annotation-chip .chip-dismiss {',
252
- ' cursor: pointer; opacity: 0.6; font-size: 0.9em; margin-left: 0.2em;',
258
+ ' cursor: pointer; opacity: 0.6; font-size: 0.9em; margin-left: 0.2em; flex-shrink: 0;',
253
259
  '}',
254
260
  '.cumulus-annotation-chip .chip-dismiss:hover { opacity: 1; }',
255
261
 
@@ -1226,6 +1232,26 @@
1226
1232
  // WeakMap: DOM element -> BlockHandle[] for lifecycle management
1227
1233
  var blexHandles = typeof WeakMap !== 'undefined' ? new WeakMap() : null;
1228
1234
 
1235
+ // Per-thread store for blex interaction values (poll answers, confirm clicks, etc.)
1236
+ // Key: "threadName" → Map<"msgTimestamp:blockIdx", interactionValue>
1237
+ // Backed by localStorage for persistence across page refreshes
1238
+ var BLEX_STORE_KEY = 'cumulus-blex-interactions';
1239
+ var blexInteractionStore = {};
1240
+ try {
1241
+ var saved = localStorage.getItem(BLEX_STORE_KEY);
1242
+ if (saved) blexInteractionStore = JSON.parse(saved);
1243
+ } catch (e) {
1244
+ /* ignore corrupt/missing localStorage */
1245
+ }
1246
+
1247
+ function _flushBlexStore() {
1248
+ try {
1249
+ localStorage.setItem(BLEX_STORE_KEY, JSON.stringify(blexInteractionStore));
1250
+ } catch (e) {
1251
+ /* quota exceeded or private browsing */
1252
+ }
1253
+ }
1254
+
1229
1255
  // Extract ~~~blex:TYPE\n{json}\n~~~ fences from text
1230
1256
  // Returns { text: string (with placeholders), blocks: Array<{type, json, idx}> }
1231
1257
  function extractBlexBlocks(text) {
@@ -1240,18 +1266,23 @@
1240
1266
  }
1241
1267
 
1242
1268
  // Render blex blocks into placeholder divs within a message element
1243
- function renderBlexBlocks(el, blexBlocks, isStreaming) {
1269
+ function renderBlexBlocks(el, blexBlocks, isStreaming, msgKey, threadName) {
1244
1270
  if (!blexBlocks || blexBlocks.length === 0) return;
1245
1271
  if (typeof Blex === 'undefined') {
1246
1272
  console.warn('[blex] Blex library not loaded — blex.min.js may have failed to fetch');
1247
1273
  return;
1248
1274
  }
1249
1275
  var placeholders = el.querySelectorAll('.blex-block-container');
1276
+ // Get the interaction store for this thread
1277
+ var store = threadName
1278
+ ? blexInteractionStore[threadName] || (blexInteractionStore[threadName] = {})
1279
+ : null;
1250
1280
 
1251
1281
  placeholders.forEach(function (container) {
1252
1282
  var idx = parseInt(container.getAttribute('data-blex-idx'), 10);
1253
1283
  if (isNaN(idx) || idx >= blexBlocks.length) return;
1254
1284
  var block = blexBlocks[idx];
1285
+ var storeKey = msgKey ? msgKey + ':' + idx : null;
1255
1286
 
1256
1287
  try {
1257
1288
  var data = JSON.parse(block.json);
@@ -1261,8 +1292,14 @@
1261
1292
  // During streaming, show placeholder skeleton
1262
1293
  Blex.renderPlaceholder(block.type, container);
1263
1294
  } else {
1295
+ // Check for previously stored interaction value
1296
+ var renderOpts = {};
1297
+ if (store && storeKey && store[storeKey]) {
1298
+ renderOpts.previousValue = store[storeKey];
1299
+ }
1300
+
1264
1301
  // Final render — full interactive block
1265
- Blex.renderBlock(blockObj, container)
1302
+ Blex.renderBlock(blockObj, container, renderOpts)
1266
1303
  .then(function (handle) {
1267
1304
  // Store handle immediately (inside async callback, not after sync loop)
1268
1305
  if (blexHandles) {
@@ -1273,6 +1310,12 @@
1273
1310
  if (handle && handle.onInteraction) {
1274
1311
  // Wire interaction handler
1275
1312
  handle.onInteraction(function (interaction) {
1313
+ // Store the interaction value for persistence across re-renders + refreshes
1314
+ if (store && storeKey && interaction.value !== undefined) {
1315
+ store[storeKey] = interaction.value;
1316
+ _flushBlexStore();
1317
+ }
1318
+
1276
1319
  // Find the panel — try el.closest first, fall back to document query
1277
1320
  // (el may be detached from DOM if renderPanelMessages rebuilt the message list)
1278
1321
  var panel = el.closest('.cumulus-thread-panel') || el.closest('.cumulus-panel');
@@ -1287,8 +1330,11 @@
1287
1330
 
1288
1331
  if (inputEl && interaction.serialized) {
1289
1332
  if (interaction.immediate) {
1290
- // Immediate: inject into textarea and auto-send
1291
- inputEl.value = interaction.serialized;
1333
+ // Immediate: prepend any existing input text, then auto-send
1334
+ var existingText = inputEl.value.trim();
1335
+ inputEl.value = existingText
1336
+ ? existingText + '\n\n' + interaction.serialized
1337
+ : interaction.serialized;
1292
1338
  inputEl.dispatchEvent(new Event('input', { bubbles: true }));
1293
1339
  // Find and click the send button
1294
1340
  var sendBtn = panel ? panel.querySelector('[data-testid*="send"]') : null;
@@ -1296,8 +1342,8 @@
1296
1342
  sendBtn.click();
1297
1343
  }
1298
1344
  } else {
1299
- // Deferred: add to chip tray
1300
- addInteractionChip(panel, interaction, handle);
1345
+ // Deferred: add to chip tray (sourceId deduplicates rapid-fire from same block)
1346
+ addInteractionChip(panel, interaction, handle, block.type + '-' + idx);
1301
1347
  }
1302
1348
  }
1303
1349
  });
@@ -1337,16 +1383,27 @@
1337
1383
  }
1338
1384
 
1339
1385
  // Add a deferred interaction chip to the chip tray
1340
- function addInteractionChip(panel, interaction, handle) {
1386
+ // If a chip from the same source already exists, replace it (prevents spam from rapid interactions)
1387
+ function addInteractionChip(panel, interaction, handle, sourceId) {
1341
1388
  if (!panel) return;
1342
1389
  var tray = panel.querySelector('.cumulus-chip-tray');
1343
1390
  if (!tray) return;
1344
1391
 
1392
+ // Replace existing chip from same source instead of appending
1393
+ if (sourceId) {
1394
+ var existing = tray.querySelector(
1395
+ '.cumulus-interaction-chip[data-source="' + sourceId + '"]'
1396
+ );
1397
+ if (existing) existing.remove();
1398
+ }
1399
+
1345
1400
  var chip = document.createElement('span');
1346
1401
  chip.className = 'cumulus-interaction-chip';
1347
1402
  chip.setAttribute('data-serialized', interaction.serialized || '');
1403
+ if (sourceId) chip.setAttribute('data-source', sourceId);
1348
1404
 
1349
1405
  var label = document.createElement('span');
1406
+ label.className = 'chip-label';
1350
1407
  label.textContent =
1351
1408
  (interaction.icon || '\u2022') +
1352
1409
  ' ' +
@@ -1389,29 +1446,53 @@
1389
1446
  // ── Inline Annotations (Google Docs-style highlight + comment) ──────────
1390
1447
 
1391
1448
  // Add an annotation chip to the chip tray
1392
- function addAnnotationChip(panel, quote, comment) {
1449
+ function addAnnotationChip(panel, quote, comment, isCodeBlock, codeLang) {
1393
1450
  if (!panel) return;
1394
1451
  var tray = panel.querySelector('.cumulus-chip-tray');
1395
1452
  if (!tray) return;
1396
1453
 
1397
1454
  var truncatedQuote = quote.length > 30 ? quote.substring(0, 30) + '\u2026' : quote;
1398
1455
  var truncatedComment = comment.length > 30 ? comment.substring(0, 30) + '\u2026' : comment;
1399
- var serialized = '> ' + quote.replace(/\n/g, '\n> ') + '\n' + comment;
1456
+ var serialized;
1457
+ if (isCodeBlock) {
1458
+ serialized =
1459
+ '```' +
1460
+ (codeLang && codeLang !== 'text' ? codeLang : '') +
1461
+ '\n' +
1462
+ quote +
1463
+ '\n```\n' +
1464
+ comment;
1465
+ } else {
1466
+ serialized = '> ' + quote.replace(/\n/g, '\n> ') + '\n' + comment;
1467
+ }
1400
1468
 
1401
1469
  var chip = document.createElement('span');
1402
1470
  chip.className = 'cumulus-annotation-chip';
1403
1471
  chip.setAttribute('data-serialized', serialized);
1472
+ chip.setAttribute('data-quote', quote);
1473
+ chip.setAttribute('data-comment', comment);
1474
+ chip.setAttribute('data-is-code', isCodeBlock ? '1' : '');
1475
+ chip.setAttribute('data-code-lang', codeLang || '');
1404
1476
  chip.title = '"' + quote + '"\n\n' + comment;
1405
1477
 
1406
1478
  var label = document.createElement('span');
1479
+ label.className = 'chip-label';
1407
1480
  label.textContent =
1408
1481
  '\uD83D\uDCDD \u201C' + truncatedQuote + '\u201D \u2192 ' + truncatedComment;
1409
1482
  chip.appendChild(label);
1410
1483
 
1484
+ // Click label to edit
1485
+ label.style.cursor = 'pointer';
1486
+ label.addEventListener('click', function (e) {
1487
+ e.stopPropagation();
1488
+ showAnnotationEditPopover(panel, chip);
1489
+ });
1490
+
1411
1491
  var dismiss = document.createElement('span');
1412
1492
  dismiss.className = 'chip-dismiss';
1413
1493
  dismiss.textContent = '\u00d7';
1414
- dismiss.addEventListener('click', function () {
1494
+ dismiss.addEventListener('click', function (e) {
1495
+ e.stopPropagation();
1415
1496
  chip.remove();
1416
1497
  });
1417
1498
  chip.appendChild(dismiss);
@@ -1419,8 +1500,106 @@
1419
1500
  tray.appendChild(chip);
1420
1501
  }
1421
1502
 
1503
+ // Show the annotation popover near a chip for editing
1504
+ function showAnnotationEditPopover(panel, chip) {
1505
+ dismissAnnotationPopover(panel);
1506
+
1507
+ var quote = chip.getAttribute('data-quote') || '';
1508
+ var comment = chip.getAttribute('data-comment') || '';
1509
+ var isCodeBlock = chip.getAttribute('data-is-code') === '1';
1510
+ var codeLang = chip.getAttribute('data-code-lang') || '';
1511
+
1512
+ var popover = document.createElement('div');
1513
+ popover.className = 'cumulus-annotation-popover';
1514
+
1515
+ // Position near the chip
1516
+ var panelRect = panel.getBoundingClientRect();
1517
+ var chipRect = chip.getBoundingClientRect();
1518
+ var top = chipRect.top - panelRect.top + panel.scrollTop - 160;
1519
+ if (top < 4) top = chipRect.bottom - panelRect.top + panel.scrollTop + 4;
1520
+ var left = chipRect.left - panelRect.left;
1521
+ if (left + 260 > panelRect.width) left = panelRect.width - 270;
1522
+ if (left < 4) left = 4;
1523
+ popover.style.top = top + 'px';
1524
+ popover.style.left = left + 'px';
1525
+
1526
+ var quoteEl = document.createElement('div');
1527
+ quoteEl.className = 'annotation-quote';
1528
+ quoteEl.textContent = '\u201C' + quote + '\u201D';
1529
+ popover.appendChild(quoteEl);
1530
+
1531
+ var textarea = document.createElement('textarea');
1532
+ textarea.placeholder = 'Edit comment\u2026';
1533
+ textarea.rows = 4;
1534
+ textarea.value = comment;
1535
+ textarea.setAttribute('data-testid', 'annotation-edit-comment');
1536
+ popover.appendChild(textarea);
1537
+
1538
+ var actions = document.createElement('div');
1539
+ actions.className = 'annotation-actions';
1540
+
1541
+ var cancelBtn = document.createElement('button');
1542
+ cancelBtn.className = 'annotation-cancel';
1543
+ cancelBtn.textContent = 'Cancel';
1544
+ cancelBtn.addEventListener('click', function () {
1545
+ dismissAnnotationPopover(panel);
1546
+ });
1547
+
1548
+ var submitBtn = document.createElement('button');
1549
+ submitBtn.className = 'annotation-submit';
1550
+ submitBtn.textContent = 'Update';
1551
+ submitBtn.setAttribute('data-testid', 'annotation-edit-submit');
1552
+ submitBtn.addEventListener('click', function () {
1553
+ var newComment = textarea.value.trim();
1554
+ if (!newComment) {
1555
+ dismissAnnotationPopover(panel);
1556
+ return;
1557
+ }
1558
+ // Update chip data
1559
+ var newSerialized;
1560
+ if (isCodeBlock) {
1561
+ newSerialized =
1562
+ '```' +
1563
+ (codeLang && codeLang !== 'text' ? codeLang : '') +
1564
+ '\n' +
1565
+ quote +
1566
+ '\n```\n' +
1567
+ newComment;
1568
+ } else {
1569
+ newSerialized = '> ' + quote.replace(/\n/g, '\n> ') + '\n' + newComment;
1570
+ }
1571
+ chip.setAttribute('data-serialized', newSerialized);
1572
+ chip.setAttribute('data-comment', newComment);
1573
+ var truncQ = quote.length > 30 ? quote.substring(0, 30) + '\u2026' : quote;
1574
+ var truncC = newComment.length > 30 ? newComment.substring(0, 30) + '\u2026' : newComment;
1575
+ var labelEl = chip.querySelector('.chip-label');
1576
+ if (labelEl) {
1577
+ labelEl.textContent = '\uD83D\uDCDD \u201C' + truncQ + '\u201D \u2192 ' + truncC;
1578
+ }
1579
+ chip.title = '"' + quote + '"\n\n' + newComment;
1580
+ dismissAnnotationPopover(panel);
1581
+ });
1582
+
1583
+ actions.appendChild(cancelBtn);
1584
+ actions.appendChild(submitBtn);
1585
+ popover.appendChild(actions);
1586
+
1587
+ textarea.addEventListener('keydown', function (e) {
1588
+ if (e.key === 'Enter' && !e.shiftKey) {
1589
+ e.preventDefault();
1590
+ submitBtn.click();
1591
+ } else if (e.key === 'Escape') {
1592
+ dismissAnnotationPopover(panel);
1593
+ }
1594
+ });
1595
+
1596
+ panel.appendChild(popover);
1597
+ textarea.focus();
1598
+ textarea.setSelectionRange(textarea.value.length, textarea.value.length);
1599
+ }
1600
+
1422
1601
  // Show the annotation popover near a text selection
1423
- function showAnnotationPopover(panel, selectedText, anchorRect) {
1602
+ function showAnnotationPopover(panel, selectedText, anchorRect, isCodeBlock, codeLang) {
1424
1603
  // Remove any existing popover
1425
1604
  dismissAnnotationPopover(panel);
1426
1605
 
@@ -1469,7 +1648,7 @@
1469
1648
  submitBtn.addEventListener('click', function () {
1470
1649
  var comment = textarea.value.trim();
1471
1650
  if (comment) {
1472
- addAnnotationChip(panel, selectedText, comment);
1651
+ addAnnotationChip(panel, selectedText, comment, isCodeBlock, codeLang);
1473
1652
  }
1474
1653
  dismissAnnotationPopover(panel);
1475
1654
  window.getSelection().removeAllRanges();
@@ -1519,10 +1698,22 @@
1519
1698
  var msgEl = container.closest ? container.closest('.cumulus-msg.assistant') : null;
1520
1699
  if (!msgEl) return;
1521
1700
 
1701
+ // Detect if selection is inside a code block
1702
+ var codeAncestor = container.closest ? container.closest('pre, code') : null;
1703
+ var isCodeBlock = !!codeAncestor;
1704
+ var codeLang = '';
1705
+ if (isCodeBlock) {
1706
+ var wrapper = container.closest ? container.closest('.code-block-wrapper') : null;
1707
+ if (wrapper) {
1708
+ var langEl = wrapper.querySelector('.code-block-language');
1709
+ if (langEl) codeLang = langEl.textContent.trim();
1710
+ }
1711
+ }
1712
+
1522
1713
  // Get anchor position for the popover
1523
1714
  var rect = range.getBoundingClientRect();
1524
1715
 
1525
- showAnnotationPopover(panel, selectedText, rect);
1716
+ showAnnotationPopover(panel, selectedText, rect, isCodeBlock, codeLang);
1526
1717
  }, 10);
1527
1718
  });
1528
1719
 
@@ -2168,7 +2359,8 @@
2168
2359
  function buildUserMsgEl(msg) {
2169
2360
  var el = document.createElement('div');
2170
2361
  el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
2171
- el.textContent = msg.content;
2362
+ var mdResult = renderMarkdown(msg.content);
2363
+ el.innerHTML = mdResult.html;
2172
2364
  if (msg.attachments && msg.attachments.length > 0) {
2173
2365
  var attRow = document.createElement('div');
2174
2366
  attRow.className = 'cumulus-msg-attachments';
@@ -2193,7 +2385,7 @@
2193
2385
  return el;
2194
2386
  }
2195
2387
 
2196
- function buildAssistantMsgEl(content, isStreaming) {
2388
+ function buildAssistantMsgEl(content, isStreaming, msgKey) {
2197
2389
  var el = document.createElement('div');
2198
2390
  el.className = 'cumulus-msg assistant';
2199
2391
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
@@ -2205,7 +2397,7 @@
2205
2397
  }
2206
2398
  // Render blex blocks — skip during streaming to avoid destroy/recreate churn
2207
2399
  if (!isStreaming) {
2208
- renderBlexBlocks(el, mdResult.blexBlocks, false);
2400
+ renderBlexBlocks(el, mdResult.blexBlocks, false, msgKey, threadName);
2209
2401
  }
2210
2402
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
2211
2403
  btn.addEventListener('click', function () {
@@ -2296,7 +2488,7 @@
2296
2488
  row.appendChild(buildUserMsgEl(msg));
2297
2489
  }
2298
2490
  } else {
2299
- row.appendChild(buildAssistantMsgEl(msg.content, false));
2491
+ row.appendChild(buildAssistantMsgEl(msg.content, false, String(msg.timestamp)));
2300
2492
  }
2301
2493
  appendTimestamp(row, msg.timestamp);
2302
2494
  messagesEl.appendChild(row);
@@ -3733,7 +3925,8 @@
3733
3925
  function buildUserMsgEl(msg) {
3734
3926
  var el = document.createElement('div');
3735
3927
  el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
3736
- el.textContent = msg.content;
3928
+ var mdResult = renderMarkdown(msg.content);
3929
+ el.innerHTML = mdResult.html;
3737
3930
  if (msg.attachments && msg.attachments.length > 0) {
3738
3931
  var attRow = document.createElement('div');
3739
3932
  attRow.className = 'cumulus-msg-attachments';
@@ -3758,7 +3951,7 @@
3758
3951
  return el;
3759
3952
  }
3760
3953
 
3761
- function buildAssistantMsgEl(content, isStreaming) {
3954
+ function buildAssistantMsgEl(content, isStreaming, msgKey) {
3762
3955
  var el = document.createElement('div');
3763
3956
  el.className = 'cumulus-msg assistant';
3764
3957
  if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
@@ -3770,7 +3963,7 @@
3770
3963
  }
3771
3964
  // Render blex blocks — skip during streaming to avoid destroy/recreate churn
3772
3965
  if (!isStreaming) {
3773
- renderBlexBlocks(el, mdResult.blexBlocks, false);
3966
+ renderBlexBlocks(el, mdResult.blexBlocks, false, msgKey, threadName);
3774
3967
  }
3775
3968
  el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
3776
3969
  btn.addEventListener('click', function () {
@@ -3853,7 +4046,7 @@
3853
4046
  row.appendChild(buildUserMsgEl(msg));
3854
4047
  }
3855
4048
  } else {
3856
- row.appendChild(buildAssistantMsgEl(msg.content, false));
4049
+ row.appendChild(buildAssistantMsgEl(msg.content, false, String(msg.timestamp)));
3857
4050
  }
3858
4051
  appendTimestamp(row, msg.timestamp);
3859
4052
  messagesEl.appendChild(row);
@@ -4129,13 +4322,15 @@
4129
4322
  return;
4130
4323
  }
4131
4324
 
4132
- // Freeze the partial assistant response as a message
4325
+ // Freeze the partial assistant response as a message so the interjection
4326
+ // appears AFTER the (partial) LLM response, not next to the prior user message
4133
4327
  if (state.streamBuffer) {
4134
4328
  state.messages.push({ role: 'assistant', content: state.streamBuffer });
4135
4329
  }
4136
4330
  // Reset streaming state — the server will send 'interjected' for the old stream
4137
4331
  state.streamBuffer = '';
4138
4332
  state.interjecting = true;
4333
+ state.interjectionStreamId = (state.interjectionStreamId || 0) + 1;
4139
4334
  // Don't set streaming=false — the new message will keep streaming
4140
4335
  }
4141
4336
 
@@ -4537,55 +4732,72 @@
4537
4732
  // ── Push notification subscription ──
4538
4733
  function registerPushNotifications() {
4539
4734
  if (!('serviceWorker' in navigator) || !('PushManager' in window)) {
4540
- console.log('[Cumulus] Push notifications not supported');
4735
+ console.log('[Cumulus] Push notifications not supported in this browser');
4541
4736
  return;
4542
4737
  }
4543
4738
 
4544
- navigator.serviceWorker
4545
- .register('/sw.js')
4546
- .then(function (registration) {
4547
- console.log('[Cumulus] Service worker registered');
4548
-
4549
- // Check existing subscription
4550
- return registration.pushManager.getSubscription().then(function (existing) {
4551
- if (existing) {
4552
- // Already subscribed — send to server in case it's new/different
4553
- sendPushSubscription(existing);
4554
- return;
4555
- }
4739
+ if (!('Notification' in window)) {
4740
+ console.log('[Cumulus] Notification API not available');
4741
+ return;
4742
+ }
4556
4743
 
4557
- // Get VAPID key from server
4558
- var loc = window.location;
4559
- var apiUrl = loc.protocol + '//' + loc.host;
4560
- fetch(apiUrl + '/api/push/vapid-key')
4561
- .then(function (r) {
4562
- return r.json();
4563
- })
4564
- .then(function (data) {
4565
- if (!data.publicKey) {
4566
- console.log('[Cumulus] Push not configured on server');
4744
+ // Must request permission explicitly (required for iOS PWA)
4745
+ Notification.requestPermission().then(function (permission) {
4746
+ console.log('[Cumulus] Notification permission:', permission);
4747
+ if (permission !== 'granted') {
4748
+ console.log('[Cumulus] Push notifications denied by user');
4749
+ return;
4750
+ }
4751
+
4752
+ navigator.serviceWorker
4753
+ .register('/sw.js', { scope: '/' })
4754
+ .then(function (registration) {
4755
+ console.log('[Cumulus] Service worker registered, scope:', registration.scope);
4756
+
4757
+ // Wait for the service worker to be ready
4758
+ return navigator.serviceWorker.ready.then(function (reg) {
4759
+ // Check existing subscription
4760
+ return reg.pushManager.getSubscription().then(function (existing) {
4761
+ if (existing) {
4762
+ console.log('[Cumulus] Existing push subscription found');
4763
+ sendPushSubscription(existing);
4567
4764
  return;
4568
4765
  }
4569
4766
 
4570
- // Request permission and subscribe
4571
- return registration.pushManager
4572
- .subscribe({
4573
- userVisibleOnly: true,
4574
- applicationServerKey: urlBase64ToUint8Array(data.publicKey),
4767
+ // Get VAPID key from server
4768
+ var loc = window.location;
4769
+ var apiUrl = loc.protocol + '//' + loc.host;
4770
+ fetch(apiUrl + '/api/push/vapid-key')
4771
+ .then(function (r) {
4772
+ return r.json();
4773
+ })
4774
+ .then(function (data) {
4775
+ if (!data.publicKey) {
4776
+ console.log('[Cumulus] Push not configured on server (no VAPID key)');
4777
+ return;
4778
+ }
4779
+
4780
+ console.log('[Cumulus] Subscribing to push with VAPID key');
4781
+ return reg.pushManager
4782
+ .subscribe({
4783
+ userVisibleOnly: true,
4784
+ applicationServerKey: urlBase64ToUint8Array(data.publicKey),
4785
+ })
4786
+ .then(function (subscription) {
4787
+ console.log('[Cumulus] Push subscription created:', subscription.endpoint);
4788
+ sendPushSubscription(subscription);
4789
+ });
4575
4790
  })
4576
- .then(function (subscription) {
4577
- console.log('[Cumulus] Push subscription created');
4578
- sendPushSubscription(subscription);
4791
+ .catch(function (err) {
4792
+ console.warn('[Cumulus] Push subscription failed:', err.message || err);
4579
4793
  });
4580
- })
4581
- .catch(function (err) {
4582
- console.warn('[Cumulus] Push subscription failed:', err);
4583
4794
  });
4795
+ });
4796
+ })
4797
+ .catch(function (err) {
4798
+ console.warn('[Cumulus] Service worker registration failed:', err.message || err);
4584
4799
  });
4585
- })
4586
- .catch(function (err) {
4587
- console.warn('[Cumulus] Service worker registration failed:', err);
4588
- });
4800
+ });
4589
4801
  }
4590
4802
 
4591
4803
  function sendPushSubscription(subscription) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@luckydraw/cumulus",
3
- "version": "0.28.3",
3
+ "version": "0.28.5",
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",