@luckydraw/cumulus 0.15.1 → 0.16.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.
@@ -3,29 +3,27 @@
3
3
  * Embeddable via <script src="/widget.js" data-api-key="..."></script>
4
4
  * or usable standalone at /chat.
5
5
  *
6
- * < 50KB unminified.
6
+ * < 80KB unminified.
7
7
  */
8
8
  (function () {
9
9
  'use strict';
10
10
 
11
- // ─── Configuration ──────────────────────────────────────────
12
- const script = document.currentScript;
13
- const API_KEY = script?.getAttribute('data-api-key') || '';
14
- const STANDALONE = script?.getAttribute('data-standalone') === 'true';
15
- const WS_URL_OVERRIDE = script?.getAttribute('data-ws-url') || '';
11
+ // ─── Configuration ──────────────────────────────────────────────────────────
12
+ var script = document.currentScript;
13
+ var API_KEY = (script && script.getAttribute('data-api-key')) || '';
14
+ var STANDALONE = (script && script.getAttribute('data-standalone')) === 'true';
15
+ var WS_URL_OVERRIDE = (script && script.getAttribute('data-ws-url')) || '';
16
16
 
17
- // Derive WebSocket URL from script src or page location
18
17
  function getWsUrl() {
19
18
  if (WS_URL_OVERRIDE) return WS_URL_OVERRIDE;
20
- const loc = window.location;
21
- const proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
22
- return `${proto}//${loc.host}/ws`;
19
+ var loc = window.location;
20
+ var proto = loc.protocol === 'https:' ? 'wss:' : 'ws:';
21
+ return proto + '//' + loc.host + '/ws';
23
22
  }
24
23
 
25
- // Session ID — persisted in localStorage for returning visitors
26
24
  function getSessionId() {
27
- const key = 'cumulus-session-id';
28
- let id = localStorage.getItem(key);
25
+ var key = 'cumulus-session-id';
26
+ var id = localStorage.getItem(key);
29
27
  if (!id) {
30
28
  id = 'web-' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
31
29
  localStorage.setItem(key, id);
@@ -33,366 +31,841 @@
33
31
  return id;
34
32
  }
35
33
 
36
- // API key — persisted in localStorage for standalone mode
37
34
  function getStoredApiKey() {
38
35
  return localStorage.getItem('cumulus-api-key') || '';
39
36
  }
40
- function setStoredApiKey(key) {
41
- localStorage.setItem('cumulus-api-key', key);
37
+ function setStoredApiKey(k) {
38
+ localStorage.setItem('cumulus-api-key', k);
42
39
  }
43
40
  function clearStoredApiKey() {
44
41
  localStorage.removeItem('cumulus-api-key');
45
42
  }
46
43
 
47
- // ─── Styles ─────────────────────────────────────────────────
48
- const STYLES = `
49
- .cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }
50
- .cumulus-widget {
51
- font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-serif;
52
- font-size: 14px;
53
- line-height: 1.5;
54
- color: #e0e0e0;
55
- }
56
-
57
- /* Floating bubble (embeddable mode only) */
58
- .cumulus-bubble {
59
- position: fixed;
60
- bottom: 24px;
61
- right: 24px;
62
- width: 56px;
63
- height: 56px;
64
- border-radius: 50%;
65
- background: #6366f1;
66
- border: none;
67
- cursor: pointer;
68
- box-shadow: 0 4px 12px rgba(0,0,0,0.3);
69
- display: flex;
70
- align-items: center;
71
- justify-content: center;
72
- z-index: 10000;
73
- transition: transform 0.15s ease;
74
- }
75
- .cumulus-bubble:hover { transform: scale(1.08); }
76
- .cumulus-bubble svg { width: 28px; height: 28px; fill: white; }
77
-
78
- /* Panel container */
79
- .cumulus-panel {
80
- background: #1a1a2e;
81
- border: 1px solid #2a2a4a;
82
- border-radius: 12px;
83
- display: flex;
84
- flex-direction: column;
85
- overflow: hidden;
86
- }
87
- .cumulus-panel.floating {
88
- position: fixed;
89
- bottom: 92px;
90
- right: 24px;
91
- width: 400px;
92
- height: 560px;
93
- z-index: 10000;
94
- box-shadow: 0 8px 32px rgba(0,0,0,0.5);
95
- }
96
- .cumulus-panel.standalone {
97
- width: 100%;
98
- height: 100vh;
99
- border: none;
100
- border-radius: 0;
101
- }
102
-
103
- /* Header */
104
- .cumulus-header {
105
- display: flex;
106
- align-items: center;
107
- justify-content: space-between;
108
- padding: 12px 16px;
109
- background: #16162b;
110
- border-bottom: 1px solid #2a2a4a;
111
- }
112
- .cumulus-header-title {
113
- font-weight: 600;
114
- font-size: 15px;
115
- color: #f0f0ff;
116
- }
117
- .cumulus-header-status {
118
- font-size: 11px;
119
- display: flex;
120
- align-items: center;
121
- gap: 6px;
122
- }
123
- .cumulus-status-dot {
124
- width: 8px;
125
- height: 8px;
126
- border-radius: 50%;
127
- background: #666;
128
- }
129
- .cumulus-status-dot.connected { background: #22c55e; }
130
- .cumulus-status-dot.connecting { background: #f59e0b; }
131
- .cumulus-status-dot.disconnected { background: #ef4444; }
132
- .cumulus-close-btn {
133
- background: none;
134
- border: none;
135
- color: #888;
136
- cursor: pointer;
137
- font-size: 20px;
138
- padding: 4px;
139
- line-height: 1;
140
- }
141
- .cumulus-close-btn:hover { color: #fff; }
142
-
143
- /* Messages area */
144
- .cumulus-messages {
145
- flex: 1;
146
- overflow-y: auto;
147
- padding: 16px;
148
- display: flex;
149
- flex-direction: column;
150
- gap: 12px;
151
- }
152
- .cumulus-messages::-webkit-scrollbar { width: 6px; }
153
- .cumulus-messages::-webkit-scrollbar-track { background: transparent; }
154
- .cumulus-messages::-webkit-scrollbar-thumb { background: #3a3a5a; border-radius: 3px; }
155
-
156
- /* Message bubbles */
157
- .cumulus-msg {
158
- max-width: 85%;
159
- padding: 10px 14px;
160
- border-radius: 12px;
161
- word-wrap: break-word;
162
- white-space: pre-wrap;
163
- }
164
- .cumulus-msg.user {
165
- align-self: flex-end;
166
- background: #4f46e5;
167
- color: #fff;
168
- border-bottom-right-radius: 4px;
169
- }
170
- .cumulus-msg.assistant {
171
- align-self: flex-start;
172
- background: #2a2a4a;
173
- color: #e0e0e0;
174
- border-bottom-left-radius: 4px;
175
- }
176
-
177
- /* Streaming cursor */
178
- .cumulus-cursor {
179
- display: inline-block;
180
- width: 2px;
181
- height: 1em;
182
- background: #6366f1;
183
- margin-left: 2px;
184
- vertical-align: text-bottom;
185
- animation: cumulus-blink 0.8s step-end infinite;
186
- }
187
- @keyframes cumulus-blink { 50% { opacity: 0; } }
188
-
189
- /* Loading indicator */
190
- .cumulus-loading {
191
- align-self: flex-start;
192
- padding: 10px 14px;
193
- color: #888;
194
- font-style: italic;
195
- }
196
-
197
- /* Markdown basics */
198
- .cumulus-msg code {
199
- background: rgba(255,255,255,0.1);
200
- padding: 2px 6px;
201
- border-radius: 4px;
202
- font-family: 'SF Mono', 'Fira Code', monospace;
203
- font-size: 13px;
204
- }
205
- .cumulus-msg pre {
206
- background: #0d0d1a;
207
- padding: 12px;
208
- border-radius: 8px;
209
- overflow-x: auto;
210
- margin: 8px 0;
211
- }
212
- .cumulus-msg pre code {
213
- background: none;
214
- padding: 0;
215
- display: block;
216
- }
217
- .cumulus-msg a { color: #818cf8; }
218
- .cumulus-msg strong { font-weight: 600; }
219
- .cumulus-msg em { font-style: italic; }
220
-
221
- /* Input area */
222
- .cumulus-input-area {
223
- display: flex;
224
- gap: 8px;
225
- padding: 12px 16px;
226
- background: #16162b;
227
- border-top: 1px solid #2a2a4a;
228
- }
229
- .cumulus-input {
230
- flex: 1;
231
- background: #1a1a2e;
232
- border: 1px solid #3a3a5a;
233
- border-radius: 8px;
234
- color: #e0e0e0;
235
- padding: 10px 12px;
236
- font-size: 14px;
237
- font-family: inherit;
238
- resize: none;
239
- min-height: 40px;
240
- max-height: 120px;
241
- outline: none;
242
- }
243
- .cumulus-input:focus { border-color: #6366f1; }
244
- .cumulus-input::placeholder { color: #666; }
245
- .cumulus-send-btn {
246
- background: #6366f1;
247
- border: none;
248
- border-radius: 8px;
249
- color: white;
250
- padding: 0 16px;
251
- cursor: pointer;
252
- font-size: 14px;
253
- font-weight: 500;
254
- white-space: nowrap;
255
- }
256
- .cumulus-send-btn:hover { background: #5558e6; }
257
- .cumulus-send-btn:disabled { opacity: 0.5; cursor: not-allowed; }
258
-
259
- /* Empty state */
260
- .cumulus-empty {
261
- display: flex;
262
- align-items: center;
263
- justify-content: center;
264
- flex: 1;
265
- color: #555;
266
- font-size: 15px;
267
- text-align: center;
268
- padding: 20px;
269
- }
270
-
271
- /* Auth panel */
272
- .cumulus-auth {
273
- display: flex;
274
- flex-direction: column;
275
- align-items: center;
276
- justify-content: center;
277
- flex: 1;
278
- padding: 32px;
279
- gap: 16px;
280
- }
281
- .cumulus-auth-title {
282
- font-size: 20px;
283
- font-weight: 600;
284
- color: #f0f0ff;
285
- }
286
- .cumulus-auth-subtitle {
287
- color: #888;
288
- font-size: 13px;
289
- text-align: center;
290
- max-width: 280px;
291
- }
292
- .cumulus-auth-input {
293
- width: 100%;
294
- max-width: 320px;
295
- background: #1a1a2e;
296
- border: 1px solid #3a3a5a;
297
- border-radius: 8px;
298
- color: #e0e0e0;
299
- padding: 12px 14px;
300
- font-size: 14px;
301
- font-family: 'SF Mono', 'Fira Code', monospace;
302
- outline: none;
303
- }
304
- .cumulus-auth-input:focus { border-color: #6366f1; }
305
- .cumulus-auth-input::placeholder { color: #555; }
306
- .cumulus-auth-btn {
307
- background: #6366f1;
308
- border: none;
309
- border-radius: 8px;
310
- color: white;
311
- padding: 10px 32px;
312
- cursor: pointer;
313
- font-size: 14px;
314
- font-weight: 500;
315
- }
316
- .cumulus-auth-btn:hover { background: #5558e6; }
317
- .cumulus-auth-btn:disabled { opacity: 0.5; cursor: not-allowed; }
318
- .cumulus-auth-error {
319
- color: #ef4444;
320
- font-size: 13px;
321
- text-align: center;
322
- }
323
- .cumulus-auth-logout {
324
- background: none;
325
- border: none;
326
- color: #666;
327
- cursor: pointer;
328
- font-size: 11px;
329
- padding: 2px 6px;
330
- }
331
- .cumulus-auth-logout:hover { color: #ef4444; }
332
- `;
333
-
334
- // ─── Simple Markdown Renderer ───────────────────────────────
44
+ // ─── Styles ─────────────────────────────────────────────────────────────────
45
+ var STYLES = [
46
+ '.cumulus-widget * { box-sizing: border-box; margin: 0; padding: 0; }',
47
+
48
+ '.cumulus-widget {',
49
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
50
+ ' font-size: 14px;',
51
+ ' line-height: 1.6;',
52
+ ' color: #e0e0e0;',
53
+ '}',
54
+
55
+ /* ── Floating bubble ── */
56
+ '.cumulus-bubble {',
57
+ ' position: fixed; bottom: 24px; right: 24px;',
58
+ ' width: 56px; height: 56px;',
59
+ ' border-radius: 50%;',
60
+ ' background: #0066cc;',
61
+ ' border: none; cursor: pointer;',
62
+ ' box-shadow: 0 4px 14px rgba(0,0,0,0.45);',
63
+ ' display: flex; align-items: center; justify-content: center;',
64
+ ' z-index: 10000;',
65
+ ' transition: background 0.15s ease, transform 0.15s ease;',
66
+ '}',
67
+ '.cumulus-bubble:hover { background: #0077ee; transform: scale(1.07); }',
68
+ '.cumulus-bubble svg { width: 26px; height: 26px; fill: white; }',
69
+
70
+ /* ── Panel ── */
71
+ '.cumulus-panel {',
72
+ ' background: #1e1e1e;',
73
+ ' border: 1px solid #3a3a3a;',
74
+ ' display: flex; flex-direction: column; overflow: hidden;',
75
+ '}',
76
+ '.cumulus-panel.floating {',
77
+ ' position: fixed; bottom: 90px; right: 24px;',
78
+ ' width: 420px; height: 580px;',
79
+ ' border-radius: 10px;',
80
+ ' z-index: 10000;',
81
+ ' box-shadow: 0 10px 36px rgba(0,0,0,0.55);',
82
+ '}',
83
+ '.cumulus-panel.standalone {',
84
+ ' width: 100%; height: 100vh;',
85
+ ' border: none; border-radius: 0;',
86
+ '}',
87
+
88
+ /* ── Header ── */
89
+ '.cumulus-header {',
90
+ ' display: flex; align-items: center; justify-content: space-between;',
91
+ ' padding: 0.4em 0.85em;',
92
+ ' background: #2d2d2d;',
93
+ ' border-bottom: 1px solid #3a3a3a;',
94
+ ' flex-shrink: 0;',
95
+ '}',
96
+ '.cumulus-header-title { font-weight: 600; font-size: 15px; color: #e0e0e0; }',
97
+ '.cumulus-header-status { font-size: 11px; display: flex; align-items: center; gap: 6px; color: #aaa; }',
98
+ '.cumulus-status-dot { width: 8px; height: 8px; border-radius: 50%; background: #555; }',
99
+ '.cumulus-status-dot.connected { background: #22c55e; }',
100
+ '.cumulus-status-dot.connecting { background: #f59e0b; }',
101
+ '.cumulus-status-dot.disconnected { background: #ef4444; }',
102
+ '.cumulus-close-btn { background: none; border: none; color: #888; cursor: pointer; font-size: 20px; padding: 2px 4px; line-height: 1; }',
103
+ '.cumulus-close-btn:hover { color: #e0e0e0; }',
104
+
105
+ /* ── Messages scroll area ── */
106
+ '.cumulus-messages {',
107
+ ' flex: 1; overflow-y: auto;',
108
+ ' padding: 1.5em 1em;',
109
+ ' display: flex; flex-direction: column;',
110
+ ' gap: 1.5em;',
111
+ '}',
112
+ '.cumulus-messages::-webkit-scrollbar { width: 6px; }',
113
+ '.cumulus-messages::-webkit-scrollbar-track { background: transparent; }',
114
+ '.cumulus-messages::-webkit-scrollbar-thumb { background: #444; border-radius: 3px; }',
115
+
116
+ /* ── Message inner container (max-width centering) ── */
117
+ '.cumulus-msg-row {',
118
+ ' display: flex; flex-direction: column;',
119
+ ' max-width: 78ch; width: 100%;',
120
+ ' margin: 0 auto;',
121
+ '}',
122
+
123
+ /* ── User messages ── */
124
+ '.cumulus-msg.user {',
125
+ ' align-self: flex-end;',
126
+ ' background: #2a3a4a;',
127
+ ' color: #d4e8f8;',
128
+ ' border: 1px solid #3a5060;',
129
+ ' border-radius: 0.85em 0.85em 0.25em 0.85em;',
130
+ ' padding: 0.55em 0.9em;',
131
+ ' max-width: 42ch;',
132
+ ' word-wrap: break-word;',
133
+ ' white-space: pre-wrap;',
134
+ '}',
135
+ '.cumulus-msg.user.wide { max-width: 100%; }',
136
+
137
+ /* ── User message attachments ── */
138
+ '.cumulus-msg-attachments { display: flex; flex-wrap: wrap; gap: 0.4em; margin-top: 0.4em; }',
139
+ '.cumulus-msg-img { max-width: 16em; max-height: 12em; border-radius: 0.3em; object-fit: cover; border: 1px solid #3a5060; display: block; }',
140
+ '.cumulus-msg-file-badge {',
141
+ ' display: inline-flex; align-items: center; gap: 0.3em;',
142
+ ' background: #1e2a35; border: 1px solid #3a5060; border-radius: 0.3em;',
143
+ ' padding: 0.2em 0.5em; font-size: 0.8em; color: #8ab4cc;',
144
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
145
+ '}',
146
+
147
+ /* ── Assistant messages ── */
148
+ '.cumulus-msg.assistant {',
149
+ ' align-self: flex-start;',
150
+ ' background: transparent;',
151
+ ' color: #e0e0e0;',
152
+ ' padding: 0.4em 0;',
153
+ ' width: 100%;',
154
+ ' word-wrap: break-word;',
155
+ '}',
156
+
157
+ /* ── Markdown rendered inside assistant messages ── */
158
+ '.cumulus-msg.assistant p { margin: 0.4em 0; }',
159
+ '.cumulus-msg.assistant p:first-child { margin-top: 0; }',
160
+ '.cumulus-msg.assistant p:last-child { margin-bottom: 0; }',
161
+
162
+ '.cumulus-msg a { color: #4da6ff; text-decoration: none; }',
163
+ '.cumulus-msg a:hover { text-decoration: underline; }',
164
+ '.cumulus-msg strong { font-weight: 600; }',
165
+ '.cumulus-msg em { font-style: italic; }',
166
+ '.cumulus-msg del { text-decoration: line-through; color: #888; }',
167
+
168
+ /* Headings */
169
+ '.cumulus-msg h1,.cumulus-msg h2,.cumulus-msg h3,.cumulus-msg h4,.cumulus-msg h5,.cumulus-msg h6 {',
170
+ ' font-weight: 600; margin: 0.7em 0 0.3em; line-height: 1.3; color: #e8e8e8;',
171
+ '}',
172
+ '.cumulus-msg h1 { font-size: 1.55em; border-bottom: 1px solid #3a3a3a; padding-bottom: 0.2em; }',
173
+ '.cumulus-msg h2 { font-size: 1.3em; border-bottom: 1px solid #3a3a3a; padding-bottom: 0.2em; }',
174
+ '.cumulus-msg h3 { font-size: 1.12em; }',
175
+ '.cumulus-msg h4 { font-size: 1em; }',
176
+ '.cumulus-msg h5 { font-size: 0.9em; }',
177
+ '.cumulus-msg h6 { font-size: 0.85em; color: #aaa; }',
178
+
179
+ /* Horizontal rule */
180
+ '.cumulus-msg hr { border: none; border-top: 1px solid #3a3a3a; margin: 0.75em 0; }',
181
+
182
+ /* Blockquote */
183
+ '.cumulus-msg blockquote {',
184
+ ' background: #252525;',
185
+ ' border-left: 0.2em solid #0066cc;',
186
+ ' margin: 0.5em 0;',
187
+ ' padding: 0.4em 0.8em;',
188
+ ' border-radius: 0 0.25em 0.25em 0;',
189
+ ' color: #bbb;',
190
+ '}',
191
+
192
+ /* Lists */
193
+ '.cumulus-msg ul,.cumulus-msg ol { padding-left: 1.5em; margin: 0.4em 0; }',
194
+ '.cumulus-msg li { margin: 0.15em 0; }',
195
+ '.cumulus-msg li > ul,.cumulus-msg li > ol { margin: 0.1em 0; }',
196
+
197
+ /* Inline code */
198
+ '.cumulus-msg code {',
199
+ ' background: #2a2a2a;',
200
+ ' border: 1px solid #3a3a3a;',
201
+ ' border-radius: 0.25em;',
202
+ ' color: #ce9178;',
203
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
204
+ ' font-size: 0.875em;',
205
+ ' padding: 0.1em 0.35em;',
206
+ '}',
207
+
208
+ /* Code block wrapper */
209
+ '.code-block-wrapper {',
210
+ ' border: 1px solid #3a3a3a;',
211
+ ' border-radius: 0.4em;',
212
+ ' background: #1e1e1e;',
213
+ ' overflow: hidden;',
214
+ ' margin: 0.6em 0;',
215
+ '}',
216
+ '.code-block-header {',
217
+ ' background: #3d3d3d;',
218
+ ' padding: 0.35em 0.85em;',
219
+ ' border-bottom: 1px solid #4a4a4a;',
220
+ ' display: flex; justify-content: space-between; align-items: center;',
221
+ '}',
222
+ '.code-block-language {',
223
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
224
+ ' font-size: 0.8em; color: #aaa; text-transform: lowercase;',
225
+ '}',
226
+ '.code-block-copy-btn {',
227
+ ' background: transparent;',
228
+ ' border: 1px solid #555;',
229
+ ' border-radius: 0.3em;',
230
+ ' color: #aaa;',
231
+ ' font-size: 0.8em;',
232
+ ' padding: 0.15em 0.55em;',
233
+ ' cursor: pointer;',
234
+ ' font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;',
235
+ '}',
236
+ '.code-block-copy-btn:hover { border-color: #0066cc; color: #ddd; }',
237
+ '.code-block-wrapper pre {',
238
+ ' padding: 1em 1.15em; margin: 0;',
239
+ ' overflow-x: auto;',
240
+ ' font-size: 0.925em; line-height: 1.5;',
241
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
242
+ '}',
243
+ '.code-block-wrapper pre code {',
244
+ ' background: none; border: none; color: #e0e0e0;',
245
+ ' padding: 0; font-size: inherit;',
246
+ '}',
247
+
248
+ /* Tables */
249
+ '.cumulus-msg table {',
250
+ ' border-collapse: collapse; width: 100%;',
251
+ ' margin: 0.6em 0; font-size: 0.93em;',
252
+ '}',
253
+ '.cumulus-msg th {',
254
+ ' background: #2a2a2a;',
255
+ ' border: 1px solid #3a3a3a;',
256
+ ' padding: 0.4em 0.7em;',
257
+ ' text-align: left; font-weight: 600; color: #ccc;',
258
+ '}',
259
+ '.cumulus-msg td {',
260
+ ' border: 1px solid #3a3a3a;',
261
+ ' padding: 0.35em 0.7em; color: #ddd;',
262
+ '}',
263
+ '.cumulus-msg tr:nth-child(even) td { background: #222; }',
264
+ '.cumulus-msg tr:nth-child(odd) td { background: #1e1e1e; }',
265
+
266
+ /* ── Streaming cursor ── */
267
+ '.cumulus-cursor {',
268
+ ' display: inline-block;',
269
+ ' width: 0.55em; height: 1em;',
270
+ ' background: #0066cc;',
271
+ ' margin-left: 2px;',
272
+ ' vertical-align: text-bottom;',
273
+ ' animation: cumulus-blink 0.8s step-end infinite;',
274
+ '}',
275
+ '@keyframes cumulus-blink { 50% { opacity: 0; } }',
276
+
277
+ /* ── Loading indicator ── */
278
+ '.cumulus-loading {',
279
+ ' color: #666; font-style: italic; padding: 0.4em 0;',
280
+ ' align-self: flex-start;',
281
+ '}',
282
+
283
+ /* ── Empty state ── */
284
+ '.cumulus-empty {',
285
+ ' display: flex; align-items: center; justify-content: center;',
286
+ ' flex: 1; color: #555; font-size: 15px;',
287
+ ' text-align: center; padding: 20px;',
288
+ '}',
289
+
290
+ /* ── Input area ── */
291
+ '.cumulus-input-area {',
292
+ ' display: flex; flex-direction: column;',
293
+ ' padding: 0.6em 0.85em 0.75em;',
294
+ ' background: #2d2d2d;',
295
+ ' border-top: 1px solid #3a3a3a;',
296
+ ' gap: 0.45em;',
297
+ ' flex-shrink: 0;',
298
+ '}',
299
+
300
+ /* Attachment strip */
301
+ '.cumulus-attach-strip {',
302
+ ' display: flex; flex-direction: row;',
303
+ ' gap: 0.5em; overflow-x: auto;',
304
+ ' max-width: 78ch; align-self: center; width: 100%;',
305
+ ' padding-bottom: 0.1em;',
306
+ '}',
307
+ '.cumulus-attach-strip::-webkit-scrollbar { height: 4px; }',
308
+ '.cumulus-attach-strip::-webkit-scrollbar-thumb { background: #444; border-radius: 2px; }',
309
+
310
+ /* Attachment chip */
311
+ '.cumulus-attach-chip {',
312
+ ' position: relative;',
313
+ ' display: flex; flex-direction: column; align-items: center;',
314
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
315
+ ' border-radius: 0.4em; padding: 0.3em;',
316
+ ' flex-shrink: 0; width: 5.5em; overflow: hidden;',
317
+ '}',
318
+ '.cumulus-attach-chip-thumb {',
319
+ ' width: 3em; height: 3em; object-fit: cover;',
320
+ ' border-radius: 0.25em; display: block;',
321
+ '}',
322
+ '.cumulus-attach-chip-icon {',
323
+ ' width: 3em; height: 3em;',
324
+ ' background: #2a2a2a; border-radius: 0.25em;',
325
+ ' display: flex; align-items: center; justify-content: center;',
326
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
327
+ ' font-size: 0.65em; color: #aaa; text-transform: uppercase;',
328
+ '}',
329
+ '.cumulus-attach-chip-name {',
330
+ ' font-size: 0.72em; color: #ccc;',
331
+ ' white-space: nowrap; overflow: hidden; text-overflow: ellipsis;',
332
+ ' width: 100%; text-align: center; margin-top: 0.2em;',
333
+ '}',
334
+ '.cumulus-attach-chip-remove {',
335
+ ' position: absolute; top: 0.1em; right: 0.15em;',
336
+ ' background: none; border: none; color: #888;',
337
+ ' font-size: 0.85em; line-height: 1; cursor: pointer; padding: 0 0.1em;',
338
+ '}',
339
+ '.cumulus-attach-chip-remove:hover { color: #ff6666; }',
340
+
341
+ /* Input row (attach btn + textarea + send btn) */
342
+ '.cumulus-input-row {',
343
+ ' display: flex; flex-direction: row;',
344
+ ' gap: 0.5em; align-items: flex-end;',
345
+ ' max-width: 78ch; align-self: center; width: 100%;',
346
+ '}',
347
+ '.cumulus-attach-btn {',
348
+ ' width: 2.7em; height: 2.7em; flex-shrink: 0;',
349
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
350
+ ' border-radius: 0.55em; color: #aaa;',
351
+ ' font-size: 1.4em; line-height: 1; cursor: pointer;',
352
+ ' display: flex; align-items: center; justify-content: center;',
353
+ ' padding: 0;',
354
+ '}',
355
+ '.cumulus-attach-btn:hover { border-color: #0066cc; color: #ddd; }',
356
+
357
+ '.cumulus-input {',
358
+ ' flex: 1;',
359
+ ' background: #3d3d3d;',
360
+ ' border: 1px solid #4a4a4a;',
361
+ ' border-radius: 0.55em;',
362
+ ' color: #e0e0e0;',
363
+ ' padding: 0.55em 0.75em;',
364
+ ' font-size: 14px;',
365
+ ' font-family: inherit;',
366
+ ' resize: none;',
367
+ ' min-height: 2.7em;',
368
+ ' max-height: 120px;',
369
+ ' outline: none;',
370
+ ' line-height: 1.5;',
371
+ '}',
372
+ '.cumulus-input:focus { border-color: #0066cc; }',
373
+ '.cumulus-input::placeholder { color: #666; }',
374
+
375
+ '.cumulus-send-btn {',
376
+ ' background: #0066cc;',
377
+ ' border: none;',
378
+ ' border-radius: 0.55em;',
379
+ ' color: white;',
380
+ ' padding: 0 1em;',
381
+ ' height: 2.7em;',
382
+ ' flex-shrink: 0;',
383
+ ' cursor: pointer;',
384
+ ' font-size: 14px;',
385
+ ' font-weight: 600;',
386
+ ' font-family: inherit;',
387
+ ' white-space: nowrap;',
388
+ '}',
389
+ '.cumulus-send-btn:hover:not(:disabled) { background: #0077ee; }',
390
+ '.cumulus-send-btn:disabled { background: #334455; color: #668; cursor: not-allowed; }',
391
+ '.cumulus-send-btn.stop { background: #8b2222; color: #ffcccc; }',
392
+ '.cumulus-send-btn.stop:hover:not(:disabled) { background: #a02828; }',
393
+
394
+ /* ── Auth panel ── */
395
+ '.cumulus-auth {',
396
+ ' display: flex; flex-direction: column;',
397
+ ' align-items: center; justify-content: center;',
398
+ ' flex: 1; padding: 2em 2em; gap: 1em;',
399
+ '}',
400
+ '.cumulus-auth-title { font-size: 20px; font-weight: 600; color: #e0e0e0; }',
401
+ '.cumulus-auth-subtitle { color: #888; font-size: 13px; text-align: center; max-width: 280px; }',
402
+ '.cumulus-auth-input {',
403
+ ' width: 100%; max-width: 320px;',
404
+ ' background: #3d3d3d; border: 1px solid #4a4a4a;',
405
+ ' border-radius: 0.55em; color: #e0e0e0;',
406
+ ' padding: 0.7em 0.85em; font-size: 14px;',
407
+ ' font-family: "SF Mono", "Fira Code", "Cascadia Code", Consolas, monospace;',
408
+ ' outline: none;',
409
+ '}',
410
+ '.cumulus-auth-input:focus { border-color: #0066cc; }',
411
+ '.cumulus-auth-input::placeholder { color: #555; }',
412
+ '.cumulus-auth-btn {',
413
+ ' background: #0066cc; border: none; border-radius: 0.55em;',
414
+ ' color: white; padding: 0.6em 2em;',
415
+ ' cursor: pointer; font-size: 14px; font-weight: 600; font-family: inherit;',
416
+ '}',
417
+ '.cumulus-auth-btn:hover { background: #0077ee; }',
418
+ '.cumulus-auth-btn:disabled { background: #334455; color: #668; cursor: not-allowed; }',
419
+ '.cumulus-auth-error { color: #ef4444; font-size: 13px; text-align: center; min-height: 1.2em; }',
420
+ '.cumulus-auth-logout {',
421
+ ' background: none; border: none; color: #666;',
422
+ ' cursor: pointer; font-size: 11px; padding: 2px 6px; font-family: inherit;',
423
+ '}',
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; }',
577
+ ].join('\n');
578
+
579
+ // ─── HTML Escaping ───────────────────────────────────────────────────────────
580
+ function escapeHtml(text) {
581
+ var div = document.createElement('div');
582
+ div.textContent = text;
583
+ return div.innerHTML;
584
+ }
585
+
586
+ // ─── Markdown Renderer ───────────────────────────────────────────────────────
587
+ // Uses a placeholder strategy: extract code blocks first, render everything
588
+ // else, then reinsert code blocks. This prevents markdown patterns inside
589
+ // code from being processed.
590
+
335
591
  function renderMarkdown(text) {
336
- let html = escapeHtml(text);
592
+ // Phase 1: Extract fenced code blocks — replace with unique tokens
593
+ var codeBlocks = [];
594
+ var TOKEN_PREFIX = '\x00CODEBLOCK_';
595
+ var processed = text.replace(/```([^\n`]*)\n([\s\S]*?)```/g, function (_, lang, code) {
596
+ var idx = codeBlocks.length;
597
+ var langLabel = lang.trim() || 'text';
598
+ var escapedCode = escapeHtml(code.replace(/\n$/, '')); // trim trailing newline
599
+ var tokenId = 'cb' + idx;
600
+ codeBlocks.push(buildCodeBlock(langLabel, escapedCode, tokenId));
601
+ return TOKEN_PREFIX + idx + '\x00';
602
+ });
337
603
 
338
- // Code blocks (``` ... ```)
339
- html = html.replace(/```(\w*)\n([\s\S]*?)```/g, function (_, lang, code) {
340
- return '<pre><code>' + code + '</code></pre>';
604
+ // Phase 2: Escape HTML in the non-code portions
605
+ // We need to escape line by line between tokens
606
+ var parts = processed.split(/(\x00CODEBLOCK_\d+\x00)/);
607
+ var escapedParts = parts.map(function (part) {
608
+ if (/^\x00CODEBLOCK_\d+\x00$/.test(part)) return part; // keep token as-is
609
+ return escapeHtml(part);
341
610
  });
611
+ var html = escapedParts.join('');
342
612
 
343
- // Inline code
344
- html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
613
+ // Phase 3: Process tables (before other inline markdown)
614
+ html = renderTables(html);
615
+
616
+ // Phase 4: Process block-level markdown elements
617
+ // Horizontal rules
618
+ html = html.replace(/^[ \t]*(---+|___+|\*\*\*+)[ \t]*$/gm, '<hr>');
619
+
620
+ // Headers
621
+ html = html.replace(/^######[ \t]+(.+)$/gm, '<h6>$1</h6>');
622
+ html = html.replace(/^#####[ \t]+(.+)$/gm, '<h5>$1</h5>');
623
+ html = html.replace(/^####[ \t]+(.+)$/gm, '<h4>$1</h4>');
624
+ html = html.replace(/^###[ \t]+(.+)$/gm, '<h3>$1</h3>');
625
+ html = html.replace(/^##[ \t]+(.+)$/gm, '<h2>$1</h2>');
626
+ html = html.replace(/^#[ \t]+(.+)$/gm, '<h1>$1</h1>');
627
+
628
+ // Blockquotes (simple single-level)
629
+ html = renderBlockquotes(html);
345
630
 
346
- // Bold
631
+ // Lists
632
+ html = renderLists(html);
633
+
634
+ // Phase 5: Inline markdown
635
+ // Bold (** and __)
347
636
  html = html.replace(/\*\*(.+?)\*\*/g, '<strong>$1</strong>');
637
+ html = html.replace(/__(.+?)__/g, '<strong>$1</strong>');
638
+
639
+ // Italic (* and _) — careful not to match inside words for _
640
+ html = html.replace(/\*(?!\s)(.+?)(?<!\s)\*/g, '<em>$1</em>');
641
+ html = html.replace(/(?<!\w)_(?!\s)(.+?)(?<!\s)_(?!\w)/g, '<em>$1</em>');
642
+
643
+ // Strikethrough
644
+ html = html.replace(/~~(.+?)~~/g, '<del>$1</del>');
348
645
 
349
- // Italic
350
- html = html.replace(/\*(.+?)\*/g, '<em>$1</em>');
646
+ // Inline code (backtick)
647
+ html = html.replace(/`([^`]+)`/g, '<code>$1</code>');
351
648
 
352
649
  // Links
353
650
  html = html.replace(
354
651
  /\[([^\]]+)\]\(([^)]+)\)/g,
355
- '<a href="$2" target="_blank" rel="noopener">$1</a>'
652
+ '<a href="$2" target="_blank" rel="noopener noreferrer">$1</a>'
356
653
  );
357
654
 
358
- // Line breaks
359
- html = html.replace(/\n/g, '<br>');
655
+ // Phase 6: Convert remaining newlines to <br> (skip block-level elements)
656
+ // Wrap sequences of plain text lines in <p> tags
657
+ html = renderParagraphs(html);
658
+
659
+ // Phase 7: Reinsert code blocks
660
+ html = html.replace(/\x00CODEBLOCK_(\d+)\x00/g, function (_, idx) {
661
+ return codeBlocks[parseInt(idx, 10)];
662
+ });
360
663
 
361
664
  return html;
362
665
  }
363
666
 
364
- function escapeHtml(text) {
365
- const div = document.createElement('div');
366
- div.textContent = text;
367
- return div.innerHTML;
667
+ function buildCodeBlock(lang, escapedCode, tokenId) {
668
+ return (
669
+ '<div class="code-block-wrapper">' +
670
+ '<div class="code-block-header">' +
671
+ '<span class="code-block-language">' + escapeHtml(lang) + '</span>' +
672
+ '<button class="code-block-copy-btn" data-copy-target="' + tokenId + '" data-testid="webchat-copy-code">Copy</button>' +
673
+ '</div>' +
674
+ '<pre><code data-code-id="' + tokenId + '">' + escapedCode + '</code></pre>' +
675
+ '</div>'
676
+ );
677
+ }
678
+
679
+ function renderTables(html) {
680
+ // Split on double newlines to find table blocks
681
+ // A table block: lines where most lines start with |
682
+ var lines = html.split('\n');
683
+ var result = [];
684
+ var i = 0;
685
+ while (i < lines.length) {
686
+ var line = lines[i];
687
+ // Check if this line looks like a table row
688
+ if (/^\|.+\|/.test(line.trim())) {
689
+ // Collect contiguous table lines
690
+ var tableLines = [];
691
+ while (i < lines.length && /^\|/.test(lines[i].trim())) {
692
+ tableLines.push(lines[i]);
693
+ i++;
694
+ }
695
+ result.push(buildTable(tableLines));
696
+ } else {
697
+ result.push(line);
698
+ i++;
699
+ }
700
+ }
701
+ return result.join('\n');
702
+ }
703
+
704
+ function buildTable(lines) {
705
+ // lines[0] = header row, lines[1] = separator row (|---|), lines[2+] = data rows
706
+ if (lines.length < 2) return lines.join('\n');
707
+
708
+ var isSepRow = /^\|[\s|:-]+\|$/.test(lines[1].trim());
709
+ if (!isSepRow) return lines.join('\n');
710
+
711
+ var headerCells = parseTableRow(lines[0]);
712
+ var dataRows = lines.slice(2);
713
+
714
+ var thead = '<thead><tr>' + headerCells.map(function (c) {
715
+ return '<th>' + c + '</th>';
716
+ }).join('') + '</tr></thead>';
717
+
718
+ var tbody = '<tbody>' + dataRows.map(function (row) {
719
+ var cells = parseTableRow(row);
720
+ return '<tr>' + cells.map(function (c) {
721
+ return '<td>' + c + '</td>';
722
+ }).join('') + '</tr>';
723
+ }).join('') + '</tbody>';
724
+
725
+ return '<table>' + thead + tbody + '</table>';
726
+ }
727
+
728
+ function parseTableRow(line) {
729
+ // Split on | and trim, filter empty first/last from leading/trailing |
730
+ var parts = line.split('|');
731
+ // Remove first and last if empty (from leading/trailing |)
732
+ if (parts.length > 0 && parts[0].trim() === '') parts = parts.slice(1);
733
+ if (parts.length > 0 && parts[parts.length - 1].trim() === '') parts = parts.slice(0, -1);
734
+ return parts.map(function (c) { return c.trim(); });
735
+ }
736
+
737
+ function renderBlockquotes(html) {
738
+ // Find consecutive lines starting with &gt; (escaped >)
739
+ var lines = html.split('\n');
740
+ var result = [];
741
+ var i = 0;
742
+ while (i < lines.length) {
743
+ if (/^&gt;[ \t]?/.test(lines[i])) {
744
+ var bqLines = [];
745
+ while (i < lines.length && /^&gt;[ \t]?/.test(lines[i])) {
746
+ bqLines.push(lines[i].replace(/^&gt;[ \t]?/, ''));
747
+ i++;
748
+ }
749
+ result.push('<blockquote>' + bqLines.join('<br>') + '</blockquote>');
750
+ } else {
751
+ result.push(lines[i]);
752
+ i++;
753
+ }
754
+ }
755
+ return result.join('\n');
756
+ }
757
+
758
+ function renderLists(html) {
759
+ // Process unordered then ordered lists
760
+ // Match blocks of consecutive list lines
761
+ var lines = html.split('\n');
762
+ var result = [];
763
+ var i = 0;
764
+
765
+ while (i < lines.length) {
766
+ // Unordered list item: "- " or "* " or "+ " (with optional indent)
767
+ if (/^[ \t]*[-*+][ \t]+/.test(lines[i])) {
768
+ var listLines = [];
769
+ while (i < lines.length && /^[ \t]*[-*+][ \t]+/.test(lines[i])) {
770
+ listLines.push(lines[i].replace(/^[ \t]*[-*+][ \t]+/, ''));
771
+ i++;
772
+ }
773
+ result.push('<ul>' + listLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ul>');
774
+ }
775
+ // Ordered list item: "1. " "2. " etc.
776
+ else if (/^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
777
+ var olLines = [];
778
+ while (i < lines.length && /^[ \t]*\d+\.[ \t]+/.test(lines[i])) {
779
+ olLines.push(lines[i].replace(/^[ \t]*\d+\.[ \t]+/, ''));
780
+ i++;
781
+ }
782
+ result.push('<ol>' + olLines.map(function (l) { return '<li>' + l + '</li>'; }).join('') + '</ol>');
783
+ }
784
+ else {
785
+ result.push(lines[i]);
786
+ i++;
787
+ }
788
+ }
789
+ return result.join('\n');
790
+ }
791
+
792
+ function renderParagraphs(html) {
793
+ // Split on double newlines; wrap plain-text segments in <p> while preserving
794
+ // existing block-level HTML elements.
795
+ // Strategy: split into chunks on blank lines (two+ newlines), then for each
796
+ // chunk: if it starts with a block-level tag, keep as-is; else wrap in <p>.
797
+ var blockTags = /^<(h[1-6]|hr|ul|ol|blockquote|table|div|pre)\b/i;
798
+
799
+ var chunks = html.split(/\n{2,}/);
800
+ var parts = chunks.map(function (chunk) {
801
+ chunk = chunk.trim();
802
+ if (!chunk) return '';
803
+ if (blockTags.test(chunk)) return chunk;
804
+ // It's a text/inline-html chunk — convert single newlines to <br>
805
+ // but only outside of block tags within the chunk
806
+ var inner = chunk.replace(/\n/g, '<br>');
807
+ return '<p>' + inner + '</p>';
808
+ });
809
+
810
+ return parts.filter(function (p) { return p !== ''; }).join('\n');
811
+ }
812
+
813
+ // ─── Attachment helpers ──────────────────────────────────────────────────────
814
+ function fileExtension(name) {
815
+ var parts = name.split('.');
816
+ return parts.length > 1 ? parts[parts.length - 1].slice(0, 5) : 'file';
368
817
  }
369
818
 
370
- // ─── WebSocket Connection ───────────────────────────────────
819
+ function readFileAsBase64(file) {
820
+ return new Promise(function (resolve, reject) {
821
+ var reader = new FileReader();
822
+ reader.onload = function (e) {
823
+ // result is "data:mime/type;base64,XXXX" — strip the prefix
824
+ var result = e.target.result;
825
+ var comma = result.indexOf(',');
826
+ resolve({ base64: result.slice(comma + 1), mimeType: file.type || 'application/octet-stream', name: file.name });
827
+ };
828
+ reader.onerror = reject;
829
+ reader.readAsDataURL(file);
830
+ });
831
+ }
832
+
833
+ function readFileAsDataUrl(file) {
834
+ return new Promise(function (resolve, reject) {
835
+ var reader = new FileReader();
836
+ reader.onload = function (e) { resolve(e.target.result); };
837
+ reader.onerror = reject;
838
+ reader.readAsDataURL(file);
839
+ });
840
+ }
841
+
842
+ // ─── WebSocket Connection ────────────────────────────────────────────────────
371
843
  function createConnection(opts) {
372
- const { wsUrl, apiKey, sessionId, onMessage, onStatus } = opts;
373
- let ws = null;
374
- let reconnectTimer = null;
375
- let reconnectDelay = 1000;
844
+ var wsUrl = opts.wsUrl;
845
+ var apiKey = opts.apiKey;
846
+ var sessionId = opts.sessionId;
847
+ var onMessage = opts.onMessage;
848
+ var onStatus = opts.onStatus;
849
+ var skipHistory = opts.skipHistory || false;
850
+
851
+ var ws = null;
852
+ var reconnectTimer = null;
853
+ var reconnectDelay = 1000;
854
+ var destroyed = false;
376
855
 
377
856
  function connect() {
857
+ if (destroyed) return;
378
858
  onStatus('connecting');
379
859
  ws = new WebSocket(wsUrl);
380
860
 
381
861
  ws.onopen = function () {
862
+ if (destroyed) { ws.close(); return; }
382
863
  onStatus('connected');
383
864
  reconnectDelay = 1000;
384
-
385
- // Authenticate
386
865
  ws.send(JSON.stringify({ type: 'auth', apiKey: apiKey }));
387
-
388
- // Load history
389
- ws.send(
390
- JSON.stringify({
391
- type: 'history',
392
- threadName: sessionId,
393
- limit: 50,
394
- })
395
- );
866
+ if (!skipHistory) {
867
+ ws.send(JSON.stringify({ type: 'history', threadName: sessionId, limit: 50 }));
868
+ }
396
869
  };
397
870
 
398
871
  ws.onmessage = function (event) {
@@ -405,17 +878,18 @@
405
878
  };
406
879
 
407
880
  ws.onclose = function () {
881
+ if (destroyed) return;
408
882
  onStatus('disconnected');
409
883
  scheduleReconnect();
410
884
  };
411
885
 
412
886
  ws.onerror = function () {
413
- // onclose will fire after this
887
+ // onclose fires after onerror
414
888
  };
415
889
  }
416
890
 
417
891
  function scheduleReconnect() {
418
- if (reconnectTimer) return;
892
+ if (reconnectTimer || destroyed) return;
419
893
  reconnectTimer = setTimeout(function () {
420
894
  reconnectTimer = null;
421
895
  reconnectDelay = Math.min(reconnectDelay * 1.5, 30000);
@@ -430,12 +904,13 @@
430
904
  }
431
905
 
432
906
  function close() {
907
+ destroyed = true;
433
908
  if (reconnectTimer) {
434
909
  clearTimeout(reconnectTimer);
435
910
  reconnectTimer = null;
436
911
  }
437
912
  if (ws) {
438
- ws.onclose = null; // Prevent reconnect
913
+ ws.onclose = null;
439
914
  ws.close();
440
915
  ws = null;
441
916
  }
@@ -445,52 +920,565 @@
445
920
  return { send: send, close: close };
446
921
  }
447
922
 
448
- // ─── Widget UI ──────────────────────────────────────────────
923
+ // ─── Widget ──────────────────────────────────────────────────────────────────
449
924
  function createWidget(opts) {
450
- const { standalone } = opts;
451
- const sessionId = getSessionId();
452
- const wsUrl = getWsUrl();
453
- // For standalone mode, use stored key or empty; for embedded, use data-api-key
925
+ var standalone = opts.standalone;
926
+ var sessionId = getSessionId();
927
+ var wsUrl = getWsUrl();
454
928
  var activeApiKey = standalone ? getStoredApiKey() : API_KEY;
455
929
 
456
930
  // Inject styles
457
- const styleEl = document.createElement('style');
931
+ var styleEl = document.createElement('style');
458
932
  styleEl.textContent = STYLES;
459
933
  document.head.appendChild(styleEl);
460
934
 
461
- // State
462
- var messages = [];
463
- var streaming = false;
464
- var streamBuffer = '';
935
+ // ── Root container ──
936
+ var container = document.createElement('div');
937
+ container.className = 'cumulus-widget';
938
+
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
+ // ═══════════════════════════════════════════════════════════════════════════
1426
+
1427
+ // ── Standalone state ──
465
1428
  var connection = null;
466
1429
  var connectionStatus = 'disconnected';
467
1430
  var authenticated = false;
468
1431
 
469
- // ─── DOM elements ──────────────────────────────
470
- const container = document.createElement('div');
471
- container.className = 'cumulus-widget';
472
-
473
- // Build panel
474
- const panel = document.createElement('div');
475
- panel.className = 'cumulus-panel ' + (standalone ? 'standalone' : 'floating');
476
- panel.style.display = standalone ? 'flex' : 'none';
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
+ }
477
1458
 
478
- // Header
479
- const header = document.createElement('div');
480
- header.className = 'cumulus-header';
481
- header.innerHTML =
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 =
482
1474
  '<span class="cumulus-header-title">Cumulus</span>' +
483
1475
  '<span class="cumulus-header-status">' +
484
- '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
485
- '<span data-testid="webchat-status-text">Disconnected</span>' +
486
- '</span>' +
487
- (standalone
488
- ? '<button class="cumulus-auth-logout" data-testid="webchat-logout" style="display:none">Logout</button>'
489
- : '<button class="cumulus-close-btn" data-testid="webchat-close">&times;</button>');
490
- panel.appendChild(header);
491
-
492
- // Auth panel (standalone mode — shown when no API key)
493
- const authPanel = document.createElement('div');
1476
+ '<span class="cumulus-status-dot" data-testid="webchat-status-dot"></span>' +
1477
+ '<span data-testid="webchat-status-text">Disconnected</span>' +
1478
+ '</span>';
1479
+ authWrapper.appendChild(authHeader);
1480
+
1481
+ var authPanel = document.createElement('div');
494
1482
  authPanel.className = 'cumulus-auth';
495
1483
  authPanel.setAttribute('data-testid', 'webchat-auth');
496
1484
  authPanel.innerHTML =
@@ -499,245 +1487,836 @@
499
1487
  '<input class="cumulus-auth-input" data-testid="webchat-auth-input" type="password" placeholder="sk-cumulus-..." autocomplete="off" />' +
500
1488
  '<button class="cumulus-auth-btn" data-testid="webchat-auth-submit">Connect</button>' +
501
1489
  '<div class="cumulus-auth-error" data-testid="webchat-auth-error"></div>';
502
- panel.appendChild(authPanel);
503
-
504
- // Messages area
505
- const messagesEl = document.createElement('div');
506
- messagesEl.className = 'cumulus-messages';
507
- messagesEl.setAttribute('data-testid', 'webchat-messages');
508
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
509
- messagesEl.style.display = 'none';
510
- panel.appendChild(messagesEl);
511
-
512
- // Input area
513
- const inputArea = document.createElement('div');
514
- inputArea.className = 'cumulus-input-area';
515
- inputArea.style.display = 'none';
516
- const input = document.createElement('textarea');
517
- input.className = 'cumulus-input';
518
- input.setAttribute('data-testid', 'webchat-input');
519
- input.placeholder = 'Type a message...';
520
- input.rows = 1;
521
- const sendBtn = document.createElement('button');
522
- sendBtn.className = 'cumulus-send-btn';
523
- sendBtn.setAttribute('data-testid', 'webchat-send');
524
- sendBtn.textContent = 'Send';
525
- inputArea.appendChild(input);
526
- inputArea.appendChild(sendBtn);
527
- panel.appendChild(inputArea);
528
-
529
- container.appendChild(panel);
530
-
531
- // ─── Auth/Chat view switching ────────────────────
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);
1574
+ }
1575
+
1576
+ // ── View switching ──
532
1577
  function showAuthView() {
533
1578
  authenticated = false;
534
- authPanel.style.display = 'flex';
535
- messagesEl.style.display = 'none';
536
- inputArea.style.display = 'none';
537
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
538
- 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; }
539
1585
  var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
540
1586
  if (errEl) errEl.textContent = '';
541
1587
  }
542
1588
 
543
- function showChatView() {
1589
+ function showAppView() {
544
1590
  authenticated = true;
545
- authPanel.style.display = 'none';
546
- messagesEl.style.display = 'flex';
547
- inputArea.style.display = 'flex';
548
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
549
- if (logoutBtn) logoutBtn.style.display = 'inline-block';
550
- 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);
551
1598
  }
552
1599
 
553
- // Floating bubble (embeddable mode only)
554
- var bubble = null;
555
- if (!standalone) {
556
- bubble = document.createElement('button');
557
- bubble.className = 'cumulus-bubble';
558
- bubble.setAttribute('data-testid', 'webchat-bubble');
559
- bubble.innerHTML =
560
- '<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>';
561
- container.appendChild(bubble);
1600
+ // ── Thread list ──
1601
+ function requestThreadList() {
1602
+ if (connection) {
1603
+ connection.send({ type: 'threads' });
1604
+ }
1605
+ }
1606
+
1607
+ // ── Sidebar rendering ──
1608
+ function renderSidebar() {
1609
+ sidebarScroll.innerHTML = '';
1610
+
1611
+ var visibleSet = {};
1612
+ for (var i = 0; i < visibleThreads.length; i++) {
1613
+ visibleSet[visibleThreads[i]] = true;
1614
+ }
1615
+
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));
1628
+ });
1629
+ sidebarScroll.appendChild(activeSection);
1630
+
1631
+ var divider = document.createElement('hr');
1632
+ divider.className = 'cumulus-sidebar-divider';
1633
+ sidebarScroll.appendChild(divider);
1634
+ }
1635
+
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));
1657
+ });
1658
+ }
1659
+ sidebarScroll.appendChild(allSection);
1660
+ }
1661
+
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);
1683
+ }
1684
+
1685
+ item.addEventListener('click', function (e) {
1686
+ var multiSelect = e.metaKey || e.ctrlKey;
1687
+ if (multiSelect) {
1688
+ togglePanelThread(name);
1689
+ } else {
1690
+ soloThread(name);
1691
+ }
1692
+ });
1693
+
1694
+ return item;
1695
+ }
1696
+
1697
+ // ── Panel management ──
1698
+ function soloThread(name) {
1699
+ visibleThreads = [name];
1700
+ renderSidebar();
1701
+ renderContentArea();
1702
+ loadThreadHistoryIfNeeded(name);
562
1703
  }
563
1704
 
564
- // ─── Rendering ─────────────────────────────────
565
- function scrollToBottom() {
566
- messagesEl.scrollTop = messagesEl.scrollHeight;
1705
+ function togglePanelThread(name) {
1706
+ var idx = visibleThreads.indexOf(name);
1707
+ if (idx !== -1) {
1708
+ // Remove from visible
1709
+ visibleThreads.splice(idx, 1);
1710
+ } else {
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);
1724
+ }
1725
+ }
1726
+
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
+ }
567
1737
  }
568
1738
 
569
- function renderMessages() {
570
- messagesEl.innerHTML = '';
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
+ }
571
1746
 
572
- if (messages.length === 0 && !streaming) {
573
- messagesEl.innerHTML = '<div class="cumulus-empty">Send a message to start chatting</div>';
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);
574
1763
  return;
575
1764
  }
576
1765
 
577
- for (var i = 0; i < messages.length; i++) {
578
- var msg = messages[i];
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) {
579
1876
  var el = document.createElement('div');
580
- el.className = 'cumulus-msg ' + msg.role;
581
- if (msg.role === 'assistant') {
582
- el.innerHTML = renderMarkdown(msg.content);
583
- } else {
584
- el.textContent = msg.content;
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);
585
1899
  }
586
- messagesEl.appendChild(el);
1900
+ return el;
587
1901
  }
588
1902
 
589
- // Streaming indicator
590
- if (streaming) {
591
- var streamEl = document.createElement('div');
592
- streamEl.className = 'cumulus-msg assistant';
593
- streamEl.setAttribute('data-testid', 'webchat-streaming');
594
- if (streamBuffer) {
595
- streamEl.innerHTML =
596
- renderMarkdown(streamBuffer) + '<span class="cumulus-cursor"></span>';
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
+ });
1923
+ });
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;
597
1963
  } else {
598
- streamEl.innerHTML =
599
- '<span style="color:#888;font-style:italic">Thinking...</span><span class="cumulus-cursor"></span>';
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;
600
1975
  }
601
- messagesEl.appendChild(streamEl);
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
+ });
602
2011
  }
603
2012
 
604
- scrollToBottom();
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);
2082
+
2083
+ inputEl.addEventListener('keydown', function (e) {
2084
+ if (e.key === 'Enter' && !e.shiftKey) {
2085
+ e.preventDefault();
2086
+ sendPanelMessage();
2087
+ }
2088
+ });
2089
+
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 = '';
2116
+ }
2117
+ });
2118
+
2119
+ attachBtn.addEventListener('click', function () {
2120
+ fileInput.click();
2121
+ });
2122
+
2123
+ // Initial render
2124
+ renderPanelMessages();
2125
+ renderPanelAttachStrip();
2126
+
2127
+ return panel;
605
2128
  }
606
2129
 
607
- function updateStatus(status) {
608
- connectionStatus = status;
609
- var dot = panel.querySelector('.cumulus-status-dot');
610
- var text = panel.querySelector('[data-testid="webchat-status-text"]');
611
- if (dot) {
612
- dot.className = 'cumulus-status-dot ' + status;
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();
613
2139
  }
614
- if (text) {
615
- text.textContent = status.charAt(0).toUpperCase() + status.slice(1);
2140
+ if (state._updateSendBtn) {
2141
+ state._updateSendBtn();
616
2142
  }
617
2143
  }
618
2144
 
619
- // ─── Message handling ──────────────────────────
2145
+ // ── Message router ──
620
2146
  function handleServerMessage(data) {
621
2147
  switch (data.type) {
622
2148
  case 'auth_ok':
623
- showChatView();
2149
+ showAppView();
624
2150
  break;
625
2151
 
626
2152
  case 'auth_error':
627
2153
  console.error('[Cumulus] Auth failed:', data.error);
628
- // If we had a stored key and it failed, show auth panel again
629
- if (standalone) {
630
- clearStoredApiKey();
631
- activeApiKey = '';
632
- if (connection) { connection.close(); connection = null; }
633
- showAuthView();
634
- var errEl = authPanel.querySelector('[data-testid="webchat-auth-error"]');
635
- 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);
636
2182
  }
637
2183
  break;
638
2184
 
639
2185
  case 'history':
640
- if (data.messages && data.messages.length > 0) {
641
- messages = data.messages.map(function (m) {
642
- return { role: m.role, content: m.content };
643
- });
644
- 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);
645
2194
  }
646
2195
  break;
647
2196
 
648
2197
  case 'token':
649
- if (!streaming) {
650
- streaming = true;
651
- streamBuffer = '';
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);
652
2209
  }
653
- streamBuffer += data.text;
654
- renderMessages();
655
2210
  break;
656
2211
 
657
2212
  case 'segment':
658
- // Tool use/result could show in verbose mode later
2213
+ // Reserved for future verbose display
659
2214
  break;
660
2215
 
661
2216
  case 'done':
662
- streaming = false;
663
- messages.push({ role: 'assistant', content: data.response || streamBuffer });
664
- streamBuffer = '';
665
- renderMessages();
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;
2229
+ }
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);
2235
+ }
666
2236
  break;
667
2237
 
668
2238
  case 'error':
669
- streaming = false;
670
- if (streamBuffer) {
671
- messages.push({ role: 'assistant', content: streamBuffer + '\n\n[Error: ' + data.error + ']' });
672
- } else {
673
- messages.push({ role: 'assistant', content: '[Error: ' + data.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);
674
2250
  }
675
- streamBuffer = '';
676
- renderMessages();
677
2251
  break;
678
2252
  }
679
2253
  }
680
2254
 
681
- function sendMessage() {
682
- var text = input.value.trim();
683
- if (!text || streaming) return;
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;
2260
+ }
2261
+ }
2262
+ // Re-render sidebar to reflect new sort order / move thread up
2263
+ renderSidebar();
2264
+ }
684
2265
 
685
- messages.push({ role: 'user', content: text });
686
- input.value = '';
687
- input.style.height = 'auto';
688
- streaming = true;
689
- streamBuffer = '';
690
- renderMessages();
2266
+ // ── New thread creation ──
2267
+ var newThreadCreating = false;
691
2268
 
692
- connection.send({
693
- type: 'message',
694
- threadName: sessionId,
695
- message: text,
696
- });
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();
697
2276
  }
698
2277
 
699
- // ─── Event listeners ───────────────────────────
700
- sendBtn.addEventListener('click', sendMessage);
2278
+ function hideNewThreadInput() {
2279
+ newThreadCreating = false;
2280
+ newThreadBtn.style.display = 'block';
2281
+ newThreadInput.style.display = 'none';
2282
+ newThreadInput.value = '';
2283
+ }
701
2284
 
702
- input.addEventListener('keydown', function (e) {
703
- if (e.key === 'Enter' && !e.shiftKey) {
704
- e.preventDefault();
705
- 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 });
706
2290
  }
707
- });
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; }
2296
+ }
2297
+ if (!exists) {
2298
+ allThreads.push({ name: name, messageCount: 0, lastActivity: Date.now() });
2299
+ }
2300
+ soloThread(name);
2301
+ }
708
2302
 
709
- // Auto-resize textarea
710
- input.addEventListener('input', function () {
711
- input.style.height = 'auto';
712
- input.style.height = Math.min(input.scrollHeight, 120) + 'px';
713
- });
2303
+ newThreadBtn.addEventListener('click', showNewThreadInput);
714
2304
 
715
- // Bubble toggle (embeddable mode)
716
- if (bubble) {
717
- bubble.addEventListener('click', function () {
718
- var showing = panel.style.display !== 'none';
719
- panel.style.display = showing ? 'none' : 'flex';
720
- if (!showing && !connection) {
721
- startConnection();
722
- }
723
- if (!showing) {
724
- input.focus();
725
- scrollToBottom();
726
- }
727
- });
2305
+ newThreadInput.addEventListener('keydown', function (e) {
2306
+ if (e.key === 'Enter') { e.preventDefault(); submitNewThread(); }
2307
+ if (e.key === 'Escape') { hideNewThreadInput(); }
2308
+ });
728
2309
 
729
- // Close button
730
- var closeBtn = panel.querySelector('.cumulus-close-btn');
731
- if (closeBtn) {
732
- closeBtn.addEventListener('click', function () {
733
- panel.style.display = 'none';
734
- });
735
- }
736
- }
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
+ });
737
2316
 
738
- // ─── Auth panel events ─────────────────────────
2317
+ // ── Auth events ──
739
2318
  var authInput = authPanel.querySelector('[data-testid="webchat-auth-input"]');
740
- var authBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
2319
+ var authSubmitBtn = authPanel.querySelector('[data-testid="webchat-auth-submit"]');
741
2320
 
742
2321
  function submitAuth() {
743
2322
  var key = authInput.value.trim();
@@ -749,30 +2328,24 @@
749
2328
  startConnection();
750
2329
  }
751
2330
 
752
- authBtn.addEventListener('click', submitAuth);
2331
+ authSubmitBtn.addEventListener('click', submitAuth);
753
2332
  authInput.addEventListener('keydown', function (e) {
754
- if (e.key === 'Enter') {
755
- e.preventDefault();
756
- submitAuth();
757
- }
2333
+ if (e.key === 'Enter') { e.preventDefault(); submitAuth(); }
758
2334
  });
759
2335
 
760
- // Logout button (standalone only)
761
- var logoutBtn = panel.querySelector('[data-testid="webchat-logout"]');
2336
+ // ── Logout ──
2337
+ var logoutBtn = appLayout.querySelector('[data-testid="webchat-logout"]');
762
2338
  if (logoutBtn) {
763
2339
  logoutBtn.addEventListener('click', function () {
764
2340
  clearStoredApiKey();
765
2341
  activeApiKey = '';
766
2342
  if (connection) { connection.close(); connection = null; }
767
- messages = [];
768
- streaming = false;
769
- streamBuffer = '';
770
2343
  showAuthView();
771
2344
  authInput.value = '';
772
2345
  });
773
2346
  }
774
2347
 
775
- // ─── Connect ───────────────────────────────────
2348
+ // ── Connection ──
776
2349
  function startConnection() {
777
2350
  if (connection) { connection.close(); connection = null; }
778
2351
  connection = createConnection({
@@ -781,37 +2354,33 @@
781
2354
  sessionId: sessionId,
782
2355
  onMessage: handleServerMessage,
783
2356
  onStatus: updateStatus,
2357
+ skipHistory: true, // standalone manages history per-thread
784
2358
  });
785
2359
  }
786
2360
 
787
- // ─── Mount ─────────────────────────────────────
2361
+ // ── Mount / Destroy ──
788
2362
  function mount(target) {
789
2363
  (target || document.body).appendChild(container);
790
- if (standalone) {
791
- if (activeApiKey) {
792
- // Have a stored key — try connecting directly
793
- startConnection();
794
- } else {
795
- // No key — show auth panel
796
- showAuthView();
797
- authInput.focus();
798
- }
2364
+ if (activeApiKey) {
2365
+ startConnection();
2366
+ } else {
2367
+ showAuthView();
2368
+ authInput.focus();
799
2369
  }
800
2370
  }
801
2371
 
802
2372
  function destroy() {
803
2373
  if (connection) connection.close();
2374
+ if (refreshTimer) clearInterval(refreshTimer);
804
2375
  container.remove();
805
2376
  }
806
2377
 
807
- return { mount: mount, destroy: destroy, send: sendMessage };
2378
+ return { mount: mount, destroy: destroy };
808
2379
  }
809
2380
 
810
- // ─── Auto-mount ─────────────────────────────────────────────
2381
+ // ─── Auto-mount ──────────────────────────────────────────────────────────────
811
2382
  if (script) {
812
- var widget = createWidget({
813
- standalone: STANDALONE,
814
- });
2383
+ var widget = createWidget({ standalone: STANDALONE });
815
2384
 
816
2385
  if (document.readyState === 'loading') {
817
2386
  document.addEventListener('DOMContentLoaded', function () {
@@ -821,7 +2390,6 @@
821
2390
  widget.mount();
822
2391
  }
823
2392
 
824
- // Expose for programmatic access
825
2393
  window.CumulusChat = widget;
826
2394
  }
827
2395
  })();