@luckydraw/cumulus 0.18.2 → 0.20.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/dist/gateway/adapters/discord.d.ts.map +1 -1
  2. package/dist/gateway/adapters/discord.js +3 -3
  3. package/dist/gateway/adapters/discord.js.map +1 -1
  4. package/dist/gateway/adapters/slack.d.ts.map +1 -1
  5. package/dist/gateway/adapters/slack.js +2 -2
  6. package/dist/gateway/adapters/slack.js.map +1 -1
  7. package/dist/gateway/adapters/webchat.d.ts +3 -0
  8. package/dist/gateway/adapters/webchat.d.ts.map +1 -1
  9. package/dist/gateway/adapters/webchat.js +129 -10
  10. package/dist/gateway/adapters/webchat.js.map +1 -1
  11. package/dist/gateway/cli.js +1 -1
  12. package/dist/gateway/cli.js.map +1 -1
  13. package/dist/gateway/config.d.ts +15 -0
  14. package/dist/gateway/config.d.ts.map +1 -1
  15. package/dist/gateway/config.js +18 -1
  16. package/dist/gateway/config.js.map +1 -1
  17. package/dist/gateway/daemon.d.ts.map +1 -1
  18. package/dist/gateway/daemon.js +11 -11
  19. package/dist/gateway/daemon.js.map +1 -1
  20. package/dist/gateway/logger.js +1 -1
  21. package/dist/gateway/logger.js.map +1 -1
  22. package/dist/gateway/server.d.ts +3 -0
  23. package/dist/gateway/server.d.ts.map +1 -1
  24. package/dist/gateway/server.js +220 -12
  25. package/dist/gateway/server.js.map +1 -1
  26. package/dist/gateway/static/widget.js +520 -89
  27. package/dist/index.d.ts +5 -1
  28. package/dist/index.d.ts.map +1 -1
  29. package/dist/index.js +3 -0
  30. package/dist/index.js.map +1 -1
  31. package/dist/lib/gateway.d.ts +1 -1
  32. package/dist/lib/gateway.d.ts.map +1 -1
  33. package/dist/lib/gateway.js +48 -30
  34. package/dist/lib/gateway.js.map +1 -1
  35. package/dist/lib/projects.d.ts +67 -0
  36. package/dist/lib/projects.d.ts.map +1 -0
  37. package/dist/lib/projects.js +229 -0
  38. package/dist/lib/projects.js.map +1 -0
  39. package/dist/lib/retriever.d.ts.map +1 -1
  40. package/dist/lib/retriever.js +1 -3
  41. package/dist/lib/retriever.js.map +1 -1
  42. package/dist/lib/segments.d.ts.map +1 -1
  43. package/dist/lib/segments.js.map +1 -1
  44. package/dist/lib/summaries.d.ts.map +1 -1
  45. package/dist/lib/summaries.js +7 -10
  46. package/dist/lib/summaries.js.map +1 -1
  47. package/dist/lib/templates.d.ts +25 -0
  48. package/dist/lib/templates.d.ts.map +1 -0
  49. package/dist/lib/templates.js +102 -0
  50. package/dist/lib/templates.js.map +1 -0
  51. package/package.json +1 -1
@@ -502,6 +502,84 @@
502
502
  ' border-top: 1px solid #333;',
503
503
  ' flex-shrink: 0;',
504
504
  '}',
505
+
506
+ /* ── Project items (in sidebar) ── */
507
+ '.cumulus-project-item {',
508
+ ' display: flex; align-items: center; gap: 8px;',
509
+ ' padding: 7px 12px;',
510
+ ' cursor: pointer;',
511
+ ' font-size: 13px; color: #ccc;',
512
+ ' border-left: 2px solid transparent;',
513
+ ' user-select: none;',
514
+ ' transition: background 0.1s ease;',
515
+ '}',
516
+ '.cumulus-project-item:hover { background: #2a2a2a; }',
517
+ '.cumulus-project-item.selected {',
518
+ ' border-left-color: #7c3aed;',
519
+ ' background: #1e1a2e;',
520
+ ' color: #e0e0e0;',
521
+ '}',
522
+ '.cumulus-project-icon {',
523
+ ' width: 7px; height: 7px; border-radius: 2px;',
524
+ ' background: #7c3aed; flex-shrink: 0;',
525
+ '}',
526
+ '.cumulus-project-name {',
527
+ ' flex: 1; overflow: hidden;',
528
+ ' text-overflow: ellipsis; white-space: nowrap;',
529
+ '}',
530
+ '.cumulus-project-badge {',
531
+ ' font-size: 10px; color: #888;',
532
+ ' background: #2a2a2a; border: 1px solid #3a3a3a;',
533
+ ' border-radius: 8px; padding: 0 5px;',
534
+ ' flex-shrink: 0; min-width: 1.6em; text-align: center;',
535
+ '}',
536
+ '.cumulus-new-project-btn {',
537
+ ' width: 100%;',
538
+ ' background: none; border: none;',
539
+ ' color: #666; padding: 5px 12px;',
540
+ ' font-size: 12px; cursor: pointer;',
541
+ ' font-family: inherit; text-align: left;',
542
+ ' border-top: 1px solid #2a2a2a;',
543
+ '}',
544
+ '.cumulus-new-project-btn:hover { color: #a78bfa; }',
545
+ '.cumulus-new-project-form {',
546
+ ' padding: 6px 10px;',
547
+ ' display: flex; flex-direction: column; gap: 5px;',
548
+ ' border-top: 1px solid #2a2a2a;',
549
+ '}',
550
+ '.cumulus-new-project-input {',
551
+ ' width: 100%;',
552
+ ' background: #3d3d3d; border: 1px solid #7c3aed;',
553
+ ' border-radius: 0.45em; color: #e0e0e0;',
554
+ ' padding: 6px 8px; font-size: 12px;',
555
+ ' font-family: inherit; outline: none;',
556
+ '}',
557
+ '.cumulus-new-project-select {',
558
+ ' width: 100%;',
559
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
560
+ ' border-radius: 0.45em; color: #ccc;',
561
+ ' padding: 5px 7px; font-size: 12px;',
562
+ ' font-family: inherit; outline: none; cursor: pointer;',
563
+ '}',
564
+ '.cumulus-new-project-select:focus { border-color: #7c3aed; }',
565
+ '.cumulus-new-project-actions {',
566
+ ' display: flex; gap: 5px;',
567
+ '}',
568
+ '.cumulus-new-project-create-btn {',
569
+ ' flex: 1; background: #7c3aed; border: none;',
570
+ ' border-radius: 0.45em; color: white;',
571
+ ' padding: 5px 8px; font-size: 12px;',
572
+ ' cursor: pointer; font-family: inherit; font-weight: 600;',
573
+ '}',
574
+ '.cumulus-new-project-create-btn:hover { background: #6d28d9; }',
575
+ '.cumulus-new-project-cancel-btn {',
576
+ ' background: #2a2a2a; border: 1px solid #3a3a3a;',
577
+ ' border-radius: 0.45em; color: #888;',
578
+ ' padding: 5px 8px; font-size: 12px;',
579
+ ' cursor: pointer; font-family: inherit;',
580
+ '}',
581
+ '.cumulus-new-project-cancel-btn:hover { background: #333; color: #ccc; }',
582
+
505
583
  '.cumulus-new-thread-btn {',
506
584
  ' width: 100%;',
507
585
  ' background: #2a2a2a; border: 1px solid #3a3a3a;',
@@ -804,7 +882,6 @@
804
882
  ' .cumulus-standalone-empty-hint { display: none; }',
805
883
 
806
884
  '}',
807
-
808
885
  ].join('\n');
809
886
 
810
887
  // ─── HTML Escaping ───────────────────────────────────────────────────────────
@@ -898,11 +975,19 @@
898
975
  function buildCodeBlock(lang, escapedCode, tokenId) {
899
976
  return (
900
977
  '<div class="code-block-wrapper">' +
901
- '<div class="code-block-header">' +
902
- '<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
903
- '<button class="code-block-copy-btn" data-copy-target="' + tokenId + '" data-testid="webchat-copy-code">Copy</button>' +
904
- '</div>' +
905
- '<pre><code data-code-id="' + tokenId + '">' + escapedCode + '</code></pre>' +
978
+ '<div class="code-block-header">' +
979
+ '<span class="code-block-language">' +
980
+ escapeHtml(lang) +
981
+ '</span>' +
982
+ '<button class="code-block-copy-btn" data-copy-target="' +
983
+ tokenId +
984
+ '" data-testid="webchat-copy-code">Copy</button>' +
985
+ '</div>' +
986
+ '<pre><code data-code-id="' +
987
+ tokenId +
988
+ '">' +
989
+ escapedCode +
990
+ '</code></pre>' +
906
991
  '</div>'
907
992
  );
908
993
  }
@@ -942,16 +1027,32 @@
942
1027
  var headerCells = parseTableRow(lines[0]);
943
1028
  var dataRows = lines.slice(2);
944
1029
 
945
- var thead = '<thead><tr>' + headerCells.map(function (c) {
946
- return '<th>' + c + '</th>';
947
- }).join('') + '</tr></thead>';
948
-
949
- var tbody = '<tbody>' + dataRows.map(function (row) {
950
- var cells = parseTableRow(row);
951
- return '<tr>' + cells.map(function (c) {
952
- return '<td>' + c + '</td>';
953
- }).join('') + '</tr>';
954
- }).join('') + '</tbody>';
1030
+ var thead =
1031
+ '<thead><tr>' +
1032
+ headerCells
1033
+ .map(function (c) {
1034
+ return '<th>' + c + '</th>';
1035
+ })
1036
+ .join('') +
1037
+ '</tr></thead>';
1038
+
1039
+ var tbody =
1040
+ '<tbody>' +
1041
+ dataRows
1042
+ .map(function (row) {
1043
+ var cells = parseTableRow(row);
1044
+ return (
1045
+ '<tr>' +
1046
+ cells
1047
+ .map(function (c) {
1048
+ return '<td>' + c + '</td>';
1049
+ })
1050
+ .join('') +
1051
+ '</tr>'
1052
+ );
1053
+ })
1054
+ .join('') +
1055
+ '</tbody>';
955
1056
 
956
1057
  return '<table>' + thead + tbody + '</table>';
957
1058
  }
@@ -962,7 +1063,9 @@
962
1063
  // Remove first and last if empty (from leading/trailing |)
963
1064
  if (parts.length > 0 && parts[0].trim() === '') parts = parts.slice(1);
964
1065
  if (parts.length > 0 && parts[parts.length - 1].trim() === '') parts = parts.slice(0, -1);
965
- return parts.map(function (c) { return c.trim(); });
1066
+ return parts.map(function (c) {
1067
+ return c.trim();
1068
+ });
966
1069
  }
967
1070
 
968
1071
  function renderBlockquotes(html) {
@@ -1001,7 +1104,15 @@
1001
1104
  listLines.push(lines[i].replace(/^[ \t]*[-*+][ \t]+/, ''));
1002
1105
  i++;
1003
1106
  }
1004
- result.push('<ul>' + listLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ul>');
1107
+ result.push(
1108
+ '<ul>' +
1109
+ listLines
1110
+ .map(function (l) {
1111
+ return '<li>' + l + '</li>';
1112
+ })
1113
+ .join('') +
1114
+ '</ul>'
1115
+ );
1005
1116
  }
1006
1117
  // Ordered list item: "1. " "2. " etc.
1007
1118
  else if (/^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
@@ -1010,9 +1121,16 @@
1010
1121
  olLines.push(lines[i].replace(/^[ \t]*\d+\.[ \t]+/, ''));
1011
1122
  i++;
1012
1123
  }
1013
- result.push('<ol>' + olLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ol>');
1014
- }
1015
- else {
1124
+ result.push(
1125
+ '<ol>' +
1126
+ olLines
1127
+ .map(function (l) {
1128
+ return '<li>' + l + '</li>';
1129
+ })
1130
+ .join('') +
1131
+ '</ol>'
1132
+ );
1133
+ } else {
1016
1134
  result.push(lines[i]);
1017
1135
  i++;
1018
1136
  }
@@ -1038,7 +1156,11 @@
1038
1156
  return '<p>' + inner + '</p>';
1039
1157
  });
1040
1158
 
1041
- return parts.filter(function (p) { return p !== ''; }).join('\n');
1159
+ return parts
1160
+ .filter(function (p) {
1161
+ return p !== '';
1162
+ })
1163
+ .join('\n');
1042
1164
  }
1043
1165
 
1044
1166
  // ─── Attachment helpers ──────────────────────────────────────────────────────
@@ -1054,7 +1176,11 @@
1054
1176
  // result is "data:mime/type;base64,XXXX" — strip the prefix
1055
1177
  var result = e.target.result;
1056
1178
  var comma = result.indexOf(',');
1057
- resolve({ base64: result.slice(comma + 1), mimeType: file.type || 'application/octet-stream', name: file.name });
1179
+ resolve({
1180
+ base64: result.slice(comma + 1),
1181
+ mimeType: file.type || 'application/octet-stream',
1182
+ name: file.name,
1183
+ });
1058
1184
  };
1059
1185
  reader.onerror = reject;
1060
1186
  reader.readAsDataURL(file);
@@ -1064,7 +1190,9 @@
1064
1190
  function readFileAsDataUrl(file) {
1065
1191
  return new Promise(function (resolve, reject) {
1066
1192
  var reader = new FileReader();
1067
- reader.onload = function (e) { resolve(e.target.result); };
1193
+ reader.onload = function (e) {
1194
+ resolve(e.target.result);
1195
+ };
1068
1196
  reader.onerror = reject;
1069
1197
  reader.readAsDataURL(file);
1070
1198
  });
@@ -1090,7 +1218,10 @@
1090
1218
  ws = new WebSocket(wsUrl);
1091
1219
 
1092
1220
  ws.onopen = function () {
1093
- if (destroyed) { ws.close(); return; }
1221
+ if (destroyed) {
1222
+ ws.close();
1223
+ return;
1224
+ }
1094
1225
  onStatus('connected');
1095
1226
  reconnectDelay = 1000;
1096
1227
  ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
@@ -1193,8 +1324,8 @@
1193
1324
  header.innerHTML =
1194
1325
  '<span class="cumulus-header-title">Cumulus</span>' +
1195
1326
  '<span class="cumulus-header-status">' +
1196
- '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1197
- '<span data-testid="webchat-status-text">Disconnected</span>' +
1327
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1328
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
1198
1329
  '</span>' +
1199
1330
  '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>';
1200
1331
  panel.appendChild(header);
@@ -1347,7 +1478,9 @@
1347
1478
  if (codeEl && navigator.clipboard) {
1348
1479
  navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
1349
1480
  btn.textContent = 'Copied!';
1350
- setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
1481
+ setTimeout(function () {
1482
+ btn.textContent = 'Copy';
1483
+ }, 2000);
1351
1484
  });
1352
1485
  }
1353
1486
  });
@@ -1363,7 +1496,8 @@
1363
1496
  function renderMessages() {
1364
1497
  messagesEl.innerHTML = '';
1365
1498
  if (messages.length === 0 && !streaming) {
1366
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
1499
+ messagesEl.innerHTML =
1500
+ '<div class="cumulus-empty">Send a message to start chatting</div>';
1367
1501
  return;
1368
1502
  }
1369
1503
  for (var i = 0; i < messages.length; i++) {
@@ -1519,9 +1653,15 @@
1519
1653
  stopRequested = false;
1520
1654
  streaming = false;
1521
1655
  if (streamBuffer) {
1522
- messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
1656
+ messages.push({
1657
+ role: 'assistant',
1658
+ content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']',
1659
+ });
1523
1660
  } else {
1524
- messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
1661
+ messages.push({
1662
+ role: 'assistant',
1663
+ content: '[Error: ' + (data.error || 'Unknown error') + ']',
1664
+ });
1525
1665
  }
1526
1666
  streamBuffer = '';
1527
1667
  updateSendBtn();
@@ -1558,8 +1698,12 @@
1558
1698
  updateSendBtn();
1559
1699
  renderMessages();
1560
1700
  var imagePayload = attachSnapshot
1561
- .filter(function (a) { return a.isImage; })
1562
- .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
1701
+ .filter(function (a) {
1702
+ return a.isImage;
1703
+ })
1704
+ .map(function (a) {
1705
+ return { mimeType: a.mimeType, base64: a.base64 };
1706
+ });
1563
1707
  var payload = {
1564
1708
  type: 'message',
1565
1709
  threadName: sessionId,
@@ -1628,7 +1772,10 @@
1628
1772
 
1629
1773
  // Embedded mode: no auth panel handling needed (API key from attribute)
1630
1774
  function startConnection() {
1631
- if (connection) { connection.close(); connection = null; }
1775
+ if (connection) {
1776
+ connection.close();
1777
+ connection = null;
1778
+ }
1632
1779
  connection = createConnection({
1633
1780
  wsUrl: wsUrl,
1634
1781
  apiKey: activeApiKey,
@@ -1666,6 +1813,9 @@
1666
1813
  // Thread list from server: [{ name, messageCount, lastActivity }]
1667
1814
  var allThreads = [];
1668
1815
 
1816
+ // Project list from server: [{ name, path, template, threadCount, primaryThread, lastActivity, createdAt }]
1817
+ var allProjects = [];
1818
+
1669
1819
  // Currently visible panel names (ordered, max 3)
1670
1820
  var visibleThreads = [];
1671
1821
 
@@ -1704,8 +1854,8 @@
1704
1854
  authHeader.innerHTML =
1705
1855
  '<span class="cumulus-header-title">Cumulus</span>' +
1706
1856
  '<span class="cumulus-header-status">' +
1707
- '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1708
- '<span data-testid="webchat-status-text">Disconnected</span>' +
1857
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1858
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
1709
1859
  '</span>';
1710
1860
  authWrapper.appendChild(authHeader);
1711
1861
 
@@ -1722,7 +1872,8 @@
1722
1872
 
1723
1873
  // App layout — shown after auth
1724
1874
  var appLayout = document.createElement('div');
1725
- appLayout.style.cssText = 'display:none; flex-direction:column; width:100%; max-width:100vw; height:100vh; height:100dvh; overflow:hidden;';
1875
+ appLayout.style.cssText =
1876
+ 'display:none; flex-direction:column; width:100%; max-width:100vw; height:100vh; height:100dvh; overflow:hidden;';
1726
1877
 
1727
1878
  // Top bar
1728
1879
  var topbar = document.createElement('div');
@@ -1731,11 +1882,11 @@
1731
1882
  '<button class="cumulus-sidebar-toggle" data-testid="webchat-sidebar-toggle" aria-label="Toggle sidebar">&#9776;</button>' +
1732
1883
  '<span class="cumulus-topbar-title">Cumulus</span>' +
1733
1884
  '<div class="cumulus-topbar-right">' +
1734
- '<span class="cumulus-header-status">' +
1735
- '<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
1736
- '<span data-testid="webchat-status-text-app">Disconnected</span>' +
1737
- '</span>' +
1738
- '<button class="cumulus-auth-logout" data-testid="webchat-logout">Logout</button>' +
1885
+ '<span class="cumulus-header-status">' +
1886
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
1887
+ '<span data-testid="webchat-status-text-app">Disconnected</span>' +
1888
+ '</span>' +
1889
+ '<button class="cumulus-auth-logout" data-testid="webchat-logout">Logout</button>' +
1739
1890
  '</div>';
1740
1891
  appLayout.appendChild(topbar);
1741
1892
 
@@ -1778,6 +1929,52 @@
1778
1929
  sidebarFooter.appendChild(newThreadInput);
1779
1930
  sidebar.appendChild(sidebarFooter);
1780
1931
 
1932
+ // ── New project form nodes (persistent; re-appended into projects section on render) ──
1933
+ var newProjectBtn = document.createElement('button');
1934
+ newProjectBtn.className = 'cumulus-new-project-btn';
1935
+ newProjectBtn.setAttribute('data-testid', 'webchat-project-new');
1936
+ newProjectBtn.textContent = '+ New project';
1937
+
1938
+ var newProjectForm = document.createElement('div');
1939
+ newProjectForm.className = 'cumulus-new-project-form';
1940
+ newProjectForm.style.display = 'none';
1941
+ newProjectForm.setAttribute('data-testid', 'webchat-project-new-form');
1942
+
1943
+ var newProjectNameInput = document.createElement('input');
1944
+ newProjectNameInput.className = 'cumulus-new-project-input';
1945
+ newProjectNameInput.setAttribute('data-testid', 'webchat-project-new-name');
1946
+ newProjectNameInput.placeholder = 'Project name\u2026';
1947
+ newProjectNameInput.type = 'text';
1948
+
1949
+ var newProjectTemplateSelect = document.createElement('select');
1950
+ newProjectTemplateSelect.className = 'cumulus-new-project-select';
1951
+ newProjectTemplateSelect.setAttribute('data-testid', 'webchat-project-new-template');
1952
+ [['default', 'Default'], ['web-app', 'Web App']].forEach(function (opt) {
1953
+ var el = document.createElement('option');
1954
+ el.value = opt[0];
1955
+ el.textContent = opt[1];
1956
+ newProjectTemplateSelect.appendChild(el);
1957
+ });
1958
+
1959
+ var newProjectActions = document.createElement('div');
1960
+ newProjectActions.className = 'cumulus-new-project-actions';
1961
+
1962
+ var newProjectCreateBtn = document.createElement('button');
1963
+ newProjectCreateBtn.className = 'cumulus-new-project-create-btn';
1964
+ newProjectCreateBtn.setAttribute('data-testid', 'webchat-project-create');
1965
+ newProjectCreateBtn.textContent = 'Create';
1966
+
1967
+ var newProjectCancelBtn = document.createElement('button');
1968
+ newProjectCancelBtn.className = 'cumulus-new-project-cancel-btn';
1969
+ newProjectCancelBtn.setAttribute('data-testid', 'webchat-project-cancel');
1970
+ newProjectCancelBtn.textContent = 'Cancel';
1971
+
1972
+ newProjectActions.appendChild(newProjectCreateBtn);
1973
+ newProjectActions.appendChild(newProjectCancelBtn);
1974
+ newProjectForm.appendChild(newProjectNameInput);
1975
+ newProjectForm.appendChild(newProjectTemplateSelect);
1976
+ newProjectForm.appendChild(newProjectActions);
1977
+
1781
1978
  // ── Content area ──
1782
1979
  var contentArea = document.createElement('div');
1783
1980
  contentArea.className = 'cumulus-content-area';
@@ -1793,7 +1990,9 @@
1793
1990
  appLayout.appendChild(appRow);
1794
1991
 
1795
1992
  // ── Mobile sidebar toggle ──
1796
- var isMobile = function () { return window.innerWidth <= 768; };
1993
+ var isMobile = function () {
1994
+ return window.innerWidth <= 768;
1995
+ };
1797
1996
 
1798
1997
  function openSidebar() {
1799
1998
  sidebar.classList.add('open');
@@ -1841,7 +2040,11 @@
1841
2040
  visibleThreads = [];
1842
2041
  threadStates = {};
1843
2042
  allThreads = [];
1844
- if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
2043
+ allProjects = [];
2044
+ if (refreshTimer) {
2045
+ clearInterval(refreshTimer);
2046
+ refreshTimer = null;
2047
+ }
1845
2048
  var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
1846
2049
  if (errEl) errEl.textContent = '';
1847
2050
  }
@@ -1861,6 +2064,14 @@
1861
2064
  function requestThreadList() {
1862
2065
  if (connection) {
1863
2066
  connection.send({ type: 'threads' });
2067
+ connection.send({ type: 'projects' });
2068
+ }
2069
+ }
2070
+
2071
+ // ── Project list ──
2072
+ function requestProjectList() {
2073
+ if (connection) {
2074
+ connection.send({ type: 'projects' });
1864
2075
  }
1865
2076
  }
1866
2077
 
@@ -1873,6 +2084,39 @@
1873
2084
  visibleSet[visibleThreads[i]] = true;
1874
2085
  }
1875
2086
 
2087
+ // Projects section: shown above active threads
2088
+ var projectsLabel = document.createElement('div');
2089
+ projectsLabel.className = 'cumulus-sidebar-section-label';
2090
+ projectsLabel.textContent = 'Projects';
2091
+ sidebarScroll.appendChild(projectsLabel);
2092
+
2093
+ var projectsSection = document.createElement('div');
2094
+ projectsSection.setAttribute('data-testid', 'webchat-sidebar-projects');
2095
+
2096
+ if (allProjects.length === 0) {
2097
+ var projectsEmptyNote = document.createElement('div');
2098
+ projectsEmptyNote.style.cssText =
2099
+ 'padding: 4px 12px 2px; font-size: 12px; color: #555; font-style: italic;';
2100
+ projectsEmptyNote.textContent = 'No projects yet';
2101
+ projectsSection.appendChild(projectsEmptyNote);
2102
+ } else {
2103
+ allProjects.forEach(function (p) {
2104
+ projectsSection.appendChild(buildProjectItem(p, visibleSet));
2105
+ });
2106
+ }
2107
+
2108
+ // + New project button / form (persistent nodes re-appended each render)
2109
+ newProjectBtn.style.display = newProjectCreating ? 'none' : 'block';
2110
+ newProjectForm.style.display = newProjectCreating ? 'flex' : 'none';
2111
+ projectsSection.appendChild(newProjectBtn);
2112
+ projectsSection.appendChild(newProjectForm);
2113
+
2114
+ sidebarScroll.appendChild(projectsSection);
2115
+
2116
+ var projectsDivider = document.createElement('hr');
2117
+ projectsDivider.className = 'cumulus-sidebar-divider';
2118
+ sidebarScroll.appendChild(projectsDivider);
2119
+
1876
2120
  // Active section: threads currently visible as panels
1877
2121
  var activeThreads = visibleThreads.slice(); // preserve order
1878
2122
  if (activeThreads.length > 0) {
@@ -1904,7 +2148,8 @@
1904
2148
 
1905
2149
  if (allThreads.length === 0) {
1906
2150
  var emptyNote = document.createElement('div');
1907
- emptyNote.style.cssText = 'padding: 8px 12px; font-size: 12px; color: #555; font-style: italic;';
2151
+ emptyNote.style.cssText =
2152
+ 'padding: 8px 12px; font-size: 12px; color: #555; font-style: italic;';
1908
2153
  emptyNote.textContent = 'No threads yet';
1909
2154
  allSection.appendChild(emptyNote);
1910
2155
  } else {
@@ -1913,7 +2158,9 @@
1913
2158
  return (b.lastActivity || 0) - (a.lastActivity || 0);
1914
2159
  });
1915
2160
  sorted.forEach(function (t) {
1916
- allSection.appendChild(buildThreadItem(t.name, visibleSet[t.name] || false, false, t.messageCount));
2161
+ allSection.appendChild(
2162
+ buildThreadItem(t.name, visibleSet[t.name] || false, false, t.messageCount)
2163
+ );
1917
2164
  });
1918
2165
  }
1919
2166
  sidebarScroll.appendChild(allSection);
@@ -1954,6 +2201,41 @@
1954
2201
  return item;
1955
2202
  }
1956
2203
 
2204
+ function buildProjectItem(project, visibleSet) {
2205
+ var primaryThread = project.primaryThread;
2206
+ var isSelected = primaryThread ? (visibleSet[primaryThread] || false) : false;
2207
+
2208
+ var item = document.createElement('div');
2209
+ item.className = 'cumulus-project-item' + (isSelected ? ' selected' : '');
2210
+ item.setAttribute('data-testid', 'webchat-project-item');
2211
+ item.setAttribute('data-project-name', project.name);
2212
+
2213
+ var icon = document.createElement('span');
2214
+ icon.className = 'cumulus-project-icon';
2215
+ item.appendChild(icon);
2216
+
2217
+ var nameEl = document.createElement('span');
2218
+ nameEl.className = 'cumulus-project-name';
2219
+ nameEl.textContent = project.name;
2220
+ nameEl.setAttribute('title', project.name);
2221
+ item.appendChild(nameEl);
2222
+
2223
+ if (project.threadCount !== undefined && project.threadCount !== null) {
2224
+ var badge = document.createElement('span');
2225
+ badge.className = 'cumulus-project-badge';
2226
+ badge.textContent = project.threadCount;
2227
+ item.appendChild(badge);
2228
+ }
2229
+
2230
+ item.addEventListener('click', function () {
2231
+ if (primaryThread) {
2232
+ soloThread(primaryThread);
2233
+ }
2234
+ });
2235
+
2236
+ return item;
2237
+ }
2238
+
1957
2239
  // ── Panel management ──
1958
2240
  function soloThread(name) {
1959
2241
  visibleThreads = [name];
@@ -1996,7 +2278,10 @@
1996
2278
  visibleThreads.splice(idx, 1);
1997
2279
  // Clear stale DOM render hooks so background messages don't update detached nodes
1998
2280
  var state = threadStates[name];
1999
- if (state) { state._renderMessages = null; state._updateSendBtn = null; }
2281
+ if (state) {
2282
+ state._renderMessages = null;
2283
+ state._updateSendBtn = null;
2284
+ }
2000
2285
  renderSidebar();
2001
2286
  renderContentArea();
2002
2287
  }
@@ -2015,7 +2300,10 @@
2015
2300
  // Clear stale render hooks before wiping the DOM
2016
2301
  for (var _ci = 0; _ci < visibleThreads.length; _ci++) {
2017
2302
  var _cs = threadStates[visibleThreads[_ci]];
2018
- if (_cs) { _cs._renderMessages = null; _cs._updateSendBtn = null; }
2303
+ if (_cs) {
2304
+ _cs._renderMessages = null;
2305
+ _cs._updateSendBtn = null;
2306
+ }
2019
2307
  }
2020
2308
  contentArea.innerHTML = '';
2021
2309
 
@@ -2209,7 +2497,9 @@
2209
2497
  if (codeEl && navigator.clipboard) {
2210
2498
  navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
2211
2499
  btn.textContent = 'Copied!';
2212
- setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
2500
+ setTimeout(function () {
2501
+ btn.textContent = 'Copy';
2502
+ }, 2000);
2213
2503
  });
2214
2504
  }
2215
2505
  });
@@ -2225,7 +2515,8 @@
2225
2515
  function renderPanelMessages() {
2226
2516
  messagesEl.innerHTML = '';
2227
2517
  if (state.messages.length === 0 && !state.streaming) {
2228
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
2518
+ messagesEl.innerHTML =
2519
+ '<div class="cumulus-empty">Send a message to start chatting</div>';
2229
2520
  return;
2230
2521
  }
2231
2522
  for (var i = 0; i < state.messages.length; i++) {
@@ -2358,8 +2649,12 @@
2358
2649
  renderPanelMessages();
2359
2650
 
2360
2651
  var imagePayload = attachSnapshot
2361
- .filter(function (a) { return a.isImage; })
2362
- .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
2652
+ .filter(function (a) {
2653
+ return a.isImage;
2654
+ })
2655
+ .map(function (a) {
2656
+ return { mimeType: a.mimeType, base64: a.base64 };
2657
+ });
2363
2658
 
2364
2659
  var payload = {
2365
2660
  type: 'message',
@@ -2439,7 +2734,10 @@
2439
2734
  function openIncludeOverlay(panel, threadName) {
2440
2735
  // Remove existing overlay if any
2441
2736
  var existing = panel.querySelector('.cumulus-overlay');
2442
- if (existing) { existing.remove(); return; }
2737
+ if (existing) {
2738
+ existing.remove();
2739
+ return;
2740
+ }
2443
2741
 
2444
2742
  var overlay = document.createElement('div');
2445
2743
  overlay.className = 'cumulus-overlay';
@@ -2453,7 +2751,9 @@
2453
2751
  var closeBtn = document.createElement('button');
2454
2752
  closeBtn.className = 'cumulus-overlay-close';
2455
2753
  closeBtn.textContent = '\xD7';
2456
- closeBtn.addEventListener('click', function () { overlay.remove(); });
2754
+ closeBtn.addEventListener('click', function () {
2755
+ overlay.remove();
2756
+ });
2457
2757
  header.appendChild(closeBtn);
2458
2758
  overlay.appendChild(header);
2459
2759
 
@@ -2490,15 +2790,22 @@
2490
2790
  addBtn.addEventListener('click', function () {
2491
2791
  var filePath = addInput.value.trim();
2492
2792
  if (!filePath) return;
2493
- connection && connection.send(JSON.stringify({
2494
- type: 'include_add', threadName: threadName,
2495
- filePath: filePath, scope: currentScope,
2496
- }));
2793
+ connection &&
2794
+ connection.send(
2795
+ JSON.stringify({
2796
+ type: 'include_add',
2797
+ threadName: threadName,
2798
+ filePath: filePath,
2799
+ scope: currentScope,
2800
+ })
2801
+ );
2497
2802
  addInput.value = '';
2498
2803
  });
2499
2804
 
2500
2805
  addInput.addEventListener('keydown', function (e) {
2501
- if (e.key === 'Enter') { addBtn.click(); }
2806
+ if (e.key === 'Enter') {
2807
+ addBtn.click();
2808
+ }
2502
2809
  });
2503
2810
 
2504
2811
  addRow.appendChild(addInput);
@@ -2514,7 +2821,8 @@
2514
2821
  panel.appendChild(overlay);
2515
2822
 
2516
2823
  // Request the list
2517
- connection && connection.send(JSON.stringify({ type: 'include_list', threadName: threadName }));
2824
+ connection &&
2825
+ connection.send(JSON.stringify({ type: 'include_list', threadName: threadName }));
2518
2826
  }
2519
2827
 
2520
2828
  function renderIncludeList(panel, threadName, files) {
@@ -2525,7 +2833,8 @@
2525
2833
 
2526
2834
  body.innerHTML = '';
2527
2835
  if (files.length === 0) {
2528
- body.innerHTML = '<div class="cumulus-overlay-empty">No always-include files configured.</div>';
2836
+ body.innerHTML =
2837
+ '<div class="cumulus-overlay-empty">No always-include files configured.</div>';
2529
2838
  return;
2530
2839
  }
2531
2840
 
@@ -2549,9 +2858,14 @@
2549
2858
  removeBtn.textContent = '\xD7';
2550
2859
  removeBtn.setAttribute('title', 'Remove');
2551
2860
  removeBtn.addEventListener('click', function () {
2552
- connection && connection.send(JSON.stringify({
2553
- type: 'include_remove', threadName: threadName, filePath: f.path,
2554
- }));
2861
+ connection &&
2862
+ connection.send(
2863
+ JSON.stringify({
2864
+ type: 'include_remove',
2865
+ threadName: threadName,
2866
+ filePath: f.path,
2867
+ })
2868
+ );
2555
2869
  });
2556
2870
  item.appendChild(removeBtn);
2557
2871
 
@@ -2562,7 +2876,10 @@
2562
2876
  // ── Revert overlay ──
2563
2877
  function openRevertOverlay(panel, threadName) {
2564
2878
  var existing = panel.querySelector('.cumulus-overlay');
2565
- if (existing) { existing.remove(); return; }
2879
+ if (existing) {
2880
+ existing.remove();
2881
+ return;
2882
+ }
2566
2883
 
2567
2884
  var overlay = document.createElement('div');
2568
2885
  overlay.className = 'cumulus-overlay';
@@ -2576,7 +2893,9 @@
2576
2893
  var closeBtn = document.createElement('button');
2577
2894
  closeBtn.className = 'cumulus-overlay-close';
2578
2895
  closeBtn.textContent = '\xD7';
2579
- closeBtn.addEventListener('click', function () { overlay.remove(); });
2896
+ closeBtn.addEventListener('click', function () {
2897
+ overlay.remove();
2898
+ });
2580
2899
  header.appendChild(closeBtn);
2581
2900
  overlay.appendChild(header);
2582
2901
 
@@ -2591,7 +2910,8 @@
2591
2910
  panel.appendChild(overlay);
2592
2911
 
2593
2912
  // Request the turn list
2594
- connection && connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
2913
+ connection &&
2914
+ connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
2595
2915
  }
2596
2916
 
2597
2917
  function renderRevertList(panel, threadName, turns) {
@@ -2670,7 +2990,8 @@
2670
2990
  cancelBtn.textContent = 'Cancel';
2671
2991
  cancelBtn.addEventListener('click', function () {
2672
2992
  // Re-request list
2673
- connection && connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
2993
+ connection &&
2994
+ connection.send(JSON.stringify({ type: 'revert_list', threadName: threadName }));
2674
2995
  });
2675
2996
  actions.appendChild(cancelBtn);
2676
2997
 
@@ -2681,10 +3002,14 @@
2681
3002
  revertBtn.addEventListener('click', function () {
2682
3003
  revertBtn.disabled = true;
2683
3004
  revertBtn.textContent = 'Reverting\u2026';
2684
- connection && connection.send(JSON.stringify({
2685
- type: 'revert_execute', threadName: threadName,
2686
- targetMessageId: turn.userMessageId,
2687
- }));
3005
+ connection &&
3006
+ connection.send(
3007
+ JSON.stringify({
3008
+ type: 'revert_execute',
3009
+ threadName: threadName,
3010
+ targetMessageId: turn.userMessageId,
3011
+ })
3012
+ );
2688
3013
  });
2689
3014
  actions.appendChild(revertBtn);
2690
3015
 
@@ -2712,7 +3037,10 @@
2712
3037
  console.error('[Cumulus] Auth failed:', data.error);
2713
3038
  clearStoredApiKey();
2714
3039
  activeApiKey = '';
2715
- if (connection) { connection.close(); connection = null; }
3040
+ if (connection) {
3041
+ connection.close();
3042
+ connection = null;
3043
+ }
2716
3044
  showAuthView();
2717
3045
  var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
2718
3046
  if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
@@ -2726,13 +3054,34 @@
2726
3054
  }
2727
3055
  break;
2728
3056
 
3057
+ case 'projects':
3058
+ // Server response to { type: 'projects' } request
3059
+ if (data.projects) {
3060
+ allProjects = data.projects;
3061
+ renderSidebar();
3062
+ }
3063
+ break;
3064
+
3065
+ case 'project_created':
3066
+ // Server confirms new project; refresh project list and open primary thread
3067
+ if (data.project) {
3068
+ requestProjectList();
3069
+ if (data.project.primaryThread) {
3070
+ soloThread(data.project.primaryThread);
3071
+ }
3072
+ }
3073
+ break;
3074
+
2729
3075
  case 'thread_created':
2730
3076
  // Server confirms new thread; add to list and select it
2731
3077
  if (data.threadName) {
2732
3078
  // Add to allThreads if not already present
2733
3079
  var exists = false;
2734
3080
  for (var i = 0; i < allThreads.length; i++) {
2735
- if (allThreads[i].name === data.threadName) { exists = true; break; }
3081
+ if (allThreads[i].name === data.threadName) {
3082
+ exists = true;
3083
+ break;
3084
+ }
2736
3085
  }
2737
3086
  if (!exists) {
2738
3087
  allThreads.push({ name: data.threadName, messageCount: 0, lastActivity: Date.now() });
@@ -2787,7 +3136,10 @@
2787
3136
  break;
2788
3137
  }
2789
3138
  state.streaming = false;
2790
- state.messages.push({ role: 'assistant', content: data.response || state.streamBuffer });
3139
+ state.messages.push({
3140
+ role: 'assistant',
3141
+ content: data.response || state.streamBuffer,
3142
+ });
2791
3143
  state.streamBuffer = '';
2792
3144
  updateThreadActivity(data.threadName);
2793
3145
  refreshThreadPanel(data.threadName);
@@ -2800,9 +3152,16 @@
2800
3152
  state.stopRequested = false;
2801
3153
  state.streaming = false;
2802
3154
  if (state.streamBuffer) {
2803
- state.messages.push({ role: 'assistant', content: state.streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
3155
+ state.messages.push({
3156
+ role: 'assistant',
3157
+ content:
3158
+ state.streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']',
3159
+ });
2804
3160
  } else {
2805
- state.messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
3161
+ state.messages.push({
3162
+ role: 'assistant',
3163
+ content: '[Error: ' + (data.error || 'Unknown error') + ']',
3164
+ });
2806
3165
  }
2807
3166
  state.streamBuffer = '';
2808
3167
  refreshThreadPanel(data.threadName);
@@ -2840,15 +3199,22 @@
2840
3199
  tState.messages = [];
2841
3200
  tState.historyLoaded = false;
2842
3201
  // Re-request history
2843
- connection && connection.send(JSON.stringify({
2844
- type: 'history', threadName: data.threadName, limit: 200,
2845
- }));
3202
+ connection &&
3203
+ connection.send(
3204
+ JSON.stringify({
3205
+ type: 'history',
3206
+ threadName: data.threadName,
3207
+ limit: 200,
3208
+ })
3209
+ );
2846
3210
  if (overlay) overlay.remove();
2847
3211
  } else {
2848
3212
  // Show error in overlay
2849
3213
  if (overlay && overlay._revertBody) {
2850
- overlay._revertBody.innerHTML = '<div class="cumulus-overlay-status error">' +
2851
- escapeHtml(data.error || 'Revert failed') + '</div>';
3214
+ overlay._revertBody.innerHTML =
3215
+ '<div class="cumulus-overlay-status error">' +
3216
+ escapeHtml(data.error || 'Revert failed') +
3217
+ '</div>';
2852
3218
  }
2853
3219
  }
2854
3220
  }
@@ -2889,7 +3255,10 @@
2889
3255
 
2890
3256
  function submitNewThread() {
2891
3257
  var name = newThreadInput.value.trim();
2892
- if (!name) { hideNewThreadInput(); return; }
3258
+ if (!name) {
3259
+ hideNewThreadInput();
3260
+ return;
3261
+ }
2893
3262
  if (connection) {
2894
3263
  connection.send({ type: 'create_thread', threadName: name });
2895
3264
  }
@@ -2897,7 +3266,10 @@
2897
3266
  // Optimistically add and show (server will confirm with thread_created)
2898
3267
  var exists = false;
2899
3268
  for (var i = 0; i < allThreads.length; i++) {
2900
- if (allThreads[i].name === name) { exists = true; break; }
3269
+ if (allThreads[i].name === name) {
3270
+ exists = true;
3271
+ break;
3272
+ }
2901
3273
  }
2902
3274
  if (!exists) {
2903
3275
  allThreads.push({ name: name, messageCount: 0, lastActivity: Date.now() });
@@ -2908,8 +3280,13 @@
2908
3280
  newThreadBtn.addEventListener('click', showNewThreadInput);
2909
3281
 
2910
3282
  newThreadInput.addEventListener('keydown', function (e) {
2911
- if (e.key === 'Enter') { e.preventDefault(); submitNewThread(); }
2912
- if (e.key === 'Escape') { hideNewThreadInput(); }
3283
+ if (e.key === 'Enter') {
3284
+ e.preventDefault();
3285
+ submitNewThread();
3286
+ }
3287
+ if (e.key === 'Escape') {
3288
+ hideNewThreadInput();
3289
+ }
2913
3290
  });
2914
3291
 
2915
3292
  newThreadInput.addEventListener('blur', function () {
@@ -2919,6 +3296,51 @@
2919
3296
  }, 150);
2920
3297
  });
2921
3298
 
3299
+ // ── New project creation ──
3300
+ var newProjectCreating = false;
3301
+
3302
+ function showNewProjectForm() {
3303
+ if (newProjectCreating) return;
3304
+ newProjectCreating = true;
3305
+ newProjectNameInput.value = '';
3306
+ newProjectTemplateSelect.value = 'default';
3307
+ renderSidebar();
3308
+ newProjectNameInput.focus();
3309
+ }
3310
+
3311
+ function hideNewProjectForm() {
3312
+ newProjectCreating = false;
3313
+ newProjectNameInput.value = '';
3314
+ renderSidebar();
3315
+ }
3316
+
3317
+ function submitNewProject() {
3318
+ var name = newProjectNameInput.value.trim();
3319
+ if (!name) {
3320
+ hideNewProjectForm();
3321
+ return;
3322
+ }
3323
+ var template = newProjectTemplateSelect.value || 'default';
3324
+ if (connection) {
3325
+ connection.send({ type: 'create_project', name: name, template: template });
3326
+ }
3327
+ hideNewProjectForm();
3328
+ }
3329
+
3330
+ newProjectBtn.addEventListener('click', showNewProjectForm);
3331
+ newProjectCancelBtn.addEventListener('click', hideNewProjectForm);
3332
+ newProjectCreateBtn.addEventListener('click', submitNewProject);
3333
+
3334
+ newProjectNameInput.addEventListener('keydown', function (e) {
3335
+ if (e.key === 'Enter') {
3336
+ e.preventDefault();
3337
+ submitNewProject();
3338
+ }
3339
+ if (e.key === 'Escape') {
3340
+ hideNewProjectForm();
3341
+ }
3342
+ });
3343
+
2922
3344
  // ── Auth events ──
2923
3345
  var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
2924
3346
  var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
@@ -2935,7 +3357,10 @@
2935
3357
 
2936
3358
  authSubmitBtn.addEventListener('click', submitAuth);
2937
3359
  authInput.addEventListener('keydown', function (e) {
2938
- if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
3360
+ if (e.key === 'Enter') {
3361
+ e.preventDefault();
3362
+ submitAuth();
3363
+ }
2939
3364
  });
2940
3365
 
2941
3366
  // ── Logout ──
@@ -2944,7 +3369,10 @@
2944
3369
  logoutBtn.addEventListener('click', function () {
2945
3370
  clearStoredApiKey();
2946
3371
  activeApiKey = '';
2947
- if (connection) { connection.close(); connection = null; }
3372
+ if (connection) {
3373
+ connection.close();
3374
+ connection = null;
3375
+ }
2948
3376
  showAuthView();
2949
3377
  authInput.value = '';
2950
3378
  });
@@ -2952,7 +3380,10 @@
2952
3380
 
2953
3381
  // ── Connection ──
2954
3382
  function startConnection() {
2955
- if (connection) { connection.close(); connection = null; }
3383
+ if (connection) {
3384
+ connection.close();
3385
+ connection = null;
3386
+ }
2956
3387
  connection = createConnection({
2957
3388
  wsUrl: wsUrl,
2958
3389
  apiKey: activeApiKey,