claude-relay 2.2.3 → 2.3.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 +12 -0
- package/bin/cli.js +271 -22
- package/lib/config.js +39 -2
- package/lib/daemon.js +53 -1
- package/lib/ipc.js +7 -3
- package/lib/pages.js +15 -1
- package/lib/project.js +324 -27
- package/lib/public/app.js +313 -7
- package/lib/public/css/base.css +5 -0
- package/lib/public/css/diff.css +128 -0
- package/lib/public/css/filebrowser.css +541 -0
- package/lib/public/css/input.css +1 -0
- package/lib/public/css/menus.css +89 -5
- package/lib/public/css/messages.css +84 -49
- package/lib/public/css/overlays.css +40 -0
- package/lib/public/index.html +100 -17
- 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/tools.js +43 -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 +40 -7
- package/lib/server.js +37 -4
- package/lib/sessions.js +14 -5
- package/lib/terminal.js +2 -1
- 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,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
|
|
|
@@ -813,7 +814,28 @@ export function updateToolExecuting(id, name, input) {
|
|
|
813
814
|
if (!tool) return;
|
|
814
815
|
|
|
815
816
|
tool.input = input;
|
|
816
|
-
tool.el.querySelector(".tool-desc")
|
|
817
|
+
var descEl = tool.el.querySelector(".tool-desc");
|
|
818
|
+
descEl.textContent = toolSummary(name, input);
|
|
819
|
+
|
|
820
|
+
// Make file path clickable for Read/Edit/Write tools
|
|
821
|
+
var filePath = input && input.file_path;
|
|
822
|
+
if (filePath && (name === "Read" || name === "Edit" || name === "Write")) {
|
|
823
|
+
descEl.classList.add("tool-desc-link");
|
|
824
|
+
descEl.dataset.filePath = filePath;
|
|
825
|
+
descEl.insertAdjacentHTML("beforeend", '<span class="tool-desc-link-icon">' + iconHtml("external-link") + '</span>');
|
|
826
|
+
refreshIcons();
|
|
827
|
+
(function (toolName, toolInput) {
|
|
828
|
+
descEl.onclick = function (e) {
|
|
829
|
+
e.stopPropagation();
|
|
830
|
+
if (toolName === "Edit" && toolInput && (toolInput.old_string || toolInput.new_string)) {
|
|
831
|
+
openFile(filePath, { diff: { oldStr: toolInput.old_string || "", newStr: toolInput.new_string || "" } });
|
|
832
|
+
} else {
|
|
833
|
+
openFile(filePath);
|
|
834
|
+
}
|
|
835
|
+
};
|
|
836
|
+
})(name, input);
|
|
837
|
+
}
|
|
838
|
+
|
|
817
839
|
ctx.setActivity(toolActivityText(name, input));
|
|
818
840
|
|
|
819
841
|
var subtitleText = tool.el.querySelector(".tool-subtitle-text");
|
|
@@ -822,112 +844,26 @@ export function updateToolExecuting(id, name, input) {
|
|
|
822
844
|
ctx.scrollToBottom();
|
|
823
845
|
}
|
|
824
846
|
|
|
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
847
|
function renderEditDiff(oldStr, newStr, filePath) {
|
|
918
848
|
var wrapper = document.createElement("div");
|
|
919
849
|
wrapper.className = "edit-diff";
|
|
920
|
-
|
|
921
|
-
var oldLines = oldStr.split("\n");
|
|
922
|
-
var newLines = newStr.split("\n");
|
|
850
|
+
var lang = getLanguageFromPath(filePath);
|
|
923
851
|
|
|
924
852
|
// Header with file path and split toggle (desktop only)
|
|
925
853
|
var header = document.createElement("div");
|
|
926
854
|
header.className = "edit-diff-header";
|
|
927
855
|
|
|
928
856
|
var pathSpan = document.createElement("span");
|
|
929
|
-
pathSpan.className = "edit-diff-path";
|
|
857
|
+
pathSpan.className = "edit-diff-path edit-diff-path-link";
|
|
930
858
|
pathSpan.textContent = filePath || "";
|
|
859
|
+
if (filePath) {
|
|
860
|
+
(function (fp, os, ns) {
|
|
861
|
+
pathSpan.addEventListener("click", function (e) {
|
|
862
|
+
e.stopPropagation();
|
|
863
|
+
openFile(fp, { diff: { oldStr: os || "", newStr: ns || "" } });
|
|
864
|
+
});
|
|
865
|
+
})(filePath, oldStr, newStr);
|
|
866
|
+
}
|
|
931
867
|
header.appendChild(pathSpan);
|
|
932
868
|
|
|
933
869
|
var isMobile = "ontouchstart" in window;
|
|
@@ -952,27 +888,29 @@ function renderEditDiff(oldStr, newStr, filePath) {
|
|
|
952
888
|
|
|
953
889
|
wrapper.appendChild(header);
|
|
954
890
|
|
|
955
|
-
var currentBody =
|
|
891
|
+
var currentBody = renderUnifiedDiff(oldStr, newStr, lang);
|
|
956
892
|
wrapper.appendChild(currentBody);
|
|
957
893
|
|
|
958
|
-
unifiedBtn.addEventListener("click", function () {
|
|
894
|
+
unifiedBtn.addEventListener("click", function (e) {
|
|
895
|
+
e.stopPropagation();
|
|
959
896
|
if (!isSplit) return;
|
|
960
897
|
isSplit = false;
|
|
961
898
|
unifiedBtn.classList.add("active");
|
|
962
899
|
splitBtn.classList.remove("active");
|
|
963
900
|
wrapper.removeChild(currentBody);
|
|
964
|
-
currentBody =
|
|
901
|
+
currentBody = renderUnifiedDiff(oldStr, newStr, lang);
|
|
965
902
|
wrapper.appendChild(currentBody);
|
|
966
903
|
refreshIcons();
|
|
967
904
|
});
|
|
968
905
|
|
|
969
|
-
splitBtn.addEventListener("click", function () {
|
|
906
|
+
splitBtn.addEventListener("click", function (e) {
|
|
907
|
+
e.stopPropagation();
|
|
970
908
|
if (isSplit) return;
|
|
971
909
|
isSplit = true;
|
|
972
910
|
splitBtn.classList.add("active");
|
|
973
911
|
unifiedBtn.classList.remove("active");
|
|
974
912
|
wrapper.removeChild(currentBody);
|
|
975
|
-
currentBody =
|
|
913
|
+
currentBody = renderSplitDiff(oldStr, newStr, lang);
|
|
976
914
|
wrapper.appendChild(currentBody);
|
|
977
915
|
refreshIcons();
|
|
978
916
|
});
|
|
@@ -1075,7 +1013,8 @@ export function updateToolResult(id, content, isError) {
|
|
|
1075
1013
|
if (hasEditDiff) {
|
|
1076
1014
|
resultBlock.appendChild(renderEditDiff(tool.input.old_string, tool.input.new_string, tool.input.file_path));
|
|
1077
1015
|
} else if (!isError && isDiffContent(displayContent)) {
|
|
1078
|
-
|
|
1016
|
+
var patchLang = tool.input && tool.input.file_path ? getLanguageFromPath(tool.input.file_path) : null;
|
|
1017
|
+
resultBlock.appendChild(renderPatchDiff(displayContent, patchLang));
|
|
1079
1018
|
} else if (!isError && tool.name === "Read" && tool.input && tool.input.file_path) {
|
|
1080
1019
|
var parsed = parseLineNumberedContent(displayContent);
|
|
1081
1020
|
if (parsed) {
|
|
@@ -1,13 +1,21 @@
|
|
|
1
|
-
export function showToast(message) {
|
|
1
|
+
export function showToast(message, level, detail) {
|
|
2
2
|
var el = document.createElement("div");
|
|
3
3
|
el.className = "toast";
|
|
4
|
+
if (level) el.classList.add("toast-" + level);
|
|
4
5
|
el.textContent = message;
|
|
6
|
+
if (detail) {
|
|
7
|
+
var detailEl = document.createElement("div");
|
|
8
|
+
detailEl.style.cssText = "font-size:11px;opacity:0.7;margin-top:4px;max-width:300px;overflow:hidden;text-overflow:ellipsis;white-space:nowrap";
|
|
9
|
+
detailEl.textContent = detail.split("\n")[0];
|
|
10
|
+
el.appendChild(detailEl);
|
|
11
|
+
}
|
|
5
12
|
document.body.appendChild(el);
|
|
6
13
|
requestAnimationFrame(function () { el.classList.add("visible"); });
|
|
14
|
+
var duration = level === "warn" ? 5000 : 1500;
|
|
7
15
|
setTimeout(function () {
|
|
8
16
|
el.classList.remove("visible");
|
|
9
17
|
setTimeout(function () { el.remove(); }, 300);
|
|
10
|
-
},
|
|
18
|
+
}, duration);
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
export function copyToClipboard(text) {
|
package/lib/public/style.css
CHANGED
package/lib/public/sw.js
CHANGED
|
@@ -10,6 +10,9 @@ self.addEventListener("push", function (event) {
|
|
|
10
10
|
var data = {};
|
|
11
11
|
try { data = event.data.json(); } catch (e) { return; }
|
|
12
12
|
|
|
13
|
+
// Silent validation push, do not show notification
|
|
14
|
+
if (data.type === "test") return;
|
|
15
|
+
|
|
13
16
|
var options = {
|
|
14
17
|
body: data.body || "",
|
|
15
18
|
tag: data.tag || "claude-relay",
|
|
@@ -31,9 +34,12 @@ self.addEventListener("push", function (event) {
|
|
|
31
34
|
|
|
32
35
|
event.waitUntil(
|
|
33
36
|
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
|
|
34
|
-
//
|
|
35
|
-
|
|
36
|
-
|
|
37
|
+
// Always show permission requests, questions, and errors
|
|
38
|
+
// Only suppress "done" notifications when app is in foreground
|
|
39
|
+
if (data.type !== "permission_request" && data.type !== "ask_user" && data.type !== "error") {
|
|
40
|
+
for (var i = 0; i < clientList.length; i++) {
|
|
41
|
+
if (clientList[i].focused || clientList[i].visibilityState === "visible") return;
|
|
42
|
+
}
|
|
37
43
|
}
|
|
38
44
|
return self.registration.showNotification(data.title || "Claude Relay", options);
|
|
39
45
|
}).catch(function () {})
|
|
@@ -44,18 +50,26 @@ self.addEventListener("notificationclick", function (event) {
|
|
|
44
50
|
var data = event.notification.data || {};
|
|
45
51
|
event.notification.close();
|
|
46
52
|
|
|
47
|
-
//
|
|
48
|
-
|
|
49
|
-
var
|
|
53
|
+
// Build target URL from slug so we open the correct project
|
|
54
|
+
var baseUrl = self.registration.scope || "/";
|
|
55
|
+
var targetUrl = data.slug ? baseUrl + "p/" + data.slug + "/" : baseUrl;
|
|
56
|
+
|
|
50
57
|
event.waitUntil(
|
|
51
58
|
self.clients.matchAll({ type: "window", includeUncontrolled: true }).then(function (clientList) {
|
|
59
|
+
// Prefer a client already on the correct project
|
|
60
|
+
for (var i = 0; i < clientList.length; i++) {
|
|
61
|
+
if (clientList[i].url.indexOf(targetUrl) !== -1) {
|
|
62
|
+
return clientList[i].focus();
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
// Fall back to any visible client
|
|
52
66
|
for (var i = 0; i < clientList.length; i++) {
|
|
53
67
|
if (clientList[i].visibilityState !== "hidden") {
|
|
54
68
|
return clientList[i].focus();
|
|
55
69
|
}
|
|
56
70
|
}
|
|
57
71
|
if (clientList.length > 0) return clientList[0].focus();
|
|
58
|
-
return self.clients.openWindow(
|
|
72
|
+
return self.clients.openWindow(targetUrl);
|
|
59
73
|
})
|
|
60
74
|
);
|
|
61
75
|
});
|
package/lib/push.js
CHANGED
|
@@ -75,8 +75,12 @@ function initPush() {
|
|
|
75
75
|
})(startupEndpoints[si]);
|
|
76
76
|
}
|
|
77
77
|
|
|
78
|
-
function addSubscription(sub) {
|
|
78
|
+
function addSubscription(sub, replaceEndpoint) {
|
|
79
79
|
if (!sub || !sub.endpoint) return;
|
|
80
|
+
// Remove previous subscription from the same client if endpoint changed
|
|
81
|
+
if (replaceEndpoint && replaceEndpoint !== sub.endpoint) {
|
|
82
|
+
subscriptions.delete(replaceEndpoint);
|
|
83
|
+
}
|
|
80
84
|
// Store immediately, then validate async. Invalid subs get cleaned on first sendPush.
|
|
81
85
|
subscriptions.set(sub.endpoint, sub);
|
|
82
86
|
save();
|