@luckydraw/cumulus 0.15.2 → 0.16.1

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.
@@ -345,10 +345,10 @@
345
345
  ' max-width: 78ch; align-self: center; width: 100%;',
346
346
  '}',
347
347
  '.cumulus-attach-btn {',
348
- ' width: 2.7em; height: 2.7em; flex-shrink: 0;',
348
+ ' width: 2em; height: 2em; flex-shrink: 0;',
349
349
  ' background: #3d3d3d; border: 1px solid #4a4a4a;',
350
- ' border-radius: 0.55em; color: #aaa;',
351
- ' font-size: 1.4em; line-height: 1; cursor: pointer;',
350
+ ' border-radius: 0.45em; color: #aaa;',
351
+ ' font-size: 1em; line-height: 1; cursor: pointer;',
352
352
  ' display: flex; align-items: center; justify-content: center;',
353
353
  ' padding: 0;',
354
354
  '}',
@@ -422,6 +422,158 @@
422
422
  ' cursor: pointer; font-size: 11px; padding: 2px 6px; font-family: inherit;',
423
423
  '}',
424
424
  '.cumulus-auth-logout:hover { color: #ef4444; }',
425
+
426
+ /* ── Standalone layout ── */
427
+ '.cumulus-standalone-root {',
428
+ ' display: flex; flex-direction: row;',
429
+ ' width: 100%; height: 100vh;',
430
+ ' background: #1e1e1e; overflow: hidden;',
431
+ '}',
432
+
433
+ /* ── Sidebar ── */
434
+ '.cumulus-sidebar {',
435
+ ' width: 220px; flex-shrink: 0;',
436
+ ' background: #252525;',
437
+ ' border-right: 1px solid #333;',
438
+ ' display: flex; flex-direction: column;',
439
+ ' overflow: hidden;',
440
+ '}',
441
+ '.cumulus-sidebar-header {',
442
+ ' padding: 10px 12px 6px;',
443
+ ' display: flex; align-items: center; justify-content: space-between;',
444
+ ' border-bottom: 1px solid #333;',
445
+ ' flex-shrink: 0;',
446
+ '}',
447
+ '.cumulus-sidebar-title {',
448
+ ' font-size: 11px; font-weight: 600; color: #888;',
449
+ ' text-transform: uppercase; letter-spacing: 0.05em;',
450
+ '}',
451
+ '.cumulus-sidebar-status {',
452
+ ' display: flex; align-items: center; gap: 5px;',
453
+ '}',
454
+ '.cumulus-sidebar-scroll {',
455
+ ' flex: 1; overflow-y: auto; padding: 6px 0;',
456
+ '}',
457
+ '.cumulus-sidebar-scroll::-webkit-scrollbar { width: 4px; }',
458
+ '.cumulus-sidebar-scroll::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }',
459
+ '.cumulus-sidebar-section-label {',
460
+ ' font-size: 10px; font-weight: 600; color: #666;',
461
+ ' text-transform: uppercase; letter-spacing: 0.06em;',
462
+ ' padding: 8px 12px 4px;',
463
+ '}',
464
+ '.cumulus-sidebar-divider {',
465
+ ' border: none; border-top: 1px solid #333;',
466
+ ' margin: 6px 0;',
467
+ '}',
468
+ '.cumulus-thread-item {',
469
+ ' display: flex; align-items: center; gap: 8px;',
470
+ ' padding: 7px 12px;',
471
+ ' cursor: pointer;',
472
+ ' font-size: 13px; color: #ccc;',
473
+ ' border-left: 2px solid transparent;',
474
+ ' user-select: none;',
475
+ ' transition: background 0.1s ease;',
476
+ '}',
477
+ '.cumulus-thread-item:hover { background: #2a2a2a; }',
478
+ '.cumulus-thread-item.selected {',
479
+ ' border-left-color: #0066cc;',
480
+ ' background: #1e2a3a;',
481
+ ' color: #e0e0e0;',
482
+ '}',
483
+ '.cumulus-thread-dot {',
484
+ ' width: 7px; height: 7px; border-radius: 50%;',
485
+ ' flex-shrink: 0;',
486
+ '}',
487
+ '.cumulus-thread-dot.active { background: #22c55e; }',
488
+ '.cumulus-thread-dot.inactive {',
489
+ ' background: transparent;',
490
+ ' border: 1px solid #555;',
491
+ '}',
492
+ '.cumulus-thread-name {',
493
+ ' flex: 1; overflow: hidden;',
494
+ ' text-overflow: ellipsis; white-space: nowrap;',
495
+ '}',
496
+ '.cumulus-thread-count {',
497
+ ' font-size: 11px; color: #666;',
498
+ ' flex-shrink: 0;',
499
+ '}',
500
+ '.cumulus-sidebar-footer {',
501
+ ' padding: 8px 10px;',
502
+ ' border-top: 1px solid #333;',
503
+ ' flex-shrink: 0;',
504
+ '}',
505
+ '.cumulus-new-thread-btn {',
506
+ ' width: 100%;',
507
+ ' background: #2a2a2a; border: 1px solid #3a3a3a;',
508
+ ' border-radius: 0.45em; color: #aaa;',
509
+ ' padding: 7px 10px; font-size: 13px;',
510
+ ' cursor: pointer; font-family: inherit;',
511
+ ' text-align: left;',
512
+ '}',
513
+ '.cumulus-new-thread-btn:hover { background: #333; color: #ddd; border-color: #0066cc; }',
514
+ '.cumulus-new-thread-input {',
515
+ ' width: 100%;',
516
+ ' background: #3d3d3d; border: 1px solid #0066cc;',
517
+ ' border-radius: 0.45em; color: #e0e0e0;',
518
+ ' padding: 7px 10px; font-size: 13px;',
519
+ ' font-family: inherit; outline: none;',
520
+ '}',
521
+
522
+ /* ── Content area (holds panels) ── */
523
+ '.cumulus-content-area {',
524
+ ' flex: 1; display: flex; flex-direction: row;',
525
+ ' overflow: hidden;',
526
+ '}',
527
+
528
+ /* ── Thread panel (inside content area) ── */
529
+ '.cumulus-thread-panel {',
530
+ ' flex: 1; display: flex; flex-direction: column;',
531
+ ' overflow: hidden; background: #1e1e1e;',
532
+ ' border-left: 1px solid #333;',
533
+ ' min-width: 0;',
534
+ '}',
535
+ '.cumulus-thread-panel:first-child { border-left: none; }',
536
+ '.cumulus-thread-panel-header {',
537
+ ' display: flex; align-items: center; justify-content: space-between;',
538
+ ' padding: 0 0.85em;',
539
+ ' height: 40px;',
540
+ ' background: #2d2d2d;',
541
+ ' border-bottom: 1px solid #3a3a3a;',
542
+ ' flex-shrink: 0;',
543
+ '}',
544
+ '.cumulus-thread-panel-title {',
545
+ ' font-weight: 600; font-size: 14px; color: #e0e0e0;',
546
+ ' overflow: hidden; text-overflow: ellipsis; white-space: nowrap;',
547
+ '}',
548
+ '.cumulus-thread-panel-close {',
549
+ ' background: none; border: none; color: #666;',
550
+ ' cursor: pointer; font-size: 18px; padding: 2px 4px;',
551
+ ' line-height: 1; flex-shrink: 0;',
552
+ '}',
553
+ '.cumulus-thread-panel-close:hover { color: #e0e0e0; }',
554
+
555
+ /* ── Standalone empty state (no threads selected) ── */
556
+ '.cumulus-standalone-empty {',
557
+ ' flex: 1; display: flex; flex-direction: column;',
558
+ ' align-items: center; justify-content: center;',
559
+ ' color: #555; font-size: 15px; text-align: center; padding: 20px;',
560
+ ' gap: 10px;',
561
+ '}',
562
+ '.cumulus-standalone-empty-hint {',
563
+ ' font-size: 12px; color: #444;',
564
+ '}',
565
+
566
+ /* ── Top bar for standalone (connection status + logout) ── */
567
+ '.cumulus-topbar {',
568
+ ' display: flex; align-items: center; justify-content: space-between;',
569
+ ' padding: 0 14px;',
570
+ ' height: 40px;',
571
+ ' background: #2d2d2d;',
572
+ ' border-bottom: 1px solid #3a3a3a;',
573
+ ' flex-shrink: 0;',
574
+ '}',
575
+ '.cumulus-topbar-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
576
+ '.cumulus-topbar-right { display: flex; align-items: center; gap: 10px; }',
425
577
  ].join('\n');
426
578
 
427
579
  // ─── HTML Escaping ───────────────────────────────────────────────────────────
@@ -694,6 +846,7 @@
694
846
  var sessionId = opts.sessionId;
695
847
  var onMessage = opts.onMessage;
696
848
  var onStatus = opts.onStatus;
849
+ var skipHistory = opts.skipHistory || false;
697
850
 
698
851
  var ws = null;
699
852
  var reconnectTimer = null;
@@ -710,7 +863,9 @@
710
863
  onStatus('connected');
711
864
  reconnectDelay = 1000;
712
865
  ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
713
- ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
866
+ if (!skipHistory) {
867
+ ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
868
+ }
714
869
  };
715
870
 
716
871
  ws.onmessage = function (event) {
@@ -777,41 +932,552 @@
777
932
  styleEl.textContent = STYLES;
778
933
  document.head.appendChild(styleEl);
779
934
 
780
- // ── State ──
781
- var messages = []; // { role, content, attachments? }
782
- var streaming = false;
783
- var stopRequested = false;
784
- var streamBuffer = '';
785
- var connection = null;
786
- var connectionStatus = 'disconnected';
787
- var authenticated = false;
788
- var pendingAttachments = []; // { base64, mimeType, name, dataUrl, isImage }
789
-
790
935
  // ── Root container ──
791
936
  var container = document.createElement('div');
792
937
  container.className = 'cumulus-widget';
793
938
 
794
- // ── Panel ──
795
- var panel = document.createElement('div');
796
- panel.className = 'cumulus-panel ' + (standalone ? 'standalone' : 'floating');
797
- panel.style.display = standalone ? 'flex' : 'none';
798
- panel.setAttribute('data-testid', 'webchat-panel');
939
+ // ═══════════════════════════════════════════════════════════════════════════
940
+ // EMBEDDED MODE — unchanged from original behavior
941
+ // ═══════════════════════════════════════════════════════════════════════════
942
+ if (!standalone) {
943
+ // ── State ──
944
+ var messages = [];
945
+ var streaming = false;
946
+ var stopRequested = false;
947
+ var streamBuffer = '';
948
+ var connection = null;
949
+ var connectionStatus = 'disconnected';
950
+ var authenticated = false;
951
+ var pendingAttachments = [];
952
+
953
+ // ── Panel ──
954
+ var panel = document.createElement('div');
955
+ panel.className = 'cumulus-panel floating';
956
+ panel.style.display = 'none';
957
+ panel.setAttribute('data-testid', 'webchat-panel');
958
+
959
+ // ── Header ──
960
+ var header = document.createElement('div');
961
+ header.className = 'cumulus-header';
962
+ header.innerHTML =
963
+ '<span class="cumulus-header-title">Cumulus</span>' +
964
+ '<span class="cumulus-header-status">' +
965
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
966
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
967
+ '</span>' +
968
+ '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>';
969
+ panel.appendChild(header);
970
+
971
+ // ── Auth panel ──
972
+ var authPanel = document.createElement('div');
973
+ authPanel.className = 'cumulus-auth';
974
+ authPanel.setAttribute('data-testid', 'webchat-auth');
975
+ authPanel.innerHTML =
976
+ '<div class="cumulus-auth-title">Cumulus Chat</div>' +
977
+ '<div class="cumulus-auth-subtitle">Enter your API key to connect. The key will be saved in your browser.</div>' +
978
+ '<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
979
+ '<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
980
+ '<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
981
+ panel.appendChild(authPanel);
982
+
983
+ // ── Messages area ──
984
+ var messagesEl = document.createElement('div');
985
+ messagesEl.className = 'cumulus-messages';
986
+ messagesEl.setAttribute('data-testid', 'webchat-messages');
987
+ messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
988
+ messagesEl.style.display = 'none';
989
+ panel.appendChild(messagesEl);
990
+
991
+ // ── Input area ──
992
+ var inputArea = document.createElement('div');
993
+ inputArea.className = 'cumulus-input-area';
994
+ inputArea.style.display = 'none';
995
+
996
+ var attachStrip = document.createElement('div');
997
+ attachStrip.className = 'cumulus-attach-strip';
998
+ attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
999
+ attachStrip.style.display = 'none';
1000
+ inputArea.appendChild(attachStrip);
1001
+
1002
+ var inputRow = document.createElement('div');
1003
+ inputRow.className = 'cumulus-input-row';
1004
+
1005
+ var fileInput = document.createElement('input');
1006
+ fileInput.type = 'file';
1007
+ fileInput.multiple = true;
1008
+ fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
1009
+ fileInput.style.display = 'none';
1010
+ fileInput.setAttribute('data-testid', 'webchat-file-input');
1011
+
1012
+ var attachBtn = document.createElement('button');
1013
+ attachBtn.className = 'cumulus-attach-btn';
1014
+ attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
1015
+ attachBtn.setAttribute('title', 'Attach files');
1016
+ attachBtn.textContent = '+';
1017
+
1018
+ var input = document.createElement('textarea');
1019
+ input.className = 'cumulus-input';
1020
+ input.setAttribute('data-testid', 'webchat-input');
1021
+ input.placeholder = 'Type a message\u2026';
1022
+ input.rows = 1;
1023
+
1024
+ var sendBtn = document.createElement('button');
1025
+ sendBtn.className = 'cumulus-send-btn';
1026
+ sendBtn.setAttribute('data-testid', 'webchat-send');
1027
+ sendBtn.textContent = 'Send';
1028
+
1029
+ inputRow.appendChild(fileInput);
1030
+ inputRow.appendChild(attachBtn);
1031
+ inputRow.appendChild(input);
1032
+ inputRow.appendChild(sendBtn);
1033
+ inputArea.appendChild(inputRow);
1034
+ panel.appendChild(inputArea);
1035
+ container.appendChild(panel);
1036
+
1037
+ // ── Floating bubble ──
1038
+ var bubble = document.createElement('button');
1039
+ bubble.className = 'cumulus-bubble';
1040
+ bubble.setAttribute('data-testid', 'webchat-bubble');
1041
+ bubble.setAttribute('title', 'Open chat');
1042
+ bubble.innerHTML =
1043
+ '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>';
1044
+ container.appendChild(bubble);
1045
+
1046
+ // ── View switching ──
1047
+ function showAuthView() {
1048
+ authenticated = false;
1049
+ authPanel.style.display = 'flex';
1050
+ messagesEl.style.display = 'none';
1051
+ inputArea.style.display = 'none';
1052
+ var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
1053
+ if (errEl) errEl.textContent = '';
1054
+ }
1055
+
1056
+ function showChatView() {
1057
+ authenticated = true;
1058
+ authPanel.style.display = 'none';
1059
+ messagesEl.style.display = 'flex';
1060
+ inputArea.style.display = 'flex';
1061
+ input.focus();
1062
+ }
1063
+
1064
+ // ── Rendering ──
1065
+ function scrollToBottom() {
1066
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1067
+ }
1068
+
1069
+ function isWideUserMessage(content) {
1070
+ if (/```/.test(content)) return true;
1071
+ var lineCount = (content.match(/\n/g) || []).length + 1;
1072
+ return lineCount > 3;
1073
+ }
1074
+
1075
+ function buildUserMsgEl(msg) {
1076
+ var el = document.createElement('div');
1077
+ el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
1078
+ el.textContent = msg.content;
1079
+ if (msg.attachments && msg.attachments.length > 0) {
1080
+ var attRow = document.createElement('div');
1081
+ attRow.className = 'cumulus-msg-attachments';
1082
+ msg.attachments.forEach(function (att) {
1083
+ if (att.isImage) {
1084
+ var img = document.createElement('img');
1085
+ img.className = 'cumulus-msg-img';
1086
+ img.src = att.dataUrl;
1087
+ img.alt = att.name;
1088
+ img.setAttribute('data-testid', 'webchat-msg-img');
1089
+ attRow.appendChild(img);
1090
+ } else {
1091
+ var badge = document.createElement('span');
1092
+ badge.className = 'cumulus-msg-file-badge';
1093
+ badge.setAttribute('data-testid', 'webchat-msg-file');
1094
+ badge.textContent = '\uD83D\uDCCE ' + att.name;
1095
+ attRow.appendChild(badge);
1096
+ }
1097
+ });
1098
+ el.appendChild(attRow);
1099
+ }
1100
+ return el;
1101
+ }
1102
+
1103
+ function buildAssistantMsgEl(content, isStreaming) {
1104
+ var el = document.createElement('div');
1105
+ el.className = 'cumulus-msg assistant';
1106
+ if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
1107
+ if (content) {
1108
+ el.innerHTML = renderMarkdown(content);
1109
+ if (isStreaming) {
1110
+ el.innerHTML += '<span class="cumulus-cursor"></span>';
1111
+ }
1112
+ el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
1113
+ btn.addEventListener('click', function () {
1114
+ var targetId = btn.getAttribute('data-copy-target');
1115
+ var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
1116
+ if (codeEl && navigator.clipboard) {
1117
+ navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
1118
+ btn.textContent = 'Copied!';
1119
+ setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
1120
+ });
1121
+ }
1122
+ });
1123
+ });
1124
+ } else if (isStreaming) {
1125
+ el.innerHTML =
1126
+ '<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
1127
+ '<span class="cumulus-cursor"></span>';
1128
+ }
1129
+ return el;
1130
+ }
1131
+
1132
+ function renderMessages() {
1133
+ messagesEl.innerHTML = '';
1134
+ if (messages.length === 0 && !streaming) {
1135
+ messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
1136
+ return;
1137
+ }
1138
+ for (var i = 0; i < messages.length; i++) {
1139
+ var msg = messages[i];
1140
+ var row = document.createElement('div');
1141
+ row.className = 'cumulus-msg-row';
1142
+ if (msg.role === 'user') {
1143
+ row.appendChild(buildUserMsgEl(msg));
1144
+ } else {
1145
+ row.appendChild(buildAssistantMsgEl(msg.content, false));
1146
+ }
1147
+ messagesEl.appendChild(row);
1148
+ }
1149
+ if (streaming) {
1150
+ var row = document.createElement('div');
1151
+ row.className = 'cumulus-msg-row';
1152
+ row.appendChild(buildAssistantMsgEl(streamBuffer, true));
1153
+ messagesEl.appendChild(row);
1154
+ }
1155
+ scrollToBottom();
1156
+ }
1157
+
1158
+ function updateSendBtn() {
1159
+ if (streaming) {
1160
+ sendBtn.textContent = 'Stop';
1161
+ sendBtn.classList.add('stop');
1162
+ sendBtn.disabled = false;
1163
+ } else {
1164
+ sendBtn.textContent = 'Send';
1165
+ sendBtn.classList.remove('stop');
1166
+ sendBtn.disabled = false;
1167
+ }
1168
+ }
1169
+
1170
+ function updateStatus(status) {
1171
+ connectionStatus = status;
1172
+ var dot = panel.querySelector('.cumulus-status-dot');
1173
+ var text = panel.querySelector('[data-testid="webchat-status-text"]');
1174
+ if (dot) dot.className = 'cumulus-status-dot ' + status;
1175
+ if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1176
+ }
1177
+
1178
+ function renderAttachStrip() {
1179
+ attachStrip.innerHTML = '';
1180
+ if (pendingAttachments.length === 0) {
1181
+ attachStrip.style.display = 'none';
1182
+ return;
1183
+ }
1184
+ attachStrip.style.display = 'flex';
1185
+ pendingAttachments.forEach(function (att, idx) {
1186
+ var chip = document.createElement('div');
1187
+ chip.className = 'cumulus-attach-chip';
1188
+ chip.setAttribute('data-testid', 'webchat-attach-chip');
1189
+ if (att.isImage) {
1190
+ var thumb = document.createElement('img');
1191
+ thumb.className = 'cumulus-attach-chip-thumb';
1192
+ thumb.src = att.dataUrl;
1193
+ thumb.alt = att.name;
1194
+ chip.appendChild(thumb);
1195
+ } else {
1196
+ var icon = document.createElement('div');
1197
+ icon.className = 'cumulus-attach-chip-icon';
1198
+ icon.textContent = fileExtension(att.name);
1199
+ chip.appendChild(icon);
1200
+ }
1201
+ var nameEl = document.createElement('div');
1202
+ nameEl.className = 'cumulus-attach-chip-name';
1203
+ nameEl.textContent = att.name;
1204
+ chip.appendChild(nameEl);
1205
+ var removeBtn = document.createElement('button');
1206
+ removeBtn.className = 'cumulus-attach-chip-remove';
1207
+ removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
1208
+ removeBtn.textContent = '\xD7';
1209
+ removeBtn.setAttribute('title', 'Remove attachment');
1210
+ (function (index) {
1211
+ removeBtn.addEventListener('click', function () {
1212
+ pendingAttachments.splice(index, 1);
1213
+ renderAttachStrip();
1214
+ });
1215
+ })(idx);
1216
+ chip.appendChild(removeBtn);
1217
+ attachStrip.appendChild(chip);
1218
+ });
1219
+ }
1220
+
1221
+ async function addFilesToPending(files) {
1222
+ for (var i = 0; i < files.length; i++) {
1223
+ var file = files[i];
1224
+ try {
1225
+ var info = await readFileAsBase64(file);
1226
+ var dataUrl = await readFileAsDataUrl(file);
1227
+ pendingAttachments.push({
1228
+ base64: info.base64,
1229
+ mimeType: info.mimeType,
1230
+ name: file.name,
1231
+ dataUrl: dataUrl,
1232
+ isImage: file.type.startsWith('image/'),
1233
+ });
1234
+ } catch (e) {
1235
+ console.error('[Cumulus] Failed to read file:', file.name, e);
1236
+ }
1237
+ }
1238
+ renderAttachStrip();
1239
+ }
1240
+
1241
+ function handleServerMessage(data) {
1242
+ switch (data.type) {
1243
+ case 'auth_ok':
1244
+ showChatView();
1245
+ break;
1246
+ case 'auth_error':
1247
+ console.error('[Cumulus] Auth failed:', data.error);
1248
+ break;
1249
+ case 'history':
1250
+ if (data.messages && data.messages.length > 0) {
1251
+ messages = data.messages.map(function (m) {
1252
+ return { role: m.role, content: m.content };
1253
+ });
1254
+ renderMessages();
1255
+ }
1256
+ break;
1257
+ case 'token':
1258
+ if (stopRequested) break;
1259
+ if (!streaming) {
1260
+ streaming = true;
1261
+ streamBuffer = '';
1262
+ updateSendBtn();
1263
+ }
1264
+ streamBuffer += data.text;
1265
+ renderMessages();
1266
+ break;
1267
+ case 'segment':
1268
+ break;
1269
+ case 'done':
1270
+ if (stopRequested) {
1271
+ stopRequested = false;
1272
+ streaming = false;
1273
+ if (streamBuffer) {
1274
+ messages.push({ role: 'assistant', content: streamBuffer });
1275
+ }
1276
+ streamBuffer = '';
1277
+ updateSendBtn();
1278
+ renderMessages();
1279
+ break;
1280
+ }
1281
+ streaming = false;
1282
+ messages.push({ role: 'assistant', content: data.response || streamBuffer });
1283
+ streamBuffer = '';
1284
+ updateSendBtn();
1285
+ renderMessages();
1286
+ break;
1287
+ case 'error':
1288
+ stopRequested = false;
1289
+ streaming = false;
1290
+ if (streamBuffer) {
1291
+ messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
1292
+ } else {
1293
+ messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
1294
+ }
1295
+ streamBuffer = '';
1296
+ updateSendBtn();
1297
+ renderMessages();
1298
+ break;
1299
+ }
1300
+ }
1301
+
1302
+ function sendMessage() {
1303
+ if (streaming) {
1304
+ stopRequested = true;
1305
+ streaming = false;
1306
+ updateSendBtn();
1307
+ if (streamBuffer) {
1308
+ messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
1309
+ streamBuffer = '';
1310
+ }
1311
+ renderMessages();
1312
+ return;
1313
+ }
1314
+ var text = input.value.trim();
1315
+ if (!text && pendingAttachments.length === 0) return;
1316
+ if (!connection) return;
1317
+ var attachSnapshot = pendingAttachments.slice();
1318
+ var displayText = text || '(attachment)';
1319
+ messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
1320
+ input.value = '';
1321
+ input.style.height = 'auto';
1322
+ pendingAttachments = [];
1323
+ renderAttachStrip();
1324
+ streaming = true;
1325
+ stopRequested = false;
1326
+ streamBuffer = '';
1327
+ updateSendBtn();
1328
+ renderMessages();
1329
+ var imagePayload = attachSnapshot
1330
+ .filter(function (a) { return a.isImage; })
1331
+ .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
1332
+ var payload = {
1333
+ type: 'message',
1334
+ threadName: sessionId,
1335
+ message: text || ' ',
1336
+ };
1337
+ if (imagePayload.length > 0) payload.images = imagePayload;
1338
+ connection.send(payload);
1339
+ }
1340
+
1341
+ // ── Event listeners ──
1342
+ sendBtn.addEventListener('click', sendMessage);
1343
+ input.addEventListener('keydown', function (e) {
1344
+ if (e.key === 'Enter' && !e.shiftKey) {
1345
+ e.preventDefault();
1346
+ sendMessage();
1347
+ }
1348
+ });
1349
+ input.addEventListener('input', function () {
1350
+ input.style.height = 'auto';
1351
+ input.style.height = Math.min(input.scrollHeight, 120) + 'px';
1352
+ });
1353
+ input.addEventListener('paste', function (e) {
1354
+ var items = e.clipboardData && e.clipboardData.items;
1355
+ if (!items) return;
1356
+ var filesToAdd = [];
1357
+ for (var i = 0; i < items.length; i++) {
1358
+ var item = items[i];
1359
+ if (item.kind === 'file') {
1360
+ var file = item.getAsFile();
1361
+ if (file) filesToAdd.push(file);
1362
+ }
1363
+ }
1364
+ if (filesToAdd.length > 0) {
1365
+ e.preventDefault();
1366
+ addFilesToPending(filesToAdd);
1367
+ }
1368
+ });
1369
+ fileInput.addEventListener('change', function () {
1370
+ if (fileInput.files && fileInput.files.length > 0) {
1371
+ addFilesToPending(Array.from(fileInput.files));
1372
+ fileInput.value = '';
1373
+ }
1374
+ });
1375
+ attachBtn.addEventListener('click', function () {
1376
+ fileInput.click();
1377
+ });
1378
+
1379
+ bubble.addEventListener('click', function () {
1380
+ var showing = panel.style.display !== 'none';
1381
+ panel.style.display = showing ? 'none' : 'flex';
1382
+ if (!showing && !connection) {
1383
+ startConnection();
1384
+ }
1385
+ if (!showing) {
1386
+ input.focus();
1387
+ scrollToBottom();
1388
+ }
1389
+ });
1390
+
1391
+ var closeBtn = panel.querySelector('.cumulus-close-btn');
1392
+ if (closeBtn) {
1393
+ closeBtn.addEventListener('click', function () {
1394
+ panel.style.display = 'none';
1395
+ });
1396
+ }
1397
+
1398
+ // Embedded mode: no auth panel handling needed (API key from attribute)
1399
+ function startConnection() {
1400
+ if (connection) { connection.close(); connection = null; }
1401
+ connection = createConnection({
1402
+ wsUrl: wsUrl,
1403
+ apiKey: activeApiKey,
1404
+ sessionId: sessionId,
1405
+ onMessage: handleServerMessage,
1406
+ onStatus: updateStatus,
1407
+ });
1408
+ }
1409
+
1410
+ function mount(target) {
1411
+ (target || document.body).appendChild(container);
1412
+ // Embedded: connect on first open (bubble click), not eagerly
1413
+ }
1414
+
1415
+ function destroy() {
1416
+ if (connection) connection.close();
1417
+ container.remove();
1418
+ }
1419
+
1420
+ return { mount: mount, destroy: destroy, send: sendMessage };
1421
+ }
1422
+
1423
+ // ═══════════════════════════════════════════════════════════════════════════
1424
+ // STANDALONE MODE — sidebar + multi-panel layout
1425
+ // ═══════════════════════════════════════════════════════════════════════════
799
1426
 
800
- // ── Header ──
801
- var header = document.createElement('div');
802
- header.className = 'cumulus-header';
803
- header.innerHTML =
1427
+ // ── Standalone state ──
1428
+ var connection = null;
1429
+ var connectionStatus = 'disconnected';
1430
+ var authenticated = false;
1431
+
1432
+ // Thread registry: threadName -> { messages, streaming, streamBuffer, stopRequested, pendingAttachments }
1433
+ var threadStates = {};
1434
+
1435
+ // Thread list from server: [{ name, messageCount, lastActivity }]
1436
+ var allThreads = [];
1437
+
1438
+ // Currently visible panel names (ordered, max 3)
1439
+ var visibleThreads = [];
1440
+
1441
+ // Periodic refresh timer
1442
+ var refreshTimer = null;
1443
+
1444
+ // ── Helper: get or create per-thread state ──
1445
+ function getThreadState(threadName) {
1446
+ if (!threadStates[threadName]) {
1447
+ threadStates[threadName] = {
1448
+ messages: [],
1449
+ streaming: false,
1450
+ streamBuffer: '',
1451
+ stopRequested: false,
1452
+ pendingAttachments: [],
1453
+ historyLoaded: false,
1454
+ };
1455
+ }
1456
+ return threadStates[threadName];
1457
+ }
1458
+
1459
+ // ── Root layout ──
1460
+ var standaloneRoot = document.createElement('div');
1461
+ standaloneRoot.className = 'cumulus-standalone-root';
1462
+
1463
+ // ── Outer wrapper (auth view or app view) ──
1464
+ // Auth panel — shown before connection
1465
+ var authWrapper = document.createElement('div');
1466
+ authWrapper.className = 'cumulus-panel standalone';
1467
+ authWrapper.setAttribute('data-testid', 'webchat-panel');
1468
+ authWrapper.style.display = 'flex';
1469
+
1470
+ // Auth header
1471
+ var authHeader = document.createElement('div');
1472
+ authHeader.className = 'cumulus-header';
1473
+ authHeader.innerHTML =
804
1474
  '<span class="cumulus-header-title">Cumulus</span>' +
805
1475
  '<span class="cumulus-header-status">' +
806
1476
  '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
807
1477
  '<span data-testid="webchat-status-text">Disconnected</span>' +
808
- '</span>' +
809
- (standalone
810
- ? '<button class="cumulus-auth-logout" data-testid="webchat-logout" style="display:none">Logout</button>'
811
- : '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>');
812
- panel.appendChild(header);
1478
+ '</span>';
1479
+ authWrapper.appendChild(authHeader);
813
1480
 
814
- // ── Auth panel ──
815
1481
  var authPanel = document.createElement('div');
816
1482
  authPanel.className = 'cumulus-auth';
817
1483
  authPanel.setAttribute('data-testid', 'webchat-auth');
@@ -821,492 +1487,834 @@
821
1487
  '<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
822
1488
  '<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
823
1489
  '<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
824
- panel.appendChild(authPanel);
825
-
826
- // ── Messages area ──
827
- var messagesEl = document.createElement('div');
828
- messagesEl.className = 'cumulus-messages';
829
- messagesEl.setAttribute('data-testid', 'webchat-messages');
830
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
831
- messagesEl.style.display = 'none';
832
- panel.appendChild(messagesEl);
833
-
834
- // ── Input area ──
835
- var inputArea = document.createElement('div');
836
- inputArea.className = 'cumulus-input-area';
837
- inputArea.style.display = 'none';
838
-
839
- // Attachment strip (conditionally shown)
840
- var attachStrip = document.createElement('div');
841
- attachStrip.className = 'cumulus-attach-strip';
842
- attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
843
- attachStrip.style.display = 'none';
844
- inputArea.appendChild(attachStrip);
845
-
846
- // Input row
847
- var inputRow = document.createElement('div');
848
- inputRow.className = 'cumulus-input-row';
849
-
850
- // Hidden file input
851
- var fileInput = document.createElement('input');
852
- fileInput.type = 'file';
853
- fileInput.multiple = true;
854
- fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
855
- fileInput.style.display = 'none';
856
- fileInput.setAttribute('data-testid', 'webchat-file-input');
857
-
858
- // Attach button
859
- var attachBtn = document.createElement('button');
860
- attachBtn.className = 'cumulus-attach-btn';
861
- attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
862
- attachBtn.setAttribute('title', 'Attach files');
863
- attachBtn.textContent = '+';
864
-
865
- // Textarea
866
- var input = document.createElement('textarea');
867
- input.className = 'cumulus-input';
868
- input.setAttribute('data-testid', 'webchat-input');
869
- input.placeholder = 'Type a message…';
870
- input.rows = 1;
871
-
872
- // Send/Stop button
873
- var sendBtn = document.createElement('button');
874
- sendBtn.className = 'cumulus-send-btn';
875
- sendBtn.setAttribute('data-testid', 'webchat-send');
876
- sendBtn.textContent = 'Send';
877
-
878
- inputRow.appendChild(fileInput);
879
- inputRow.appendChild(attachBtn);
880
- inputRow.appendChild(input);
881
- inputRow.appendChild(sendBtn);
882
- inputArea.appendChild(inputRow);
883
- panel.appendChild(inputArea);
884
-
885
- container.appendChild(panel);
886
-
887
- // ── Floating bubble (embedded mode only) ──
888
- var bubble = null;
889
- if (!standalone) {
890
- bubble = document.createElement('button');
891
- bubble.className = 'cumulus-bubble';
892
- bubble.setAttribute('data-testid', 'webchat-bubble');
893
- bubble.setAttribute('title', 'Open chat');
894
- bubble.innerHTML =
895
- '<svg viewBox="0 0 24 24"><path d="M20 2H4c-1.1 0-2 .9-2 2v18l4-4h14c1.1 0 2-.9 2-2V4c0-1.1-.9-2-2-2zm0 14H6l-2 2V4h16v12z"/></svg>';
896
- container.appendChild(bubble);
1490
+ authWrapper.appendChild(authPanel);
1491
+
1492
+ // App layout shown after auth
1493
+ var appLayout = document.createElement('div');
1494
+ appLayout.style.cssText = 'display:none; flex-direction:column; width:100%; height:100vh; overflow:hidden;';
1495
+
1496
+ // Top bar
1497
+ var topbar = document.createElement('div');
1498
+ topbar.className = 'cumulus-topbar';
1499
+ topbar.innerHTML =
1500
+ '<span class="cumulus-topbar-title">Cumulus</span>' +
1501
+ '<div class="cumulus-topbar-right">' +
1502
+ '<span class="cumulus-header-status">' +
1503
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot-app"></span>' +
1504
+ '<span data-testid="webchat-status-text-app">Disconnected</span>' +
1505
+ '</span>' +
1506
+ '<button class="cumulus-auth-logout" data-testid="webchat-logout">Logout</button>' +
1507
+ '</div>';
1508
+ appLayout.appendChild(topbar);
1509
+
1510
+ // Sidebar + content row
1511
+ var appRow = document.createElement('div');
1512
+ appRow.style.cssText = 'display:flex; flex-direction:row; flex:1; overflow:hidden;';
1513
+
1514
+ // ── Sidebar ──
1515
+ var sidebar = document.createElement('div');
1516
+ sidebar.className = 'cumulus-sidebar';
1517
+ sidebar.setAttribute('data-testid', 'webchat-sidebar');
1518
+
1519
+ // Sidebar header
1520
+ var sidebarHeader = document.createElement('div');
1521
+ sidebarHeader.className = 'cumulus-sidebar-header';
1522
+ sidebarHeader.innerHTML = '<span class="cumulus-sidebar-title">Threads</span>';
1523
+ sidebar.appendChild(sidebarHeader);
1524
+
1525
+ // Sidebar scroll area
1526
+ var sidebarScroll = document.createElement('div');
1527
+ sidebarScroll.className = 'cumulus-sidebar-scroll';
1528
+ sidebar.appendChild(sidebarScroll);
1529
+
1530
+ // Sidebar footer with + New button
1531
+ var sidebarFooter = document.createElement('div');
1532
+ sidebarFooter.className = 'cumulus-sidebar-footer';
1533
+
1534
+ var newThreadBtn = document.createElement('button');
1535
+ newThreadBtn.className = 'cumulus-new-thread-btn';
1536
+ newThreadBtn.setAttribute('data-testid', 'webchat-thread-new');
1537
+ newThreadBtn.textContent = '+ New thread';
1538
+
1539
+ var newThreadInput = document.createElement('input');
1540
+ newThreadInput.className = 'cumulus-new-thread-input';
1541
+ newThreadInput.setAttribute('data-testid', 'webchat-thread-new-input');
1542
+ newThreadInput.placeholder = 'Thread name\u2026';
1543
+ newThreadInput.style.display = 'none';
1544
+
1545
+ sidebarFooter.appendChild(newThreadBtn);
1546
+ sidebarFooter.appendChild(newThreadInput);
1547
+ sidebar.appendChild(sidebarFooter);
1548
+
1549
+ // ── Content area ──
1550
+ var contentArea = document.createElement('div');
1551
+ contentArea.className = 'cumulus-content-area';
1552
+
1553
+ appRow.appendChild(sidebar);
1554
+ appRow.appendChild(contentArea);
1555
+ appLayout.appendChild(appRow);
1556
+
1557
+ standaloneRoot.appendChild(authWrapper);
1558
+ standaloneRoot.appendChild(appLayout);
1559
+ container.appendChild(standaloneRoot);
1560
+
1561
+ // ── Status update (updates both auth-phase and app-phase dots) ──
1562
+ function updateStatus(status) {
1563
+ connectionStatus = status;
1564
+ // Auth phase dot
1565
+ var dot = authWrapper.querySelector('[data-testid="webchat-status-dot"]');
1566
+ var text = authWrapper.querySelector('[data-testid="webchat-status-text"]');
1567
+ if (dot) dot.className = 'cumulus-status-dot ' + status;
1568
+ if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1569
+ // App phase dot
1570
+ var dotApp = appLayout.querySelector('[data-testid="webchat-status-dot-app"]');
1571
+ var textApp = appLayout.querySelector('[data-testid="webchat-status-text-app"]');
1572
+ if (dotApp) dotApp.className = 'cumulus-status-dot ' + status;
1573
+ if (textApp) textApp.textContent = status.charAt(0).toUpperCase() + status.slice(1);
897
1574
  }
898
1575
 
899
1576
  // ── View switching ──
900
1577
  function showAuthView() {
901
1578
  authenticated = false;
902
- authPanel.style.display = 'flex';
903
- messagesEl.style.display = 'none';
904
- inputArea.style.display = 'none';
905
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
906
- if (logoutBtn) logoutBtn.style.display = 'none';
1579
+ authWrapper.style.display = 'flex';
1580
+ appLayout.style.display = 'none';
1581
+ visibleThreads = [];
1582
+ threadStates = {};
1583
+ allThreads = [];
1584
+ if (refreshTimer) { clearInterval(refreshTimer); refreshTimer = null; }
907
1585
  var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
908
1586
  if (errEl) errEl.textContent = '';
909
1587
  }
910
1588
 
911
- function showChatView() {
1589
+ function showAppView() {
912
1590
  authenticated = true;
913
- authPanel.style.display = 'none';
914
- messagesEl.style.display = 'flex';
915
- inputArea.style.display = 'flex';
916
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
917
- if (logoutBtn) logoutBtn.style.display = 'inline-block';
918
- input.focus();
1591
+ authWrapper.style.display = 'none';
1592
+ appLayout.style.display = 'flex';
1593
+ // Request thread list from server
1594
+ requestThreadList();
1595
+ // Periodic refresh every 30s
1596
+ if (refreshTimer) clearInterval(refreshTimer);
1597
+ refreshTimer = setInterval(requestThreadList, 30000);
919
1598
  }
920
1599
 
921
- // ── Rendering ──
922
- function scrollToBottom() {
923
- messagesEl.scrollTop = messagesEl.scrollHeight;
1600
+ // ── Thread list ──
1601
+ function requestThreadList() {
1602
+ if (connection) {
1603
+ connection.send({ type: 'threads' });
1604
+ }
924
1605
  }
925
1606
 
926
- function isWideUserMessage(content) {
927
- // Wide if it contains a code fence or has more than 3 lines
928
- if (/```/.test(content)) return true;
929
- var lineCount = (content.match(/\n/g) || []).length + 1;
930
- return lineCount > 3;
931
- }
1607
+ // ── Sidebar rendering ──
1608
+ function renderSidebar() {
1609
+ sidebarScroll.innerHTML = '';
932
1610
 
933
- function buildUserMsgEl(msg) {
934
- var el = document.createElement('div');
935
- el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
936
- el.textContent = msg.content;
1611
+ var visibleSet = {};
1612
+ for (var i = 0; i < visibleThreads.length; i++) {
1613
+ visibleSet[visibleThreads[i]] = true;
1614
+ }
937
1615
 
938
- if (msg.attachments && msg.attachments.length > 0) {
939
- var attRow = document.createElement('div');
940
- attRow.className = 'cumulus-msg-attachments';
941
- msg.attachments.forEach(function (att) {
942
- if (att.isImage) {
943
- var img = document.createElement('img');
944
- img.className = 'cumulus-msg-img';
945
- img.src = att.dataUrl;
946
- img.alt = att.name;
947
- img.setAttribute('data-testid', 'webchat-msg-img');
948
- attRow.appendChild(img);
949
- } else {
950
- var badge = document.createElement('span');
951
- badge.className = 'cumulus-msg-file-badge';
952
- badge.setAttribute('data-testid', 'webchat-msg-file');
953
- badge.textContent = '\uD83D\uDCCE ' + att.name; // 📎
954
- attRow.appendChild(badge);
955
- }
1616
+ // Active section: threads currently visible as panels
1617
+ var activeThreads = visibleThreads.slice(); // preserve order
1618
+ if (activeThreads.length > 0) {
1619
+ var activeLabel = document.createElement('div');
1620
+ activeLabel.className = 'cumulus-sidebar-section-label';
1621
+ activeLabel.textContent = 'Active';
1622
+ sidebarScroll.appendChild(activeLabel);
1623
+
1624
+ var activeSection = document.createElement('div');
1625
+ activeSection.setAttribute('data-testid', 'webchat-sidebar-active');
1626
+ activeThreads.forEach(function (name) {
1627
+ activeSection.appendChild(buildThreadItem(name, true, true));
956
1628
  });
957
- el.appendChild(attRow);
1629
+ sidebarScroll.appendChild(activeSection);
1630
+
1631
+ var divider = document.createElement('hr');
1632
+ divider.className = 'cumulus-sidebar-divider';
1633
+ sidebarScroll.appendChild(divider);
958
1634
  }
959
- return el;
960
- }
961
1635
 
962
- function buildAssistantMsgEl(content, isStreaming) {
963
- var el = document.createElement('div');
964
- el.className = 'cumulus-msg assistant';
965
- if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
966
- if (content) {
967
- el.innerHTML = renderMarkdown(content);
968
- if (isStreaming) {
969
- el.innerHTML += '<span class="cumulus-cursor"></span>';
970
- }
971
- // Wire up copy buttons
972
- el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
973
- btn.addEventListener('click', function () {
974
- var targetId = btn.getAttribute('data-copy-target');
975
- var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
976
- if (codeEl && navigator.clipboard) {
977
- navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
978
- btn.textContent = 'Copied!';
979
- setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
980
- });
981
- }
982
- });
1636
+ // All section: all threads from server, sorted by lastActivity desc
1637
+ var allLabel = document.createElement('div');
1638
+ allLabel.className = 'cumulus-sidebar-section-label';
1639
+ allLabel.textContent = 'All';
1640
+ sidebarScroll.appendChild(allLabel);
1641
+
1642
+ var allSection = document.createElement('div');
1643
+ allSection.setAttribute('data-testid', 'webchat-sidebar-all');
1644
+
1645
+ if (allThreads.length === 0) {
1646
+ var emptyNote = document.createElement('div');
1647
+ emptyNote.style.cssText = 'padding: 8px 12px; font-size: 12px; color: #555; font-style: italic;';
1648
+ emptyNote.textContent = 'No threads yet';
1649
+ allSection.appendChild(emptyNote);
1650
+ } else {
1651
+ // Sort by lastActivity descending
1652
+ var sorted = allThreads.slice().sort(function (a, b) {
1653
+ return (b.lastActivity || 0) - (a.lastActivity || 0);
1654
+ });
1655
+ sorted.forEach(function (t) {
1656
+ allSection.appendChild(buildThreadItem(t.name, visibleSet[t.name] || false, false, t.messageCount));
983
1657
  });
984
- } else if (isStreaming) {
985
- el.innerHTML =
986
- '<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
987
- '<span class="cumulus-cursor"></span>';
988
1658
  }
989
- return el;
1659
+ sidebarScroll.appendChild(allSection);
990
1660
  }
991
1661
 
992
- function renderMessages() {
993
- messagesEl.innerHTML = '';
994
-
995
- if (messages.length === 0 && !streaming) {
996
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
997
- return;
1662
+ function buildThreadItem(name, isSelected, isActive, messageCount) {
1663
+ var item = document.createElement('div');
1664
+ item.className = 'cumulus-thread-item' + (isSelected ? ' selected' : '');
1665
+ item.setAttribute('data-testid', 'webchat-thread-item');
1666
+ item.setAttribute('data-thread-name', name);
1667
+
1668
+ var dot = document.createElement('span');
1669
+ dot.className = 'cumulus-thread-dot ' + (isActive ? 'active' : 'inactive');
1670
+ item.appendChild(dot);
1671
+
1672
+ var nameEl = document.createElement('span');
1673
+ nameEl.className = 'cumulus-thread-name';
1674
+ nameEl.textContent = name;
1675
+ nameEl.setAttribute('title', name);
1676
+ item.appendChild(nameEl);
1677
+
1678
+ if (messageCount !== undefined && messageCount !== null) {
1679
+ var countEl = document.createElement('span');
1680
+ countEl.className = 'cumulus-thread-count';
1681
+ countEl.textContent = messageCount;
1682
+ item.appendChild(countEl);
998
1683
  }
999
1684
 
1000
- for (var i = 0; i < messages.length; i++) {
1001
- var msg = messages[i];
1002
- var row = document.createElement('div');
1003
- row.className = 'cumulus-msg-row';
1004
-
1005
- if (msg.role === 'user') {
1006
- row.appendChild(buildUserMsgEl(msg));
1685
+ item.addEventListener('click', function (e) {
1686
+ var multiSelect = e.metaKey || e.ctrlKey;
1687
+ if (multiSelect) {
1688
+ togglePanelThread(name);
1007
1689
  } else {
1008
- row.appendChild(buildAssistantMsgEl(msg.content, false));
1690
+ soloThread(name);
1009
1691
  }
1010
- messagesEl.appendChild(row);
1011
- }
1692
+ });
1012
1693
 
1013
- if (streaming) {
1014
- var row = document.createElement('div');
1015
- row.className = 'cumulus-msg-row';
1016
- row.appendChild(buildAssistantMsgEl(streamBuffer, true));
1017
- messagesEl.appendChild(row);
1018
- }
1694
+ return item;
1695
+ }
1019
1696
 
1020
- scrollToBottom();
1697
+ // ── Panel management ──
1698
+ function soloThread(name) {
1699
+ visibleThreads = [name];
1700
+ renderSidebar();
1701
+ renderContentArea();
1702
+ loadThreadHistoryIfNeeded(name);
1021
1703
  }
1022
1704
 
1023
- function updateSendBtn() {
1024
- if (streaming) {
1025
- sendBtn.textContent = 'Stop';
1026
- sendBtn.classList.add('stop');
1027
- sendBtn.disabled = false;
1705
+ function togglePanelThread(name) {
1706
+ var idx = visibleThreads.indexOf(name);
1707
+ if (idx !== -1) {
1708
+ // Remove from visible
1709
+ visibleThreads.splice(idx, 1);
1028
1710
  } else {
1029
- sendBtn.textContent = 'Send';
1030
- sendBtn.classList.remove('stop');
1031
- sendBtn.disabled = false;
1711
+ // Add if under max
1712
+ if (visibleThreads.length < 3) {
1713
+ visibleThreads.push(name);
1714
+ }
1715
+ // If already at max 3, replace the last one
1716
+ else {
1717
+ visibleThreads[visibleThreads.length - 1] = name;
1718
+ }
1719
+ }
1720
+ renderSidebar();
1721
+ renderContentArea();
1722
+ if (idx === -1) {
1723
+ loadThreadHistoryIfNeeded(name);
1032
1724
  }
1033
1725
  }
1034
1726
 
1035
- function updateStatus(status) {
1036
- connectionStatus = status;
1037
- var dot = panel.querySelector('.cumulus-status-dot');
1038
- var text = panel.querySelector('[data-testid="webchat-status-text"]');
1039
- if (dot) dot.className = 'cumulus-status-dot ' + status;
1040
- if (text) text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
1727
+ function removeThreadFromView(name) {
1728
+ var idx = visibleThreads.indexOf(name);
1729
+ if (idx !== -1) {
1730
+ visibleThreads.splice(idx, 1);
1731
+ // Clear stale DOM render hooks so background messages don't update detached nodes
1732
+ var state = threadStates[name];
1733
+ if (state) { state._renderMessages = null; state._updateSendBtn = null; }
1734
+ renderSidebar();
1735
+ renderContentArea();
1736
+ }
1041
1737
  }
1042
1738
 
1043
- // ── Attachment strip ──
1044
- function renderAttachStrip() {
1045
- attachStrip.innerHTML = '';
1046
- if (pendingAttachments.length === 0) {
1047
- attachStrip.style.display = 'none';
1739
+ function loadThreadHistoryIfNeeded(name) {
1740
+ var state = getThreadState(name);
1741
+ if (!state.historyLoaded && connection) {
1742
+ state.historyLoaded = true;
1743
+ connection.send({ type: 'history', threadName: name, limit: 50 });
1744
+ }
1745
+ }
1746
+
1747
+ // ── Content area rendering ──
1748
+ function renderContentArea() {
1749
+ // Clear stale render hooks before wiping the DOM
1750
+ for (var _ci = 0; _ci < visibleThreads.length; _ci++) {
1751
+ var _cs = threadStates[visibleThreads[_ci]];
1752
+ if (_cs) { _cs._renderMessages = null; _cs._updateSendBtn = null; }
1753
+ }
1754
+ contentArea.innerHTML = '';
1755
+
1756
+ if (visibleThreads.length === 0) {
1757
+ var emptyEl = document.createElement('div');
1758
+ emptyEl.className = 'cumulus-standalone-empty';
1759
+ emptyEl.innerHTML =
1760
+ '<div>Select a thread from the sidebar or create a new one</div>' +
1761
+ '<div class="cumulus-standalone-empty-hint">Cmd/Ctrl+Click to open multiple threads side by side</div>';
1762
+ contentArea.appendChild(emptyEl);
1048
1763
  return;
1049
1764
  }
1050
- attachStrip.style.display = 'flex';
1051
- pendingAttachments.forEach(function (att, idx) {
1052
- var chip = document.createElement('div');
1053
- chip.className = 'cumulus-attach-chip';
1054
- chip.setAttribute('data-testid', 'webchat-attach-chip');
1055
-
1056
- if (att.isImage) {
1057
- var thumb = document.createElement('img');
1058
- thumb.className = 'cumulus-attach-chip-thumb';
1059
- thumb.src = att.dataUrl;
1060
- thumb.alt = att.name;
1061
- chip.appendChild(thumb);
1062
- } else {
1063
- var icon = document.createElement('div');
1064
- icon.className = 'cumulus-attach-chip-icon';
1065
- icon.textContent = fileExtension(att.name);
1066
- chip.appendChild(icon);
1765
+
1766
+ for (var i = 0; i < visibleThreads.length; i++) {
1767
+ contentArea.appendChild(buildThreadPanel(visibleThreads[i]));
1768
+ }
1769
+ }
1770
+
1771
+ // ── Thread panel builder ──
1772
+ function buildThreadPanel(threadName) {
1773
+ var state = getThreadState(threadName);
1774
+
1775
+ var panel = document.createElement('div');
1776
+ panel.className = 'cumulus-thread-panel';
1777
+ panel.setAttribute('data-testid', 'webchat-panel-' + threadName);
1778
+ panel.setAttribute('data-thread-panel', threadName);
1779
+
1780
+ // Panel header
1781
+ var panelHeader = document.createElement('div');
1782
+ panelHeader.className = 'cumulus-thread-panel-header';
1783
+
1784
+ var titleLeft = document.createElement('div');
1785
+ titleLeft.style.cssText = 'display:flex; align-items:center; gap:7px; overflow:hidden;';
1786
+
1787
+ var statusDot = document.createElement('span');
1788
+ statusDot.className = 'cumulus-status-dot ' + connectionStatus;
1789
+ statusDot.setAttribute('data-thread-status-dot', threadName);
1790
+ titleLeft.appendChild(statusDot);
1791
+
1792
+ var titleEl = document.createElement('span');
1793
+ titleEl.className = 'cumulus-thread-panel-title';
1794
+ titleEl.textContent = threadName;
1795
+ titleEl.setAttribute('title', threadName);
1796
+ titleLeft.appendChild(titleEl);
1797
+
1798
+ panelHeader.appendChild(titleLeft);
1799
+
1800
+ var closeBtn = document.createElement('button');
1801
+ closeBtn.className = 'cumulus-thread-panel-close';
1802
+ closeBtn.setAttribute('data-testid', 'webchat-panel-close');
1803
+ closeBtn.setAttribute('title', 'Close panel');
1804
+ closeBtn.textContent = '\xD7';
1805
+ closeBtn.addEventListener('click', function () {
1806
+ removeThreadFromView(threadName);
1807
+ });
1808
+ panelHeader.appendChild(closeBtn);
1809
+ panel.appendChild(panelHeader);
1810
+
1811
+ // Messages area
1812
+ var messagesEl = document.createElement('div');
1813
+ messagesEl.className = 'cumulus-messages';
1814
+ messagesEl.setAttribute('data-testid', 'webchat-messages');
1815
+ messagesEl.setAttribute('data-thread-messages', threadName);
1816
+ panel.appendChild(messagesEl);
1817
+
1818
+ // Input area
1819
+ var inputArea = document.createElement('div');
1820
+ inputArea.className = 'cumulus-input-area';
1821
+
1822
+ // Attachment strip
1823
+ var attachStrip = document.createElement('div');
1824
+ attachStrip.className = 'cumulus-attach-strip';
1825
+ attachStrip.setAttribute('data-testid', 'webchat-attach-strip');
1826
+ attachStrip.style.display = 'none';
1827
+ inputArea.appendChild(attachStrip);
1828
+
1829
+ // Input row
1830
+ var inputRow = document.createElement('div');
1831
+ inputRow.className = 'cumulus-input-row';
1832
+
1833
+ var fileInput = document.createElement('input');
1834
+ fileInput.type = 'file';
1835
+ fileInput.multiple = true;
1836
+ fileInput.accept = 'image/*,.pdf,.txt,.md,.js,.ts,.py,.json,.csv';
1837
+ fileInput.style.display = 'none';
1838
+ fileInput.setAttribute('data-testid', 'webchat-file-input');
1839
+
1840
+ var attachBtn = document.createElement('button');
1841
+ attachBtn.className = 'cumulus-attach-btn';
1842
+ attachBtn.setAttribute('data-testid', 'webchat-attach-btn');
1843
+ attachBtn.setAttribute('title', 'Attach files');
1844
+ attachBtn.textContent = '+';
1845
+
1846
+ var inputEl = document.createElement('textarea');
1847
+ inputEl.className = 'cumulus-input';
1848
+ inputEl.setAttribute('data-testid', 'webchat-input');
1849
+ inputEl.placeholder = 'Message ' + threadName + '\u2026';
1850
+ inputEl.rows = 1;
1851
+
1852
+ var sendBtn = document.createElement('button');
1853
+ sendBtn.className = 'cumulus-send-btn';
1854
+ sendBtn.setAttribute('data-testid', 'webchat-send');
1855
+ sendBtn.textContent = 'Send';
1856
+
1857
+ inputRow.appendChild(fileInput);
1858
+ inputRow.appendChild(attachBtn);
1859
+ inputRow.appendChild(inputEl);
1860
+ inputRow.appendChild(sendBtn);
1861
+ inputArea.appendChild(inputRow);
1862
+ panel.appendChild(inputArea);
1863
+
1864
+ // ── Panel-local render functions ──
1865
+ function scrollToBottom() {
1866
+ messagesEl.scrollTop = messagesEl.scrollHeight;
1867
+ }
1868
+
1869
+ function isWideUserMessage(content) {
1870
+ if (/```/.test(content)) return true;
1871
+ var lineCount = (content.match(/\n/g) || []).length + 1;
1872
+ return lineCount > 3;
1873
+ }
1874
+
1875
+ function buildUserMsgEl(msg) {
1876
+ var el = document.createElement('div');
1877
+ el.className = 'cumulus-msg user' + (isWideUserMessage(msg.content) ? ' wide' : '');
1878
+ el.textContent = msg.content;
1879
+ if (msg.attachments && msg.attachments.length > 0) {
1880
+ var attRow = document.createElement('div');
1881
+ attRow.className = 'cumulus-msg-attachments';
1882
+ msg.attachments.forEach(function (att) {
1883
+ if (att.isImage) {
1884
+ var img = document.createElement('img');
1885
+ img.className = 'cumulus-msg-img';
1886
+ img.src = att.dataUrl;
1887
+ img.alt = att.name;
1888
+ img.setAttribute('data-testid', 'webchat-msg-img');
1889
+ attRow.appendChild(img);
1890
+ } else {
1891
+ var badge = document.createElement('span');
1892
+ badge.className = 'cumulus-msg-file-badge';
1893
+ badge.setAttribute('data-testid', 'webchat-msg-file');
1894
+ badge.textContent = '\uD83D\uDCCE ' + att.name;
1895
+ attRow.appendChild(badge);
1896
+ }
1897
+ });
1898
+ el.appendChild(attRow);
1067
1899
  }
1900
+ return el;
1901
+ }
1068
1902
 
1069
- var nameEl = document.createElement('div');
1070
- nameEl.className = 'cumulus-attach-chip-name';
1071
- nameEl.textContent = att.name;
1072
- chip.appendChild(nameEl);
1073
-
1074
- var removeBtn = document.createElement('button');
1075
- removeBtn.className = 'cumulus-attach-chip-remove';
1076
- removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
1077
- removeBtn.textContent = '\xD7'; // ×
1078
- removeBtn.setAttribute('title', 'Remove attachment');
1079
- (function (index) {
1080
- removeBtn.addEventListener('click', function () {
1081
- pendingAttachments.splice(index, 1);
1082
- renderAttachStrip();
1903
+ function buildAssistantMsgEl(content, isStreaming) {
1904
+ var el = document.createElement('div');
1905
+ el.className = 'cumulus-msg assistant';
1906
+ if (isStreaming) el.setAttribute('data-testid', 'webchat-streaming');
1907
+ if (content) {
1908
+ el.innerHTML = renderMarkdown(content);
1909
+ if (isStreaming) {
1910
+ el.innerHTML += '<span class="cumulus-cursor"></span>';
1911
+ }
1912
+ el.querySelectorAll('.code-block-copy-btn').forEach(function (btn) {
1913
+ btn.addEventListener('click', function () {
1914
+ var targetId = btn.getAttribute('data-copy-target');
1915
+ var codeEl = el.querySelector('[data-code-id="' + targetId + '"]');
1916
+ if (codeEl && navigator.clipboard) {
1917
+ navigator.clipboard.writeText(codeEl.textContent || '').then(function () {
1918
+ btn.textContent = 'Copied!';
1919
+ setTimeout(function () { btn.textContent = 'Copy'; }, 2000);
1920
+ });
1921
+ }
1922
+ });
1083
1923
  });
1084
- })(idx);
1924
+ } else if (isStreaming) {
1925
+ el.innerHTML =
1926
+ '<span style="color:#666;font-style:italic">Thinking\u2026</span>' +
1927
+ '<span class="cumulus-cursor"></span>';
1928
+ }
1929
+ return el;
1930
+ }
1931
+
1932
+ function renderPanelMessages() {
1933
+ messagesEl.innerHTML = '';
1934
+ if (state.messages.length === 0 && !state.streaming) {
1935
+ messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
1936
+ return;
1937
+ }
1938
+ for (var i = 0; i < state.messages.length; i++) {
1939
+ var msg = state.messages[i];
1940
+ var row = document.createElement('div');
1941
+ row.className = 'cumulus-msg-row';
1942
+ if (msg.role === 'user') {
1943
+ row.appendChild(buildUserMsgEl(msg));
1944
+ } else {
1945
+ row.appendChild(buildAssistantMsgEl(msg.content, false));
1946
+ }
1947
+ messagesEl.appendChild(row);
1948
+ }
1949
+ if (state.streaming) {
1950
+ var row = document.createElement('div');
1951
+ row.className = 'cumulus-msg-row';
1952
+ row.appendChild(buildAssistantMsgEl(state.streamBuffer, true));
1953
+ messagesEl.appendChild(row);
1954
+ }
1955
+ scrollToBottom();
1956
+ }
1957
+
1958
+ function updatePanelSendBtn() {
1959
+ if (state.streaming) {
1960
+ sendBtn.textContent = 'Stop';
1961
+ sendBtn.classList.add('stop');
1962
+ sendBtn.disabled = false;
1963
+ } else {
1964
+ sendBtn.textContent = 'Send';
1965
+ sendBtn.classList.remove('stop');
1966
+ sendBtn.disabled = false;
1967
+ }
1968
+ }
1969
+
1970
+ function renderPanelAttachStrip() {
1971
+ attachStrip.innerHTML = '';
1972
+ if (state.pendingAttachments.length === 0) {
1973
+ attachStrip.style.display = 'none';
1974
+ return;
1975
+ }
1976
+ attachStrip.style.display = 'flex';
1977
+ state.pendingAttachments.forEach(function (att, idx) {
1978
+ var chip = document.createElement('div');
1979
+ chip.className = 'cumulus-attach-chip';
1980
+ chip.setAttribute('data-testid', 'webchat-attach-chip');
1981
+ if (att.isImage) {
1982
+ var thumb = document.createElement('img');
1983
+ thumb.className = 'cumulus-attach-chip-thumb';
1984
+ thumb.src = att.dataUrl;
1985
+ thumb.alt = att.name;
1986
+ chip.appendChild(thumb);
1987
+ } else {
1988
+ var icon = document.createElement('div');
1989
+ icon.className = 'cumulus-attach-chip-icon';
1990
+ icon.textContent = fileExtension(att.name);
1991
+ chip.appendChild(icon);
1992
+ }
1993
+ var nameEl = document.createElement('div');
1994
+ nameEl.className = 'cumulus-attach-chip-name';
1995
+ nameEl.textContent = att.name;
1996
+ chip.appendChild(nameEl);
1997
+ var removeBtn = document.createElement('button');
1998
+ removeBtn.className = 'cumulus-attach-chip-remove';
1999
+ removeBtn.setAttribute('data-testid', 'webchat-attach-remove');
2000
+ removeBtn.textContent = '\xD7';
2001
+ removeBtn.setAttribute('title', 'Remove attachment');
2002
+ (function (index) {
2003
+ removeBtn.addEventListener('click', function () {
2004
+ state.pendingAttachments.splice(index, 1);
2005
+ renderPanelAttachStrip();
2006
+ });
2007
+ })(idx);
2008
+ chip.appendChild(removeBtn);
2009
+ attachStrip.appendChild(chip);
2010
+ });
2011
+ }
2012
+
2013
+ async function addFilesToPending(files) {
2014
+ for (var i = 0; i < files.length; i++) {
2015
+ var file = files[i];
2016
+ try {
2017
+ var info = await readFileAsBase64(file);
2018
+ var dataUrl = await readFileAsDataUrl(file);
2019
+ state.pendingAttachments.push({
2020
+ base64: info.base64,
2021
+ mimeType: info.mimeType,
2022
+ name: file.name,
2023
+ dataUrl: dataUrl,
2024
+ isImage: file.type.startsWith('image/'),
2025
+ });
2026
+ } catch (e) {
2027
+ console.error('[Cumulus] Failed to read file:', file.name, e);
2028
+ }
2029
+ }
2030
+ renderPanelAttachStrip();
2031
+ }
2032
+
2033
+ // Store references on the state so the router can call re-render
2034
+ state._renderMessages = renderPanelMessages;
2035
+ state._updateSendBtn = updatePanelSendBtn;
2036
+
2037
+ // ── Send message for this panel ──
2038
+ function sendPanelMessage() {
2039
+ if (state.streaming) {
2040
+ state.stopRequested = true;
2041
+ state.streaming = false;
2042
+ updatePanelSendBtn();
2043
+ if (state.streamBuffer) {
2044
+ state.messages.push({ role: 'assistant', content: state.streamBuffer + ' [stopped]' });
2045
+ state.streamBuffer = '';
2046
+ }
2047
+ renderPanelMessages();
2048
+ return;
2049
+ }
2050
+ var text = inputEl.value.trim();
2051
+ if (!text && state.pendingAttachments.length === 0) return;
2052
+ if (!connection) return;
2053
+
2054
+ var attachSnapshot = state.pendingAttachments.slice();
2055
+ var displayText = text || '(attachment)';
2056
+ state.messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
2057
+ inputEl.value = '';
2058
+ inputEl.style.height = 'auto';
2059
+ state.pendingAttachments = [];
2060
+ renderPanelAttachStrip();
2061
+ state.streaming = true;
2062
+ state.stopRequested = false;
2063
+ state.streamBuffer = '';
2064
+ updatePanelSendBtn();
2065
+ renderPanelMessages();
2066
+
2067
+ var imagePayload = attachSnapshot
2068
+ .filter(function (a) { return a.isImage; })
2069
+ .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
2070
+
2071
+ var payload = {
2072
+ type: 'message',
2073
+ threadName: threadName,
2074
+ message: text || ' ',
2075
+ };
2076
+ if (imagePayload.length > 0) payload.images = imagePayload;
2077
+ connection.send(payload);
2078
+ }
2079
+
2080
+ // ── Panel event listeners ──
2081
+ sendBtn.addEventListener('click', sendPanelMessage);
1085
2082
 
1086
- chip.appendChild(removeBtn);
1087
- attachStrip.appendChild(chip);
2083
+ inputEl.addEventListener('keydown', function (e) {
2084
+ if (e.key === 'Enter' && !e.shiftKey) {
2085
+ e.preventDefault();
2086
+ sendPanelMessage();
2087
+ }
1088
2088
  });
1089
- }
1090
2089
 
1091
- async function addFilesToPending(files) {
1092
- for (var i = 0; i < files.length; i++) {
1093
- var file = files[i];
1094
- try {
1095
- var info = await readFileAsBase64(file);
1096
- var dataUrl = await readFileAsDataUrl(file);
1097
- pendingAttachments.push({
1098
- base64: info.base64,
1099
- mimeType: info.mimeType,
1100
- name: file.name,
1101
- dataUrl: dataUrl,
1102
- isImage: file.type.startsWith('image/'),
1103
- });
1104
- } catch (e) {
1105
- console.error('[Cumulus] Failed to read file:', file.name, e);
2090
+ inputEl.addEventListener('input', function () {
2091
+ inputEl.style.height = 'auto';
2092
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + 'px';
2093
+ });
2094
+
2095
+ inputEl.addEventListener('paste', function (e) {
2096
+ var items = e.clipboardData && e.clipboardData.items;
2097
+ if (!items) return;
2098
+ var filesToAdd = [];
2099
+ for (var i = 0; i < items.length; i++) {
2100
+ var item = items[i];
2101
+ if (item.kind === 'file') {
2102
+ var file = item.getAsFile();
2103
+ if (file) filesToAdd.push(file);
2104
+ }
2105
+ }
2106
+ if (filesToAdd.length > 0) {
2107
+ e.preventDefault();
2108
+ addFilesToPending(filesToAdd);
2109
+ }
2110
+ });
2111
+
2112
+ fileInput.addEventListener('change', function () {
2113
+ if (fileInput.files && fileInput.files.length > 0) {
2114
+ addFilesToPending(Array.from(fileInput.files));
2115
+ fileInput.value = '';
1106
2116
  }
2117
+ });
2118
+
2119
+ attachBtn.addEventListener('click', function () {
2120
+ fileInput.click();
2121
+ });
2122
+
2123
+ // Initial render
2124
+ renderPanelMessages();
2125
+ renderPanelAttachStrip();
2126
+
2127
+ return panel;
2128
+ }
2129
+
2130
+ // ── Re-render a visible thread panel's messages in-place ──
2131
+ // Called by the message router after state mutation
2132
+ function refreshThreadPanel(threadName) {
2133
+ var state = threadStates[threadName];
2134
+ if (!state) return;
2135
+
2136
+ // Re-render messages in-place if panel is visible
2137
+ if (state._renderMessages) {
2138
+ state._renderMessages();
2139
+ }
2140
+ if (state._updateSendBtn) {
2141
+ state._updateSendBtn();
1107
2142
  }
1108
- renderAttachStrip();
1109
2143
  }
1110
2144
 
1111
- // ── Message handling ──
2145
+ // ── Message router ──
1112
2146
  function handleServerMessage(data) {
1113
2147
  switch (data.type) {
1114
2148
  case 'auth_ok':
1115
- showChatView();
2149
+ showAppView();
1116
2150
  break;
1117
2151
 
1118
2152
  case 'auth_error':
1119
2153
  console.error('[Cumulus] Auth failed:', data.error);
1120
- if (standalone) {
1121
- clearStoredApiKey();
1122
- activeApiKey = '';
1123
- if (connection) { connection.close(); connection = null; }
1124
- showAuthView();
1125
- var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
1126
- if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
2154
+ clearStoredApiKey();
2155
+ activeApiKey = '';
2156
+ if (connection) { connection.close(); connection = null; }
2157
+ showAuthView();
2158
+ var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
2159
+ if (errEl) errEl.textContent = 'Authentication failed. Check your API key.';
2160
+ break;
2161
+
2162
+ case 'threads':
2163
+ // Server response to { type: 'threads' } request
2164
+ if (data.threads) {
2165
+ allThreads = data.threads;
2166
+ renderSidebar();
2167
+ }
2168
+ break;
2169
+
2170
+ case 'thread_created':
2171
+ // Server confirms new thread; add to list and select it
2172
+ if (data.threadName) {
2173
+ // Add to allThreads if not already present
2174
+ var exists = false;
2175
+ for (var i = 0; i < allThreads.length; i++) {
2176
+ if (allThreads[i].name === data.threadName) { exists = true; break; }
2177
+ }
2178
+ if (!exists) {
2179
+ allThreads.push({ name: data.threadName, messageCount: 0, lastActivity: Date.now() });
2180
+ }
2181
+ soloThread(data.threadName);
1127
2182
  }
1128
2183
  break;
1129
2184
 
1130
2185
  case 'history':
1131
- if (data.messages && data.messages.length > 0) {
1132
- messages = data.messages.map(function (m) {
1133
- return { role: m.role, content: m.content };
1134
- });
1135
- renderMessages();
2186
+ if (data.threadName) {
2187
+ var state = getThreadState(data.threadName);
2188
+ if (data.messages && data.messages.length > 0) {
2189
+ state.messages = data.messages.map(function (m) {
2190
+ return { role: m.role, content: m.content };
2191
+ });
2192
+ }
2193
+ refreshThreadPanel(data.threadName);
1136
2194
  }
1137
2195
  break;
1138
2196
 
1139
2197
  case 'token':
1140
- if (stopRequested) break; // discard after stop
1141
- if (!streaming) {
1142
- streaming = true;
1143
- streamBuffer = '';
1144
- updateSendBtn();
2198
+ if (data.threadName) {
2199
+ var state = getThreadState(data.threadName);
2200
+ if (state.stopRequested) break;
2201
+ if (!state.streaming) {
2202
+ state.streaming = true;
2203
+ state.streamBuffer = '';
2204
+ }
2205
+ state.streamBuffer += data.text;
2206
+ // Update lastActivity for this thread in allThreads
2207
+ updateThreadActivity(data.threadName);
2208
+ refreshThreadPanel(data.threadName);
1145
2209
  }
1146
- streamBuffer += data.text;
1147
- renderMessages();
1148
2210
  break;
1149
2211
 
1150
2212
  case 'segment':
1151
- // Tool use/segment info — reserved for future verbose display
2213
+ // Reserved for future verbose display
1152
2214
  break;
1153
2215
 
1154
2216
  case 'done':
1155
- if (stopRequested) {
1156
- // Stopped by user — finalize with what we have
1157
- stopRequested = false;
1158
- streaming = false;
1159
- if (streamBuffer) {
1160
- messages.push({ role: 'assistant', content: streamBuffer });
2217
+ if (data.threadName) {
2218
+ var state = getThreadState(data.threadName);
2219
+ if (state.stopRequested) {
2220
+ state.stopRequested = false;
2221
+ state.streaming = false;
2222
+ if (state.streamBuffer) {
2223
+ state.messages.push({ role: 'assistant', content: state.streamBuffer });
2224
+ }
2225
+ state.streamBuffer = '';
2226
+ updateThreadActivity(data.threadName);
2227
+ refreshThreadPanel(data.threadName);
2228
+ break;
1161
2229
  }
1162
- streamBuffer = '';
1163
- updateSendBtn();
1164
- renderMessages();
1165
- break;
2230
+ state.streaming = false;
2231
+ state.messages.push({ role: 'assistant', content: data.response || state.streamBuffer });
2232
+ state.streamBuffer = '';
2233
+ updateThreadActivity(data.threadName);
2234
+ refreshThreadPanel(data.threadName);
1166
2235
  }
1167
- streaming = false;
1168
- messages.push({ role: 'assistant', content: data.response || streamBuffer });
1169
- streamBuffer = '';
1170
- updateSendBtn();
1171
- renderMessages();
1172
2236
  break;
1173
2237
 
1174
2238
  case 'error':
1175
- stopRequested = false;
1176
- streaming = false;
1177
- if (streamBuffer) {
1178
- messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
1179
- } else {
1180
- messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
2239
+ if (data.threadName) {
2240
+ var state = getThreadState(data.threadName);
2241
+ state.stopRequested = false;
2242
+ state.streaming = false;
2243
+ if (state.streamBuffer) {
2244
+ state.messages.push({ role: 'assistant', content: state.streamBuffer + '\n\n[Error: ' + (data.error || 'Unknown error') + ']' });
2245
+ } else {
2246
+ state.messages.push({ role: 'assistant', content: '[Error: ' + (data.error || 'Unknown error') + ']' });
2247
+ }
2248
+ state.streamBuffer = '';
2249
+ refreshThreadPanel(data.threadName);
1181
2250
  }
1182
- streamBuffer = '';
1183
- updateSendBtn();
1184
- renderMessages();
1185
2251
  break;
1186
2252
  }
1187
2253
  }
1188
2254
 
1189
- function sendMessage() {
1190
- // If streaming, treat as Stop
1191
- if (streaming) {
1192
- stopRequested = true;
1193
- streaming = false;
1194
- updateSendBtn();
1195
- // Finalize current buffer
1196
- if (streamBuffer) {
1197
- messages.push({ role: 'assistant', content: streamBuffer + ' [stopped]' });
1198
- streamBuffer = '';
2255
+ function updateThreadActivity(threadName) {
2256
+ for (var i = 0; i < allThreads.length; i++) {
2257
+ if (allThreads[i].name === threadName) {
2258
+ allThreads[i].lastActivity = Date.now();
2259
+ break;
1199
2260
  }
1200
- renderMessages();
1201
- return;
1202
2261
  }
2262
+ // Re-render sidebar to reflect new sort order / move thread up
2263
+ renderSidebar();
2264
+ }
1203
2265
 
1204
- var text = input.value.trim();
1205
- if (!text && pendingAttachments.length === 0) return;
1206
- if (!connection) return;
1207
-
1208
- var attachSnapshot = pendingAttachments.slice();
1209
- var displayText = text || '(attachment)';
1210
-
1211
- messages.push({ role: 'user', content: displayText, attachments: attachSnapshot });
1212
- input.value = '';
1213
- input.style.height = 'auto';
1214
- pendingAttachments = [];
1215
- renderAttachStrip();
1216
- streaming = true;
1217
- stopRequested = false;
1218
- streamBuffer = '';
1219
- updateSendBtn();
1220
- renderMessages();
1221
-
1222
- var imagePayload = attachSnapshot
1223
- .filter(function (a) { return a.isImage; })
1224
- .map(function (a) { return { mimeType: a.mimeType, base64: a.base64 }; });
1225
-
1226
- var payload = {
1227
- type: 'message',
1228
- threadName: sessionId,
1229
- message: text || ' ',
1230
- };
1231
- if (imagePayload.length > 0) payload.images = imagePayload;
2266
+ // ── New thread creation ──
2267
+ var newThreadCreating = false;
1232
2268
 
1233
- connection.send(payload);
2269
+ function showNewThreadInput() {
2270
+ if (newThreadCreating) return;
2271
+ newThreadCreating = true;
2272
+ newThreadBtn.style.display = 'none';
2273
+ newThreadInput.style.display = 'block';
2274
+ newThreadInput.value = '';
2275
+ newThreadInput.focus();
1234
2276
  }
1235
2277
 
1236
- // ── Event listeners ──
1237
- sendBtn.addEventListener('click', sendMessage);
2278
+ function hideNewThreadInput() {
2279
+ newThreadCreating = false;
2280
+ newThreadBtn.style.display = 'block';
2281
+ newThreadInput.style.display = 'none';
2282
+ newThreadInput.value = '';
2283
+ }
1238
2284
 
1239
- input.addEventListener('keydown', function (e) {
1240
- if (e.key === 'Enter' && !e.shiftKey) {
1241
- e.preventDefault();
1242
- sendMessage();
2285
+ function submitNewThread() {
2286
+ var name = newThreadInput.value.trim();
2287
+ if (!name) { hideNewThreadInput(); return; }
2288
+ if (connection) {
2289
+ connection.send({ type: 'create_thread', threadName: name });
1243
2290
  }
1244
- });
1245
-
1246
- input.addEventListener('input', function () {
1247
- input.style.height = 'auto';
1248
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
1249
- });
1250
-
1251
- // Paste handler
1252
- input.addEventListener('paste', function (e) {
1253
- var items = e.clipboardData && e.clipboardData.items;
1254
- if (!items) return;
1255
-
1256
- var hasNonText = false;
1257
- var filesToAdd = [];
1258
- for (var i = 0; i < items.length; i++) {
1259
- var item = items[i];
1260
- if (item.kind === 'file') {
1261
- hasNonText = true;
1262
- var file = item.getAsFile();
1263
- if (file) filesToAdd.push(file);
1264
- }
2291
+ hideNewThreadInput();
2292
+ // Optimistically add and show (server will confirm with thread_created)
2293
+ var exists = false;
2294
+ for (var i = 0; i < allThreads.length; i++) {
2295
+ if (allThreads[i].name === name) { exists = true; break; }
1265
2296
  }
1266
-
1267
- if (filesToAdd.length > 0) {
1268
- e.preventDefault(); // prevent default paste of binary data
1269
- addFilesToPending(filesToAdd);
2297
+ if (!exists) {
2298
+ allThreads.push({ name: name, messageCount: 0, lastActivity: Date.now() });
1270
2299
  }
1271
- // If only text, let default browser paste behavior handle it
1272
- });
2300
+ soloThread(name);
2301
+ }
1273
2302
 
1274
- // File input change
1275
- fileInput.addEventListener('change', function () {
1276
- if (fileInput.files && fileInput.files.length > 0) {
1277
- addFilesToPending(Array.from(fileInput.files));
1278
- fileInput.value = ''; // reset so same file can be re-added
1279
- }
1280
- });
2303
+ newThreadBtn.addEventListener('click', showNewThreadInput);
1281
2304
 
1282
- // Attach button click
1283
- attachBtn.addEventListener('click', function () {
1284
- fileInput.click();
2305
+ newThreadInput.addEventListener('keydown', function (e) {
2306
+ if (e.key === 'Enter') { e.preventDefault(); submitNewThread(); }
2307
+ if (e.key === 'Escape') { hideNewThreadInput(); }
1285
2308
  });
1286
2309
 
1287
- // Bubble toggle (embedded mode)
1288
- if (bubble) {
1289
- bubble.addEventListener('click', function () {
1290
- var showing = panel.style.display !== 'none';
1291
- panel.style.display = showing ? 'none' : 'flex';
1292
- if (!showing && !connection) {
1293
- startConnection();
1294
- }
1295
- if (!showing) {
1296
- input.focus();
1297
- scrollToBottom();
1298
- }
1299
- });
1300
-
1301
- var closeBtn = panel.querySelector('.cumulus-close-btn');
1302
- if (closeBtn) {
1303
- closeBtn.addEventListener('click', function () {
1304
- panel.style.display = 'none';
1305
- });
1306
- }
1307
- }
2310
+ newThreadInput.addEventListener('blur', function () {
2311
+ // Small delay so click on submit doesn't race with blur
2312
+ setTimeout(function () {
2313
+ if (newThreadCreating) hideNewThreadInput();
2314
+ }, 150);
2315
+ });
1308
2316
 
1309
- // ── Auth panel events ──
2317
+ // ── Auth events ──
1310
2318
  var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
1311
2319
  var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
1312
2320
 
@@ -1325,20 +2333,13 @@
1325
2333
  if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
1326
2334
  });
1327
2335
 
1328
- // Logout (standalone only)
1329
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
2336
+ // ── Logout ──
2337
+ var logoutBtn = appLayout.querySelector('[data-testid="webchat-logout"]');
1330
2338
  if (logoutBtn) {
1331
2339
  logoutBtn.addEventListener('click', function () {
1332
2340
  clearStoredApiKey();
1333
2341
  activeApiKey = '';
1334
2342
  if (connection) { connection.close(); connection = null; }
1335
- messages = [];
1336
- streaming = false;
1337
- stopRequested = false;
1338
- streamBuffer = '';
1339
- pendingAttachments = [];
1340
- renderAttachStrip();
1341
- updateSendBtn();
1342
2343
  showAuthView();
1343
2344
  authInput.value = '';
1344
2345
  });
@@ -1353,28 +2354,28 @@
1353
2354
  sessionId: sessionId,
1354
2355
  onMessage: handleServerMessage,
1355
2356
  onStatus: updateStatus,
2357
+ skipHistory: true, // standalone manages history per-thread
1356
2358
  });
1357
2359
  }
1358
2360
 
1359
2361
  // ── Mount / Destroy ──
1360
2362
  function mount(target) {
1361
2363
  (target || document.body).appendChild(container);
1362
- if (standalone) {
1363
- if (activeApiKey) {
1364
- startConnection();
1365
- } else {
1366
- showAuthView();
1367
- authInput.focus();
1368
- }
2364
+ if (activeApiKey) {
2365
+ startConnection();
2366
+ } else {
2367
+ showAuthView();
2368
+ authInput.focus();
1369
2369
  }
1370
2370
  }
1371
2371
 
1372
2372
  function destroy() {
1373
2373
  if (connection) connection.close();
2374
+ if (refreshTimer) clearInterval(refreshTimer);
1374
2375
  container.remove();
1375
2376
  }
1376
2377
 
1377
- return { mount: mount, destroy: destroy, send: sendMessage };
2378
+ return { mount: mount, destroy: destroy };
1378
2379
  }
1379
2380
 
1380
2381
  // ─── Auto-mount ──────────────────────────────────────────────────────────────