clay-server 2.5.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (87) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +281 -0
  3. package/bin/cli.js +2385 -0
  4. package/lib/cli-sessions.js +270 -0
  5. package/lib/config.js +237 -0
  6. package/lib/daemon.js +489 -0
  7. package/lib/ipc.js +112 -0
  8. package/lib/notes.js +120 -0
  9. package/lib/pages.js +664 -0
  10. package/lib/project.js +1433 -0
  11. package/lib/public/app.js +2795 -0
  12. package/lib/public/apple-touch-icon-dark.png +0 -0
  13. package/lib/public/apple-touch-icon.png +0 -0
  14. package/lib/public/css/base.css +264 -0
  15. package/lib/public/css/diff.css +128 -0
  16. package/lib/public/css/filebrowser.css +1114 -0
  17. package/lib/public/css/highlight.css +144 -0
  18. package/lib/public/css/icon-strip.css +296 -0
  19. package/lib/public/css/input.css +573 -0
  20. package/lib/public/css/menus.css +856 -0
  21. package/lib/public/css/messages.css +1445 -0
  22. package/lib/public/css/mobile-nav.css +354 -0
  23. package/lib/public/css/overlays.css +697 -0
  24. package/lib/public/css/rewind.css +505 -0
  25. package/lib/public/css/server-settings.css +761 -0
  26. package/lib/public/css/sidebar.css +936 -0
  27. package/lib/public/css/sticky-notes.css +358 -0
  28. package/lib/public/css/title-bar.css +314 -0
  29. package/lib/public/favicon-dark.svg +1 -0
  30. package/lib/public/favicon.svg +1 -0
  31. package/lib/public/icon-192-dark.png +0 -0
  32. package/lib/public/icon-192.png +0 -0
  33. package/lib/public/icon-512-dark.png +0 -0
  34. package/lib/public/icon-512.png +0 -0
  35. package/lib/public/icon-mono.svg +1 -0
  36. package/lib/public/index.html +762 -0
  37. package/lib/public/manifest.json +27 -0
  38. package/lib/public/modules/diff.js +398 -0
  39. package/lib/public/modules/events.js +21 -0
  40. package/lib/public/modules/filebrowser.js +1411 -0
  41. package/lib/public/modules/fileicons.js +172 -0
  42. package/lib/public/modules/icons.js +54 -0
  43. package/lib/public/modules/input.js +584 -0
  44. package/lib/public/modules/markdown.js +356 -0
  45. package/lib/public/modules/notifications.js +649 -0
  46. package/lib/public/modules/qrcode.js +70 -0
  47. package/lib/public/modules/rewind.js +345 -0
  48. package/lib/public/modules/server-settings.js +510 -0
  49. package/lib/public/modules/sidebar.js +1083 -0
  50. package/lib/public/modules/state.js +3 -0
  51. package/lib/public/modules/sticky-notes.js +688 -0
  52. package/lib/public/modules/terminal.js +697 -0
  53. package/lib/public/modules/theme.js +738 -0
  54. package/lib/public/modules/tools.js +1608 -0
  55. package/lib/public/modules/utils.js +56 -0
  56. package/lib/public/style.css +15 -0
  57. package/lib/public/sw.js +75 -0
  58. package/lib/push.js +124 -0
  59. package/lib/sdk-bridge.js +989 -0
  60. package/lib/server.js +582 -0
  61. package/lib/sessions.js +424 -0
  62. package/lib/terminal-manager.js +187 -0
  63. package/lib/terminal.js +24 -0
  64. package/lib/themes/ayu-light.json +9 -0
  65. package/lib/themes/catppuccin-latte.json +9 -0
  66. package/lib/themes/catppuccin-mocha.json +9 -0
  67. package/lib/themes/clay-light.json +10 -0
  68. package/lib/themes/clay.json +10 -0
  69. package/lib/themes/dracula.json +9 -0
  70. package/lib/themes/everforest-light.json +9 -0
  71. package/lib/themes/everforest.json +9 -0
  72. package/lib/themes/github-light.json +9 -0
  73. package/lib/themes/gruvbox-dark.json +9 -0
  74. package/lib/themes/gruvbox-light.json +9 -0
  75. package/lib/themes/monokai.json +9 -0
  76. package/lib/themes/nord-light.json +9 -0
  77. package/lib/themes/nord.json +9 -0
  78. package/lib/themes/one-dark.json +9 -0
  79. package/lib/themes/one-light.json +9 -0
  80. package/lib/themes/rose-pine-dawn.json +9 -0
  81. package/lib/themes/rose-pine.json +9 -0
  82. package/lib/themes/solarized-dark.json +9 -0
  83. package/lib/themes/solarized-light.json +9 -0
  84. package/lib/themes/tokyo-night-light.json +9 -0
  85. package/lib/themes/tokyo-night.json +9 -0
  86. package/lib/updater.js +97 -0
  87. package/package.json +47 -0
@@ -0,0 +1,1608 @@
1
+ import { escapeHtml, copyToClipboard } from './utils.js';
2
+ import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
3
+ import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
4
+ import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
5
+ import { openFile } from './filebrowser.js';
6
+
7
+ var ctx;
8
+
9
+ // --- Plan mode state ---
10
+ var inPlanMode = false;
11
+ var planContent = null;
12
+
13
+ // --- Todo state ---
14
+ var todoItems = [];
15
+ var todoWidgetEl = null;
16
+ var todoWidgetVisible = true; // whether in-chat widget is in viewport
17
+ var todoObserver = null;
18
+
19
+ // --- Tool tracking ---
20
+ var tools = {};
21
+ var currentThinking = null;
22
+ var thinkingGroup = null; // { el, count, totalDuration }
23
+ var pendingPermissions = {};
24
+
25
+ // --- Tool group tracking ---
26
+ var currentToolGroup = null;
27
+ var toolGroupCounter = 0;
28
+ var toolGroups = {};
29
+
30
+ // --- Tool helpers ---
31
+ var PLAN_MODE_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1 };
32
+ var TODO_TOOLS = { TodoWrite: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1 };
33
+ var HIDDEN_RESULT_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1, TodoWrite: 1 };
34
+
35
+ // --- Tool group helpers ---
36
+ function closeToolGroup() {
37
+ if (currentToolGroup) {
38
+ currentToolGroup.closed = true;
39
+ }
40
+ currentToolGroup = null;
41
+ }
42
+
43
+ function findToolGroup(groupId) {
44
+ return toolGroups[groupId] || null;
45
+ }
46
+
47
+ function toolGroupSummary(group) {
48
+ var names = group.toolNames;
49
+ var count = names.length;
50
+ var allDone = group.doneCount >= count;
51
+
52
+ // Count by tool name
53
+ var counts = {};
54
+ for (var i = 0; i < names.length; i++) {
55
+ counts[names[i]] = (counts[names[i]] || 0) + 1;
56
+ }
57
+ var uniqueNames = Object.keys(counts);
58
+
59
+ if (uniqueNames.length === 1) {
60
+ var name = uniqueNames[0];
61
+ var n = counts[name];
62
+ if (allDone) {
63
+ switch (name) {
64
+ case "Read": return "Read " + n + " file" + (n > 1 ? "s" : "");
65
+ case "Edit": return "Edited " + n + " file" + (n > 1 ? "s" : "");
66
+ case "Write": return "Wrote " + n + " file" + (n > 1 ? "s" : "");
67
+ case "Bash": return "Ran " + n + " command" + (n > 1 ? "s" : "");
68
+ case "Grep": return "Searched " + n + " pattern" + (n > 1 ? "s" : "");
69
+ case "Glob": return "Found " + n + " pattern" + (n > 1 ? "s" : "");
70
+ case "Task": return "Ran " + n + " task" + (n > 1 ? "s" : "");
71
+ case "WebSearch": return "Searched " + n + " quer" + (n > 1 ? "ies" : "y");
72
+ case "WebFetch": return "Fetched " + n + " URL" + (n > 1 ? "s" : "");
73
+ default: return "Ran " + n + " tool" + (n > 1 ? "s" : "");
74
+ }
75
+ }
76
+ switch (name) {
77
+ case "Read": return "Reading " + n + " file" + (n > 1 ? "s" : "") + "...";
78
+ case "Edit": return "Editing " + n + " file" + (n > 1 ? "s" : "") + "...";
79
+ case "Write": return "Writing " + n + " file" + (n > 1 ? "s" : "") + "...";
80
+ case "Bash": return "Running " + n + " command" + (n > 1 ? "s" : "") + "...";
81
+ case "Grep": return "Searching " + n + " pattern" + (n > 1 ? "s" : "") + "...";
82
+ case "Glob": return "Finding " + n + " pattern" + (n > 1 ? "s" : "") + "...";
83
+ case "Task": return "Running " + n + " task" + (n > 1 ? "s" : "") + "...";
84
+ case "WebSearch": return "Searching " + n + " quer" + (n > 1 ? "ies" : "y") + "...";
85
+ case "WebFetch": return "Fetching " + n + " URL" + (n > 1 ? "s" : "") + "...";
86
+ default: return "Running " + n + " tool" + (n > 1 ? "s" : "") + "...";
87
+ }
88
+ }
89
+
90
+ // Mixed tools
91
+ if (allDone) return "Ran " + count + " tools";
92
+ return "Running " + count + " tools...";
93
+ }
94
+
95
+ function updateToolGroupHeader(group) {
96
+ if (!group || !group.el) return;
97
+ var label = group.el.querySelector(".tool-group-label");
98
+ if (label) label.textContent = toolGroupSummary(group);
99
+
100
+ var allDone = group.doneCount >= group.toolCount;
101
+ var statusIcon = group.el.querySelector(".tool-group-status-icon");
102
+ var bullet = group.el.querySelector(".tool-group-bullet");
103
+
104
+ if (allDone) {
105
+ group.el.classList.add("done");
106
+ if (group.errorCount > 0) {
107
+ statusIcon.innerHTML = '<span class="err-icon">' + iconHtml("alert-triangle") + '</span>';
108
+ if (bullet) bullet.classList.add("error");
109
+ } else {
110
+ statusIcon.innerHTML = '<span class="check">' + iconHtml("check") + '</span>';
111
+ }
112
+ refreshIcons();
113
+ }
114
+
115
+ // Show group header only when 2+ visible tools
116
+ var header = group.el.querySelector(".tool-group-header");
117
+ if (group.toolCount >= 2) {
118
+ header.style.display = "";
119
+ // When 2+ tools, ensure collapsed by default (unless user already toggled)
120
+ if (!group.userToggled && !group.el.classList.contains("expanded-by-user")) {
121
+ group.el.classList.add("collapsed");
122
+ }
123
+ } else {
124
+ header.style.display = "none";
125
+ group.el.classList.remove("collapsed");
126
+ }
127
+ }
128
+
129
+ function isPlanFile(filePath) {
130
+ return filePath && filePath.indexOf(".claude/plans/") !== -1;
131
+ }
132
+
133
+ export function toolSummary(name, input) {
134
+ if (!input || typeof input !== "object") return "";
135
+ switch (name) {
136
+ case "Read": return shortPath(input.file_path);
137
+ case "Edit": return shortPath(input.file_path);
138
+ case "Write": return shortPath(input.file_path);
139
+ case "Bash": return (input.command || "").substring(0, 80);
140
+ case "Glob": return input.pattern || "";
141
+ case "Grep": return (input.pattern || "") + (input.path ? " in " + shortPath(input.path) : "");
142
+ case "WebFetch": return input.url || "";
143
+ case "WebSearch": return input.query || "";
144
+ case "Task": return input.description || "";
145
+ case "EnterPlanMode": return "";
146
+ case "ExitPlanMode": return "";
147
+ default: return JSON.stringify(input).substring(0, 60);
148
+ }
149
+ }
150
+
151
+ export function toolActivityText(name, input) {
152
+ if (name === "Bash" && input && input.description) return input.description;
153
+ if (name === "Read" && input && input.file_path) return "Reading " + shortPath(input.file_path);
154
+ if (name === "Edit" && input && input.file_path) return "Editing " + shortPath(input.file_path);
155
+ if (name === "Write" && input && input.file_path) return "Writing " + shortPath(input.file_path);
156
+ if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
157
+ if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
158
+ if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
159
+ if (name === "WebFetch") return "Fetching URL...";
160
+ if (name === "Task" && input && input.description) return input.description;
161
+ if (name === "EnterPlanMode") return "Entering plan mode...";
162
+ if (name === "ExitPlanMode") return "Finalizing the plan...";
163
+ return "Running " + name + "...";
164
+ }
165
+
166
+ function shortPath(p) {
167
+ if (!p) return "";
168
+ var parts = p.split("/");
169
+ return parts.length > 3 ? ".../" + parts.slice(-3).join("/") : p;
170
+ }
171
+
172
+ // --- AskUserQuestion ---
173
+ export function renderAskUserQuestion(toolId, input) {
174
+ ctx.finalizeAssistantBlock();
175
+ stopThinking();
176
+ closeToolGroup();
177
+
178
+ var questions = input.questions || [];
179
+ if (questions.length === 0) return;
180
+
181
+ var container = document.createElement("div");
182
+ container.className = "ask-user-container";
183
+ container.dataset.toolId = toolId;
184
+
185
+ var answers = {};
186
+ var multiSelections = {};
187
+
188
+ questions.forEach(function (q, qIdx) {
189
+ var qDiv = document.createElement("div");
190
+ qDiv.className = "ask-user-question";
191
+
192
+ var qText = document.createElement("div");
193
+ qText.className = "ask-user-question-text";
194
+ qText.textContent = q.question || "";
195
+ qDiv.appendChild(qText);
196
+
197
+ var optionsDiv = document.createElement("div");
198
+ optionsDiv.className = "ask-user-options";
199
+
200
+ var isMulti = q.multiSelect || false;
201
+ if (isMulti) multiSelections[qIdx] = new Set();
202
+
203
+ (q.options || []).forEach(function (opt) {
204
+ var btn = document.createElement("button");
205
+ btn.className = "ask-user-option";
206
+ btn.innerHTML =
207
+ '<div class="option-label"></div>' +
208
+ (opt.description ? '<div class="option-desc"></div>' : '');
209
+ btn.querySelector(".option-label").textContent = opt.label;
210
+ if (opt.description) btn.querySelector(".option-desc").textContent = opt.description;
211
+
212
+ btn.addEventListener("click", function () {
213
+ if (container.classList.contains("answered")) return;
214
+
215
+ if (isMulti) {
216
+ var set = multiSelections[qIdx];
217
+ if (set.has(opt.label)) {
218
+ set.delete(opt.label);
219
+ btn.classList.remove("selected");
220
+ } else {
221
+ set.add(opt.label);
222
+ btn.classList.add("selected");
223
+ }
224
+ } else {
225
+ optionsDiv.querySelectorAll(".ask-user-option").forEach(function (b) {
226
+ b.classList.remove("selected");
227
+ });
228
+ btn.classList.add("selected");
229
+ answers[qIdx] = opt.label;
230
+ var otherInput = qDiv.querySelector(".ask-user-other input");
231
+ if (otherInput) otherInput.value = "";
232
+ if (questions.length === 1) {
233
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
234
+ }
235
+ }
236
+ });
237
+
238
+ optionsDiv.appendChild(btn);
239
+ });
240
+
241
+ qDiv.appendChild(optionsDiv);
242
+
243
+ // "Other" text input
244
+ var otherDiv = document.createElement("div");
245
+ otherDiv.className = "ask-user-other";
246
+ var otherInput = document.createElement("input");
247
+ otherInput.type = "text";
248
+ otherInput.placeholder = "Other...";
249
+ otherInput.addEventListener("input", function () {
250
+ if (container.classList.contains("answered")) return;
251
+ if (otherInput.value.trim()) {
252
+ optionsDiv.querySelectorAll(".ask-user-option").forEach(function (b) {
253
+ b.classList.remove("selected");
254
+ });
255
+ if (isMulti) multiSelections[qIdx] = new Set();
256
+ answers[qIdx] = otherInput.value.trim();
257
+ }
258
+ });
259
+ otherInput.addEventListener("keydown", function (e) {
260
+ if (e.key === "Enter" && !e.shiftKey) {
261
+ e.preventDefault();
262
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
263
+ }
264
+ });
265
+ otherDiv.appendChild(otherInput);
266
+ qDiv.appendChild(otherDiv);
267
+ container.appendChild(qDiv);
268
+ });
269
+
270
+ // Single submit button at the bottom (only for multi-question)
271
+ if (questions.length > 1) {
272
+ var submitBtn = document.createElement("button");
273
+ submitBtn.className = "ask-user-submit";
274
+ submitBtn.textContent = "Submit";
275
+ submitBtn.addEventListener("click", function () {
276
+ submitAskUserAnswer(container, toolId, questions, answers, multiSelections);
277
+ });
278
+ container.appendChild(submitBtn);
279
+ }
280
+
281
+ // Skip button
282
+ var skipBtn = document.createElement("button");
283
+ skipBtn.className = "ask-user-skip";
284
+ skipBtn.textContent = "Skip";
285
+ skipBtn.addEventListener("click", function () {
286
+ if (container.classList.contains("answered")) return;
287
+ container.classList.add("answered");
288
+ enableMainInput();
289
+ if (ctx.ws && ctx.connected) {
290
+ ctx.ws.send(JSON.stringify({ type: "stop" }));
291
+ }
292
+ });
293
+ container.appendChild(skipBtn);
294
+
295
+ ctx.addToMessages(container);
296
+ disableMainInput();
297
+ ctx.setActivity(null);
298
+ ctx.scrollToBottom();
299
+ }
300
+
301
+ export function disableMainInput() {
302
+ ctx.inputEl.disabled = true;
303
+ ctx.inputEl.placeholder = "Answer the question above to continue...";
304
+ }
305
+
306
+ export function enableMainInput() {
307
+ ctx.inputEl.disabled = false;
308
+ ctx.inputEl.placeholder = "Message Claude Code...";
309
+ }
310
+
311
+ function submitAskUserAnswer(container, toolId, questions, answers, multiSelections) {
312
+ if (container.classList.contains("answered")) return;
313
+
314
+ var result = {};
315
+ for (var i = 0; i < questions.length; i++) {
316
+ var q = questions[i];
317
+ if (q.multiSelect && multiSelections[i] && multiSelections[i].size > 0) {
318
+ result[i] = Array.from(multiSelections[i]).join(", ");
319
+ } else if (answers[i]) {
320
+ result[i] = answers[i];
321
+ }
322
+ }
323
+
324
+ if (Object.keys(result).length === 0) return;
325
+
326
+ container.classList.add("answered");
327
+ enableMainInput();
328
+ if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
329
+
330
+ if (ctx.ws && ctx.connected) {
331
+ ctx.ws.send(JSON.stringify({
332
+ type: "ask_user_response",
333
+ toolId: toolId,
334
+ answers: result,
335
+ }));
336
+ }
337
+ }
338
+
339
+ export function markAskUserAnswered(toolId) {
340
+ var container = document.querySelector('.ask-user-container[data-tool-id="' + toolId + '"]');
341
+ if (container && !container.classList.contains("answered")) {
342
+ container.classList.add("answered");
343
+ enableMainInput();
344
+ }
345
+ }
346
+
347
+ // --- Permission request ---
348
+ function permissionInputSummary(toolName, input) {
349
+ if (!input || typeof input !== "object") return "";
350
+ switch (toolName) {
351
+ case "Bash": return input.command || input.description || "";
352
+ case "Edit": return shortPath(input.file_path);
353
+ case "Write": return shortPath(input.file_path);
354
+ case "Read": return shortPath(input.file_path);
355
+ case "Glob": return input.pattern || "";
356
+ case "Grep": return (input.pattern || "") + (input.path ? " in " + shortPath(input.path) : "");
357
+ default: return toolSummary(toolName, input);
358
+ }
359
+ }
360
+
361
+ export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason) {
362
+ if (pendingPermissions[requestId]) return;
363
+ ctx.finalizeAssistantBlock();
364
+ stopThinking();
365
+ closeToolGroup();
366
+
367
+ // ExitPlanMode: render as plan confirmation instead of generic permission
368
+ if (toolName === "ExitPlanMode") {
369
+ renderPlanPermission(requestId);
370
+ return;
371
+ }
372
+
373
+ var container = document.createElement("div");
374
+ container.className = "permission-container";
375
+ container.dataset.requestId = requestId;
376
+
377
+ // Header
378
+ var header = document.createElement("div");
379
+ header.className = "permission-header";
380
+ header.innerHTML =
381
+ '<span class="permission-icon">' + iconHtml("shield") + '</span>' +
382
+ '<span class="permission-title">Permission Required</span>';
383
+
384
+ // Body
385
+ var body = document.createElement("div");
386
+ body.className = "permission-body";
387
+
388
+ var summary = document.createElement("div");
389
+ summary.className = "permission-summary";
390
+ var summaryText = permissionInputSummary(toolName, toolInput);
391
+ summary.innerHTML =
392
+ '<span class="permission-tool-name"></span>' +
393
+ (summaryText ? '<span class="permission-tool-desc"></span>' : '');
394
+ summary.querySelector(".permission-tool-name").textContent = toolName;
395
+ if (summaryText) {
396
+ summary.querySelector(".permission-tool-desc").textContent = summaryText;
397
+ }
398
+ body.appendChild(summary);
399
+
400
+ if (decisionReason) {
401
+ var reason = document.createElement("div");
402
+ reason.className = "permission-reason";
403
+ reason.textContent = decisionReason;
404
+ body.appendChild(reason);
405
+ }
406
+
407
+ // Collapsible details
408
+ var details = document.createElement("details");
409
+ details.className = "permission-details";
410
+ var detailsSummary = document.createElement("summary");
411
+ detailsSummary.textContent = "Details";
412
+ var detailsPre = document.createElement("pre");
413
+ detailsPre.textContent = JSON.stringify(toolInput, null, 2);
414
+ details.appendChild(detailsSummary);
415
+ details.appendChild(detailsPre);
416
+ body.appendChild(details);
417
+
418
+ // Actions
419
+ var actions = document.createElement("div");
420
+ actions.className = "permission-actions";
421
+
422
+ var allowBtn = document.createElement("button");
423
+ allowBtn.className = "permission-btn permission-allow";
424
+ allowBtn.textContent = "Allow Once";
425
+ allowBtn.addEventListener("click", function () {
426
+ sendPermissionResponse(container, requestId, "allow");
427
+ });
428
+
429
+ var allowAlwaysBtn = document.createElement("button");
430
+ allowAlwaysBtn.className = "permission-btn permission-allow-session";
431
+ allowAlwaysBtn.textContent = "Always Allow";
432
+ allowAlwaysBtn.addEventListener("click", function () {
433
+ sendPermissionResponse(container, requestId, "allow_always");
434
+ });
435
+
436
+ var denyBtn = document.createElement("button");
437
+ denyBtn.className = "permission-btn permission-deny";
438
+ denyBtn.textContent = "Deny";
439
+ denyBtn.addEventListener("click", function () {
440
+ sendPermissionResponse(container, requestId, "deny");
441
+ });
442
+
443
+ actions.appendChild(allowBtn);
444
+ actions.appendChild(allowAlwaysBtn);
445
+ actions.appendChild(denyBtn);
446
+
447
+ container.appendChild(header);
448
+ container.appendChild(body);
449
+ container.appendChild(actions);
450
+ ctx.addToMessages(container);
451
+
452
+ pendingPermissions[requestId] = container;
453
+ refreshIcons();
454
+ ctx.setActivity(null);
455
+ ctx.scrollToBottom();
456
+ }
457
+
458
+ function renderPlanPermission(requestId) {
459
+ if (pendingPermissions[requestId]) return;
460
+ var container = document.createElement("div");
461
+ container.className = "permission-container plan-permission";
462
+ container.dataset.requestId = requestId;
463
+
464
+ // Header
465
+ var header = document.createElement("div");
466
+ header.className = "permission-header plan-permission-header";
467
+ header.innerHTML =
468
+ '<span class="permission-icon">' + iconHtml("check-circle") + '</span>' +
469
+ '<span class="permission-title">Plan Approval</span>';
470
+
471
+ // Body (plan content already visible above, no need to repeat)
472
+ var body = document.createElement("div");
473
+ body.className = "permission-body";
474
+
475
+ // Actions row 1: main buttons
476
+ var actions = document.createElement("div");
477
+ actions.className = "permission-actions plan-permission-actions";
478
+
479
+ // Option 1: Clear context & auto-accept
480
+ var clearBtn = document.createElement("button");
481
+ clearBtn.className = "permission-btn plan-btn-clear";
482
+ var contextPct = ctx.getContextPercent ? ctx.getContextPercent() : 0;
483
+ clearBtn.innerHTML = iconHtml("refresh-cw") + ' <span>Clear context' +
484
+ (contextPct > 0 ? ' <span class="plan-ctx-pct">(' + contextPct + '% used)</span>' : '') +
485
+ ' &amp; auto-accept</span>';
486
+ clearBtn.addEventListener("click", function () {
487
+ sendPlanResponse(container, requestId, "allow_clear_context");
488
+ });
489
+
490
+ // Option 2: Auto-accept edits
491
+ var approveBtn = document.createElement("button");
492
+ approveBtn.className = "permission-btn permission-allow";
493
+ approveBtn.textContent = "Auto-accept edits";
494
+ approveBtn.addEventListener("click", function () {
495
+ sendPlanResponse(container, requestId, "allow_accept_edits");
496
+ });
497
+
498
+ // Option 3: Manually approve edits
499
+ var manualBtn = document.createElement("button");
500
+ manualBtn.className = "permission-btn permission-allow-session";
501
+ manualBtn.textContent = "Manually approve";
502
+ manualBtn.addEventListener("click", function () {
503
+ sendPlanResponse(container, requestId, "allow");
504
+ });
505
+
506
+ // Option 4: Reject
507
+ var rejectBtn = document.createElement("button");
508
+ rejectBtn.className = "permission-btn permission-deny";
509
+ rejectBtn.textContent = "Reject";
510
+ rejectBtn.addEventListener("click", function () {
511
+ sendPlanResponse(container, requestId, "deny");
512
+ });
513
+
514
+ actions.appendChild(clearBtn);
515
+ actions.appendChild(approveBtn);
516
+ actions.appendChild(manualBtn);
517
+ actions.appendChild(rejectBtn);
518
+
519
+ // Feedback input row (Option 4: tell Claude what to change)
520
+ var feedbackRow = document.createElement("div");
521
+ feedbackRow.className = "plan-feedback-row";
522
+ var feedbackInput = document.createElement("input");
523
+ feedbackInput.type = "text";
524
+ feedbackInput.className = "plan-feedback-input";
525
+ feedbackInput.placeholder = "Tell Claude what to change...";
526
+ var feedbackSendBtn = document.createElement("button");
527
+ feedbackSendBtn.className = "plan-feedback-send";
528
+ feedbackSendBtn.innerHTML = iconHtml("arrow-up");
529
+ feedbackSendBtn.disabled = true;
530
+
531
+ feedbackInput.addEventListener("input", function () {
532
+ feedbackSendBtn.disabled = !feedbackInput.value.trim();
533
+ });
534
+ feedbackInput.addEventListener("keydown", function (e) {
535
+ if (e.key === "Enter" && !e.shiftKey && feedbackInput.value.trim()) {
536
+ e.preventDefault();
537
+ submitPlanFeedback();
538
+ }
539
+ });
540
+ feedbackSendBtn.addEventListener("click", function () {
541
+ if (feedbackInput.value.trim()) submitPlanFeedback();
542
+ });
543
+
544
+ function submitPlanFeedback() {
545
+ var text = feedbackInput.value.trim();
546
+ if (!text) return;
547
+ sendPlanResponse(container, requestId, "deny_with_feedback", text);
548
+ }
549
+
550
+ feedbackRow.appendChild(feedbackInput);
551
+ feedbackRow.appendChild(feedbackSendBtn);
552
+
553
+ container.appendChild(header);
554
+ container.appendChild(body);
555
+ container.appendChild(actions);
556
+ container.appendChild(feedbackRow);
557
+ ctx.addToMessages(container);
558
+
559
+ pendingPermissions[requestId] = container;
560
+ refreshIcons();
561
+ ctx.setActivity(null);
562
+ ctx.scrollToBottom();
563
+ // Focus the feedback input after render
564
+ setTimeout(function () { feedbackInput.focus(); }, 50);
565
+ }
566
+
567
+ function sendPlanResponse(container, requestId, decision, feedback) {
568
+ if (container.classList.contains("resolved")) return;
569
+ container.classList.add("resolved");
570
+ if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
571
+
572
+ var labelMap = {
573
+ "allow": "Approved (manual)",
574
+ "allow_accept_edits": "Approved (auto-accept)",
575
+ "allow_clear_context": "Approved (clear + auto-accept)",
576
+ "deny": "Rejected",
577
+ "deny_with_feedback": "Feedback sent",
578
+ };
579
+ var label = labelMap[decision] || decision;
580
+ var isDeny = decision === "deny" || decision === "deny_with_feedback";
581
+ var resolvedClass = isDeny ? "resolved-denied" : "resolved-allowed";
582
+ container.classList.add(resolvedClass);
583
+
584
+ // Replace actions + feedback with decision label
585
+ var actionsEl = container.querySelector(".plan-permission-actions");
586
+ if (actionsEl) {
587
+ actionsEl.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
588
+ }
589
+ var feedbackRowEl = container.querySelector(".plan-feedback-row");
590
+ if (feedbackRowEl) feedbackRowEl.remove();
591
+
592
+ if (ctx.ws && ctx.connected) {
593
+ var payload = {
594
+ type: "permission_response",
595
+ requestId: requestId,
596
+ decision: decision,
597
+ };
598
+ if (feedback) payload.feedback = feedback;
599
+ if (decision === "allow_clear_context" && planContent) {
600
+ payload.planContent = planContent;
601
+ }
602
+ ctx.ws.send(JSON.stringify(payload));
603
+ }
604
+
605
+ delete pendingPermissions[requestId];
606
+ }
607
+
608
+ function sendPermissionResponse(container, requestId, decision) {
609
+ if (container.classList.contains("resolved")) return;
610
+ container.classList.add("resolved");
611
+ if (ctx.stopUrgentBlink) ctx.stopUrgentBlink();
612
+
613
+ var label = decision === "deny" ? "Denied" : "Allowed";
614
+ var resolvedClass = decision === "deny" ? "resolved-denied" : "resolved-allowed";
615
+ container.classList.add(resolvedClass);
616
+
617
+ // Replace actions with decision label
618
+ var actions = container.querySelector(".permission-actions");
619
+ if (actions) {
620
+ actions.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
621
+ }
622
+
623
+ if (ctx.ws && ctx.connected) {
624
+ ctx.ws.send(JSON.stringify({
625
+ type: "permission_response",
626
+ requestId: requestId,
627
+ decision: decision,
628
+ }));
629
+ }
630
+
631
+ delete pendingPermissions[requestId];
632
+ }
633
+
634
+ export function markPermissionResolved(requestId, decision) {
635
+ var container = pendingPermissions[requestId];
636
+ if (!container) {
637
+ // Find by data attribute (history replay)
638
+ container = ctx.messagesEl.querySelector('[data-request-id="' + requestId + '"]');
639
+ }
640
+ if (!container || container.classList.contains("resolved")) return;
641
+
642
+ container.classList.add("resolved");
643
+
644
+ // Plan-specific decisions
645
+ var planLabelMap = {
646
+ "allow_accept_edits": "Approved (auto-accept)",
647
+ "allow_clear_context": "Approved (clear + auto-accept)",
648
+ "deny_with_feedback": "Feedback sent",
649
+ };
650
+ var isDeny = decision === "deny" || decision === "deny_with_feedback";
651
+ var resolvedClass = isDeny ? "resolved-denied" : "resolved-allowed";
652
+ container.classList.add(resolvedClass);
653
+
654
+ var label = planLabelMap[decision] || (decision === "deny" ? "Denied" : "Allowed");
655
+ var actions = container.querySelector(".permission-actions") || container.querySelector(".plan-permission-actions");
656
+ if (actions) {
657
+ actions.innerHTML = '<span class="permission-decision-label">' + label + '</span>';
658
+ }
659
+ // Remove feedback row if present (plan permission)
660
+ var feedbackRow = container.querySelector(".plan-feedback-row");
661
+ if (feedbackRow) feedbackRow.remove();
662
+
663
+ delete pendingPermissions[requestId];
664
+ }
665
+
666
+ export function markPermissionCancelled(requestId) {
667
+ var container = pendingPermissions[requestId];
668
+ if (!container) {
669
+ container = ctx.messagesEl.querySelector('[data-request-id="' + requestId + '"]');
670
+ }
671
+ if (!container || container.classList.contains("resolved")) return;
672
+
673
+ container.classList.add("resolved", "resolved-cancelled");
674
+ var actions = container.querySelector(".permission-actions");
675
+ if (actions) {
676
+ actions.innerHTML = '<span class="permission-decision-label">Cancelled</span>';
677
+ }
678
+
679
+ delete pendingPermissions[requestId];
680
+ }
681
+
682
+ // --- Plan mode rendering ---
683
+ export function renderPlanBanner(type) {
684
+ ctx.finalizeAssistantBlock();
685
+ stopThinking();
686
+ closeToolGroup();
687
+
688
+ var el = document.createElement("div");
689
+ el.className = "plan-banner";
690
+
691
+ if (type === "enter") {
692
+ inPlanMode = true;
693
+ planContent = null;
694
+ el.innerHTML =
695
+ '<span class="plan-banner-icon">' + iconHtml("map") + '</span>' +
696
+ '<span class="plan-banner-text">Entered plan mode</span>' +
697
+ '<span class="plan-banner-hint">Exploring codebase and designing implementation...</span>';
698
+ el.classList.add("plan-enter");
699
+ } else {
700
+ inPlanMode = false;
701
+ el.innerHTML =
702
+ '<span class="plan-banner-icon">' + iconHtml("check-circle") + '</span>' +
703
+ '<span class="plan-banner-text">Plan ready for review</span>';
704
+ el.classList.add("plan-exit");
705
+ }
706
+
707
+ ctx.addToMessages(el);
708
+ refreshIcons();
709
+ ctx.scrollToBottom();
710
+ return el;
711
+ }
712
+
713
+ export function renderPlanCard(content) {
714
+ ctx.finalizeAssistantBlock();
715
+ closeToolGroup();
716
+
717
+ var el = document.createElement("div");
718
+ el.className = "plan-card";
719
+
720
+ var header = document.createElement("div");
721
+ header.className = "plan-card-header";
722
+ header.innerHTML =
723
+ '<span class="plan-card-icon">' + iconHtml("file-text") + '</span>' +
724
+ '<span class="plan-card-title">Implementation Plan</span>' +
725
+ '<button class="plan-card-copy" title="Copy plan">' + iconHtml("copy") + '</button>' +
726
+ '<span class="plan-card-chevron">' + iconHtml("chevron-down") + '</span>';
727
+
728
+ var body = document.createElement("div");
729
+ body.className = "plan-card-body";
730
+ body.innerHTML = renderMarkdown(content);
731
+ highlightCodeBlocks(body);
732
+ renderMermaidBlocks(body);
733
+
734
+ var copyBtn = header.querySelector(".plan-card-copy");
735
+ if (copyBtn) {
736
+ copyBtn.addEventListener("click", function (e) {
737
+ e.stopPropagation();
738
+ copyToClipboard(content).then(function () {
739
+ copyBtn.innerHTML = iconHtml("check");
740
+ refreshIcons();
741
+ setTimeout(function () {
742
+ copyBtn.innerHTML = iconHtml("copy");
743
+ refreshIcons();
744
+ }, 1500);
745
+ });
746
+ });
747
+ }
748
+
749
+ header.addEventListener("click", function () {
750
+ el.classList.toggle("collapsed");
751
+ });
752
+
753
+ el.appendChild(header);
754
+ el.appendChild(body);
755
+ ctx.addToMessages(el);
756
+ refreshIcons();
757
+ ctx.scrollToBottom();
758
+ return el;
759
+ }
760
+
761
+ // --- Todo rendering ---
762
+ function todoStatusIcon(status) {
763
+ switch (status) {
764
+ case "completed": return iconHtml("check-circle");
765
+ case "in_progress": return iconHtml("loader", "icon-spin");
766
+ default: return iconHtml("circle");
767
+ }
768
+ }
769
+
770
+ export function handleTodoWrite(input) {
771
+ if (!input || !Array.isArray(input.todos)) return;
772
+ todoItems = input.todos.map(function (t, i) {
773
+ return {
774
+ id: t.id || String(i + 1),
775
+ content: t.content || t.subject || "",
776
+ status: t.status || "pending",
777
+ activeForm: t.activeForm || "",
778
+ };
779
+ });
780
+ renderTodoWidget();
781
+ }
782
+
783
+ export function handleTaskCreate(input) {
784
+ if (!input) return;
785
+ var id = String(todoItems.length + 1);
786
+ todoItems.push({
787
+ id: id,
788
+ content: input.subject || input.description || "",
789
+ status: "pending",
790
+ activeForm: input.activeForm || "",
791
+ });
792
+ renderTodoWidget();
793
+ }
794
+
795
+ export function handleTaskUpdate(input) {
796
+ if (!input || !input.taskId) return;
797
+ for (var i = 0; i < todoItems.length; i++) {
798
+ if (todoItems[i].id === input.taskId) {
799
+ if (input.status === "deleted") {
800
+ todoItems.splice(i, 1);
801
+ } else {
802
+ if (input.status) todoItems[i].status = input.status;
803
+ if (input.subject) todoItems[i].content = input.subject;
804
+ if (input.activeForm) todoItems[i].activeForm = input.activeForm;
805
+ }
806
+ break;
807
+ }
808
+ }
809
+ renderTodoWidget();
810
+ }
811
+
812
+ function renderTodoWidget() {
813
+ if (todoItems.length === 0) {
814
+ if (todoWidgetEl) { todoWidgetEl.remove(); todoWidgetEl = null; }
815
+ if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
816
+ todoWidgetVisible = true;
817
+ updateTodoSticky();
818
+ return;
819
+ }
820
+
821
+ var isNew = !todoWidgetEl;
822
+ if (isNew) {
823
+ todoWidgetEl = document.createElement("div");
824
+ todoWidgetEl.className = "todo-widget";
825
+ }
826
+
827
+ var completed = 0;
828
+ for (var i = 0; i < todoItems.length; i++) {
829
+ if (todoItems[i].status === "completed") completed++;
830
+ }
831
+
832
+ var html = '<div class="todo-header">' +
833
+ '<span class="todo-header-icon">' + iconHtml("list-checks") + '</span>' +
834
+ '<span class="todo-header-title">Tasks</span>' +
835
+ '<span class="todo-header-count">' + completed + '/' + todoItems.length + '</span>' +
836
+ '</div>';
837
+ html += '<div class="todo-progress"><div class="todo-progress-bar" style="width:' +
838
+ (todoItems.length > 0 ? Math.round(completed / todoItems.length * 100) : 0) + '%"></div></div>';
839
+ html += '<div class="todo-items">';
840
+ for (var i = 0; i < todoItems.length; i++) {
841
+ var t = todoItems[i];
842
+ var statusClass = t.status === "completed" ? "completed" : t.status === "in_progress" ? "in-progress" : "pending";
843
+ html += '<div class="todo-item ' + statusClass + '">' +
844
+ '<span class="todo-item-icon">' + todoStatusIcon(t.status) + '</span>' +
845
+ '<span class="todo-item-text">' + escapeHtml(t.status === "in_progress" && t.activeForm ? t.activeForm : t.content) + '</span>' +
846
+ '</div>';
847
+ }
848
+ html += '</div>';
849
+
850
+ todoWidgetEl.innerHTML = html;
851
+
852
+ if (isNew) {
853
+ ctx.addToMessages(todoWidgetEl);
854
+ setupTodoObserver();
855
+ }
856
+ updateTodoSticky();
857
+ refreshIcons();
858
+ ctx.scrollToBottom();
859
+ }
860
+
861
+ function setupTodoObserver() {
862
+ if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
863
+ if (!todoWidgetEl) return;
864
+
865
+ var messagesEl = document.getElementById("messages");
866
+ if (!messagesEl) return;
867
+
868
+ todoObserver = new IntersectionObserver(function (entries) {
869
+ todoWidgetVisible = entries[0].isIntersecting;
870
+ updateTodoStickyVisibility();
871
+ }, { root: messagesEl, threshold: 0 });
872
+
873
+ todoObserver.observe(todoWidgetEl);
874
+ }
875
+
876
+ function updateTodoStickyVisibility() {
877
+ var stickyEl = document.getElementById("todo-sticky");
878
+ if (!stickyEl) return;
879
+
880
+ if (todoWidgetVisible) {
881
+ stickyEl.classList.add("hidden");
882
+ } else {
883
+ // Only show if there are active (non-completed) tasks
884
+ var hasActive = false;
885
+ for (var i = 0; i < todoItems.length; i++) {
886
+ if (todoItems[i].status !== "completed") { hasActive = true; break; }
887
+ }
888
+ if (hasActive) {
889
+ stickyEl.classList.remove("hidden");
890
+ }
891
+ }
892
+ }
893
+
894
+ function updateTodoSticky() {
895
+ var stickyEl = document.getElementById("todo-sticky");
896
+ if (!stickyEl) return;
897
+
898
+ // Hide if no active tasks (all completed or empty)
899
+ var hasActive = false;
900
+ for (var i = 0; i < todoItems.length; i++) {
901
+ if (todoItems[i].status !== "completed") { hasActive = true; break; }
902
+ }
903
+ if (!hasActive) {
904
+ stickyEl.classList.add("hidden");
905
+ return;
906
+ }
907
+
908
+ var completed = 0;
909
+ for (var i = 0; i < todoItems.length; i++) {
910
+ if (todoItems[i].status === "completed") completed++;
911
+ }
912
+ var pct = Math.round(completed / todoItems.length * 100);
913
+ var wasCollapsed = stickyEl.innerHTML === "" ? true : stickyEl.classList.contains("collapsed");
914
+
915
+ var inProgressItem = null;
916
+ for (var j = 0; j < todoItems.length; j++) {
917
+ if (todoItems[j].status === "in_progress") { inProgressItem = todoItems[j]; break; }
918
+ }
919
+
920
+ var html = '<div class="todo-sticky-inner">' +
921
+ '<div class="todo-sticky-header">' +
922
+ '<span class="todo-sticky-icon">' + iconHtml("list-checks") + '</span>' +
923
+ '<span class="todo-sticky-title">Tasks</span>' +
924
+ (inProgressItem ? '<span class="todo-sticky-active">' + iconHtml("loader", "icon-spin") + ' ' + escapeHtml(inProgressItem.activeForm || inProgressItem.content) + '</span>' : '') +
925
+ '<span class="todo-sticky-count">' + completed + '/' + todoItems.length + '</span>' +
926
+ '<span class="todo-sticky-chevron">' + iconHtml("chevron-down") + '</span>' +
927
+ '</div>' +
928
+ '<div class="todo-sticky-progress"><div class="todo-sticky-progress-bar" style="width:' + pct + '%"></div></div>' +
929
+ '<div class="todo-sticky-items">';
930
+
931
+ for (var i = 0; i < todoItems.length; i++) {
932
+ var t = todoItems[i];
933
+ var statusClass = t.status === "completed" ? "completed" : t.status === "in_progress" ? "in-progress" : "pending";
934
+ html += '<div class="todo-sticky-item ' + statusClass + '">' +
935
+ '<span class="todo-sticky-item-icon">' + todoStatusIcon(t.status) + '</span>' +
936
+ '<span class="todo-sticky-item-text">' + escapeHtml(t.status === "in_progress" && t.activeForm ? t.activeForm : t.content) + '</span>' +
937
+ '</div>';
938
+ }
939
+
940
+ html += '</div></div>';
941
+ stickyEl.innerHTML = html;
942
+
943
+ // Only show sticky when in-chat widget is not visible in viewport
944
+ if (todoWidgetVisible) {
945
+ stickyEl.classList.add("hidden");
946
+ } else {
947
+ stickyEl.classList.remove("hidden");
948
+ }
949
+ if (wasCollapsed) stickyEl.classList.add("collapsed");
950
+
951
+ stickyEl.querySelector(".todo-sticky-header").addEventListener("click", function () {
952
+ stickyEl.classList.toggle("collapsed");
953
+ });
954
+
955
+ refreshIcons();
956
+ }
957
+
958
+ // --- Thinking ---
959
+ export function startThinking() {
960
+ ctx.finalizeAssistantBlock();
961
+
962
+ // Reuse existing thinking group if consecutive
963
+ if (thinkingGroup && thinkingGroup.el.classList.contains("done")) {
964
+ var el = thinkingGroup.el;
965
+ el.classList.remove("done");
966
+ el.querySelector(".thinking-content").textContent = "";
967
+ currentThinking = { el: el, fullText: "", startTime: Date.now() };
968
+ refreshIcons();
969
+ ctx.scrollToBottom();
970
+ ctx.setActivity(randomThinkingVerb() + "...");
971
+ return;
972
+ }
973
+
974
+ var el = document.createElement("div");
975
+ el.className = "thinking-item";
976
+ el.innerHTML =
977
+ '<div class="thinking-header">' +
978
+ '<span class="thinking-chevron">' + iconHtml("chevron-right") + '</span>' +
979
+ '<span class="thinking-label">Thinking</span>' +
980
+ '<span class="thinking-duration"></span>' +
981
+ '<span class="thinking-spinner">' + iconHtml("loader", "icon-spin") + '</span>' +
982
+ '</div>' +
983
+ '<div class="thinking-content"></div>';
984
+
985
+ el.querySelector(".thinking-header").addEventListener("click", function () {
986
+ el.classList.toggle("expanded");
987
+ });
988
+
989
+ ctx.addToMessages(el);
990
+ refreshIcons();
991
+ ctx.scrollToBottom();
992
+ thinkingGroup = { el: el, count: 0, totalDuration: 0 };
993
+ currentThinking = { el: el, fullText: "", startTime: Date.now() };
994
+ ctx.setActivity(randomThinkingVerb() + "...");
995
+ }
996
+
997
+ export function appendThinking(text) {
998
+ if (!currentThinking) return;
999
+ currentThinking.fullText += text;
1000
+ currentThinking.el.querySelector(".thinking-content").textContent = currentThinking.fullText;
1001
+ ctx.scrollToBottom();
1002
+ }
1003
+
1004
+ export function stopThinking(duration) {
1005
+ if (!currentThinking) return;
1006
+ var secs = typeof duration === "number" ? duration : (Date.now() - currentThinking.startTime) / 1000;
1007
+ currentThinking.el.classList.add("done");
1008
+ if (thinkingGroup && thinkingGroup.el === currentThinking.el) {
1009
+ thinkingGroup.count++;
1010
+ thinkingGroup.totalDuration += secs;
1011
+ currentThinking.el.querySelector(".thinking-duration").textContent = " " + thinkingGroup.totalDuration.toFixed(1) + "s";
1012
+ } else {
1013
+ currentThinking.el.querySelector(".thinking-duration").textContent = " " + secs.toFixed(1) + "s";
1014
+ }
1015
+ currentThinking = null;
1016
+ }
1017
+
1018
+ export function resetThinkingGroup() {
1019
+ thinkingGroup = null;
1020
+ }
1021
+
1022
+ // --- Tool items ---
1023
+ export function createToolItem(id, name) {
1024
+ ctx.finalizeAssistantBlock();
1025
+ stopThinking();
1026
+
1027
+ // Group management: create new group or reuse existing open group
1028
+ if (!currentToolGroup || currentToolGroup.closed) {
1029
+ toolGroupCounter++;
1030
+ var groupEl = document.createElement("div");
1031
+ groupEl.className = "tool-group";
1032
+ groupEl.dataset.groupId = "g" + toolGroupCounter;
1033
+ groupEl.innerHTML =
1034
+ '<div class="tool-group-header" style="display:none">' +
1035
+ '<span class="tool-group-chevron">' + iconHtml("chevron-right") + '</span>' +
1036
+ '<span class="tool-group-bullet"></span>' +
1037
+ '<span class="tool-group-label">Running...</span>' +
1038
+ '<span class="tool-group-status-icon">' + iconHtml("loader", "icon-spin") + '</span>' +
1039
+ '</div>' +
1040
+ '<div class="tool-group-items"></div>';
1041
+
1042
+ groupEl.querySelector(".tool-group-header").addEventListener("click", function () {
1043
+ groupEl.classList.toggle("collapsed");
1044
+ });
1045
+
1046
+ ctx.addToMessages(groupEl);
1047
+ refreshIcons();
1048
+
1049
+ currentToolGroup = {
1050
+ el: groupEl,
1051
+ id: "g" + toolGroupCounter,
1052
+ toolNames: [],
1053
+ toolCount: 0,
1054
+ doneCount: 0,
1055
+ errorCount: 0,
1056
+ closed: false,
1057
+ };
1058
+ toolGroups[currentToolGroup.id] = currentToolGroup;
1059
+ }
1060
+
1061
+ var el = document.createElement("div");
1062
+ el.className = "tool-item";
1063
+ el.dataset.toolId = id;
1064
+ el.innerHTML =
1065
+ '<div class="tool-header">' +
1066
+ '<span class="tool-chevron">' + iconHtml("chevron-right") + '</span>' +
1067
+ '<span class="tool-bullet"></span>' +
1068
+ '<span class="tool-name"></span>' +
1069
+ '<span class="tool-desc"></span>' +
1070
+ '<span class="tool-status-icon">' + iconHtml("loader", "icon-spin") + '</span>' +
1071
+ '</div>' +
1072
+ '<div class="tool-subtitle">' +
1073
+ '<span class="tool-connector">&#9492;</span>' +
1074
+ '<span class="tool-subtitle-text">Running...</span>' +
1075
+ '</div>';
1076
+
1077
+ el.querySelector(".tool-name").textContent = name;
1078
+
1079
+ // Append to group instead of messages directly
1080
+ currentToolGroup.el.querySelector(".tool-group-items").appendChild(el);
1081
+ currentToolGroup.toolNames.push(name);
1082
+ currentToolGroup.toolCount++;
1083
+ updateToolGroupHeader(currentToolGroup);
1084
+
1085
+ refreshIcons();
1086
+ ctx.scrollToBottom();
1087
+
1088
+ tools[id] = { el: el, name: name, input: null, done: false, groupId: currentToolGroup.id };
1089
+ ctx.setActivity("Running " + name + "...");
1090
+ }
1091
+
1092
+ export function updateToolExecuting(id, name, input) {
1093
+ var tool = tools[id];
1094
+ if (!tool) return;
1095
+
1096
+ tool.input = input;
1097
+ var descEl = tool.el.querySelector(".tool-desc");
1098
+ descEl.textContent = toolSummary(name, input);
1099
+
1100
+ // Make file path clickable for Read/Edit/Write tools
1101
+ var filePath = input && input.file_path;
1102
+ if (filePath && (name === "Read" || name === "Edit" || name === "Write")) {
1103
+ descEl.classList.add("tool-desc-link");
1104
+ descEl.dataset.filePath = filePath;
1105
+ descEl.insertAdjacentHTML("beforeend", '<span class="tool-desc-link-icon">' + iconHtml("external-link") + '</span>');
1106
+ refreshIcons();
1107
+ (function (toolName, toolInput) {
1108
+ descEl.onclick = function (e) {
1109
+ e.stopPropagation();
1110
+ if (toolName === "Edit" && toolInput && (toolInput.old_string || toolInput.new_string)) {
1111
+ openFile(filePath, { diff: { oldStr: toolInput.old_string || "", newStr: toolInput.new_string || "" } });
1112
+ } else {
1113
+ openFile(filePath);
1114
+ }
1115
+ };
1116
+ })(name, input);
1117
+ }
1118
+
1119
+ ctx.setActivity(toolActivityText(name, input));
1120
+
1121
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
1122
+ if (subtitleText) subtitleText.textContent = toolActivityText(name, input);
1123
+
1124
+ ctx.scrollToBottom();
1125
+ }
1126
+
1127
+ function renderEditDiff(oldStr, newStr, filePath) {
1128
+ var wrapper = document.createElement("div");
1129
+ wrapper.className = "edit-diff";
1130
+ var lang = getLanguageFromPath(filePath);
1131
+
1132
+ // Header with file path and split toggle (desktop only)
1133
+ var header = document.createElement("div");
1134
+ header.className = "edit-diff-header";
1135
+
1136
+ var pathSpan = document.createElement("span");
1137
+ pathSpan.className = "edit-diff-path edit-diff-path-link";
1138
+ pathSpan.textContent = filePath || "";
1139
+ if (filePath) {
1140
+ (function (fp, os, ns) {
1141
+ pathSpan.addEventListener("click", function (e) {
1142
+ e.stopPropagation();
1143
+ openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
1144
+ });
1145
+ })(filePath, oldStr, newStr);
1146
+ }
1147
+ header.appendChild(pathSpan);
1148
+
1149
+ var isMobile = "ontouchstart" in window;
1150
+ var isSplit = false;
1151
+
1152
+ var unifiedBtn = document.createElement("button");
1153
+ unifiedBtn.className = "edit-diff-toggle active";
1154
+ unifiedBtn.innerHTML = iconHtml("list");
1155
+ unifiedBtn.title = "Unified view";
1156
+
1157
+ var splitBtn = document.createElement("button");
1158
+ splitBtn.className = "edit-diff-toggle";
1159
+ splitBtn.innerHTML = iconHtml("columns-2");
1160
+ splitBtn.title = "Split view";
1161
+
1162
+ var toggleWrap = document.createElement("span");
1163
+ toggleWrap.className = "edit-diff-toggles";
1164
+ if (isMobile) toggleWrap.style.display = "none";
1165
+ toggleWrap.appendChild(unifiedBtn);
1166
+ toggleWrap.appendChild(splitBtn);
1167
+ header.appendChild(toggleWrap);
1168
+
1169
+ wrapper.appendChild(header);
1170
+
1171
+ var currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1172
+ wrapper.appendChild(currentBody);
1173
+
1174
+ unifiedBtn.addEventListener("click", function (e) {
1175
+ e.stopPropagation();
1176
+ if (!isSplit) return;
1177
+ isSplit = false;
1178
+ unifiedBtn.classList.add("active");
1179
+ splitBtn.classList.remove("active");
1180
+ wrapper.removeChild(currentBody);
1181
+ currentBody = renderUnifiedDiff(oldStr, newStr, lang);
1182
+ wrapper.appendChild(currentBody);
1183
+ refreshIcons();
1184
+ });
1185
+
1186
+ splitBtn.addEventListener("click", function (e) {
1187
+ e.stopPropagation();
1188
+ if (isSplit) return;
1189
+ isSplit = true;
1190
+ splitBtn.classList.add("active");
1191
+ unifiedBtn.classList.remove("active");
1192
+ wrapper.removeChild(currentBody);
1193
+ currentBody = renderSplitDiff(oldStr, newStr, lang);
1194
+ wrapper.appendChild(currentBody);
1195
+ refreshIcons();
1196
+ });
1197
+
1198
+ return wrapper;
1199
+ }
1200
+
1201
+ function isDiffContent(text) {
1202
+ var lines = text.split("\n");
1203
+ var diffMarkers = 0;
1204
+ for (var i = 0; i < Math.min(lines.length, 20); i++) {
1205
+ var l = lines[i];
1206
+ if (l.startsWith("@@") || l.startsWith("---") || l.startsWith("+++")) {
1207
+ diffMarkers++;
1208
+ }
1209
+ }
1210
+ return diffMarkers >= 2;
1211
+ }
1212
+
1213
+ function getLanguageFromPath(filePath) {
1214
+ if (!filePath) return null;
1215
+ var parts = filePath.split("/");
1216
+ var filename = parts[parts.length - 1].toLowerCase();
1217
+ var dotIdx = filename.lastIndexOf(".");
1218
+ if (dotIdx === -1 || dotIdx === filename.length - 1) return null;
1219
+ var ext = filename.substring(dotIdx + 1);
1220
+ var map = {
1221
+ js: "javascript", jsx: "javascript", mjs: "javascript", cjs: "javascript",
1222
+ ts: "typescript", tsx: "typescript", mts: "typescript",
1223
+ py: "python", rb: "ruby", rs: "rust", go: "go",
1224
+ java: "java", kt: "kotlin", kts: "kotlin",
1225
+ cs: "csharp", cpp: "cpp", cc: "cpp", c: "c", h: "c", hpp: "cpp",
1226
+ css: "css", scss: "scss", less: "less",
1227
+ html: "xml", htm: "xml", xml: "xml", svg: "xml",
1228
+ json: "json", yaml: "yaml", yml: "yaml",
1229
+ md: "markdown", sh: "bash", bash: "bash", zsh: "bash",
1230
+ sql: "sql", swift: "swift", php: "php",
1231
+ toml: "ini", ini: "ini", conf: "ini",
1232
+ lua: "lua", r: "r", pl: "perl",
1233
+ ex: "elixir", exs: "elixir",
1234
+ erl: "erlang", hs: "haskell",
1235
+ graphql: "graphql", gql: "graphql",
1236
+ };
1237
+ return map[ext] || null;
1238
+ }
1239
+
1240
+ function parseLineNumberedContent(text) {
1241
+ var lines = text.split("\n");
1242
+ if (lines.length > 0 && lines[lines.length - 1] === "") {
1243
+ lines.pop();
1244
+ }
1245
+ if (lines.length === 0) return null;
1246
+
1247
+ var pattern = /^\s*(\d+)[→\t](.*)$/;
1248
+ var checkCount = Math.min(lines.length, 5);
1249
+ var matchCount = 0;
1250
+ for (var i = 0; i < checkCount; i++) {
1251
+ if (pattern.test(lines[i])) matchCount++;
1252
+ }
1253
+ if (matchCount < Math.ceil(checkCount * 0.6)) return null;
1254
+
1255
+ var numbers = [];
1256
+ var code = [];
1257
+ for (var i = 0; i < lines.length; i++) {
1258
+ var m = lines[i].match(pattern);
1259
+ if (m) {
1260
+ numbers.push(m[1]);
1261
+ code.push(m[2]);
1262
+ } else {
1263
+ numbers.push("");
1264
+ code.push(lines[i]);
1265
+ }
1266
+ }
1267
+ return { numbers: numbers, code: code };
1268
+ }
1269
+
1270
+ export function updateToolResult(id, content, isError) {
1271
+ var tool = tools[id];
1272
+ if (!tool) return;
1273
+
1274
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
1275
+ if (subtitleText && tool.input) {
1276
+ subtitleText.textContent = toolActivityText(tool.name, tool.input);
1277
+ }
1278
+
1279
+ var resultBlock = document.createElement("div");
1280
+ var displayContent = content || "(no output)";
1281
+ displayContent = displayContent.replace(/<system-reminder>[\s\S]*?<\/system-reminder>/g, "").trim();
1282
+ if (displayContent.length > 10000) displayContent = displayContent.substring(0, 10000) + "\n... (truncated)";
1283
+
1284
+ var hasEditDiff = !isError && tool.name === "Edit" && tool.input && tool.input.old_string && tool.input.new_string;
1285
+ var expandByDefault = hasEditDiff || (!isError && tool.name === "Edit" && isDiffContent(displayContent));
1286
+ if (expandByDefault) {
1287
+ resultBlock.className = "tool-result-block";
1288
+ tool.el.classList.add("expanded");
1289
+ } else {
1290
+ resultBlock.className = "tool-result-block collapsed";
1291
+ }
1292
+
1293
+ if (hasEditDiff) {
1294
+ resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
1295
+ } else if (!isError && isDiffContent(displayContent)) {
1296
+ var patchLang = tool.input && tool.input.file_path ? getLanguageFromPath(tool.input.file_path) : null;
1297
+ resultBlock.appendChild(renderPatchDiff(displayContent, patchLang));
1298
+ } else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path) {
1299
+ var parsed = parseLineNumberedContent(displayContent);
1300
+ if (parsed) {
1301
+ var lang = getLanguageFromPath(tool.input.file_path);
1302
+ var viewer = document.createElement("div");
1303
+ viewer.className = "code-viewer";
1304
+
1305
+ var gutter = document.createElement("pre");
1306
+ gutter.className = "code-gutter";
1307
+ gutter.textContent = parsed.numbers.join("\n");
1308
+
1309
+ var codeBlock = document.createElement("pre");
1310
+ codeBlock.className = "code-content";
1311
+ var codeText = parsed.code.join("\n");
1312
+
1313
+ if (lang) {
1314
+ try {
1315
+ var highlighted = hljs.highlight(codeText, { language: lang });
1316
+ var codeEl = document.createElement("code");
1317
+ codeEl.className = "hljs language-" + lang;
1318
+ codeEl.innerHTML = highlighted.value;
1319
+ codeBlock.appendChild(codeEl);
1320
+ } catch (e) {
1321
+ codeBlock.textContent = codeText;
1322
+ }
1323
+ } else {
1324
+ codeBlock.textContent = codeText;
1325
+ }
1326
+
1327
+ viewer.appendChild(gutter);
1328
+ viewer.appendChild(codeBlock);
1329
+
1330
+ // Sync vertical scroll between gutter and code
1331
+ viewer.addEventListener("scroll", function () {
1332
+ gutter.scrollTop = viewer.scrollTop;
1333
+ codeBlock.scrollTop = viewer.scrollTop;
1334
+ });
1335
+
1336
+ resultBlock.appendChild(viewer);
1337
+ } else {
1338
+ var pre = document.createElement("pre");
1339
+ pre.textContent = displayContent;
1340
+ resultBlock.appendChild(pre);
1341
+ }
1342
+ } else {
1343
+ var pre = document.createElement("pre");
1344
+ if (isError) pre.className = "is-error";
1345
+ pre.textContent = displayContent;
1346
+ resultBlock.appendChild(pre);
1347
+ }
1348
+ tool.el.appendChild(resultBlock);
1349
+
1350
+ tool.el.querySelector(".tool-header").addEventListener("click", function () {
1351
+ resultBlock.classList.toggle("collapsed");
1352
+ tool.el.classList.toggle("expanded");
1353
+ });
1354
+
1355
+ markToolDone(id, isError);
1356
+ ctx.scrollToBottom();
1357
+ }
1358
+
1359
+ export function markToolDone(id, isError) {
1360
+ var tool = tools[id];
1361
+ if (!tool || tool.done) return;
1362
+
1363
+ tool.done = true;
1364
+ if (!tool.el) return; // hidden tool (plan mode)
1365
+
1366
+ tool.el.classList.add("done");
1367
+ if (isError) tool.el.classList.add("error");
1368
+
1369
+ var icon = tool.el.querySelector(".tool-status-icon");
1370
+ if (isError) {
1371
+ icon.innerHTML = '<span class="err-icon">' + iconHtml("alert-triangle") + '</span>';
1372
+ } else {
1373
+ icon.innerHTML = '<span class="check">' + iconHtml("check") + '</span>';
1374
+ }
1375
+ refreshIcons();
1376
+
1377
+ // Update group state
1378
+ if (tool.groupId) {
1379
+ var group = findToolGroup(tool.groupId);
1380
+ if (group) {
1381
+ group.doneCount++;
1382
+ if (isError) group.errorCount++;
1383
+ updateToolGroupHeader(group);
1384
+ }
1385
+ }
1386
+ }
1387
+
1388
+ export function markAllToolsDone() {
1389
+ for (var id in tools) {
1390
+ if (tools.hasOwnProperty(id) && !tools[id].done) {
1391
+ markToolDone(id, false);
1392
+ }
1393
+ }
1394
+ }
1395
+
1396
+ // --- Sub-agent (Task tool) log ---
1397
+ export function updateSubagentActivity(parentToolId, text) {
1398
+ var tool = tools[parentToolId];
1399
+ if (!tool || !tool.el) return;
1400
+
1401
+ // Update subtitle text with current activity
1402
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
1403
+ if (subtitleText) subtitleText.textContent = text;
1404
+
1405
+ // Update or create the subagent log
1406
+ var log = tool.el.querySelector(".subagent-log");
1407
+ if (!log) {
1408
+ log = document.createElement("div");
1409
+ log.className = "subagent-log";
1410
+ tool.el.appendChild(log);
1411
+ }
1412
+
1413
+ ctx.setActivity(text);
1414
+ ctx.scrollToBottom();
1415
+ }
1416
+
1417
+ export function addSubagentToolEntry(parentToolId, toolName, toolId, text) {
1418
+ var tool = tools[parentToolId];
1419
+ if (!tool || !tool.el) return;
1420
+
1421
+ // Update subtitle
1422
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
1423
+ if (subtitleText) subtitleText.textContent = text;
1424
+
1425
+ // Create log if needed
1426
+ var log = tool.el.querySelector(".subagent-log");
1427
+ if (!log) {
1428
+ log = document.createElement("div");
1429
+ log.className = "subagent-log";
1430
+ tool.el.appendChild(log);
1431
+ }
1432
+
1433
+ // Add entry
1434
+ var entry = document.createElement("div");
1435
+ entry.className = "subagent-log-entry";
1436
+ entry.innerHTML =
1437
+ '<span class="subagent-log-bullet"></span>' +
1438
+ '<span class="subagent-log-tool"></span>' +
1439
+ '<span class="subagent-log-text"></span>';
1440
+ entry.querySelector(".subagent-log-tool").textContent = toolName;
1441
+ entry.querySelector(".subagent-log-text").textContent = text;
1442
+ log.appendChild(entry);
1443
+
1444
+ // Auto-scroll to latest entry
1445
+ log.scrollTop = log.scrollHeight;
1446
+
1447
+ ctx.setActivity(text);
1448
+ ctx.scrollToBottom();
1449
+ }
1450
+
1451
+ function fmtTokens(n) {
1452
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
1453
+ if (n >= 1000) return (n / 1000).toFixed(1) + "K";
1454
+ return String(n);
1455
+ }
1456
+
1457
+ function fmtDuration(ms) {
1458
+ var secs = Math.floor(ms / 1000);
1459
+ if (secs >= 60) return Math.floor(secs / 60) + "m " + (secs % 60) + "s";
1460
+ return secs + "s";
1461
+ }
1462
+
1463
+ export function updateSubagentProgress(parentToolId, usage, lastToolName) {
1464
+ var tool = tools[parentToolId];
1465
+ if (!tool || !tool.el) return;
1466
+ var progressEl = tool.el.querySelector(".subagent-progress");
1467
+ if (!progressEl) {
1468
+ progressEl = document.createElement("div");
1469
+ progressEl.className = "subagent-progress";
1470
+ var log = tool.el.querySelector(".subagent-log");
1471
+ if (log) tool.el.insertBefore(progressEl, log);
1472
+ else tool.el.appendChild(progressEl);
1473
+ }
1474
+ var parts = [];
1475
+ if (usage) {
1476
+ if (usage.total_tokens) parts.push(fmtTokens(usage.total_tokens) + " tokens");
1477
+ if (usage.tool_uses) parts.push(usage.tool_uses + " tools");
1478
+ if (usage.duration_ms) parts.push(fmtDuration(usage.duration_ms));
1479
+ }
1480
+ if (lastToolName) parts.push(lastToolName);
1481
+ progressEl.textContent = parts.join(" · ");
1482
+ }
1483
+
1484
+ export function initSubagentStop(parentToolId, taskId) {
1485
+ var tool = tools[parentToolId];
1486
+ if (!tool || !tool.el) return;
1487
+ var header = tool.el.querySelector(".tool-header");
1488
+ if (!header || header.querySelector(".subagent-stop-btn")) return;
1489
+ var btn = document.createElement("button");
1490
+ btn.className = "subagent-stop-btn";
1491
+ btn.textContent = "Stop";
1492
+ btn.addEventListener("click", function(e) {
1493
+ e.stopPropagation();
1494
+ if (ctx.ws) ctx.ws.send(JSON.stringify({ type: "stop_task", taskId: taskId, parentToolId: parentToolId }));
1495
+ btn.disabled = true;
1496
+ btn.textContent = "Stopping...";
1497
+ });
1498
+ header.appendChild(btn);
1499
+ }
1500
+
1501
+ export function markSubagentDone(parentToolId, status, summary, usage) {
1502
+ var tool = tools[parentToolId];
1503
+ if (!tool || !tool.el) return;
1504
+
1505
+ var label = "Agent finished";
1506
+ if (status === "failed") label = "Agent failed";
1507
+ else if (status === "stopped") label = "Agent stopped";
1508
+
1509
+ var subtitleText = tool.el.querySelector(".tool-subtitle-text");
1510
+ if (subtitleText) subtitleText.textContent = label;
1511
+
1512
+ // Remove stop button
1513
+ var stopBtn = tool.el.querySelector(".subagent-stop-btn");
1514
+ if (stopBtn) stopBtn.remove();
1515
+
1516
+ // Final usage update
1517
+ if (usage) updateSubagentProgress(parentToolId, usage, null);
1518
+ }
1519
+
1520
+ export function addTurnMeta(cost, duration) {
1521
+ closeToolGroup();
1522
+ var div = document.createElement("div");
1523
+ div.className = "turn-meta";
1524
+ div.dataset.turn = ctx.turnCounter;
1525
+ var parts = [];
1526
+ if (cost != null) parts.push("$" + cost.toFixed(4));
1527
+ if (duration != null) parts.push((duration / 1000).toFixed(1) + "s");
1528
+ if (parts.length) {
1529
+ div.textContent = parts.join(" \u00b7 ");
1530
+ ctx.addToMessages(div);
1531
+ ctx.scrollToBottom();
1532
+ }
1533
+ }
1534
+
1535
+ // --- Tool group exports ---
1536
+ export { closeToolGroup };
1537
+
1538
+ export function removeToolFromGroup(toolId) {
1539
+ var tool = tools[toolId];
1540
+ if (!tool || !tool.groupId) return;
1541
+ var group = findToolGroup(tool.groupId);
1542
+ if (!group) return;
1543
+ group.toolCount--;
1544
+ // Remove tool name from the names array (remove first occurrence)
1545
+ var idx = group.toolNames.indexOf(tool.name);
1546
+ if (idx !== -1) group.toolNames.splice(idx, 1);
1547
+ if (tool.done) group.doneCount--;
1548
+ updateToolGroupHeader(group);
1549
+ }
1550
+
1551
+ // Expose state getters and reset
1552
+ export function getTools() { return tools; }
1553
+ export function isInPlanMode() { return inPlanMode; }
1554
+ export function getPlanContent() { return planContent; }
1555
+ export function setPlanContent(c) { planContent = c; }
1556
+ export function isPlanFilePath(fp) { return isPlanFile(fp); }
1557
+ export function getPlanModeTools() { return PLAN_MODE_TOOLS; }
1558
+ export function getTodoTools() { return TODO_TOOLS; }
1559
+ export function getHiddenResultTools() { return HIDDEN_RESULT_TOOLS; }
1560
+
1561
+ export function saveToolState() {
1562
+ return {
1563
+ tools: tools,
1564
+ currentThinking: currentThinking,
1565
+ todoWidgetEl: todoWidgetEl,
1566
+ inPlanMode: inPlanMode,
1567
+ planContent: planContent,
1568
+ currentToolGroup: currentToolGroup,
1569
+ toolGroupCounter: toolGroupCounter,
1570
+ toolGroups: toolGroups,
1571
+ };
1572
+ }
1573
+
1574
+ export function restoreToolState(saved) {
1575
+ tools = saved.tools;
1576
+ currentThinking = saved.currentThinking;
1577
+ todoWidgetEl = saved.todoWidgetEl;
1578
+ inPlanMode = saved.inPlanMode;
1579
+ planContent = saved.planContent;
1580
+ currentToolGroup = saved.currentToolGroup;
1581
+ toolGroupCounter = saved.toolGroupCounter;
1582
+ toolGroups = saved.toolGroups;
1583
+ if (todoWidgetEl) {
1584
+ setupTodoObserver();
1585
+ }
1586
+ }
1587
+
1588
+ export function resetToolState() {
1589
+ tools = {};
1590
+ currentThinking = null;
1591
+ thinkingGroup = null;
1592
+ inPlanMode = false;
1593
+ planContent = null;
1594
+ todoItems = [];
1595
+ todoWidgetEl = null;
1596
+ todoWidgetVisible = true;
1597
+ if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
1598
+ pendingPermissions = {};
1599
+ currentToolGroup = null;
1600
+ toolGroupCounter = 0;
1601
+ toolGroups = {};
1602
+ var stickyEl = document.getElementById("todo-sticky");
1603
+ if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
1604
+ }
1605
+
1606
+ export function initTools(_ctx) {
1607
+ ctx = _ctx;
1608
+ }