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.
- package/LICENSE +21 -0
- package/README.md +281 -0
- package/bin/cli.js +2385 -0
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +237 -0
- package/lib/daemon.js +489 -0
- package/lib/ipc.js +112 -0
- package/lib/notes.js +120 -0
- package/lib/pages.js +664 -0
- package/lib/project.js +1433 -0
- package/lib/public/app.js +2795 -0
- package/lib/public/apple-touch-icon-dark.png +0 -0
- package/lib/public/apple-touch-icon.png +0 -0
- package/lib/public/css/base.css +264 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +1114 -0
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/icon-strip.css +296 -0
- package/lib/public/css/input.css +573 -0
- package/lib/public/css/menus.css +856 -0
- package/lib/public/css/messages.css +1445 -0
- package/lib/public/css/mobile-nav.css +354 -0
- package/lib/public/css/overlays.css +697 -0
- package/lib/public/css/rewind.css +505 -0
- package/lib/public/css/server-settings.css +761 -0
- package/lib/public/css/sidebar.css +936 -0
- package/lib/public/css/sticky-notes.css +358 -0
- package/lib/public/css/title-bar.css +314 -0
- package/lib/public/favicon-dark.svg +1 -0
- package/lib/public/favicon.svg +1 -0
- package/lib/public/icon-192-dark.png +0 -0
- package/lib/public/icon-192.png +0 -0
- package/lib/public/icon-512-dark.png +0 -0
- package/lib/public/icon-512.png +0 -0
- package/lib/public/icon-mono.svg +1 -0
- package/lib/public/index.html +762 -0
- package/lib/public/manifest.json +27 -0
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/events.js +21 -0
- package/lib/public/modules/filebrowser.js +1411 -0
- package/lib/public/modules/fileicons.js +172 -0
- package/lib/public/modules/icons.js +54 -0
- package/lib/public/modules/input.js +584 -0
- package/lib/public/modules/markdown.js +356 -0
- package/lib/public/modules/notifications.js +649 -0
- package/lib/public/modules/qrcode.js +70 -0
- package/lib/public/modules/rewind.js +345 -0
- package/lib/public/modules/server-settings.js +510 -0
- package/lib/public/modules/sidebar.js +1083 -0
- package/lib/public/modules/state.js +3 -0
- package/lib/public/modules/sticky-notes.js +688 -0
- package/lib/public/modules/terminal.js +697 -0
- package/lib/public/modules/theme.js +738 -0
- package/lib/public/modules/tools.js +1608 -0
- package/lib/public/modules/utils.js +56 -0
- package/lib/public/style.css +15 -0
- package/lib/public/sw.js +75 -0
- package/lib/push.js +124 -0
- package/lib/sdk-bridge.js +989 -0
- package/lib/server.js +582 -0
- package/lib/sessions.js +424 -0
- package/lib/terminal-manager.js +187 -0
- package/lib/terminal.js +24 -0
- package/lib/themes/ayu-light.json +9 -0
- package/lib/themes/catppuccin-latte.json +9 -0
- package/lib/themes/catppuccin-mocha.json +9 -0
- package/lib/themes/clay-light.json +10 -0
- package/lib/themes/clay.json +10 -0
- package/lib/themes/dracula.json +9 -0
- package/lib/themes/everforest-light.json +9 -0
- package/lib/themes/everforest.json +9 -0
- package/lib/themes/github-light.json +9 -0
- package/lib/themes/gruvbox-dark.json +9 -0
- package/lib/themes/gruvbox-light.json +9 -0
- package/lib/themes/monokai.json +9 -0
- package/lib/themes/nord-light.json +9 -0
- package/lib/themes/nord.json +9 -0
- package/lib/themes/one-dark.json +9 -0
- package/lib/themes/one-light.json +9 -0
- package/lib/themes/rose-pine-dawn.json +9 -0
- package/lib/themes/rose-pine.json +9 -0
- package/lib/themes/solarized-dark.json +9 -0
- package/lib/themes/solarized-light.json +9 -0
- package/lib/themes/tokyo-night-light.json +9 -0
- package/lib/themes/tokyo-night.json +9 -0
- package/lib/updater.js +97 -0
- 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
|
+
' & 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">└</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
|
+
}
|