claude-relay 1.0.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.
@@ -0,0 +1,1217 @@
1
+ (function() {
2
+ "use strict";
3
+
4
+ // --- DOM refs ---
5
+ var $ = function(id) { return document.getElementById(id); };
6
+ var messagesEl = $("messages");
7
+ var inputEl = $("input");
8
+ var sendBtn = $("send-btn");
9
+ var statusDot = $("status-dot");
10
+ var statusTextEl = $("status-text");
11
+ var projectNameEl = $("project-name");
12
+ var slashMenu = $("slash-menu");
13
+ var sidebar = $("sidebar");
14
+ var sidebarOverlay = $("sidebar-overlay");
15
+ var sessionListEl = $("session-list");
16
+ var newSessionBtn = $("new-session-btn");
17
+ var hamburgerBtn = $("hamburger-btn");
18
+ var imagePreviewBar = $("image-preview-bar");
19
+
20
+ // --- State ---
21
+ var ws = null;
22
+ var connected = false;
23
+ var processing = false;
24
+ var isComposing = false;
25
+ var reconnectTimer = null;
26
+ var reconnectDelay = 1000;
27
+ var activityEl = null;
28
+ var currentMsgEl = null;
29
+ var currentFullText = "";
30
+ var tools = {};
31
+ var currentThinking = null;
32
+ var highlightTimer = null;
33
+ var activeSessionId = null;
34
+ var slashCommands = [];
35
+ var slashActiveIdx = -1;
36
+ var slashFiltered = [];
37
+ var pendingImages = []; // [{data: base64, mediaType: "image/png"}]
38
+
39
+ var builtinCommands = [
40
+ { name: "clear", desc: "Clear conversation" },
41
+ { name: "cost", desc: "Show session cost" },
42
+ ];
43
+
44
+ // --- Lucide icon helper ---
45
+ var _iconTimer = null;
46
+ function refreshIcons() {
47
+ if (_iconTimer) return;
48
+ _iconTimer = requestAnimationFrame(function() {
49
+ _iconTimer = null;
50
+ lucide.createIcons();
51
+ });
52
+ }
53
+
54
+ function iconHtml(name, wrapperClass) {
55
+ if (wrapperClass) {
56
+ return '<span class="' + wrapperClass + '"><i data-lucide="' + name + '"></i></span>';
57
+ }
58
+ return '<i data-lucide="' + name + '"></i>';
59
+ }
60
+
61
+ // --- Activity verbs ---
62
+ var thinkingVerbs = [
63
+ "Accomplishing","Actioning","Actualizing","Architecting","Baking","Beaming",
64
+ "Beboppin'","Befuddling","Billowing","Blanching","Bloviating","Boogieing",
65
+ "Boondoggling","Booping","Bootstrapping","Brewing","Burrowing","Calculating",
66
+ "Canoodling","Caramelizing","Cascading","Catapulting","Cerebrating","Channeling",
67
+ "Channelling","Choreographing","Churning","Clauding","Coalescing","Cogitating",
68
+ "Combobulating","Composing","Computing","Concocting","Considering","Contemplating",
69
+ "Cooking","Crafting","Creating","Crunching","Crystallizing","Cultivating",
70
+ "Deciphering","Deliberating","Determining","Dilly-dallying","Discombobulating",
71
+ "Doing","Doodling","Drizzling","Ebbing","Effecting","Elucidating","Embellishing",
72
+ "Enchanting","Envisioning","Evaporating","Fermenting","Fiddle-faddling","Finagling",
73
+ "Flambing","Flibbertigibbeting","Flowing","Flummoxing","Fluttering","Forging",
74
+ "Forming","Frolicking","Frosting","Gallivanting","Galloping","Garnishing",
75
+ "Generating","Germinating","Gitifying","Grooving","Gusting","Harmonizing",
76
+ "Hashing","Hatching","Herding","Honking","Hullaballooing","Hyperspacing",
77
+ "Ideating","Imagining","Improvising","Incubating","Inferring","Infusing",
78
+ "Ionizing","Jitterbugging","Julienning","Kneading","Leavening","Levitating",
79
+ "Lollygagging","Manifesting","Marinating","Meandering","Metamorphosing","Misting",
80
+ "Moonwalking","Moseying","Mulling","Mustering","Musing","Nebulizing","Nesting",
81
+ "Newspapering","Noodling","Nucleating","Orbiting","Orchestrating","Osmosing",
82
+ "Perambulating","Percolating","Perusing","Philosophising","Photosynthesizing",
83
+ "Pollinating","Pondering","Pontificating","Pouncing","Precipitating",
84
+ "Prestidigitating","Processing","Proofing","Propagating","Puttering","Puzzling",
85
+ "Quantumizing","Razzle-dazzling","Razzmatazzing","Recombobulating","Reticulating",
86
+ "Roosting","Ruminating","Sauting","Scampering","Schlepping","Scurrying","Seasoning",
87
+ "Shenaniganing","Shimmying","Simmering","Skedaddling","Sketching","Slithering",
88
+ "Smooshing","Sock-hopping","Spelunking","Spinning","Sprouting","Stewing",
89
+ "Sublimating","Swirling","Swooping","Symbioting","Synthesizing","Tempering",
90
+ "Thinking","Thundering","Tinkering","Tomfoolering","Topsy-turvying","Transfiguring",
91
+ "Transmuting","Twisting","Undulating","Unfurling","Unravelling","Vibing","Waddling",
92
+ "Wandering","Warping","Whatchamacalliting","Whirlpooling","Whirring","Whisking",
93
+ "Wibbling","Working","Wrangling","Zesting","Zigzagging"
94
+ ];
95
+
96
+ function randomThinkingVerb() {
97
+ return thinkingVerbs[Math.floor(Math.random() * thinkingVerbs.length)];
98
+ }
99
+
100
+ // --- Markdown setup ---
101
+ marked.use({ gfm: true, breaks: false });
102
+
103
+ function renderMarkdown(text) {
104
+ return DOMPurify.sanitize(marked.parse(text));
105
+ }
106
+
107
+ function highlightCodeBlocks(el) {
108
+ el.querySelectorAll("pre code:not(.hljs)").forEach(function(block) {
109
+ hljs.highlightElement(block);
110
+ });
111
+ }
112
+
113
+ function escapeHtml(s) {
114
+ return s.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;");
115
+ }
116
+
117
+ // --- Sidebar ---
118
+ function renderSessionList(sessions) {
119
+ sessionListEl.innerHTML = "";
120
+ for (var i = 0; i < sessions.length; i++) {
121
+ var s = sessions[i];
122
+ var el = document.createElement("div");
123
+ el.className = "session-item" + (s.active ? " active" : "");
124
+ el.dataset.sessionId = s.id;
125
+
126
+ var html = "";
127
+ if (s.isProcessing) {
128
+ html += '<span class="session-processing"></span>';
129
+ }
130
+ html += escapeHtml(s.title || "New Session");
131
+ el.innerHTML = html;
132
+
133
+ el.addEventListener("click", (function(id) {
134
+ return function() {
135
+ if (ws && connected) {
136
+ ws.send(JSON.stringify({ type: "switch_session", id: id }));
137
+ closeSidebar();
138
+ }
139
+ };
140
+ })(s.id));
141
+
142
+ sessionListEl.appendChild(el);
143
+ }
144
+ }
145
+
146
+ function openSidebar() {
147
+ sidebar.classList.add("open");
148
+ sidebarOverlay.classList.add("visible");
149
+ }
150
+
151
+ function closeSidebar() {
152
+ sidebar.classList.remove("open");
153
+ sidebarOverlay.classList.remove("visible");
154
+ }
155
+
156
+ hamburgerBtn.addEventListener("click", function() {
157
+ sidebar.classList.contains("open") ? closeSidebar() : openSidebar();
158
+ });
159
+
160
+ sidebarOverlay.addEventListener("click", closeSidebar);
161
+
162
+ newSessionBtn.addEventListener("click", function() {
163
+ if (ws && connected) {
164
+ ws.send(JSON.stringify({ type: "new_session" }));
165
+ closeSidebar();
166
+ }
167
+ });
168
+
169
+ // --- Status & Activity ---
170
+ function setStatus(status) {
171
+ statusDot.className = "status-dot";
172
+ if (status === "connected") {
173
+ statusDot.classList.add("connected");
174
+ statusTextEl.textContent = "Connected";
175
+ connected = true;
176
+ sendBtn.disabled = false;
177
+ } else if (status === "processing") {
178
+ statusDot.classList.add("processing");
179
+ statusTextEl.textContent = "";
180
+ processing = true;
181
+ sendBtn.disabled = true;
182
+ } else {
183
+ statusTextEl.textContent = "Disconnected";
184
+ connected = false;
185
+ sendBtn.disabled = true;
186
+ }
187
+ }
188
+
189
+ function setActivity(text) {
190
+ if (text) {
191
+ if (!activityEl) {
192
+ activityEl = document.createElement("div");
193
+ activityEl.className = "activity-inline";
194
+ activityEl.innerHTML =
195
+ '<span class="activity-icon">' + iconHtml("sparkles") + '</span>' +
196
+ '<span class="activity-text"></span>';
197
+ messagesEl.appendChild(activityEl);
198
+ refreshIcons();
199
+ }
200
+ activityEl.querySelector(".activity-text").textContent = text;
201
+ scrollToBottom();
202
+ } else {
203
+ if (activityEl) {
204
+ activityEl.remove();
205
+ activityEl = null;
206
+ }
207
+ }
208
+ }
209
+
210
+ function scrollToBottom() {
211
+ requestAnimationFrame(function() {
212
+ messagesEl.scrollTop = messagesEl.scrollHeight;
213
+ });
214
+ }
215
+
216
+ // --- Plan mode state ---
217
+ var inPlanMode = false;
218
+ var planContent = null; // stores plan markdown from Write tool
219
+
220
+ // --- Todo state ---
221
+ var todoItems = []; // [{id, content, status, activeForm}]
222
+ var todoWidgetEl = null;
223
+
224
+ // --- Tool helpers ---
225
+ var PLAN_MODE_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1 };
226
+ var TODO_TOOLS = { TodoWrite: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1 };
227
+ var HIDDEN_RESULT_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1, TodoWrite: 1 };
228
+
229
+ function isPlanFile(filePath) {
230
+ return filePath && filePath.indexOf(".claude/plans/") !== -1;
231
+ }
232
+
233
+ function toolSummary(name, input) {
234
+ if (!input || typeof input !== "object") return "";
235
+ switch (name) {
236
+ case "Read": return shortPath(input.file_path);
237
+ case "Edit": return shortPath(input.file_path);
238
+ case "Write": return shortPath(input.file_path);
239
+ case "Bash": return (input.command || "").substring(0, 80);
240
+ case "Glob": return input.pattern || "";
241
+ case "Grep": return (input.pattern || "") + (input.path ? " in " + shortPath(input.path) : "");
242
+ case "WebFetch": return input.url || "";
243
+ case "WebSearch": return input.query || "";
244
+ case "Task": return input.description || "";
245
+ case "EnterPlanMode": return "";
246
+ case "ExitPlanMode": return "";
247
+ default: return JSON.stringify(input).substring(0, 60);
248
+ }
249
+ }
250
+
251
+ function toolActivityText(name, input) {
252
+ if (name === "Bash" && input && input.description) return input.description;
253
+ if (name === "Read" && input && input.file_path) return "Reading " + shortPath(input.file_path);
254
+ if (name === "Edit" && input && input.file_path) return "Editing " + shortPath(input.file_path);
255
+ if (name === "Write" && input && input.file_path) return "Writing " + shortPath(input.file_path);
256
+ if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
257
+ if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
258
+ if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
259
+ if (name === "WebFetch") return "Fetching URL...";
260
+ if (name === "Task" && input && input.description) return input.description;
261
+ if (name === "EnterPlanMode") return "Entering plan mode...";
262
+ if (name === "ExitPlanMode") return "Finalizing the plan...";
263
+ return "Running " + name + "...";
264
+ }
265
+
266
+ function shortPath(p) {
267
+ if (!p) return "";
268
+ var parts = p.split("/");
269
+ return parts.length > 3 ? ".../" + parts.slice(-3).join("/") : p;
270
+ }
271
+
272
+ // --- AskUserQuestion ---
273
+ function renderAskUserQuestion(toolId, input) {
274
+ finalizeAssistantBlock();
275
+ stopThinking();
276
+
277
+ var questions = input.questions || [];
278
+ if (questions.length === 0) return;
279
+
280
+ var container = document.createElement("div");
281
+ container.className = "ask-user-container";
282
+ container.dataset.toolId = toolId;
283
+
284
+ var answers = {};
285
+ var multiSelections = {};
286
+
287
+ questions.forEach(function(q, qIdx) {
288
+ var qDiv = document.createElement("div");
289
+ qDiv.className = "ask-user-question";
290
+
291
+ var qText = document.createElement("div");
292
+ qText.className = "ask-user-question-text";
293
+ qText.textContent = q.question || "";
294
+ qDiv.appendChild(qText);
295
+
296
+ var optionsDiv = document.createElement("div");
297
+ optionsDiv.className = "ask-user-options";
298
+
299
+ var isMulti = q.multiSelect || false;
300
+ if (isMulti) multiSelections[qIdx] = new Set();
301
+
302
+ (q.options || []).forEach(function(opt) {
303
+ var btn = document.createElement("button");
304
+ btn.className = "ask-user-option";
305
+ btn.innerHTML =
306
+ '<div class="option-label"></div>' +
307
+ (opt.description ? '<div class="option-desc"></div>' : '');
308
+ btn.querySelector(".option-label").textContent = opt.label;
309
+ if (opt.description) btn.querySelector(".option-desc").textContent = opt.description;
310
+
311
+ btn.addEventListener("click", function() {
312
+ if (container.classList.contains("answered")) return;
313
+
314
+ if (isMulti) {
315
+ var set = multiSelections[qIdx];
316
+ if (set.has(opt.label)) {
317
+ set.delete(opt.label);
318
+ btn.classList.remove("selected");
319
+ } else {
320
+ set.add(opt.label);
321
+ btn.classList.add("selected");
322
+ }
323
+ } else {
324
+ optionsDiv.querySelectorAll(".ask-user-option").forEach(function(b) {
325
+ b.classList.remove("selected");
326
+ });
327
+ btn.classList.add("selected");
328
+ answers[qIdx] = opt.label;
329
+ var otherInput = qDiv.querySelector(".ask-user-other input");
330
+ if (otherInput) otherInput.value = "";
331
+ if (questions.length === 1) {
332
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
333
+ }
334
+ }
335
+ });
336
+
337
+ optionsDiv.appendChild(btn);
338
+ });
339
+
340
+ qDiv.appendChild(optionsDiv);
341
+
342
+ // "Other" text input
343
+ var otherDiv = document.createElement("div");
344
+ otherDiv.className = "ask-user-other";
345
+ var otherInput = document.createElement("input");
346
+ otherInput.type = "text";
347
+ otherInput.placeholder = "Other...";
348
+ otherInput.addEventListener("input", function() {
349
+ if (container.classList.contains("answered")) return;
350
+ if (otherInput.value.trim()) {
351
+ optionsDiv.querySelectorAll(".ask-user-option").forEach(function(b) {
352
+ b.classList.remove("selected");
353
+ });
354
+ if (isMulti) multiSelections[qIdx] = new Set();
355
+ answers[qIdx] = otherInput.value.trim();
356
+ }
357
+ });
358
+ otherInput.addEventListener("keydown", function(e) {
359
+ if (e.key === "Enter" && !e.shiftKey) {
360
+ e.preventDefault();
361
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
362
+ }
363
+ });
364
+ otherDiv.appendChild(otherInput);
365
+
366
+ var submitBtn = document.createElement("button");
367
+ submitBtn.className = "ask-user-submit";
368
+ submitBtn.textContent = "Submit";
369
+ submitBtn.addEventListener("click", function() {
370
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
371
+ });
372
+ otherDiv.appendChild(submitBtn);
373
+
374
+ qDiv.appendChild(otherDiv);
375
+ container.appendChild(qDiv);
376
+ });
377
+
378
+ messagesEl.appendChild(container);
379
+ setActivity(null);
380
+ scrollToBottom();
381
+ }
382
+
383
+ function submitAskUserAnswer(container, toolId, questions, answers, multiSelections) {
384
+ if (container.classList.contains("answered")) return;
385
+
386
+ var result = {};
387
+ for (var i = 0; i < questions.length; i++) {
388
+ var q = questions[i];
389
+ if (q.multiSelect && multiSelections[i] && multiSelections[i].size > 0) {
390
+ result[i] = Array.from(multiSelections[i]).join(", ");
391
+ } else if (answers[i]) {
392
+ result[i] = answers[i];
393
+ }
394
+ }
395
+
396
+ if (Object.keys(result).length === 0) return;
397
+
398
+ container.classList.add("answered");
399
+
400
+ if (ws && connected) {
401
+ ws.send(JSON.stringify({
402
+ type: "ask_user_response",
403
+ toolId: toolId,
404
+ answers: result,
405
+ }));
406
+ }
407
+ }
408
+
409
+ // --- Plan mode rendering ---
410
+ function renderPlanBanner(type) {
411
+ finalizeAssistantBlock();
412
+ stopThinking();
413
+
414
+ var el = document.createElement("div");
415
+ el.className = "plan-banner";
416
+
417
+ if (type === "enter") {
418
+ inPlanMode = true;
419
+ planContent = null;
420
+ el.innerHTML =
421
+ '<span class="plan-banner-icon">' + iconHtml("map") + '</span>' +
422
+ '<span class="plan-banner-text">Entered plan mode</span>' +
423
+ '<span class="plan-banner-hint">Exploring codebase and designing implementation...</span>';
424
+ el.classList.add("plan-enter");
425
+ } else {
426
+ inPlanMode = false;
427
+ el.innerHTML =
428
+ '<span class="plan-banner-icon">' + iconHtml("check-circle") + '</span>' +
429
+ '<span class="plan-banner-text">Plan ready for review</span>';
430
+ el.classList.add("plan-exit");
431
+ }
432
+
433
+ messagesEl.appendChild(el);
434
+ refreshIcons();
435
+ scrollToBottom();
436
+ return el;
437
+ }
438
+
439
+ function renderPlanCard(content) {
440
+ finalizeAssistantBlock();
441
+
442
+ var el = document.createElement("div");
443
+ el.className = "plan-card";
444
+
445
+ var header = document.createElement("div");
446
+ header.className = "plan-card-header";
447
+ header.innerHTML =
448
+ '<span class="plan-card-icon">' + iconHtml("file-text") + '</span>' +
449
+ '<span class="plan-card-title">Implementation Plan</span>' +
450
+ '<span class="plan-card-chevron">' + iconHtml("chevron-down") + '</span>';
451
+
452
+ var body = document.createElement("div");
453
+ body.className = "plan-card-body";
454
+ body.innerHTML = renderMarkdown(content);
455
+ highlightCodeBlocks(body);
456
+
457
+ header.addEventListener("click", function() {
458
+ el.classList.toggle("collapsed");
459
+ });
460
+
461
+ el.appendChild(header);
462
+ el.appendChild(body);
463
+ messagesEl.appendChild(el);
464
+ refreshIcons();
465
+ scrollToBottom();
466
+ return el;
467
+ }
468
+
469
+ // --- Todo rendering ---
470
+ function todoStatusIcon(status) {
471
+ switch (status) {
472
+ case "completed": return iconHtml("check-circle");
473
+ case "in_progress": return iconHtml("loader", "icon-spin");
474
+ default: return iconHtml("circle");
475
+ }
476
+ }
477
+
478
+ function handleTodoWrite(input) {
479
+ if (!input || !Array.isArray(input.todos)) return;
480
+ todoItems = input.todos.map(function(t, i) {
481
+ return {
482
+ id: t.id || String(i + 1),
483
+ content: t.content || t.subject || "",
484
+ status: t.status || "pending",
485
+ activeForm: t.activeForm || "",
486
+ };
487
+ });
488
+ renderTodoWidget();
489
+ }
490
+
491
+ function handleTaskCreate(input) {
492
+ if (!input) return;
493
+ var id = String(todoItems.length + 1);
494
+ todoItems.push({
495
+ id: id,
496
+ content: input.subject || input.description || "",
497
+ status: "pending",
498
+ activeForm: input.activeForm || "",
499
+ });
500
+ renderTodoWidget();
501
+ }
502
+
503
+ function handleTaskUpdate(input) {
504
+ if (!input || !input.taskId) return;
505
+ for (var i = 0; i < todoItems.length; i++) {
506
+ if (todoItems[i].id === input.taskId) {
507
+ if (input.status === "deleted") {
508
+ todoItems.splice(i, 1);
509
+ } else {
510
+ if (input.status) todoItems[i].status = input.status;
511
+ if (input.subject) todoItems[i].content = input.subject;
512
+ if (input.activeForm) todoItems[i].activeForm = input.activeForm;
513
+ }
514
+ break;
515
+ }
516
+ }
517
+ renderTodoWidget();
518
+ }
519
+
520
+ function renderTodoWidget() {
521
+ if (todoItems.length === 0) {
522
+ if (todoWidgetEl) { todoWidgetEl.remove(); todoWidgetEl = null; }
523
+ return;
524
+ }
525
+
526
+ var isNew = !todoWidgetEl;
527
+ if (isNew) {
528
+ todoWidgetEl = document.createElement("div");
529
+ todoWidgetEl.className = "todo-widget";
530
+ }
531
+
532
+ var completed = 0;
533
+ for (var i = 0; i < todoItems.length; i++) {
534
+ if (todoItems[i].status === "completed") completed++;
535
+ }
536
+
537
+ var html = '<div class="todo-header">' +
538
+ '<span class="todo-header-icon">' + iconHtml("list-checks") + '</span>' +
539
+ '<span class="todo-header-title">Tasks</span>' +
540
+ '<span class="todo-header-count">' + completed + '/' + todoItems.length + '</span>' +
541
+ '</div>';
542
+ html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
543
+ (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
544
+ html += '<div class="todo-items">';
545
+ for (var i = 0; i < todoItems.length; i++) {
546
+ var t = todoItems[i];
547
+ var statusClass = t.status === "completed" ? "completed" : t.status === "in_progress" ? "in-progress" : "pending";
548
+ html += '<div class="todo-item ' + statusClass + '">' +
549
+ '<span class="todo-item-icon">' + todoStatusIcon(t.status) + '</span>' +
550
+ '<span class="todo-item-text">' + escapeHtml(t.status === "in_progress" && t.activeForm ? t.activeForm : t.content) + '</span>' +
551
+ '</div>';
552
+ }
553
+ html += '</div>';
554
+
555
+ todoWidgetEl.innerHTML = html;
556
+
557
+ if (isNew) {
558
+ messagesEl.appendChild(todoWidgetEl);
559
+ }
560
+ refreshIcons();
561
+ scrollToBottom();
562
+ }
563
+
564
+ // --- DOM: Messages ---
565
+ function addUserMessage(text, images) {
566
+ var div = document.createElement("div");
567
+ div.className = "msg-user";
568
+ var bubble = document.createElement("div");
569
+ bubble.className = "bubble";
570
+
571
+ if (images && images.length > 0) {
572
+ var imgRow = document.createElement("div");
573
+ imgRow.className = "bubble-images";
574
+ for (var i = 0; i < images.length; i++) {
575
+ var img = document.createElement("img");
576
+ img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
577
+ img.className = "bubble-img";
578
+ imgRow.appendChild(img);
579
+ }
580
+ bubble.appendChild(imgRow);
581
+ }
582
+
583
+ if (text) {
584
+ var textEl = document.createElement("span");
585
+ textEl.textContent = text;
586
+ bubble.appendChild(textEl);
587
+ }
588
+
589
+ div.appendChild(bubble);
590
+ messagesEl.appendChild(div);
591
+ scrollToBottom();
592
+ }
593
+
594
+ function ensureAssistantBlock() {
595
+ if (!currentMsgEl) {
596
+ currentMsgEl = document.createElement("div");
597
+ currentMsgEl.className = "msg-assistant";
598
+ currentMsgEl.innerHTML = '<div class="md-content"></div>';
599
+ messagesEl.appendChild(currentMsgEl);
600
+ currentFullText = "";
601
+ }
602
+ return currentMsgEl;
603
+ }
604
+
605
+ function appendDelta(text) {
606
+ ensureAssistantBlock();
607
+ currentFullText += text;
608
+ var contentEl = currentMsgEl.querySelector(".md-content");
609
+ contentEl.innerHTML = renderMarkdown(currentFullText);
610
+
611
+ if (highlightTimer) clearTimeout(highlightTimer);
612
+ highlightTimer = setTimeout(function() {
613
+ highlightCodeBlocks(contentEl);
614
+ }, 150);
615
+
616
+ scrollToBottom();
617
+ }
618
+
619
+ function finalizeAssistantBlock() {
620
+ if (currentMsgEl) {
621
+ var contentEl = currentMsgEl.querySelector(".md-content");
622
+ if (contentEl) highlightCodeBlocks(contentEl);
623
+ }
624
+ currentMsgEl = null;
625
+ currentFullText = "";
626
+ }
627
+
628
+ function addSystemMessage(text, isError) {
629
+ var div = document.createElement("div");
630
+ div.className = "sys-msg" + (isError ? " error" : "");
631
+ div.innerHTML = '<span class="sys-text"></span>';
632
+ div.querySelector(".sys-text").textContent = text;
633
+ messagesEl.appendChild(div);
634
+ scrollToBottom();
635
+ }
636
+
637
+ function resetClientState() {
638
+ messagesEl.innerHTML = "";
639
+ currentMsgEl = null;
640
+ currentFullText = "";
641
+ tools = {};
642
+ currentThinking = null;
643
+ activityEl = null;
644
+ processing = false;
645
+ inPlanMode = false;
646
+ planContent = null;
647
+ todoItems = [];
648
+ todoWidgetEl = null;
649
+ setActivity(null);
650
+ setStatus("connected");
651
+ }
652
+
653
+ // --- Thinking ---
654
+ function startThinking() {
655
+ finalizeAssistantBlock();
656
+
657
+ var el = document.createElement("div");
658
+ el.className = "thinking-item";
659
+ el.innerHTML =
660
+ '<div class="thinking-header">' +
661
+ '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
662
+ '<span class="thinking-label">Thinking</span>' +
663
+ '<span class="thinking-duration"></span>' +
664
+ '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
665
+ '</div>' +
666
+ '<div class="thinking-content"></div>';
667
+
668
+ el.querySelector(".thinking-header").addEventListener("click", function() {
669
+ el.classList.toggle("expanded");
670
+ });
671
+
672
+ messagesEl.appendChild(el);
673
+ refreshIcons();
674
+ scrollToBottom();
675
+ currentThinking = { el: el, fullText: "", startTime: Date.now() };
676
+ setActivity(randomThinkingVerb() + "...");
677
+ }
678
+
679
+ function appendThinking(text) {
680
+ if (!currentThinking) return;
681
+ currentThinking.fullText += text;
682
+ currentThinking.el.querySelector(".thinking-content").textContent = currentThinking.fullText;
683
+ scrollToBottom();
684
+ }
685
+
686
+ function stopThinking() {
687
+ if (!currentThinking) return;
688
+ var secs = ((Date.now() - currentThinking.startTime) / 1000).toFixed(1);
689
+ currentThinking.el.classList.add("done");
690
+ currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs + "s";
691
+ currentThinking = null;
692
+ }
693
+
694
+ // --- Tool items ---
695
+ function createToolItem(id, name) {
696
+ finalizeAssistantBlock();
697
+ stopThinking();
698
+
699
+ var el = document.createElement("div");
700
+ el.className = "tool-item";
701
+ el.dataset.toolId = id;
702
+ el.innerHTML =
703
+ '<div class="tool-header">' +
704
+ '<span class="tool-bullet"></span>' +
705
+ '<span class="tool-name"></span>' +
706
+ '<span class="tool-desc"></span>' +
707
+ '<span class="tool-status-icon">' + iconHtml("loader", "icon-spin") + '</span>' +
708
+ '</div>' +
709
+ '<div class="tool-subtitle">' +
710
+ '<span class="tool-connector">&#9492;</span>' +
711
+ '<span class="tool-subtitle-text">Running...</span>' +
712
+ '</div>';
713
+
714
+ el.querySelector(".tool-name").textContent = name;
715
+
716
+ messagesEl.appendChild(el);
717
+ refreshIcons();
718
+ scrollToBottom();
719
+
720
+ tools[id] = { el: el, name: name, input: null, done: false };
721
+ setActivity("Running " + name + "...");
722
+ }
723
+
724
+ function updateToolExecuting(id, name, input) {
725
+ var tool = tools[id];
726
+ if (!tool) return;
727
+
728
+ tool.input = input;
729
+ tool.el.querySelector(".tool-desc").textContent = toolSummary(name, input);
730
+ setActivity(toolActivityText(name, input));
731
+
732
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
733
+ if (subtitleText) subtitleText.textContent = toolActivityText(name, input);
734
+
735
+ scrollToBottom();
736
+ }
737
+
738
+ function updateToolResult(id, content, isError) {
739
+ var tool = tools[id];
740
+ if (!tool) return;
741
+
742
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
743
+ if (subtitleText && tool.input) {
744
+ subtitleText.textContent = toolActivityText(tool.name, tool.input);
745
+ }
746
+
747
+ var resultBlock = document.createElement("div");
748
+ resultBlock.className = "tool-result-block";
749
+ var pre = document.createElement("pre");
750
+ if (isError) pre.className = "is-error";
751
+ var displayContent = content || "(no output)";
752
+ if (displayContent.length > 10000) displayContent = displayContent.substring(0, 10000) + "\n... (truncated)";
753
+ pre.textContent = displayContent;
754
+ resultBlock.appendChild(pre);
755
+ tool.el.appendChild(resultBlock);
756
+
757
+ tool.el.querySelector(".tool-header").addEventListener("click", function() {
758
+ resultBlock.classList.toggle("collapsed");
759
+ });
760
+
761
+ markToolDone(id, isError);
762
+ scrollToBottom();
763
+ }
764
+
765
+ function markToolDone(id, isError) {
766
+ var tool = tools[id];
767
+ if (!tool || tool.done) return;
768
+
769
+ tool.done = true;
770
+ if (!tool.el) return; // hidden tool (plan mode)
771
+
772
+ tool.el.classList.add("done");
773
+ if (isError) tool.el.classList.add("error");
774
+
775
+ var icon = tool.el.querySelector(".tool-status-icon");
776
+ if (isError) {
777
+ icon.innerHTML = '<span class="err-icon">' + iconHtml("alert-triangle") + '</span>';
778
+ } else {
779
+ icon.innerHTML = '<span class="check">' + iconHtml("check") + '</span>';
780
+ }
781
+ refreshIcons();
782
+ }
783
+
784
+ function markAllToolsDone() {
785
+ for (var id in tools) {
786
+ if (tools.hasOwnProperty(id) && !tools[id].done) {
787
+ markToolDone(id, false);
788
+ }
789
+ }
790
+ }
791
+
792
+ function addTurnMeta(cost, duration) {
793
+ var div = document.createElement("div");
794
+ div.className = "turn-meta";
795
+ var parts = [];
796
+ if (cost != null) parts.push("$" + cost.toFixed(4));
797
+ if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
798
+ if (parts.length) {
799
+ div.textContent = parts.join(" \u00b7 ");
800
+ messagesEl.appendChild(div);
801
+ scrollToBottom();
802
+ }
803
+ }
804
+
805
+ // --- WebSocket ---
806
+ function connect() {
807
+ if (ws) { ws.onclose = null; ws.close(); }
808
+
809
+ var protocol = location.protocol === "https:" ? "wss:" : "ws:";
810
+ ws = new WebSocket(protocol + "//" + location.host);
811
+
812
+ ws.onopen = function() {
813
+ setStatus("connected");
814
+ reconnectDelay = 1000;
815
+ if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
816
+ };
817
+
818
+ ws.onclose = function() {
819
+ setStatus("disconnected");
820
+ processing = false;
821
+ setActivity(null);
822
+ scheduleReconnect();
823
+ };
824
+
825
+ ws.onerror = function() {};
826
+
827
+ ws.onmessage = function(event) {
828
+ var msg;
829
+ try { msg = JSON.parse(event.data); } catch(e) { return; }
830
+
831
+ switch (msg.type) {
832
+ case "info":
833
+ projectNameEl.textContent = msg.project || msg.cwd;
834
+ break;
835
+
836
+ case "slash_commands":
837
+ slashCommands = (msg.commands || []).map(function(name) {
838
+ return { name: name, desc: "Skill" };
839
+ });
840
+ break;
841
+
842
+ case "session_list":
843
+ renderSessionList(msg.sessions || []);
844
+ break;
845
+
846
+ case "session_switched":
847
+ activeSessionId = msg.id;
848
+ resetClientState();
849
+ break;
850
+
851
+ case "user_message":
852
+ addUserMessage(msg.text, msg.images || null);
853
+ break;
854
+
855
+ case "status":
856
+ if (msg.status === "processing") {
857
+ setStatus("processing");
858
+ setActivity(randomThinkingVerb() + "...");
859
+ }
860
+ break;
861
+
862
+ case "thinking_start":
863
+ startThinking();
864
+ break;
865
+
866
+ case "thinking_delta":
867
+ if (typeof msg.text === "string") appendThinking(msg.text);
868
+ break;
869
+
870
+ case "thinking_stop":
871
+ stopThinking();
872
+ setActivity(randomThinkingVerb() + "...");
873
+ break;
874
+
875
+ case "delta":
876
+ if (typeof msg.text !== "string") break;
877
+ stopThinking();
878
+ setActivity(null);
879
+ appendDelta(msg.text);
880
+ break;
881
+
882
+ case "tool_start":
883
+ stopThinking();
884
+ markAllToolsDone();
885
+ if (msg.name === "EnterPlanMode") {
886
+ renderPlanBanner("enter");
887
+ tools[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
888
+ } else if (msg.name === "ExitPlanMode") {
889
+ if (planContent) {
890
+ renderPlanCard(planContent);
891
+ }
892
+ renderPlanBanner("exit");
893
+ tools[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
894
+ } else if (TODO_TOOLS[msg.name]) {
895
+ tools[msg.id] = { el: null, name: msg.name, input: null, done: true, hidden: true };
896
+ } else {
897
+ createToolItem(msg.id, msg.name);
898
+ }
899
+ break;
900
+
901
+ case "tool_executing":
902
+ if (msg.name === "AskUserQuestion" && msg.input && msg.input.questions) {
903
+ var askTool = tools[msg.id];
904
+ if (askTool) {
905
+ if (askTool.el) askTool.el.style.display = "none";
906
+ askTool.done = true;
907
+ }
908
+ renderAskUserQuestion(msg.id, msg.input);
909
+ } else if (msg.name === "Write" && msg.input && isPlanFile(msg.input.file_path)) {
910
+ planContent = msg.input.content || "";
911
+ updateToolExecuting(msg.id, msg.name, msg.input);
912
+ } else if (msg.name === "TodoWrite") {
913
+ handleTodoWrite(msg.input);
914
+ } else if (msg.name === "TaskCreate") {
915
+ handleTaskCreate(msg.input);
916
+ } else if (msg.name === "TaskUpdate") {
917
+ handleTaskUpdate(msg.input);
918
+ } else if (TODO_TOOLS[msg.name]) {
919
+ // TaskList, TaskGet - silently skip
920
+ } else {
921
+ var t = tools[msg.id];
922
+ if (t && t.hidden) break;
923
+ updateToolExecuting(msg.id, msg.name, msg.input);
924
+ }
925
+ break;
926
+
927
+ case "tool_result":
928
+ if (msg.content != null) {
929
+ var tr = tools[msg.id];
930
+ if (tr && tr.hidden) break; // skip hidden plan tools
931
+ updateToolResult(msg.id, msg.content, msg.is_error || false);
932
+ }
933
+ break;
934
+
935
+ case "result":
936
+ setActivity(null);
937
+ stopThinking();
938
+ markAllToolsDone();
939
+ finalizeAssistantBlock();
940
+ addTurnMeta(msg.cost, msg.duration);
941
+ break;
942
+
943
+ case "done":
944
+ setActivity(null);
945
+ stopThinking();
946
+ markAllToolsDone();
947
+ finalizeAssistantBlock();
948
+ processing = false;
949
+ setStatus("connected");
950
+ tools = {};
951
+ break;
952
+
953
+ case "stderr":
954
+ addSystemMessage(msg.text, false);
955
+ break;
956
+
957
+ case "error":
958
+ setActivity(null);
959
+ addSystemMessage(msg.text, true);
960
+ break;
961
+ }
962
+ };
963
+ }
964
+
965
+ function scheduleReconnect() {
966
+ if (reconnectTimer) return;
967
+ reconnectTimer = setTimeout(function() {
968
+ reconnectTimer = null;
969
+ connect();
970
+ }, reconnectDelay);
971
+ reconnectDelay = Math.min(reconnectDelay * 1.5, 10000);
972
+ }
973
+
974
+ // --- Sending messages ---
975
+ function sendMessage() {
976
+ var text = inputEl.value.trim();
977
+ var images = pendingImages.slice();
978
+ if (!text && images.length === 0) return;
979
+ hideSlashMenu();
980
+
981
+ if (text === "/clear") {
982
+ messagesEl.innerHTML = "";
983
+ inputEl.value = "";
984
+ clearPendingImages();
985
+ autoResize();
986
+ return;
987
+ }
988
+
989
+ if (!connected || processing) return;
990
+
991
+ addUserMessage(text, images.length > 0 ? images : null);
992
+
993
+ var payload = { type: "message", text: text || "" };
994
+ if (images.length > 0) {
995
+ payload.images = images;
996
+ }
997
+ ws.send(JSON.stringify(payload));
998
+
999
+ inputEl.value = "";
1000
+ clearPendingImages();
1001
+ autoResize();
1002
+ inputEl.focus();
1003
+ }
1004
+
1005
+ function autoResize() {
1006
+ inputEl.style.height = "auto";
1007
+ inputEl.style.height = Math.min(inputEl.scrollHeight, 120) + "px";
1008
+ }
1009
+
1010
+ // --- Image paste ---
1011
+ function addPendingImage(dataUrl) {
1012
+ // dataUrl = "data:image/png;base64,xxxx..."
1013
+ var commaIdx = dataUrl.indexOf(",");
1014
+ if (commaIdx === -1) return;
1015
+ var header = dataUrl.substring(0, commaIdx); // "data:image/png;base64"
1016
+ var data = dataUrl.substring(commaIdx + 1);
1017
+ var typeMatch = header.match(/data:(image\/[^;,]+)/);
1018
+ if (!typeMatch || !data) return;
1019
+ pendingImages.push({ mediaType: typeMatch[1], data: data });
1020
+ renderImagePreviews();
1021
+ }
1022
+
1023
+ function removePendingImage(idx) {
1024
+ pendingImages.splice(idx, 1);
1025
+ renderImagePreviews();
1026
+ }
1027
+
1028
+ function clearPendingImages() {
1029
+ pendingImages = [];
1030
+ renderImagePreviews();
1031
+ }
1032
+
1033
+ function renderImagePreviews() {
1034
+ imagePreviewBar.innerHTML = "";
1035
+ if (pendingImages.length === 0) {
1036
+ imagePreviewBar.classList.remove("visible");
1037
+ return;
1038
+ }
1039
+ imagePreviewBar.classList.add("visible");
1040
+ for (var i = 0; i < pendingImages.length; i++) {
1041
+ (function(idx) {
1042
+ var wrap = document.createElement("div");
1043
+ wrap.className = "image-preview-thumb";
1044
+ var img = document.createElement("img");
1045
+ img.src = "data:" + pendingImages[idx].mediaType + ";base64," + pendingImages[idx].data;
1046
+ var removeBtn = document.createElement("button");
1047
+ removeBtn.className = "image-preview-remove";
1048
+ removeBtn.innerHTML = iconHtml("x");
1049
+ removeBtn.addEventListener("click", function() {
1050
+ removePendingImage(idx);
1051
+ });
1052
+ wrap.appendChild(img);
1053
+ wrap.appendChild(removeBtn);
1054
+ imagePreviewBar.appendChild(wrap);
1055
+ })(i);
1056
+ }
1057
+ refreshIcons();
1058
+ }
1059
+
1060
+ function readImageBlob(blob) {
1061
+ var reader = new FileReader();
1062
+ reader.onload = function(ev) {
1063
+ addPendingImage(ev.target.result);
1064
+ };
1065
+ reader.readAsDataURL(blob);
1066
+ }
1067
+
1068
+ document.addEventListener("paste", function(e) {
1069
+ var cd = e.clipboardData;
1070
+ if (!cd) return;
1071
+
1072
+ var found = false;
1073
+
1074
+ // Try clipboardData.files first (better Safari/iOS support)
1075
+ if (cd.files && cd.files.length > 0) {
1076
+ for (var i = 0; i < cd.files.length; i++) {
1077
+ if (cd.files[i].type.indexOf("image/") === 0) {
1078
+ found = true;
1079
+ readImageBlob(cd.files[i]);
1080
+ }
1081
+ }
1082
+ }
1083
+
1084
+ // Fall back to clipboardData.items
1085
+ if (!found && cd.items) {
1086
+ for (var i = 0; i < cd.items.length; i++) {
1087
+ if (cd.items[i].type.indexOf("image/") === 0) {
1088
+ var blob = cd.items[i].getAsFile();
1089
+ if (blob) {
1090
+ found = true;
1091
+ readImageBlob(blob);
1092
+ }
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ if (found) e.preventDefault();
1098
+ });
1099
+
1100
+ // --- Slash menu ---
1101
+ function getAllCommands() {
1102
+ return builtinCommands.concat(slashCommands);
1103
+ }
1104
+
1105
+ function showSlashMenu(filter) {
1106
+ var query = filter.toLowerCase();
1107
+ slashFiltered = getAllCommands().filter(function(c) {
1108
+ return c.name.toLowerCase().indexOf(query) !== -1;
1109
+ });
1110
+ if (slashFiltered.length === 0) { hideSlashMenu(); return; }
1111
+
1112
+ slashActiveIdx = 0;
1113
+ slashMenu.innerHTML = slashFiltered.map(function(c, i) {
1114
+ return '<div class="slash-item' + (i === 0 ? ' active' : '') + '" data-idx="' + i + '">' +
1115
+ '<span class="slash-cmd">/' + c.name + '</span>' +
1116
+ '<span class="slash-desc">' + c.desc + '</span>' +
1117
+ '</div>';
1118
+ }).join("");
1119
+ slashMenu.classList.add("visible");
1120
+
1121
+ slashMenu.querySelectorAll(".slash-item").forEach(function(el) {
1122
+ el.addEventListener("click", function() {
1123
+ selectSlashItem(parseInt(el.dataset.idx));
1124
+ });
1125
+ });
1126
+ }
1127
+
1128
+ function hideSlashMenu() {
1129
+ slashMenu.classList.remove("visible");
1130
+ slashMenu.innerHTML = "";
1131
+ slashActiveIdx = -1;
1132
+ slashFiltered = [];
1133
+ }
1134
+
1135
+ function selectSlashItem(idx) {
1136
+ if (idx < 0 || idx >= slashFiltered.length) return;
1137
+ var cmd = slashFiltered[idx];
1138
+ inputEl.value = "/" + cmd.name + " ";
1139
+ hideSlashMenu();
1140
+ autoResize();
1141
+ inputEl.focus();
1142
+ }
1143
+
1144
+ function updateSlashHighlight() {
1145
+ slashMenu.querySelectorAll(".slash-item").forEach(function(el, i) {
1146
+ el.classList.toggle("active", i === slashActiveIdx);
1147
+ });
1148
+ var activeEl = slashMenu.querySelector(".slash-item.active");
1149
+ if (activeEl) activeEl.scrollIntoView({ block: "nearest" });
1150
+ }
1151
+
1152
+ // --- Input handlers ---
1153
+ inputEl.addEventListener("input", function() {
1154
+ autoResize();
1155
+ var val = inputEl.value;
1156
+ if (val.startsWith("/") && !val.includes(" ") && val.length > 1) {
1157
+ showSlashMenu(val.substring(1));
1158
+ } else if (val === "/") {
1159
+ showSlashMenu("");
1160
+ } else {
1161
+ hideSlashMenu();
1162
+ }
1163
+ });
1164
+
1165
+ inputEl.addEventListener("compositionstart", function() { isComposing = true; });
1166
+ inputEl.addEventListener("compositionend", function() { isComposing = false; });
1167
+
1168
+ inputEl.addEventListener("keydown", function(e) {
1169
+ if (slashFiltered.length > 0 && slashMenu.classList.contains("visible")) {
1170
+ if (e.key === "ArrowDown") {
1171
+ e.preventDefault();
1172
+ slashActiveIdx = (slashActiveIdx + 1) % slashFiltered.length;
1173
+ updateSlashHighlight();
1174
+ return;
1175
+ }
1176
+ if (e.key === "ArrowUp") {
1177
+ e.preventDefault();
1178
+ slashActiveIdx = (slashActiveIdx - 1 + slashFiltered.length) % slashFiltered.length;
1179
+ updateSlashHighlight();
1180
+ return;
1181
+ }
1182
+ if (e.key === "Tab" || (e.key === "Enter" && !e.shiftKey)) {
1183
+ e.preventDefault();
1184
+ selectSlashItem(slashActiveIdx);
1185
+ return;
1186
+ }
1187
+ if (e.key === "Escape") {
1188
+ e.preventDefault();
1189
+ hideSlashMenu();
1190
+ return;
1191
+ }
1192
+ }
1193
+
1194
+ if (e.key === "Enter" && !e.shiftKey && !isComposing) {
1195
+ e.preventDefault();
1196
+ sendMessage();
1197
+ }
1198
+ });
1199
+
1200
+ sendBtn.addEventListener("click", function() { sendMessage(); });
1201
+
1202
+ // --- Mobile viewport ---
1203
+ if (window.visualViewport) {
1204
+ window.visualViewport.addEventListener("resize", function() {
1205
+ $("app").style.height = window.visualViewport.height + "px";
1206
+ scrollToBottom();
1207
+ });
1208
+ window.visualViewport.addEventListener("scroll", function() {
1209
+ $("app").style.height = window.visualViewport.height + "px";
1210
+ });
1211
+ }
1212
+
1213
+ // --- Init ---
1214
+ lucide.createIcons();
1215
+ connect();
1216
+ inputEl.focus();
1217
+ })();