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.
@@ -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(json),
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
- location.href = "/setup";
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 = window.location.href;
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 = window.location.href;
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
- ctx.ws.send(JSON.stringify({ type: "rewind_execute", uuid: pendingRewindUuid }));
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 { renderDiffPre } from './rewind.js';
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").textContent = toolSummary(name, input);
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 = buildUnifiedDiff(oldLines, newLines);
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 = buildUnifiedDiff(oldLines, newLines);
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 = buildSplitDiff(oldLines, newLines);
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
- resultBlock.appendChild(renderDiffPre(displayContent));
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) {