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