claude-relay 2.3.0 → 2.4.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/README.md +21 -5
- package/bin/cli.js +214 -9
- package/lib/cli-sessions.js +270 -0
- package/lib/config.js +3 -2
- package/lib/daemon.js +45 -1
- package/lib/pages.js +8 -1
- package/lib/project.js +121 -12
- package/lib/public/app.js +411 -87
- package/lib/public/css/base.css +41 -7
- package/lib/public/css/diff.css +6 -6
- package/lib/public/css/filebrowser.css +62 -52
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/input.css +11 -9
- package/lib/public/css/menus.css +82 -23
- package/lib/public/css/messages.css +183 -35
- package/lib/public/css/overlays.css +166 -50
- package/lib/public/css/rewind.css +17 -17
- package/lib/public/css/sidebar.css +210 -137
- package/lib/public/index.html +75 -42
- package/lib/public/modules/filebrowser.js +2 -1
- package/lib/public/modules/markdown.js +10 -10
- package/lib/public/modules/notifications.js +38 -1
- package/lib/public/modules/sidebar.js +109 -31
- package/lib/public/modules/terminal.js +84 -23
- package/lib/public/modules/theme.js +622 -0
- package/lib/public/modules/tools.js +247 -4
- package/lib/public/modules/utils.js +21 -5
- package/lib/public/style.css +1 -0
- package/lib/sdk-bridge.js +95 -0
- package/lib/server.js +45 -3
- package/lib/sessions.js +16 -3
- 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/claude-light.json +9 -0
- package/lib/themes/claude.json +9 -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/package.json +2 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { escapeHtml } from './utils.js';
|
|
1
|
+
import { escapeHtml, copyToClipboard } from './utils.js';
|
|
2
2
|
import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
4
4
|
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
|
|
@@ -21,11 +21,110 @@ var tools = {};
|
|
|
21
21
|
var currentThinking = null;
|
|
22
22
|
var pendingPermissions = {};
|
|
23
23
|
|
|
24
|
+
// --- Tool group tracking ---
|
|
25
|
+
var currentToolGroup = null;
|
|
26
|
+
var toolGroupCounter = 0;
|
|
27
|
+
var toolGroups = {};
|
|
28
|
+
|
|
24
29
|
// --- Tool helpers ---
|
|
25
30
|
var PLAN_MODE_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1 };
|
|
26
31
|
var TODO_TOOLS = { TodoWrite: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1 };
|
|
27
32
|
var HIDDEN_RESULT_TOOLS = { EnterPlanMode: 1, ExitPlanMode: 1, TaskCreate: 1, TaskUpdate: 1, TaskList: 1, TaskGet: 1, TodoWrite: 1 };
|
|
28
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
|
+
|
|
29
128
|
function isPlanFile(filePath) {
|
|
30
129
|
return filePath && filePath.indexOf(".claude/plans/") !== -1;
|
|
31
130
|
}
|
|
@@ -73,6 +172,7 @@ function shortPath(p) {
|
|
|
73
172
|
export function renderAskUserQuestion(toolId, input) {
|
|
74
173
|
ctx.finalizeAssistantBlock();
|
|
75
174
|
stopThinking();
|
|
175
|
+
closeToolGroup();
|
|
76
176
|
|
|
77
177
|
var questions = input.questions || [];
|
|
78
178
|
if (questions.length === 0) return;
|
|
@@ -258,8 +358,10 @@ function permissionInputSummary(toolName, input) {
|
|
|
258
358
|
}
|
|
259
359
|
|
|
260
360
|
export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason) {
|
|
361
|
+
if (pendingPermissions[requestId]) return;
|
|
261
362
|
ctx.finalizeAssistantBlock();
|
|
262
363
|
stopThinking();
|
|
364
|
+
closeToolGroup();
|
|
263
365
|
|
|
264
366
|
// ExitPlanMode: render as plan confirmation instead of generic permission
|
|
265
367
|
if (toolName === "ExitPlanMode") {
|
|
@@ -353,6 +455,7 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
353
455
|
}
|
|
354
456
|
|
|
355
457
|
function renderPlanPermission(requestId) {
|
|
458
|
+
if (pendingPermissions[requestId]) return;
|
|
356
459
|
var container = document.createElement("div");
|
|
357
460
|
container.className = "permission-container plan-permission";
|
|
358
461
|
container.dataset.requestId = requestId;
|
|
@@ -467,6 +570,7 @@ export function markPermissionCancelled(requestId) {
|
|
|
467
570
|
export function renderPlanBanner(type) {
|
|
468
571
|
ctx.finalizeAssistantBlock();
|
|
469
572
|
stopThinking();
|
|
573
|
+
closeToolGroup();
|
|
470
574
|
|
|
471
575
|
var el = document.createElement("div");
|
|
472
576
|
el.className = "plan-banner";
|
|
@@ -495,6 +599,7 @@ export function renderPlanBanner(type) {
|
|
|
495
599
|
|
|
496
600
|
export function renderPlanCard(content) {
|
|
497
601
|
ctx.finalizeAssistantBlock();
|
|
602
|
+
closeToolGroup();
|
|
498
603
|
|
|
499
604
|
var el = document.createElement("div");
|
|
500
605
|
el.className = "plan-card";
|
|
@@ -517,7 +622,7 @@ export function renderPlanCard(content) {
|
|
|
517
622
|
if (copyBtn) {
|
|
518
623
|
copyBtn.addEventListener("click", function (e) {
|
|
519
624
|
e.stopPropagation();
|
|
520
|
-
|
|
625
|
+
copyToClipboard(content).then(function () {
|
|
521
626
|
copyBtn.innerHTML = iconHtml("check");
|
|
522
627
|
refreshIcons();
|
|
523
628
|
setTimeout(function () {
|
|
@@ -783,6 +888,40 @@ export function createToolItem(id, name) {
|
|
|
783
888
|
ctx.finalizeAssistantBlock();
|
|
784
889
|
stopThinking();
|
|
785
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
|
+
|
|
786
925
|
var el = document.createElement("div");
|
|
787
926
|
el.className = "tool-item";
|
|
788
927
|
el.dataset.toolId = id;
|
|
@@ -801,11 +940,16 @@ export function createToolItem(id, name) {
|
|
|
801
940
|
|
|
802
941
|
el.querySelector(".tool-name").textContent = name;
|
|
803
942
|
|
|
804
|
-
|
|
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
|
+
|
|
805
949
|
refreshIcons();
|
|
806
950
|
ctx.scrollToBottom();
|
|
807
951
|
|
|
808
|
-
tools[id] = { el: el, name: name, input: null, done: false };
|
|
952
|
+
tools[id] = { el: el, name: name, input: null, done: false, groupId: currentToolGroup.id };
|
|
809
953
|
ctx.setActivity("Running " + name + "...");
|
|
810
954
|
}
|
|
811
955
|
|
|
@@ -1093,6 +1237,16 @@ export function markToolDone(id, isError) {
|
|
|
1093
1237
|
icon.innerHTML = '<span class="check">' + iconHtml("check") + '</span>';
|
|
1094
1238
|
}
|
|
1095
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
|
+
}
|
|
1096
1250
|
}
|
|
1097
1251
|
|
|
1098
1252
|
export function markAllToolsDone() {
|
|
@@ -1103,7 +1257,71 @@ export function markAllToolsDone() {
|
|
|
1103
1257
|
}
|
|
1104
1258
|
}
|
|
1105
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
|
+
|
|
1106
1323
|
export function addTurnMeta(cost, duration) {
|
|
1324
|
+
closeToolGroup();
|
|
1107
1325
|
var div = document.createElement("div");
|
|
1108
1326
|
div.className = "turn-meta";
|
|
1109
1327
|
div.dataset.turn = ctx.turnCounter;
|
|
@@ -1117,6 +1335,22 @@ export function addTurnMeta(cost, duration) {
|
|
|
1117
1335
|
}
|
|
1118
1336
|
}
|
|
1119
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
|
+
|
|
1120
1354
|
// Expose state getters and reset
|
|
1121
1355
|
export function getTools() { return tools; }
|
|
1122
1356
|
export function isInPlanMode() { return inPlanMode; }
|
|
@@ -1134,6 +1368,9 @@ export function saveToolState() {
|
|
|
1134
1368
|
todoWidgetEl: todoWidgetEl,
|
|
1135
1369
|
inPlanMode: inPlanMode,
|
|
1136
1370
|
planContent: planContent,
|
|
1371
|
+
currentToolGroup: currentToolGroup,
|
|
1372
|
+
toolGroupCounter: toolGroupCounter,
|
|
1373
|
+
toolGroups: toolGroups,
|
|
1137
1374
|
};
|
|
1138
1375
|
}
|
|
1139
1376
|
|
|
@@ -1143,6 +1380,9 @@ export function restoreToolState(saved) {
|
|
|
1143
1380
|
todoWidgetEl = saved.todoWidgetEl;
|
|
1144
1381
|
inPlanMode = saved.inPlanMode;
|
|
1145
1382
|
planContent = saved.planContent;
|
|
1383
|
+
currentToolGroup = saved.currentToolGroup;
|
|
1384
|
+
toolGroupCounter = saved.toolGroupCounter;
|
|
1385
|
+
toolGroups = saved.toolGroups;
|
|
1146
1386
|
if (todoWidgetEl) {
|
|
1147
1387
|
setupTodoObserver();
|
|
1148
1388
|
}
|
|
@@ -1158,6 +1398,9 @@ export function resetToolState() {
|
|
|
1158
1398
|
todoWidgetVisible = true;
|
|
1159
1399
|
if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
|
|
1160
1400
|
pendingPermissions = {};
|
|
1401
|
+
currentToolGroup = null;
|
|
1402
|
+
toolGroupCounter = 0;
|
|
1403
|
+
toolGroups = {};
|
|
1161
1404
|
var stickyEl = document.getElementById("todo-sticky");
|
|
1162
1405
|
if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
|
|
1163
1406
|
}
|
|
@@ -18,19 +18,35 @@ export function showToast(message, level, detail) {
|
|
|
18
18
|
}, duration);
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
var isIOS = /iPad|iPhone|iPod/.test(navigator.userAgent) ||
|
|
22
|
+
(navigator.platform === "MacIntel" && navigator.maxTouchPoints > 1);
|
|
23
|
+
|
|
21
24
|
export function copyToClipboard(text) {
|
|
22
25
|
var p;
|
|
23
|
-
if (
|
|
24
|
-
|
|
25
|
-
|
|
26
|
+
if (isIOS) {
|
|
27
|
+
// iOS Safari URL-encodes clipboard text that contains colons via the
|
|
28
|
+
// Clipboard API. Use textarea + execCommand to copy raw text instead.
|
|
26
29
|
var ta = document.createElement("textarea");
|
|
27
30
|
ta.value = text;
|
|
28
|
-
ta.style.cssText = "position:fixed;opacity:0";
|
|
31
|
+
ta.style.cssText = "position:fixed;left:-9999px;opacity:0";
|
|
29
32
|
document.body.appendChild(ta);
|
|
30
|
-
ta.
|
|
33
|
+
ta.focus();
|
|
34
|
+
ta.setSelectionRange(0, ta.value.length);
|
|
31
35
|
document.execCommand("copy");
|
|
32
36
|
document.body.removeChild(ta);
|
|
33
37
|
p = Promise.resolve();
|
|
38
|
+
} else if (navigator.clipboard && navigator.clipboard.writeText) {
|
|
39
|
+
p = navigator.clipboard.writeText(text);
|
|
40
|
+
} else {
|
|
41
|
+
var ta2 = document.createElement("textarea");
|
|
42
|
+
ta2.value = text;
|
|
43
|
+
ta2.style.cssText = "position:fixed;left:-9999px;opacity:0";
|
|
44
|
+
document.body.appendChild(ta2);
|
|
45
|
+
ta2.focus();
|
|
46
|
+
ta2.setSelectionRange(0, ta2.value.length);
|
|
47
|
+
document.execCommand("copy");
|
|
48
|
+
document.body.removeChild(ta2);
|
|
49
|
+
p = Promise.resolve();
|
|
34
50
|
}
|
|
35
51
|
return p.then(function () { showToast("Copied to clipboard"); });
|
|
36
52
|
}
|
package/lib/public/style.css
CHANGED
package/lib/sdk-bridge.js
CHANGED
|
@@ -137,6 +137,13 @@ function createSDKBridge(opts) {
|
|
|
137
137
|
var input = {};
|
|
138
138
|
try { input = JSON.parse(block.inputJson); } catch {}
|
|
139
139
|
sendAndRecord(session, { type: "tool_executing", id: block.id, name: block.name, input: input });
|
|
140
|
+
|
|
141
|
+
// Track active Task tools for sub-agent done detection
|
|
142
|
+
if (block.name === "Task") {
|
|
143
|
+
if (!session.activeTaskToolIds) session.activeTaskToolIds = {};
|
|
144
|
+
session.activeTaskToolIds[block.id] = true;
|
|
145
|
+
}
|
|
146
|
+
|
|
140
147
|
if (pushModule && block.name === "AskUserQuestion" && input.questions) {
|
|
141
148
|
var q = input.questions[0];
|
|
142
149
|
pushModule.sendPush({
|
|
@@ -155,6 +162,12 @@ function createSDKBridge(opts) {
|
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
} else if ((parsed.type === "assistant" || parsed.type === "user") && parsed.message && parsed.message.content) {
|
|
165
|
+
// Sub-agent messages: extract tool_use blocks for activity display
|
|
166
|
+
if (parsed.parent_tool_use_id) {
|
|
167
|
+
processSubagentMessage(session, parsed);
|
|
168
|
+
return;
|
|
169
|
+
}
|
|
170
|
+
|
|
158
171
|
var content = parsed.message.content;
|
|
159
172
|
|
|
160
173
|
// Fallback: if assistant text wasn't streamed via deltas, send it now
|
|
@@ -191,6 +204,14 @@ function createSDKBridge(opts) {
|
|
|
191
204
|
for (var i = 0; i < content.length; i++) {
|
|
192
205
|
var block = content[i];
|
|
193
206
|
if (block.type === "tool_result" && !session.sentToolResults[block.tool_use_id]) {
|
|
207
|
+
// Clear active Task tool when its result arrives
|
|
208
|
+
if (session.activeTaskToolIds && session.activeTaskToolIds[block.tool_use_id]) {
|
|
209
|
+
sendAndRecord(session, {
|
|
210
|
+
type: "subagent_done",
|
|
211
|
+
parentToolId: block.tool_use_id,
|
|
212
|
+
});
|
|
213
|
+
delete session.activeTaskToolIds[block.tool_use_id];
|
|
214
|
+
}
|
|
194
215
|
var resultText = "";
|
|
195
216
|
if (typeof block.content === "string") {
|
|
196
217
|
resultText = block.content;
|
|
@@ -216,6 +237,7 @@ function createSDKBridge(opts) {
|
|
|
216
237
|
session.sentToolResults = {};
|
|
217
238
|
session.pendingPermissions = {};
|
|
218
239
|
session.pendingAskUser = {};
|
|
240
|
+
session.activeTaskToolIds = {};
|
|
219
241
|
session.isProcessing = false;
|
|
220
242
|
sendAndRecord(session, {
|
|
221
243
|
type: "result",
|
|
@@ -250,10 +272,82 @@ function createSDKBridge(opts) {
|
|
|
250
272
|
}
|
|
251
273
|
session.compacting = parsed.status === "compacting";
|
|
252
274
|
|
|
275
|
+
} else if (parsed.type === "tool_progress") {
|
|
276
|
+
// Sub-agent tool_progress: forward as activity update
|
|
277
|
+
var parentId = parsed.parent_tool_use_id;
|
|
278
|
+
if (parentId) {
|
|
279
|
+
sendAndRecord(session, {
|
|
280
|
+
type: "subagent_activity",
|
|
281
|
+
parentToolId: parentId,
|
|
282
|
+
text: parsed.content || "",
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
} else if (parsed.type === "task_notification") {
|
|
287
|
+
// Sub-agent finished
|
|
288
|
+
var parentId = parsed.parent_tool_use_id;
|
|
289
|
+
if (parentId) {
|
|
290
|
+
sendAndRecord(session, {
|
|
291
|
+
type: "subagent_done",
|
|
292
|
+
parentToolId: parentId,
|
|
293
|
+
});
|
|
294
|
+
}
|
|
295
|
+
|
|
253
296
|
} else if (parsed.type && parsed.type !== "system" && parsed.type !== "user") {
|
|
254
297
|
}
|
|
255
298
|
}
|
|
256
299
|
|
|
300
|
+
// --- Sub-agent message processing ---
|
|
301
|
+
|
|
302
|
+
function toolActivityTextForSubagent(name, input) {
|
|
303
|
+
if (name === "Bash" && input && input.description) return input.description;
|
|
304
|
+
if (name === "Read" && input && input.file_path) return "Reading " + input.file_path.split("/").pop();
|
|
305
|
+
if (name === "Edit" && input && input.file_path) return "Editing " + input.file_path.split("/").pop();
|
|
306
|
+
if (name === "Write" && input && input.file_path) return "Writing " + input.file_path.split("/").pop();
|
|
307
|
+
if (name === "Grep" && input && input.pattern) return "Searching for " + input.pattern;
|
|
308
|
+
if (name === "Glob" && input && input.pattern) return "Finding " + input.pattern;
|
|
309
|
+
if (name === "WebSearch" && input && input.query) return "Searching: " + input.query;
|
|
310
|
+
if (name === "WebFetch") return "Fetching URL...";
|
|
311
|
+
if (name === "Task" && input && input.description) return input.description;
|
|
312
|
+
return "Running " + name + "...";
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
function processSubagentMessage(session, parsed) {
|
|
316
|
+
var parentId = parsed.parent_tool_use_id;
|
|
317
|
+
var content = parsed.message.content;
|
|
318
|
+
if (!Array.isArray(content)) return;
|
|
319
|
+
|
|
320
|
+
if (parsed.type === "assistant") {
|
|
321
|
+
// Extract tool_use blocks from sub-agent assistant messages
|
|
322
|
+
for (var i = 0; i < content.length; i++) {
|
|
323
|
+
var block = content[i];
|
|
324
|
+
if (block.type === "tool_use") {
|
|
325
|
+
var activityText = toolActivityTextForSubagent(block.name, block.input);
|
|
326
|
+
sendAndRecord(session, {
|
|
327
|
+
type: "subagent_tool",
|
|
328
|
+
parentToolId: parentId,
|
|
329
|
+
toolName: block.name,
|
|
330
|
+
toolId: block.id,
|
|
331
|
+
text: activityText,
|
|
332
|
+
});
|
|
333
|
+
} else if (block.type === "thinking") {
|
|
334
|
+
sendAndRecord(session, {
|
|
335
|
+
type: "subagent_activity",
|
|
336
|
+
parentToolId: parentId,
|
|
337
|
+
text: "Thinking...",
|
|
338
|
+
});
|
|
339
|
+
} else if (block.type === "text" && block.text) {
|
|
340
|
+
sendAndRecord(session, {
|
|
341
|
+
type: "subagent_activity",
|
|
342
|
+
parentToolId: parentId,
|
|
343
|
+
text: "Writing response...",
|
|
344
|
+
});
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
// user messages with parent_tool_use_id contain tool_results — skip silently
|
|
349
|
+
}
|
|
350
|
+
|
|
257
351
|
// --- SDK query lifecycle ---
|
|
258
352
|
|
|
259
353
|
function handleCanUseTool(session, toolName, input, opts) {
|
|
@@ -404,6 +498,7 @@ function createSDKBridge(opts) {
|
|
|
404
498
|
session.messageQueue = createMessageQueue();
|
|
405
499
|
session.blocks = {};
|
|
406
500
|
session.sentToolResults = {};
|
|
501
|
+
session.activeTaskToolIds = {};
|
|
407
502
|
session.streamedText = false;
|
|
408
503
|
session.responsePreview = "";
|
|
409
504
|
|
package/lib/server.js
CHANGED
|
@@ -6,7 +6,11 @@ var { WebSocketServer } = require("ws");
|
|
|
6
6
|
var { pinPageHtml, setupPageHtml, dashboardPageHtml } = require("./pages");
|
|
7
7
|
var { createProjectContext } = require("./project");
|
|
8
8
|
|
|
9
|
+
var { CONFIG_DIR } = require("./config");
|
|
10
|
+
|
|
9
11
|
var publicDir = path.join(__dirname, "public");
|
|
12
|
+
var bundledThemesDir = path.join(__dirname, "themes");
|
|
13
|
+
var userThemesDir = path.join(CONFIG_DIR, "themes");
|
|
10
14
|
|
|
11
15
|
var MIME_TYPES = {
|
|
12
16
|
".html": "text/html",
|
|
@@ -124,6 +128,8 @@ function createServer(opts) {
|
|
|
124
128
|
var debug = opts.debug || false;
|
|
125
129
|
var dangerouslySkipPermissions = opts.dangerouslySkipPermissions || false;
|
|
126
130
|
var lanHost = opts.lanHost || null;
|
|
131
|
+
var onAddProject = opts.onAddProject || null;
|
|
132
|
+
var onRemoveProject = opts.onRemoveProject || null;
|
|
127
133
|
|
|
128
134
|
var authToken = pinHash || null;
|
|
129
135
|
var realVersion = require("../package.json").version;
|
|
@@ -243,6 +249,39 @@ function createServer(opts) {
|
|
|
243
249
|
return;
|
|
244
250
|
}
|
|
245
251
|
|
|
252
|
+
// Theme list: bundled (lib/themes/) + user (~/.claude-relay/themes/)
|
|
253
|
+
if (req.method === "GET" && fullUrl === "/api/themes") {
|
|
254
|
+
var bundled = {};
|
|
255
|
+
var custom = {};
|
|
256
|
+
// Read bundled themes
|
|
257
|
+
try {
|
|
258
|
+
var bFiles = fs.readdirSync(bundledThemesDir);
|
|
259
|
+
for (var i = 0; i < bFiles.length; i++) {
|
|
260
|
+
if (!bFiles[i].endsWith(".json")) continue;
|
|
261
|
+
try {
|
|
262
|
+
var raw = fs.readFileSync(path.join(bundledThemesDir, bFiles[i]), "utf8");
|
|
263
|
+
var id = bFiles[i].replace(/\.json$/, "");
|
|
264
|
+
bundled[id] = JSON.parse(raw);
|
|
265
|
+
} catch (e) {}
|
|
266
|
+
}
|
|
267
|
+
} catch (e) {}
|
|
268
|
+
// Read user themes (override bundled if same id)
|
|
269
|
+
try {
|
|
270
|
+
var uFiles = fs.readdirSync(userThemesDir);
|
|
271
|
+
for (var j = 0; j < uFiles.length; j++) {
|
|
272
|
+
if (!uFiles[j].endsWith(".json")) continue;
|
|
273
|
+
try {
|
|
274
|
+
var uRaw = fs.readFileSync(path.join(userThemesDir, uFiles[j]), "utf8");
|
|
275
|
+
var uid = uFiles[j].replace(/\.json$/, "");
|
|
276
|
+
custom[uid] = JSON.parse(uRaw);
|
|
277
|
+
} catch (e) {}
|
|
278
|
+
}
|
|
279
|
+
} catch (e) {}
|
|
280
|
+
res.writeHead(200, { "Content-Type": "application/json" });
|
|
281
|
+
res.end(JSON.stringify({ bundled: bundled, custom: custom }));
|
|
282
|
+
return;
|
|
283
|
+
}
|
|
284
|
+
|
|
246
285
|
// Root path — dashboard or redirect
|
|
247
286
|
if (fullUrl === "/" && req.method === "GET") {
|
|
248
287
|
if (!isAuthed(req, authToken)) {
|
|
@@ -250,7 +289,8 @@ function createServer(opts) {
|
|
|
250
289
|
res.end(pinPage);
|
|
251
290
|
return;
|
|
252
291
|
}
|
|
253
|
-
|
|
292
|
+
var hasGoneParam = req.url.indexOf("gone=") !== -1;
|
|
293
|
+
if (projects.size === 1 && !hasGoneParam) {
|
|
254
294
|
var slug = projects.keys().next().value;
|
|
255
295
|
res.writeHead(302, { "Location": "/p/" + slug + "/" });
|
|
256
296
|
res.end();
|
|
@@ -297,8 +337,8 @@ function createServer(opts) {
|
|
|
297
337
|
|
|
298
338
|
var ctx = projects.get(slug);
|
|
299
339
|
if (!ctx) {
|
|
300
|
-
res.writeHead(
|
|
301
|
-
res.end(
|
|
340
|
+
res.writeHead(302, { "Location": "/?gone=" + encodeURIComponent(slug) });
|
|
341
|
+
res.end();
|
|
302
342
|
return;
|
|
303
343
|
}
|
|
304
344
|
|
|
@@ -471,6 +511,8 @@ function createServer(opts) {
|
|
|
471
511
|
projects.forEach(function (ctx) { list.push(ctx.getStatus()); });
|
|
472
512
|
return list;
|
|
473
513
|
},
|
|
514
|
+
onAddProject: onAddProject,
|
|
515
|
+
onRemoveProject: onRemoveProject,
|
|
474
516
|
});
|
|
475
517
|
projects.set(slug, ctx);
|
|
476
518
|
ctx.warmup();
|
package/lib/sessions.js
CHANGED
|
@@ -253,7 +253,20 @@ function createSessionManager(opts) {
|
|
|
253
253
|
}
|
|
254
254
|
}
|
|
255
255
|
|
|
256
|
-
function resumeSession(cliSessionId) {
|
|
256
|
+
function resumeSession(cliSessionId, opts) {
|
|
257
|
+
// If a session with this cliSessionId already exists, just switch to it
|
|
258
|
+
var existing = null;
|
|
259
|
+
sessions.forEach(function (s) {
|
|
260
|
+
if (s.cliSessionId === cliSessionId) existing = s;
|
|
261
|
+
});
|
|
262
|
+
if (existing) {
|
|
263
|
+
existing.lastActivity = Date.now();
|
|
264
|
+
switchSession(existing.localId);
|
|
265
|
+
return existing;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
var cliHistory = (opts && opts.history) || [];
|
|
269
|
+
var title = (opts && opts.title) || "Resumed session";
|
|
257
270
|
var localId = nextLocalId++;
|
|
258
271
|
var session = {
|
|
259
272
|
localId: localId,
|
|
@@ -266,9 +279,9 @@ function createSessionManager(opts) {
|
|
|
266
279
|
pendingAskUser: {},
|
|
267
280
|
allowedTools: {},
|
|
268
281
|
isProcessing: false,
|
|
269
|
-
title:
|
|
282
|
+
title: title,
|
|
270
283
|
createdAt: Date.now(),
|
|
271
|
-
history:
|
|
284
|
+
history: cliHistory,
|
|
272
285
|
messageUUIDs: [],
|
|
273
286
|
};
|
|
274
287
|
sessions.set(localId, session);
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Ayu Light",
|
|
3
|
+
"author": "Ike Ku",
|
|
4
|
+
"variant": "light",
|
|
5
|
+
"base00": "FAFAFA", "base01": "EDEFF1", "base02": "D2D4D8", "base03": "A0A6AC",
|
|
6
|
+
"base04": "8A9199", "base05": "5C6166", "base06": "4E5257", "base07": "404447",
|
|
7
|
+
"base08": "F07171", "base09": "FA8D3E", "base0A": "F2AE49", "base0B": "6CBF49",
|
|
8
|
+
"base0C": "4CBF99", "base0D": "399EE6", "base0E": "A37ACC", "base0F": "E6BA7E"
|
|
9
|
+
}
|