claude-relay 2.3.1 → 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 +20 -5
- package/bin/cli.js +206 -8
- package/lib/cli-sessions.js +270 -0
- package/lib/daemon.js +40 -0
- package/lib/project.js +121 -1
- package/lib/public/app.js +385 -76
- package/lib/public/css/base.css +41 -7
- package/lib/public/css/diff.css +6 -6
- package/lib/public/css/filebrowser.css +34 -54
- package/lib/public/css/highlight.css +144 -0
- package/lib/public/css/input.css +9 -9
- package/lib/public/css/menus.css +82 -23
- package/lib/public/css/messages.css +178 -34
- 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 +73 -40
- 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 +11 -23
- package/lib/public/modules/theme.js +622 -0
- package/lib/public/modules/tools.js +245 -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 +41 -0
- 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;
|
|
@@ -261,6 +361,7 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
261
361
|
if (pendingPermissions[requestId]) return;
|
|
262
362
|
ctx.finalizeAssistantBlock();
|
|
263
363
|
stopThinking();
|
|
364
|
+
closeToolGroup();
|
|
264
365
|
|
|
265
366
|
// ExitPlanMode: render as plan confirmation instead of generic permission
|
|
266
367
|
if (toolName === "ExitPlanMode") {
|
|
@@ -469,6 +570,7 @@ export function markPermissionCancelled(requestId) {
|
|
|
469
570
|
export function renderPlanBanner(type) {
|
|
470
571
|
ctx.finalizeAssistantBlock();
|
|
471
572
|
stopThinking();
|
|
573
|
+
closeToolGroup();
|
|
472
574
|
|
|
473
575
|
var el = document.createElement("div");
|
|
474
576
|
el.className = "plan-banner";
|
|
@@ -497,6 +599,7 @@ export function renderPlanBanner(type) {
|
|
|
497
599
|
|
|
498
600
|
export function renderPlanCard(content) {
|
|
499
601
|
ctx.finalizeAssistantBlock();
|
|
602
|
+
closeToolGroup();
|
|
500
603
|
|
|
501
604
|
var el = document.createElement("div");
|
|
502
605
|
el.className = "plan-card";
|
|
@@ -519,7 +622,7 @@ export function renderPlanCard(content) {
|
|
|
519
622
|
if (copyBtn) {
|
|
520
623
|
copyBtn.addEventListener("click", function (e) {
|
|
521
624
|
e.stopPropagation();
|
|
522
|
-
|
|
625
|
+
copyToClipboard(content).then(function () {
|
|
523
626
|
copyBtn.innerHTML = iconHtml("check");
|
|
524
627
|
refreshIcons();
|
|
525
628
|
setTimeout(function () {
|
|
@@ -785,6 +888,40 @@ export function createToolItem(id, name) {
|
|
|
785
888
|
ctx.finalizeAssistantBlock();
|
|
786
889
|
stopThinking();
|
|
787
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
|
+
|
|
788
925
|
var el = document.createElement("div");
|
|
789
926
|
el.className = "tool-item";
|
|
790
927
|
el.dataset.toolId = id;
|
|
@@ -803,11 +940,16 @@ export function createToolItem(id, name) {
|
|
|
803
940
|
|
|
804
941
|
el.querySelector(".tool-name").textContent = name;
|
|
805
942
|
|
|
806
|
-
|
|
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
|
+
|
|
807
949
|
refreshIcons();
|
|
808
950
|
ctx.scrollToBottom();
|
|
809
951
|
|
|
810
|
-
tools[id] = { el: el, name: name, input: null, done: false };
|
|
952
|
+
tools[id] = { el: el, name: name, input: null, done: false, groupId: currentToolGroup.id };
|
|
811
953
|
ctx.setActivity("Running " + name + "...");
|
|
812
954
|
}
|
|
813
955
|
|
|
@@ -1095,6 +1237,16 @@ export function markToolDone(id, isError) {
|
|
|
1095
1237
|
icon.innerHTML = '<span class="check">' + iconHtml("check") + '</span>';
|
|
1096
1238
|
}
|
|
1097
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
|
+
}
|
|
1098
1250
|
}
|
|
1099
1251
|
|
|
1100
1252
|
export function markAllToolsDone() {
|
|
@@ -1105,7 +1257,71 @@ export function markAllToolsDone() {
|
|
|
1105
1257
|
}
|
|
1106
1258
|
}
|
|
1107
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
|
+
|
|
1108
1323
|
export function addTurnMeta(cost, duration) {
|
|
1324
|
+
closeToolGroup();
|
|
1109
1325
|
var div = document.createElement("div");
|
|
1110
1326
|
div.className = "turn-meta";
|
|
1111
1327
|
div.dataset.turn = ctx.turnCounter;
|
|
@@ -1119,6 +1335,22 @@ export function addTurnMeta(cost, duration) {
|
|
|
1119
1335
|
}
|
|
1120
1336
|
}
|
|
1121
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
|
+
|
|
1122
1354
|
// Expose state getters and reset
|
|
1123
1355
|
export function getTools() { return tools; }
|
|
1124
1356
|
export function isInPlanMode() { return inPlanMode; }
|
|
@@ -1136,6 +1368,9 @@ export function saveToolState() {
|
|
|
1136
1368
|
todoWidgetEl: todoWidgetEl,
|
|
1137
1369
|
inPlanMode: inPlanMode,
|
|
1138
1370
|
planContent: planContent,
|
|
1371
|
+
currentToolGroup: currentToolGroup,
|
|
1372
|
+
toolGroupCounter: toolGroupCounter,
|
|
1373
|
+
toolGroups: toolGroups,
|
|
1139
1374
|
};
|
|
1140
1375
|
}
|
|
1141
1376
|
|
|
@@ -1145,6 +1380,9 @@ export function restoreToolState(saved) {
|
|
|
1145
1380
|
todoWidgetEl = saved.todoWidgetEl;
|
|
1146
1381
|
inPlanMode = saved.inPlanMode;
|
|
1147
1382
|
planContent = saved.planContent;
|
|
1383
|
+
currentToolGroup = saved.currentToolGroup;
|
|
1384
|
+
toolGroupCounter = saved.toolGroupCounter;
|
|
1385
|
+
toolGroups = saved.toolGroups;
|
|
1148
1386
|
if (todoWidgetEl) {
|
|
1149
1387
|
setupTodoObserver();
|
|
1150
1388
|
}
|
|
@@ -1160,6 +1398,9 @@ export function resetToolState() {
|
|
|
1160
1398
|
todoWidgetVisible = true;
|
|
1161
1399
|
if (todoObserver) { todoObserver.disconnect(); todoObserver = null; }
|
|
1162
1400
|
pendingPermissions = {};
|
|
1401
|
+
currentToolGroup = null;
|
|
1402
|
+
toolGroupCounter = 0;
|
|
1403
|
+
toolGroups = {};
|
|
1163
1404
|
var stickyEl = document.getElementById("todo-sticky");
|
|
1164
1405
|
if (stickyEl) { stickyEl.classList.add("hidden"); stickyEl.innerHTML = ""; }
|
|
1165
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)) {
|
|
@@ -472,6 +511,8 @@ function createServer(opts) {
|
|
|
472
511
|
projects.forEach(function (ctx) { list.push(ctx.getStatus()); });
|
|
473
512
|
return list;
|
|
474
513
|
},
|
|
514
|
+
onAddProject: onAddProject,
|
|
515
|
+
onRemoveProject: onRemoveProject,
|
|
475
516
|
});
|
|
476
517
|
projects.set(slug, ctx);
|
|
477
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
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Catppuccin Latte",
|
|
3
|
+
"author": "catppuccin",
|
|
4
|
+
"variant": "light",
|
|
5
|
+
"base00": "eff1f5", "base01": "e6e9ef", "base02": "ccd0da", "base03": "9ca0b0",
|
|
6
|
+
"base04": "8c8fa1", "base05": "5c5f77", "base06": "4c4f69", "base07": "303446",
|
|
7
|
+
"base08": "d20f39", "base09": "fe640b", "base0A": "df8e1d", "base0B": "40a02b",
|
|
8
|
+
"base0C": "179299", "base0D": "1e66f5", "base0E": "8839ef", "base0F": "dd7878"
|
|
9
|
+
}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "Catppuccin Mocha",
|
|
3
|
+
"author": "catppuccin",
|
|
4
|
+
"variant": "dark",
|
|
5
|
+
"base00": "1e1e2e", "base01": "181825", "base02": "313244", "base03": "45475a",
|
|
6
|
+
"base04": "585b70", "base05": "cdd6f4", "base06": "f5e0dc", "base07": "b4befe",
|
|
7
|
+
"base08": "f38ba8", "base09": "fab387", "base0A": "f9e2af", "base0B": "a6e3a1",
|
|
8
|
+
"base0C": "94e2d5", "base0D": "89b4fa", "base0E": "cba6f7", "base0F": "f2cdcd"
|
|
9
|
+
}
|