claude-relay 2.2.4 → 2.3.1
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 +13 -0
- package/bin/cli.js +144 -6
- package/lib/config.js +34 -2
- package/lib/daemon.js +54 -2
- package/lib/pages.js +22 -1
- package/lib/project.js +312 -26
- package/lib/public/app.js +339 -18
- package/lib/public/css/base.css +5 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +571 -0
- package/lib/public/css/input.css +3 -0
- package/lib/public/css/menus.css +89 -5
- package/lib/public/css/messages.css +89 -50
- package/lib/public/css/overlays.css +40 -0
- package/lib/public/index.html +102 -19
- package/lib/public/modules/diff.js +398 -0
- package/lib/public/modules/filebrowser.js +1023 -11
- package/lib/public/modules/input.js +96 -2
- package/lib/public/modules/notifications.js +29 -3
- package/lib/public/modules/qrcode.js +11 -2
- package/lib/public/modules/rewind.js +51 -2
- package/lib/public/modules/terminal.js +73 -0
- package/lib/public/modules/tools.js +45 -104
- package/lib/public/modules/utils.js +10 -2
- package/lib/public/style.css +1 -0
- package/lib/public/sw.js +21 -7
- package/lib/push.js +5 -1
- package/lib/sdk-bridge.js +38 -5
- package/lib/server.js +41 -7
- package/lib/sessions.js +14 -5
- package/package.json +1 -1
|
@@ -11,10 +11,12 @@ var slashFiltered = [];
|
|
|
11
11
|
var isComposing = false;
|
|
12
12
|
var isRemoteInput = false;
|
|
13
13
|
|
|
14
|
-
var builtinCommands = [
|
|
14
|
+
export var builtinCommands = [
|
|
15
15
|
{ name: "clear", desc: "Clear conversation" },
|
|
16
|
+
{ name: "context", desc: "Context window usage" },
|
|
16
17
|
{ name: "rewind", desc: "Toggle rewind mode" },
|
|
17
18
|
{ name: "usage", desc: "Toggle usage panel" },
|
|
19
|
+
{ name: "status", desc: "Process status and resource usage" },
|
|
18
20
|
];
|
|
19
21
|
|
|
20
22
|
// --- Send ---
|
|
@@ -25,10 +27,12 @@ export function sendMessage() {
|
|
|
25
27
|
hideSlashMenu();
|
|
26
28
|
|
|
27
29
|
if (text === "/clear") {
|
|
28
|
-
ctx.messagesEl.innerHTML = "";
|
|
29
30
|
ctx.inputEl.value = "";
|
|
30
31
|
clearPendingImages();
|
|
31
32
|
autoResize();
|
|
33
|
+
if (ctx.ws && ctx.connected) {
|
|
34
|
+
ctx.ws.send(JSON.stringify({ type: "new_session" }));
|
|
35
|
+
}
|
|
32
36
|
return;
|
|
33
37
|
}
|
|
34
38
|
|
|
@@ -44,6 +48,14 @@ export function sendMessage() {
|
|
|
44
48
|
return;
|
|
45
49
|
}
|
|
46
50
|
|
|
51
|
+
if (text === "/context") {
|
|
52
|
+
ctx.inputEl.value = "";
|
|
53
|
+
clearPendingImages();
|
|
54
|
+
autoResize();
|
|
55
|
+
if (ctx.toggleContextPanel) ctx.toggleContextPanel();
|
|
56
|
+
return;
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
if (text === "/usage") {
|
|
48
60
|
ctx.inputEl.value = "";
|
|
49
61
|
clearPendingImages();
|
|
@@ -52,6 +64,14 @@ export function sendMessage() {
|
|
|
52
64
|
return;
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
if (text === "/status") {
|
|
68
|
+
ctx.inputEl.value = "";
|
|
69
|
+
clearPendingImages();
|
|
70
|
+
autoResize();
|
|
71
|
+
if (ctx.toggleStatusPanel) ctx.toggleStatusPanel();
|
|
72
|
+
return;
|
|
73
|
+
}
|
|
74
|
+
|
|
55
75
|
if (!ctx.connected) {
|
|
56
76
|
ctx.addSystemMessage("Not connected — message not sent.", true);
|
|
57
77
|
return;
|
|
@@ -81,6 +101,67 @@ export function autoResize() {
|
|
|
81
101
|
ctx.inputEl.style.height = Math.min(ctx.inputEl.scrollHeight, 120) + "px";
|
|
82
102
|
}
|
|
83
103
|
|
|
104
|
+
// --- File path extraction from clipboard ---
|
|
105
|
+
function extractFilePaths(cd) {
|
|
106
|
+
var paths = [];
|
|
107
|
+
|
|
108
|
+
// 1. Check text/uri-list for file:// URIs (Finder on some browsers)
|
|
109
|
+
var uriList = cd.getData("text/uri-list");
|
|
110
|
+
if (uriList) {
|
|
111
|
+
var lines = uriList.split(/\r?\n/);
|
|
112
|
+
for (var i = 0; i < lines.length; i++) {
|
|
113
|
+
var line = lines[i].trim();
|
|
114
|
+
if (line && !line.startsWith("#") && line.startsWith("file://")) {
|
|
115
|
+
paths.push(decodeURIComponent(line.replace("file://", "")));
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
if (paths.length > 0) return paths;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// 2. Check if text/plain looks like file path(s) while files are present
|
|
122
|
+
// (Finder Cmd+C puts filename in text/plain, Cmd+Option+C puts full path)
|
|
123
|
+
if (cd.files && cd.files.length > 0) {
|
|
124
|
+
var plainText = cd.getData("text/plain");
|
|
125
|
+
if (plainText) {
|
|
126
|
+
var textLines = plainText.split(/\r?\n/).filter(function (l) { return l.trim(); });
|
|
127
|
+
for (var i = 0; i < textLines.length; i++) {
|
|
128
|
+
var p = textLines[i].trim();
|
|
129
|
+
if (p.startsWith("/") || p.startsWith("~")) {
|
|
130
|
+
paths.push(p);
|
|
131
|
+
}
|
|
132
|
+
}
|
|
133
|
+
if (paths.length > 0) return paths;
|
|
134
|
+
}
|
|
135
|
+
// 3. Fallback: files present but no path in text, use filenames
|
|
136
|
+
for (var i = 0; i < cd.files.length; i++) {
|
|
137
|
+
var f = cd.files[i];
|
|
138
|
+
if (f.name && f.type.indexOf("image/") !== 0) {
|
|
139
|
+
paths.push(f.name);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
return paths;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
// --- Insert text at cursor in textarea ---
|
|
148
|
+
function insertTextAtCursor(text) {
|
|
149
|
+
var el = ctx.inputEl;
|
|
150
|
+
el.focus();
|
|
151
|
+
var start = el.selectionStart;
|
|
152
|
+
var end = el.selectionEnd;
|
|
153
|
+
var before = el.value.substring(0, start);
|
|
154
|
+
var after = el.value.substring(end);
|
|
155
|
+
// Add space before if cursor is right after non-space text
|
|
156
|
+
if (before.length > 0 && before[before.length - 1] !== " " && before[before.length - 1] !== "\n") {
|
|
157
|
+
text = " " + text;
|
|
158
|
+
}
|
|
159
|
+
el.value = before + text + after;
|
|
160
|
+
el.selectionStart = el.selectionEnd = start + text.length;
|
|
161
|
+
autoResize();
|
|
162
|
+
sendInputSync();
|
|
163
|
+
}
|
|
164
|
+
|
|
84
165
|
// --- Image paste ---
|
|
85
166
|
function addPendingImage(dataUrl) {
|
|
86
167
|
var commaIdx = dataUrl.indexOf(",");
|
|
@@ -125,6 +206,9 @@ function renderInputPreviews() {
|
|
|
125
206
|
wrap.className = "image-preview-thumb";
|
|
126
207
|
var img = document.createElement("img");
|
|
127
208
|
img.src = "data:" + pendingImages[idx].mediaType + ";base64," + pendingImages[idx].data;
|
|
209
|
+
img.addEventListener("click", function () {
|
|
210
|
+
if (ctx.showImageModal) ctx.showImageModal(this.src);
|
|
211
|
+
});
|
|
128
212
|
var removeBtn = document.createElement("button");
|
|
129
213
|
removeBtn.className = "image-preview-remove";
|
|
130
214
|
removeBtn.innerHTML = iconHtml("x");
|
|
@@ -385,6 +469,16 @@ export function initInput(_ctx) {
|
|
|
385
469
|
}
|
|
386
470
|
}
|
|
387
471
|
|
|
472
|
+
// File path paste: detect file:// URIs or Finder file references
|
|
473
|
+
if (!found) {
|
|
474
|
+
var filePaths = extractFilePaths(cd);
|
|
475
|
+
if (filePaths.length > 0) {
|
|
476
|
+
e.preventDefault();
|
|
477
|
+
insertTextAtCursor(filePaths.join("\n"));
|
|
478
|
+
found = true;
|
|
479
|
+
}
|
|
480
|
+
}
|
|
481
|
+
|
|
388
482
|
// Long text paste → pasted chip
|
|
389
483
|
if (!found) {
|
|
390
484
|
var pastedText = cd.getData("text/plain");
|
|
@@ -137,6 +137,7 @@ export function initNotifications(_ctx) {
|
|
|
137
137
|
var footerBtn = $("sidebar-footer-btn");
|
|
138
138
|
var footerMenu = $("sidebar-footer-menu");
|
|
139
139
|
var footerUpdateCheck = $("footer-update-check");
|
|
140
|
+
var footerStatus = $("footer-status");
|
|
140
141
|
if (!footerBtn || !footerMenu) return;
|
|
141
142
|
|
|
142
143
|
footerBtn.addEventListener("click", function (e) {
|
|
@@ -195,6 +196,14 @@ export function initNotifications(_ctx) {
|
|
|
195
196
|
}, 2000);
|
|
196
197
|
});
|
|
197
198
|
}
|
|
199
|
+
|
|
200
|
+
if (footerStatus) {
|
|
201
|
+
footerStatus.addEventListener("click", function (e) {
|
|
202
|
+
e.stopPropagation();
|
|
203
|
+
footerMenu.classList.add("hidden");
|
|
204
|
+
if (ctx.toggleStatusPanel) ctx.toggleStatusPanel();
|
|
205
|
+
});
|
|
206
|
+
}
|
|
198
207
|
})();
|
|
199
208
|
|
|
200
209
|
// --- Onboarding banner (HTTPS / Push) ---
|
|
@@ -371,14 +380,20 @@ export function initNotifications(_ctx) {
|
|
|
371
380
|
}
|
|
372
381
|
|
|
373
382
|
function sendPushSubscription(sub) {
|
|
383
|
+
var prevEndpoint = localStorage.getItem("push-endpoint");
|
|
374
384
|
window._pushSubscription = sub;
|
|
385
|
+
localStorage.setItem("push-endpoint", sub.endpoint);
|
|
375
386
|
var json = sub.toJSON();
|
|
387
|
+
var payload = { subscription: json };
|
|
388
|
+
if (prevEndpoint && prevEndpoint !== sub.endpoint) {
|
|
389
|
+
payload.replaceEndpoint = prevEndpoint;
|
|
390
|
+
}
|
|
376
391
|
if (ctx.ws && ctx.ws.readyState === 1) {
|
|
377
|
-
ctx.ws.send(JSON.stringify({ type: "push_subscribe", subscription: json }));
|
|
392
|
+
ctx.ws.send(JSON.stringify({ type: "push_subscribe", subscription: json, replaceEndpoint: payload.replaceEndpoint || null }));
|
|
378
393
|
} else {
|
|
379
394
|
fetch(basePath + "api/push-subscribe", {
|
|
380
395
|
method: "POST", headers: { "Content-Type": "application/json" },
|
|
381
|
-
credentials: "same-origin", body: JSON.stringify(
|
|
396
|
+
credentials: "same-origin", body: JSON.stringify(payload),
|
|
382
397
|
});
|
|
383
398
|
}
|
|
384
399
|
}
|
|
@@ -397,6 +412,15 @@ export function initNotifications(_ctx) {
|
|
|
397
412
|
}).then(function (sub) {
|
|
398
413
|
sendPushSubscription(sub);
|
|
399
414
|
localStorage.setItem("notif-push", "1");
|
|
415
|
+
hideOnboarding();
|
|
416
|
+
localStorage.setItem("onboarding-dismissed", "1");
|
|
417
|
+
// Show a welcome notification so the user knows it works
|
|
418
|
+
navigator.serviceWorker.ready.then(function (reg) {
|
|
419
|
+
reg.showNotification("\ud83c\udf89 Welcome to Claude Relay!", {
|
|
420
|
+
body: "\ud83d\udd14 You\u2019ll be notified when Claude responds.",
|
|
421
|
+
tag: "claude-welcome",
|
|
422
|
+
});
|
|
423
|
+
}).catch(function () {});
|
|
400
424
|
}).catch(function () {
|
|
401
425
|
notifTogglePush.checked = false;
|
|
402
426
|
localStorage.setItem("notif-push", "0");
|
|
@@ -454,6 +478,7 @@ export function initNotifications(_ctx) {
|
|
|
454
478
|
window._pushSubscription = sub;
|
|
455
479
|
notifTogglePush.checked = true;
|
|
456
480
|
sendPushSubscription(sub);
|
|
481
|
+
hideOnboarding();
|
|
457
482
|
} else if (serverKey && localStorage.getItem("notif-push") === "1") {
|
|
458
483
|
// Had push enabled but subscription is gone (VAPID key change), re-subscribe
|
|
459
484
|
var raw = atob(serverKey.replace(/-/g, "+").replace(/_/g, "/"));
|
|
@@ -474,7 +499,8 @@ export function initNotifications(_ctx) {
|
|
|
474
499
|
// Skip if setup was just completed (setup-done flag)
|
|
475
500
|
var isStandalone = window.matchMedia("(display-mode:standalone)").matches || navigator.standalone;
|
|
476
501
|
if (isStandalone && !localStorage.getItem("setup-done")) {
|
|
477
|
-
|
|
502
|
+
var isTailscale = /^100\./.test(location.hostname);
|
|
503
|
+
location.href = "/setup" + (isTailscale ? "" : "?mode=lan");
|
|
478
504
|
return;
|
|
479
505
|
}
|
|
480
506
|
// Browser: show onboarding banner
|
|
@@ -1,5 +1,14 @@
|
|
|
1
1
|
import { copyToClipboard } from './utils.js';
|
|
2
2
|
|
|
3
|
+
function getShareUrl() {
|
|
4
|
+
var url = window.location.href;
|
|
5
|
+
var h = window.location.hostname;
|
|
6
|
+
if ((h === "localhost" || h === "127.0.0.1") && window.__lanHost) {
|
|
7
|
+
url = url.replace(h + ":" + window.location.port, window.__lanHost);
|
|
8
|
+
}
|
|
9
|
+
return url;
|
|
10
|
+
}
|
|
11
|
+
|
|
3
12
|
export function initQrCode() {
|
|
4
13
|
var $ = function (id) { return document.getElementById(id); };
|
|
5
14
|
var qrBtn = $("qr-btn");
|
|
@@ -9,7 +18,7 @@ export function initQrCode() {
|
|
|
9
18
|
|
|
10
19
|
qrBtn.addEventListener("click", function (e) {
|
|
11
20
|
e.stopPropagation();
|
|
12
|
-
var url =
|
|
21
|
+
var url = getShareUrl();
|
|
13
22
|
|
|
14
23
|
// Use Web Share API if available
|
|
15
24
|
if (navigator.share) {
|
|
@@ -30,7 +39,7 @@ export function initQrCode() {
|
|
|
30
39
|
|
|
31
40
|
// click URL to copy
|
|
32
41
|
qrUrl.addEventListener("click", function () {
|
|
33
|
-
var url =
|
|
42
|
+
var url = getShareUrl();
|
|
34
43
|
copyToClipboard(url).then(function () {
|
|
35
44
|
qrUrl.innerHTML = "Copied!";
|
|
36
45
|
qrUrl.classList.add("copied");
|
|
@@ -6,7 +6,8 @@ var rewindMode = false;
|
|
|
6
6
|
var pendingRewindUuid = null;
|
|
7
7
|
var rewindBannerEl = null;
|
|
8
8
|
var rewindScrollHandler = null;
|
|
9
|
-
var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn;
|
|
9
|
+
var rewindModal, rewindSummary, rewindFilesList, rewindConfirmBtn, rewindCancelBtn, rewindModeOptions;
|
|
10
|
+
var cachedPreview = null;
|
|
10
11
|
|
|
11
12
|
export function setRewindMode(on) {
|
|
12
13
|
rewindMode = on;
|
|
@@ -64,12 +65,53 @@ function initiateRewind(uuid) {
|
|
|
64
65
|
}
|
|
65
66
|
}
|
|
66
67
|
|
|
68
|
+
function getSelectedMode() {
|
|
69
|
+
if (!rewindModeOptions) return "both";
|
|
70
|
+
var checked = rewindModeOptions.querySelector('input[name="rewind-mode"]:checked');
|
|
71
|
+
return checked ? checked.value : "both";
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function updateSummaryForMode() {
|
|
75
|
+
if (!cachedPreview) return;
|
|
76
|
+
var mode = getSelectedMode();
|
|
77
|
+
var fileCount = cachedPreview.fileCount;
|
|
78
|
+
var insertions = cachedPreview.insertions;
|
|
79
|
+
var deletions = cachedPreview.deletions;
|
|
80
|
+
|
|
81
|
+
if (mode === "chat") {
|
|
82
|
+
rewindSummary.textContent = "Conversation will be rewound. Files will not be changed.";
|
|
83
|
+
rewindFilesList.style.display = "none";
|
|
84
|
+
} else if (fileCount > 0) {
|
|
85
|
+
var summary = fileCount + " file" + (fileCount !== 1 ? "s" : "") + " will be restored.";
|
|
86
|
+
if (insertions || deletions) summary += " (+" + insertions + " / -" + deletions + " lines)";
|
|
87
|
+
if (mode === "files") summary += " Conversation will not be changed.";
|
|
88
|
+
rewindSummary.textContent = summary;
|
|
89
|
+
rewindFilesList.style.display = "";
|
|
90
|
+
} else {
|
|
91
|
+
if (mode === "files") {
|
|
92
|
+
rewindSummary.textContent = "No file changes to restore.";
|
|
93
|
+
} else {
|
|
94
|
+
rewindSummary.textContent = "No file changes to restore. Conversation will be rewound.";
|
|
95
|
+
}
|
|
96
|
+
rewindFilesList.style.display = "none";
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
67
100
|
export function showRewindModal(data) {
|
|
68
101
|
var p = data.preview || data;
|
|
69
102
|
var filePaths = p.filesChanged || p.filePaths || p.files || [];
|
|
70
103
|
var fileCount = filePaths.length;
|
|
71
104
|
var insertions = p.insertions || 0;
|
|
72
105
|
var deletions = p.deletions || 0;
|
|
106
|
+
|
|
107
|
+
cachedPreview = { fileCount: fileCount, insertions: insertions, deletions: deletions };
|
|
108
|
+
|
|
109
|
+
// Reset radio to default
|
|
110
|
+
if (rewindModeOptions) {
|
|
111
|
+
var defaultRadio = rewindModeOptions.querySelector('input[value="both"]');
|
|
112
|
+
if (defaultRadio) defaultRadio.checked = true;
|
|
113
|
+
}
|
|
114
|
+
|
|
73
115
|
if (fileCount > 0) {
|
|
74
116
|
var summary = fileCount + " file" + (fileCount !== 1 ? "s" : "") + " will be restored.";
|
|
75
117
|
if (insertions || deletions) summary += " (+" + insertions + " / -" + deletions + " lines)";
|
|
@@ -264,6 +306,12 @@ export function initRewind(_ctx) {
|
|
|
264
306
|
rewindFilesList = ctx.$("rewind-files-list");
|
|
265
307
|
rewindConfirmBtn = ctx.$("rewind-confirm");
|
|
266
308
|
rewindCancelBtn = ctx.$("rewind-cancel");
|
|
309
|
+
rewindModeOptions = ctx.$("rewind-mode-options");
|
|
310
|
+
|
|
311
|
+
// Update summary when rewind mode radio changes
|
|
312
|
+
if (rewindModeOptions) {
|
|
313
|
+
rewindModeOptions.addEventListener("change", updateSummaryForMode);
|
|
314
|
+
}
|
|
267
315
|
|
|
268
316
|
// Click on user message bubble to rewind
|
|
269
317
|
ctx.messagesEl.addEventListener("click", function(e) {
|
|
@@ -275,7 +323,8 @@ export function initRewind(_ctx) {
|
|
|
275
323
|
|
|
276
324
|
rewindConfirmBtn.addEventListener("click", function() {
|
|
277
325
|
if (pendingRewindUuid && ctx.ws && ctx.connected) {
|
|
278
|
-
|
|
326
|
+
var mode = getSelectedMode();
|
|
327
|
+
ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid, mode: mode }));
|
|
279
328
|
}
|
|
280
329
|
hideRewindModal();
|
|
281
330
|
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { iconHtml, refreshIcons } from './icons.js';
|
|
2
2
|
import { closeSidebar } from './sidebar.js';
|
|
3
3
|
import { closeFileViewer } from './filebrowser.js';
|
|
4
|
+
import { copyToClipboard } from './utils.js';
|
|
4
5
|
|
|
5
6
|
var ctx;
|
|
6
7
|
var tabs = new Map(); // termId -> { id, title, exited, xterm, fitAddon, bodyEl }
|
|
@@ -11,6 +12,7 @@ var isTouchDevice = "ontouchstart" in window;
|
|
|
11
12
|
var viewportHandler = null;
|
|
12
13
|
var resizeObserver = null;
|
|
13
14
|
var toolbarBound = false;
|
|
15
|
+
var termCtxMenu = null;
|
|
14
16
|
|
|
15
17
|
// --- Init ---
|
|
16
18
|
export function initTerminal(_ctx) {
|
|
@@ -239,6 +241,11 @@ function createXtermForTab(tab) {
|
|
|
239
241
|
}
|
|
240
242
|
});
|
|
241
243
|
|
|
244
|
+
// Right-click context menu
|
|
245
|
+
bodyEl.addEventListener("contextmenu", function (e) {
|
|
246
|
+
showTermCtxMenu(e, tab);
|
|
247
|
+
});
|
|
248
|
+
|
|
242
249
|
tab.xterm = xterm;
|
|
243
250
|
tab.fitAddon = fitAddon;
|
|
244
251
|
tab.bodyEl = bodyEl;
|
|
@@ -527,6 +534,72 @@ export function resetTerminals() {
|
|
|
527
534
|
renderTabBar();
|
|
528
535
|
}
|
|
529
536
|
|
|
537
|
+
// --- Terminal context menu ---
|
|
538
|
+
function closeTermCtxMenu() {
|
|
539
|
+
if (termCtxMenu) {
|
|
540
|
+
termCtxMenu.remove();
|
|
541
|
+
termCtxMenu = null;
|
|
542
|
+
}
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function showTermCtxMenu(e, tab) {
|
|
546
|
+
e.preventDefault();
|
|
547
|
+
e.stopPropagation();
|
|
548
|
+
closeTermCtxMenu();
|
|
549
|
+
|
|
550
|
+
var menu = document.createElement("div");
|
|
551
|
+
menu.className = "term-ctx-menu";
|
|
552
|
+
|
|
553
|
+
// Copy
|
|
554
|
+
var copyItem = document.createElement("button");
|
|
555
|
+
copyItem.className = "term-ctx-item";
|
|
556
|
+
copyItem.innerHTML = iconHtml("clipboard-copy") + " <span>Copy Terminal</span>";
|
|
557
|
+
copyItem.addEventListener("click", function (ev) {
|
|
558
|
+
ev.stopPropagation();
|
|
559
|
+
closeTermCtxMenu();
|
|
560
|
+
if (!tab.xterm) return;
|
|
561
|
+
tab.xterm.selectAll();
|
|
562
|
+
var text = tab.xterm.getSelection();
|
|
563
|
+
tab.xterm.clearSelection();
|
|
564
|
+
if (text) copyToClipboard(text);
|
|
565
|
+
});
|
|
566
|
+
menu.appendChild(copyItem);
|
|
567
|
+
|
|
568
|
+
// Clear
|
|
569
|
+
var clearItem = document.createElement("button");
|
|
570
|
+
clearItem.className = "term-ctx-item";
|
|
571
|
+
clearItem.innerHTML = iconHtml("trash-2") + " <span>Clear Terminal</span>";
|
|
572
|
+
clearItem.addEventListener("click", function (ev) {
|
|
573
|
+
ev.stopPropagation();
|
|
574
|
+
closeTermCtxMenu();
|
|
575
|
+
if (!tab.xterm) return;
|
|
576
|
+
tab.xterm.clear();
|
|
577
|
+
});
|
|
578
|
+
menu.appendChild(clearItem);
|
|
579
|
+
|
|
580
|
+
// Position at mouse cursor
|
|
581
|
+
menu.style.left = e.clientX + "px";
|
|
582
|
+
menu.style.top = e.clientY + "px";
|
|
583
|
+
document.body.appendChild(menu);
|
|
584
|
+
|
|
585
|
+
// Clamp to viewport
|
|
586
|
+
var rect = menu.getBoundingClientRect();
|
|
587
|
+
if (rect.right > window.innerWidth) {
|
|
588
|
+
menu.style.left = (window.innerWidth - rect.width - 4) + "px";
|
|
589
|
+
}
|
|
590
|
+
if (rect.bottom > window.innerHeight) {
|
|
591
|
+
menu.style.top = (window.innerHeight - rect.height - 4) + "px";
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
termCtxMenu = menu;
|
|
595
|
+
refreshIcons();
|
|
596
|
+
|
|
597
|
+
// Close on outside click (next tick to avoid immediate trigger)
|
|
598
|
+
setTimeout(function () {
|
|
599
|
+
document.addEventListener("click", closeTermCtxMenu, { once: true });
|
|
600
|
+
}, 0);
|
|
601
|
+
}
|
|
602
|
+
|
|
530
603
|
// --- Mobile toolbar ---
|
|
531
604
|
var KEY_MAP = {
|
|
532
605
|
tab: "\t",
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { escapeHtml } from './utils.js';
|
|
2
2
|
import { iconHtml, refreshIcons, randomThinkingVerb } from './icons.js';
|
|
3
3
|
import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks } from './markdown.js';
|
|
4
|
-
import {
|
|
4
|
+
import { renderUnifiedDiff, renderSplitDiff, renderPatchDiff } from './diff.js';
|
|
5
|
+
import { openFile } from './filebrowser.js';
|
|
5
6
|
|
|
6
7
|
var ctx;
|
|
7
8
|
|
|
@@ -257,6 +258,7 @@ function permissionInputSummary(toolName, input) {
|
|
|
257
258
|
}
|
|
258
259
|
|
|
259
260
|
export function renderPermissionRequest(requestId, toolName, toolInput, decisionReason) {
|
|
261
|
+
if (pendingPermissions[requestId]) return;
|
|
260
262
|
ctx.finalizeAssistantBlock();
|
|
261
263
|
stopThinking();
|
|
262
264
|
|
|
@@ -352,6 +354,7 @@ export function renderPermissionRequest(requestId, toolName, toolInput, decision
|
|
|
352
354
|
}
|
|
353
355
|
|
|
354
356
|
function renderPlanPermission(requestId) {
|
|
357
|
+
if (pendingPermissions[requestId]) return;
|
|
355
358
|
var container = document.createElement("div");
|
|
356
359
|
container.className = "permission-container plan-permission";
|
|
357
360
|
container.dataset.requestId = requestId;
|
|
@@ -813,7 +816,28 @@ export function updateToolExecuting(id, name, input) {
|
|
|
813
816
|
if (!tool) return;
|
|
814
817
|
|
|
815
818
|
tool.input = input;
|
|
816
|
-
tool.el.querySelector(".tool-desc")
|
|
819
|
+
var descEl = tool.el.querySelector(".tool-desc");
|
|
820
|
+
descEl.textContent = toolSummary(name, input);
|
|
821
|
+
|
|
822
|
+
// Make file path clickable for Read/Edit/Write tools
|
|
823
|
+
var filePath = input && input.file_path;
|
|
824
|
+
if (filePath && (name === "Read" || name === "Edit" || name === "Write")) {
|
|
825
|
+
descEl.classList.add("tool-desc-link");
|
|
826
|
+
descEl.dataset.filePath = filePath;
|
|
827
|
+
descEl.insertAdjacentHTML("beforeend", '<span class="tool-desc-link-icon">' + iconHtml("external-link") + '</span>');
|
|
828
|
+
refreshIcons();
|
|
829
|
+
(function (toolName, toolInput) {
|
|
830
|
+
descEl.onclick = function (e) {
|
|
831
|
+
e.stopPropagation();
|
|
832
|
+
if (toolName === "Edit" && toolInput && (toolInput.old_string || toolInput.new_string)) {
|
|
833
|
+
openFile(filePath, { diff: { oldStr: toolInput.old_string || "", newStr: toolInput.new_string || "" } });
|
|
834
|
+
} else {
|
|
835
|
+
openFile(filePath);
|
|
836
|
+
}
|
|
837
|
+
};
|
|
838
|
+
})(name, input);
|
|
839
|
+
}
|
|
840
|
+
|
|
817
841
|
ctx.setActivity(toolActivityText(name, input));
|
|
818
842
|
|
|
819
843
|
var subtitleText = tool.el.querySelector(".tool-subtitle-text");
|
|
@@ -822,112 +846,26 @@ export function updateToolExecuting(id, name, input) {
|
|
|
822
846
|
ctx.scrollToBottom();
|
|
823
847
|
}
|
|
824
848
|
|
|
825
|
-
function buildUnifiedDiff(oldLines, newLines) {
|
|
826
|
-
var body = document.createElement("div");
|
|
827
|
-
body.className = "edit-diff-body";
|
|
828
|
-
|
|
829
|
-
var gutter = document.createElement("pre");
|
|
830
|
-
gutter.className = "edit-diff-gutter";
|
|
831
|
-
|
|
832
|
-
var content = document.createElement("pre");
|
|
833
|
-
content.className = "edit-diff-content";
|
|
834
|
-
|
|
835
|
-
var gutterLines = [];
|
|
836
|
-
|
|
837
|
-
for (var i = 0; i < oldLines.length; i++) {
|
|
838
|
-
gutterLines.push(String(i + 1));
|
|
839
|
-
var span = document.createElement("span");
|
|
840
|
-
span.className = "diff-del";
|
|
841
|
-
span.textContent = "- " + oldLines[i];
|
|
842
|
-
content.appendChild(span);
|
|
843
|
-
content.appendChild(document.createTextNode("\n"));
|
|
844
|
-
}
|
|
845
|
-
for (var i = 0; i < newLines.length; i++) {
|
|
846
|
-
gutterLines.push(String(i + 1));
|
|
847
|
-
var span = document.createElement("span");
|
|
848
|
-
span.className = "diff-add";
|
|
849
|
-
span.textContent = "+ " + newLines[i];
|
|
850
|
-
content.appendChild(span);
|
|
851
|
-
if (i < newLines.length - 1) content.appendChild(document.createTextNode("\n"));
|
|
852
|
-
}
|
|
853
|
-
|
|
854
|
-
gutter.textContent = gutterLines.join("\n");
|
|
855
|
-
body.appendChild(gutter);
|
|
856
|
-
body.appendChild(content);
|
|
857
|
-
return body;
|
|
858
|
-
}
|
|
859
|
-
|
|
860
|
-
function buildSplitDiff(oldLines, newLines) {
|
|
861
|
-
var body = document.createElement("div");
|
|
862
|
-
body.className = "edit-diff-body edit-diff-split";
|
|
863
|
-
|
|
864
|
-
var leftGutter = document.createElement("pre");
|
|
865
|
-
leftGutter.className = "edit-diff-gutter";
|
|
866
|
-
var leftContent = document.createElement("pre");
|
|
867
|
-
leftContent.className = "edit-diff-content edit-diff-side-old";
|
|
868
|
-
var rightGutter = document.createElement("pre");
|
|
869
|
-
rightGutter.className = "edit-diff-gutter";
|
|
870
|
-
var rightContent = document.createElement("pre");
|
|
871
|
-
rightContent.className = "edit-diff-content edit-diff-side-new";
|
|
872
|
-
|
|
873
|
-
var maxLen = Math.max(oldLines.length, newLines.length);
|
|
874
|
-
var leftNums = [];
|
|
875
|
-
var rightNums = [];
|
|
876
|
-
|
|
877
|
-
for (var i = 0; i < maxLen; i++) {
|
|
878
|
-
if (i < oldLines.length) {
|
|
879
|
-
leftNums.push(String(i + 1));
|
|
880
|
-
var span = document.createElement("span");
|
|
881
|
-
span.className = "diff-del";
|
|
882
|
-
span.textContent = oldLines[i];
|
|
883
|
-
leftContent.appendChild(span);
|
|
884
|
-
} else {
|
|
885
|
-
leftNums.push("");
|
|
886
|
-
leftContent.appendChild(document.createTextNode(""));
|
|
887
|
-
}
|
|
888
|
-
if (i < oldLines.length - 1 || (i >= oldLines.length && i < maxLen - 1)) {
|
|
889
|
-
leftContent.appendChild(document.createTextNode("\n"));
|
|
890
|
-
}
|
|
891
|
-
|
|
892
|
-
if (i < newLines.length) {
|
|
893
|
-
rightNums.push(String(i + 1));
|
|
894
|
-
var span = document.createElement("span");
|
|
895
|
-
span.className = "diff-add";
|
|
896
|
-
span.textContent = newLines[i];
|
|
897
|
-
rightContent.appendChild(span);
|
|
898
|
-
} else {
|
|
899
|
-
rightNums.push("");
|
|
900
|
-
rightContent.appendChild(document.createTextNode(""));
|
|
901
|
-
}
|
|
902
|
-
if (i < newLines.length - 1 || (i >= newLines.length && i < maxLen - 1)) {
|
|
903
|
-
rightContent.appendChild(document.createTextNode("\n"));
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
leftGutter.textContent = leftNums.join("\n");
|
|
908
|
-
rightGutter.textContent = rightNums.join("\n");
|
|
909
|
-
|
|
910
|
-
body.appendChild(leftGutter);
|
|
911
|
-
body.appendChild(leftContent);
|
|
912
|
-
body.appendChild(rightGutter);
|
|
913
|
-
body.appendChild(rightContent);
|
|
914
|
-
return body;
|
|
915
|
-
}
|
|
916
|
-
|
|
917
849
|
function renderEditDiff(oldStr, newStr, filePath) {
|
|
918
850
|
var wrapper = document.createElement("div");
|
|
919
851
|
wrapper.className = "edit-diff";
|
|
920
|
-
|
|
921
|
-
var oldLines = oldStr.split("\n");
|
|
922
|
-
var newLines = newStr.split("\n");
|
|
852
|
+
var lang = getLanguageFromPath(filePath);
|
|
923
853
|
|
|
924
854
|
// Header with file path and split toggle (desktop only)
|
|
925
855
|
var header = document.createElement("div");
|
|
926
856
|
header.className = "edit-diff-header";
|
|
927
857
|
|
|
928
858
|
var pathSpan = document.createElement("span");
|
|
929
|
-
pathSpan.className = "edit-diff-path";
|
|
859
|
+
pathSpan.className = "edit-diff-path edit-diff-path-link";
|
|
930
860
|
pathSpan.textContent = filePath || "";
|
|
861
|
+
if (filePath) {
|
|
862
|
+
(function (fp, os, ns) {
|
|
863
|
+
pathSpan.addEventListener("click", function (e) {
|
|
864
|
+
e.stopPropagation();
|
|
865
|
+
openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
|
|
866
|
+
});
|
|
867
|
+
})(filePath, oldStr, newStr);
|
|
868
|
+
}
|
|
931
869
|
header.appendChild(pathSpan);
|
|
932
870
|
|
|
933
871
|
var isMobile = "ontouchstart" in window;
|
|
@@ -952,27 +890,29 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
952
890
|
|
|
953
891
|
wrapper.appendChild(header);
|
|
954
892
|
|
|
955
|
-
var currentBody =
|
|
893
|
+
var currentBody = renderUnifiedDiff(oldStr, newStr, lang);
|
|
956
894
|
wrapper.appendChild(currentBody);
|
|
957
895
|
|
|
958
|
-
unifiedBtn.addEventListener("click", function () {
|
|
896
|
+
unifiedBtn.addEventListener("click", function (e) {
|
|
897
|
+
e.stopPropagation();
|
|
959
898
|
if (!isSplit) return;
|
|
960
899
|
isSplit = false;
|
|
961
900
|
unifiedBtn.classList.add("active");
|
|
962
901
|
splitBtn.classList.remove("active");
|
|
963
902
|
wrapper.removeChild(currentBody);
|
|
964
|
-
currentBody =
|
|
903
|
+
currentBody = renderUnifiedDiff(oldStr, newStr, lang);
|
|
965
904
|
wrapper.appendChild(currentBody);
|
|
966
905
|
refreshIcons();
|
|
967
906
|
});
|
|
968
907
|
|
|
969
|
-
splitBtn.addEventListener("click", function () {
|
|
908
|
+
splitBtn.addEventListener("click", function (e) {
|
|
909
|
+
e.stopPropagation();
|
|
970
910
|
if (isSplit) return;
|
|
971
911
|
isSplit = true;
|
|
972
912
|
splitBtn.classList.add("active");
|
|
973
913
|
unifiedBtn.classList.remove("active");
|
|
974
914
|
wrapper.removeChild(currentBody);
|
|
975
|
-
currentBody =
|
|
915
|
+
currentBody = renderSplitDiff(oldStr, newStr, lang);
|
|
976
916
|
wrapper.appendChild(currentBody);
|
|
977
917
|
refreshIcons();
|
|
978
918
|
});
|
|
@@ -1075,7 +1015,8 @@ export function updateToolResult(id, content, isError) {
|
|
|
1075
1015
|
if (hasEditDiff) {
|
|
1076
1016
|
resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
|
|
1077
1017
|
} else if (!isError && isDiffContent(displayContent)) {
|
|
1078
|
-
|
|
1018
|
+
var patchLang = tool.input && tool.input.file_path ? getLanguageFromPath(tool.input.file_path) : null;
|
|
1019
|
+
resultBlock.appendChild(renderPatchDiff(displayContent, patchLang));
|
|
1079
1020
|
} else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path) {
|
|
1080
1021
|
var parsed = parseLineNumberedContent(displayContent);
|
|
1081
1022
|
if (parsed) {
|