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/lib/public/app.js CHANGED
@@ -1,12 +1,12 @@
1
- import { copyToClipboard, escapeHtml } from './modules/utils.js';
1
+ import { showToast, copyToClipboard, escapeHtml } from './modules/utils.js';
2
2
  import { refreshIcons, iconHtml, randomThinkingVerb } from './modules/icons.js';
3
3
  import { renderMarkdown, highlightCodeBlocks, renderMermaidBlocks, closeMermaidModal } from './modules/markdown.js';
4
4
  import { initSidebar, renderSessionList, handleSearchResults, updatePageTitle, getActiveSearchQuery, buildSearchTimeline, removeSearchTimeline } from './modules/sidebar.js';
5
5
  import { initRewind, setRewindMode, showRewindModal, clearPendingRewindUuid } from './modules/rewind.js';
6
6
  import { initNotifications, showDoneNotification, playDoneSound, isNotifAlertEnabled, isNotifSoundEnabled } from './modules/notifications.js';
7
- import { initInput, clearPendingImages, handleInputSync, autoResize } from './modules/input.js';
7
+ import { initInput, clearPendingImages, handleInputSync, autoResize, builtinCommands } from './modules/input.js';
8
8
  import { initQrCode } from './modules/qrcode.js';
9
- import { initFileBrowser, loadRootDirectory, handleFsList, handleFsRead, refreshIfOpen, handleFileChanged, closeFileViewer } from './modules/filebrowser.js';
9
+ import { initFileBrowser, loadRootDirectory, refreshTree, handleFsList, handleFsRead, handleDirChanged, refreshIfOpen, handleFileChanged, handleFileHistory, handleGitDiff, handleFileAt, getPendingNavigate, closeFileViewer } from './modules/filebrowser.js';
10
10
  import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
11
11
  import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, markAskUserAnswered, renderPermissionRequest, markPermissionResolved, markPermissionCancelled, renderPlanBanner, renderPlanCard, handleTodoWrite, handleTaskCreate, handleTaskUpdate, startThinking, appendThinking, stopThinking, createToolItem, updateToolExecuting, updateToolResult, markAllToolsDone, addTurnMeta, enableMainInput, getTools, getPlanContent, setPlanContent, isPlanFilePath, getTodoTools } from './modules/tools.js';
12
12
 
@@ -137,7 +137,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
137
137
  });
138
138
 
139
139
  document.addEventListener("keydown", function (e) {
140
- if (e.key === "Escape") closeProjectDropdown();
140
+ if (e.key === "Escape") {
141
+ closeProjectDropdown();
142
+ closeImageModal();
143
+ }
141
144
  });
142
145
 
143
146
  if (projectHintDismiss) {
@@ -156,6 +159,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
156
159
  });
157
160
  $("mermaid-modal").querySelector(".confirm-backdrop").addEventListener("click", closeMermaidModal);
158
161
  $("mermaid-modal").querySelector(".mermaid-modal-btn[title='Close']").addEventListener("click", closeMermaidModal);
162
+ $("image-modal").querySelector(".confirm-backdrop").addEventListener("click", closeImageModal);
163
+ $("image-modal").querySelector(".image-modal-close").addEventListener("click", closeImageModal);
164
+
165
+ function showImageModal(src) {
166
+ var modal = $("image-modal");
167
+ var img = $("image-modal-img");
168
+ if (!modal || !img) return;
169
+ img.src = src;
170
+ modal.classList.remove("hidden");
171
+ refreshIcons(modal);
172
+ }
173
+
174
+ function closeImageModal() {
175
+ var modal = $("image-modal");
176
+ if (modal) modal.classList.add("hidden");
177
+ }
159
178
 
160
179
  // --- State ---
161
180
  var ws = null;
@@ -657,6 +676,229 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
657
676
  });
658
677
  }
659
678
 
679
+ // --- Status panel ---
680
+ var statusPanel = $("status-panel");
681
+ var statusPanelClose = $("status-panel-close");
682
+ var statusPidEl = $("status-pid");
683
+ var statusUptimeEl = $("status-uptime");
684
+ var statusRssEl = $("status-rss");
685
+ var statusHeapUsedEl = $("status-heap-used");
686
+ var statusHeapTotalEl = $("status-heap-total");
687
+ var statusExternalEl = $("status-external");
688
+ var statusSessionsEl = $("status-sessions");
689
+ var statusProcessingEl = $("status-processing");
690
+ var statusClientsEl = $("status-clients");
691
+ var statusTerminalsEl = $("status-terminals");
692
+ var statusRefreshTimer = null;
693
+
694
+ function formatBytes(n) {
695
+ if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
696
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
697
+ if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
698
+ return n + " B";
699
+ }
700
+
701
+ function formatUptime(seconds) {
702
+ var d = Math.floor(seconds / 86400);
703
+ var h = Math.floor((seconds % 86400) / 3600);
704
+ var m = Math.floor((seconds % 3600) / 60);
705
+ var s = Math.floor(seconds % 60);
706
+ if (d > 0) return d + "d " + h + "h " + m + "m";
707
+ if (h > 0) return h + "h " + m + "m " + s + "s";
708
+ return m + "m " + s + "s";
709
+ }
710
+
711
+ function updateStatusPanel(data) {
712
+ if (!statusPidEl) return;
713
+ statusPidEl.textContent = String(data.pid);
714
+ statusUptimeEl.textContent = formatUptime(data.uptime);
715
+ statusRssEl.textContent = formatBytes(data.memory.rss);
716
+ statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
717
+ statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
718
+ statusExternalEl.textContent = formatBytes(data.memory.external);
719
+ statusSessionsEl.textContent = String(data.sessions);
720
+ statusProcessingEl.textContent = String(data.processing);
721
+ statusClientsEl.textContent = String(data.clients);
722
+ statusTerminalsEl.textContent = String(data.terminals);
723
+ }
724
+
725
+ function requestProcessStats() {
726
+ if (ws && ws.readyState === 1) {
727
+ ws.send(JSON.stringify({ type: "process_stats" }));
728
+ }
729
+ }
730
+
731
+ function toggleStatusPanel() {
732
+ if (!statusPanel) return;
733
+ var opening = statusPanel.classList.contains("hidden");
734
+ statusPanel.classList.toggle("hidden");
735
+ if (opening) {
736
+ requestProcessStats();
737
+ statusRefreshTimer = setInterval(requestProcessStats, 5000);
738
+ } else {
739
+ if (statusRefreshTimer) {
740
+ clearInterval(statusRefreshTimer);
741
+ statusRefreshTimer = null;
742
+ }
743
+ }
744
+ refreshIcons();
745
+ }
746
+
747
+ if (statusPanelClose) {
748
+ statusPanelClose.addEventListener("click", function () {
749
+ statusPanel.classList.add("hidden");
750
+ if (statusRefreshTimer) {
751
+ clearInterval(statusRefreshTimer);
752
+ statusRefreshTimer = null;
753
+ }
754
+ });
755
+ }
756
+
757
+ // --- Context panel ---
758
+ var contextPanel = $("context-panel");
759
+ var contextPanelClose = $("context-panel-close");
760
+ var contextPanelMinimize = $("context-panel-minimize");
761
+ var contextBarFill = $("context-bar-fill");
762
+ var contextBarPct = $("context-bar-pct");
763
+ var contextUsedEl = $("context-used");
764
+ var contextWindowEl = $("context-window");
765
+ var contextMaxOutputEl = $("context-max-output");
766
+ var contextInputEl = $("context-input");
767
+ var contextOutputEl = $("context-output");
768
+ var contextCacheReadEl = $("context-cache-read");
769
+ var contextCacheWriteEl = $("context-cache-write");
770
+ var contextModelEl = $("context-model");
771
+ var contextCostEl = $("context-cost");
772
+ var contextTurnsEl = $("context-turns");
773
+ var contextMini = $("context-mini");
774
+ var contextMiniFill = $("context-mini-fill");
775
+ var contextMiniLabel = $("context-mini-label");
776
+ var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
777
+
778
+ function contextPctClass(pct) {
779
+ return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
780
+ }
781
+
782
+ function updateContextPanel() {
783
+ if (!contextUsedEl) return;
784
+ // Context window usage = input tokens (includes cache read/write) + output tokens
785
+ var used = contextData.input + contextData.output;
786
+ var win = contextData.contextWindow;
787
+ var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
788
+ var cls = contextPctClass(pct);
789
+ // Panel bar
790
+ contextBarFill.style.width = pct.toFixed(1) + "%";
791
+ contextBarFill.className = "context-bar-fill" + cls;
792
+ contextBarPct.textContent = pct.toFixed(0) + "%";
793
+ // Mini bar
794
+ if (contextMiniFill) {
795
+ contextMiniFill.style.width = pct.toFixed(1) + "%";
796
+ contextMiniFill.className = "context-mini-fill" + cls;
797
+ }
798
+ if (contextMiniLabel) {
799
+ contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
800
+ }
801
+ contextUsedEl.textContent = formatTokens(used);
802
+ contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
803
+ contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
804
+ contextInputEl.textContent = formatTokens(contextData.input);
805
+ contextOutputEl.textContent = formatTokens(contextData.output);
806
+ contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
807
+ contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
808
+ contextModelEl.textContent = contextData.model;
809
+ contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
810
+ contextTurnsEl.textContent = String(contextData.turns);
811
+ }
812
+
813
+ function accumulateContext(cost, usage, modelUsage) {
814
+ if (cost != null) contextData.cost += cost;
815
+ // Use latest turn values (not cumulative) since each turn's input_tokens
816
+ // already includes the full conversation context up to that point
817
+ if (usage) {
818
+ contextData.input = usage.input_tokens || usage.inputTokens || 0;
819
+ contextData.output = usage.output_tokens || usage.outputTokens || 0;
820
+ contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
821
+ contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
822
+ }
823
+ contextData.turns++;
824
+ if (modelUsage) {
825
+ var models = Object.keys(modelUsage);
826
+ if (models.length > 0) {
827
+ var m = models[0];
828
+ var mu = modelUsage[m];
829
+ contextData.model = m;
830
+ if (mu.contextWindow) contextData.contextWindow = mu.contextWindow;
831
+ if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
832
+ }
833
+ }
834
+ updateContextPanel();
835
+ }
836
+
837
+ // contextView: "off" | "mini" | "panel"
838
+ function getContextView() {
839
+ try { return localStorage.getItem("claude-relay-context-view") || "off"; } catch (e) { return "off"; }
840
+ }
841
+ function setContextView(v) {
842
+ try { localStorage.setItem("claude-relay-context-view", v); } catch (e) {}
843
+ }
844
+
845
+ function applyContextView(view) {
846
+ if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
847
+ if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
848
+ if (view === "panel") refreshIcons();
849
+ }
850
+
851
+ function resetContextData() {
852
+ contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
853
+ updateContextPanel();
854
+ }
855
+
856
+ function resetContext() {
857
+ resetContextData();
858
+ // Keep view state, just reset data
859
+ applyContextView(getContextView());
860
+ }
861
+
862
+ function minimizeContext() {
863
+ setContextView("mini");
864
+ applyContextView("mini");
865
+ }
866
+
867
+ function expandContext() {
868
+ setContextView("panel");
869
+ applyContextView("panel");
870
+ }
871
+
872
+ function toggleContextPanel() {
873
+ if (!contextPanel) return;
874
+ var view = getContextView();
875
+ if (view === "panel") {
876
+ setContextView("mini");
877
+ applyContextView("mini");
878
+ } else {
879
+ setContextView("panel");
880
+ applyContextView("panel");
881
+ }
882
+ }
883
+
884
+ if (contextPanelClose) {
885
+ contextPanelClose.addEventListener("click", function () {
886
+ setContextView("off");
887
+ applyContextView("off");
888
+ });
889
+ }
890
+
891
+ if (contextPanelMinimize) {
892
+ contextPanelMinimize.addEventListener("click", minimizeContext);
893
+ }
894
+
895
+ // Restore context view on load
896
+ applyContextView(getContextView());
897
+
898
+ if (contextMini) {
899
+ contextMini.addEventListener("click", expandContext);
900
+ }
901
+
660
902
  function addToMessages(el) {
661
903
  if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
662
904
  else messagesEl.appendChild(el);
@@ -740,6 +982,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
740
982
  var img = document.createElement("img");
741
983
  img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
742
984
  img.className = "bubble-img";
985
+ img.addEventListener("click", function () { showImageModal(this.src); });
743
986
  imgRow.appendChild(img);
744
987
  }
745
988
  bubble.appendChild(imgRow);
@@ -894,6 +1137,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
894
1137
  setStatus("connected");
895
1138
  enableMainInput();
896
1139
  resetUsage();
1140
+ resetContext();
897
1141
  }
898
1142
 
899
1143
  // --- WebSocket ---
@@ -1002,6 +1246,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1002
1246
  if (pendingQuery) {
1003
1247
  requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
1004
1248
  }
1249
+ // Scroll to tool element if navigating from file edit history
1250
+ var nav = getPendingNavigate();
1251
+ if (nav && (nav.toolId || nav.assistantUuid)) {
1252
+ requestAnimationFrame(function() {
1253
+ // Prefer scrolling to the exact tool element
1254
+ var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
1255
+ if (!target && nav.assistantUuid) {
1256
+ target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
1257
+ }
1258
+ if (target) {
1259
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1260
+ target.classList.add("message-blink");
1261
+ setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
1262
+ }
1263
+ });
1264
+ }
1005
1265
  break;
1006
1266
 
1007
1267
  case "info":
@@ -1017,6 +1277,11 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1017
1277
  var debugWrap = $("debug-menu-wrap");
1018
1278
  if (debugWrap) debugWrap.classList.remove("hidden");
1019
1279
  }
1280
+ if (msg.lanHost) window.__lanHost = msg.lanHost;
1281
+ if (msg.dangerouslySkipPermissions) {
1282
+ var spBanner = $("skip-perms-banner");
1283
+ if (spBanner) spBanner.classList.remove("hidden");
1284
+ }
1020
1285
  updateProjectSwitcher(msg);
1021
1286
  break;
1022
1287
 
@@ -1045,7 +1310,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1045
1310
  break;
1046
1311
 
1047
1312
  case "slash_commands":
1048
- slashCommands = (msg.commands || []).map(function (name) {
1313
+ var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
1314
+ slashCommands = (msg.commands || []).filter(function (name) {
1315
+ return !reserved.has(name);
1316
+ }).map(function (name) {
1049
1317
  return { name: name, desc: "Skill" };
1050
1318
  });
1051
1319
  break;
@@ -1067,6 +1335,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1067
1335
  }
1068
1336
  break;
1069
1337
 
1338
+ case "toast":
1339
+ showToast(msg.message, msg.level, msg.detail);
1340
+ break;
1341
+
1070
1342
  case "input_sync":
1071
1343
  handleInputSync(msg.text);
1072
1344
  break;
@@ -1093,6 +1365,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1093
1365
  var draft = sessionDrafts[activeSessionId] || "";
1094
1366
  inputEl.value = draft;
1095
1367
  autoResize();
1368
+ if (!("ontouchstart" in window)) {
1369
+ inputEl.focus();
1370
+ }
1096
1371
  break;
1097
1372
 
1098
1373
  case "session_id":
@@ -1260,6 +1535,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1260
1535
  finalizeAssistantBlock();
1261
1536
  addTurnMeta(msg.cost, msg.duration);
1262
1537
  accumulateUsage(msg.cost, msg.usage);
1538
+ accumulateContext(msg.cost, msg.usage, msg.modelUsage);
1263
1539
  break;
1264
1540
 
1265
1541
  case "done":
@@ -1272,7 +1548,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1272
1548
  enableMainInput();
1273
1549
  resetToolState();
1274
1550
  if (document.hidden) {
1275
- if (isNotifAlertEnabled()) showDoneNotification();
1551
+ if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
1276
1552
  if (isNotifSoundEnabled()) playDoneSound();
1277
1553
  }
1278
1554
  break;
@@ -1297,7 +1573,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1297
1573
 
1298
1574
  case "rewind_complete":
1299
1575
  setRewindMode(false);
1300
- addSystemMessage("Rewound to earlier point. Files have been restored.", false);
1576
+ var rewindText = "Rewound to earlier point. Files have been restored.";
1577
+ if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
1578
+ else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
1579
+ addSystemMessage(rewindText, false);
1301
1580
  break;
1302
1581
 
1303
1582
  case "rewind_error":
@@ -1317,6 +1596,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1317
1596
  handleFileChanged(msg);
1318
1597
  break;
1319
1598
 
1599
+ case "fs_dir_changed":
1600
+ handleDirChanged(msg);
1601
+ break;
1602
+
1603
+ case "fs_file_history_result":
1604
+ handleFileHistory(msg);
1605
+ break;
1606
+
1607
+ case "fs_git_diff_result":
1608
+ handleGitDiff(msg);
1609
+ break;
1610
+
1611
+ case "fs_file_at_result":
1612
+ handleFileAt(msg);
1613
+ break;
1614
+
1320
1615
  case "term_list":
1321
1616
  handleTermList(msg);
1322
1617
  break;
@@ -1336,6 +1631,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1336
1631
  case "term_closed":
1337
1632
  handleTermClosed(msg);
1338
1633
  break;
1634
+
1635
+ case "process_stats":
1636
+ updateStatusPanel(msg);
1637
+ break;
1339
1638
  }
1340
1639
  }
1341
1640
 
@@ -1467,6 +1766,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1467
1766
  addUserMessage: addUserMessage,
1468
1767
  addSystemMessage: addSystemMessage,
1469
1768
  toggleUsagePanel: toggleUsagePanel,
1769
+ toggleStatusPanel: toggleStatusPanel,
1770
+ toggleContextPanel: toggleContextPanel,
1771
+ resetContextData: resetContextData,
1772
+ showImageModal: showImageModal,
1470
1773
  });
1471
1774
 
1472
1775
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -1479,6 +1782,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1479
1782
  scrollToBottom: scrollToBottom,
1480
1783
  basePath: basePath,
1481
1784
  toggleUsagePanel: toggleUsagePanel,
1785
+ toggleStatusPanel: toggleStatusPanel,
1482
1786
  });
1483
1787
 
1484
1788
  // --- QR code ---
@@ -1488,6 +1792,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1488
1792
  initFileBrowser({
1489
1793
  get ws() { return ws; },
1490
1794
  get connected() { return connected; },
1795
+ get activeSessionId() { return activeSessionId; },
1796
+ messagesEl: messagesEl,
1491
1797
  fileTreeEl: $("file-tree"),
1492
1798
  fileViewerEl: $("file-viewer"),
1493
1799
  });
@@ -103,4 +103,9 @@ html, body {
103
103
  opacity: 1;
104
104
  transform: translateX(-50%) translateY(0);
105
105
  }
106
+ .toast.toast-warn {
107
+ background: #3a2a00;
108
+ border-color: #7a5a00;
109
+ color: #ffc107;
110
+ }
106
111
 
@@ -0,0 +1,128 @@
1
+ /* ==========================================================================
2
+ Diff Views (unified + split)
3
+ ========================================================================== */
4
+
5
+ .diff-unified,
6
+ .diff-split-view {
7
+ overflow: auto;
8
+ font-family: "SF Mono", Menlo, Monaco, monospace;
9
+ font-size: 12px;
10
+ line-height: 1.5;
11
+ }
12
+
13
+ .diff-table {
14
+ border-collapse: collapse;
15
+ width: 100%;
16
+ table-layout: fixed;
17
+ }
18
+
19
+ /* --- Line numbers --- */
20
+ .diff-ln {
21
+ width: 40px;
22
+ min-width: 40px;
23
+ padding: 0 8px 0 4px;
24
+ text-align: right;
25
+ color: var(--text-dimmer);
26
+ user-select: none;
27
+ vertical-align: top;
28
+ white-space: nowrap;
29
+ opacity: 0.6;
30
+ border-right: 1px solid var(--border-subtle);
31
+ }
32
+
33
+ /* --- Marker column (unified only) --- */
34
+ .diff-marker {
35
+ width: 16px;
36
+ min-width: 16px;
37
+ padding: 0 2px;
38
+ text-align: center;
39
+ color: var(--text-dimmer);
40
+ user-select: none;
41
+ vertical-align: top;
42
+ }
43
+
44
+ /* --- Code cells --- */
45
+ .diff-code {
46
+ padding: 0 12px;
47
+ white-space: pre;
48
+ vertical-align: top;
49
+ tab-size: 2;
50
+ }
51
+
52
+ /* --- Unified diff row colors --- */
53
+ .diff-row-remove {
54
+ background: rgba(229, 83, 75, 0.12);
55
+ }
56
+
57
+ .diff-row-remove .diff-marker {
58
+ color: var(--diff-remove, #E5534B);
59
+ }
60
+
61
+ .diff-row-add {
62
+ background: rgba(87, 171, 90, 0.12);
63
+ }
64
+
65
+ .diff-row-add .diff-marker {
66
+ color: var(--diff-add, #57AB5A);
67
+ }
68
+
69
+ /* --- Hunk header row (patch diffs) --- */
70
+ .diff-row-hunk {
71
+ background: rgba(128, 128, 128, 0.05);
72
+ }
73
+
74
+ .diff-row-hunk .diff-hunk-text {
75
+ color: var(--text-dimmer);
76
+ font-style: italic;
77
+ padding: 2px 12px;
78
+ }
79
+
80
+ /* --- Split diff specifics --- */
81
+ .diff-table-split .diff-code-old,
82
+ .diff-table-split .diff-code-new {
83
+ max-width: 0;
84
+ overflow: hidden;
85
+ }
86
+
87
+ /* Split: divider between left and right */
88
+ .diff-table-split .diff-code-old {
89
+ border-right: 1px solid var(--border-subtle);
90
+ }
91
+
92
+ /* Split: change rows */
93
+ .diff-row-change .diff-code-old,
94
+ .diff-row-remove .diff-code-old {
95
+ background: rgba(229, 83, 75, 0.12);
96
+ }
97
+
98
+ .diff-row-change .diff-code-new,
99
+ .diff-row-add .diff-code-new {
100
+ background: rgba(87, 171, 90, 0.12);
101
+ }
102
+
103
+ /* Empty cells in split view */
104
+ .diff-row-remove .diff-code-new,
105
+ .diff-row-add .diff-code-old {
106
+ background: rgba(128, 128, 128, 0.03);
107
+ }
108
+
109
+ /* --- Compact mode for inline previews --- */
110
+ .diff-compact .diff-table {
111
+ font-size: 11px;
112
+ line-height: 1.4;
113
+ }
114
+
115
+ .diff-compact .diff-ln {
116
+ width: 28px;
117
+ min-width: 28px;
118
+ padding: 0 4px 0 2px;
119
+ }
120
+
121
+ .diff-compact .diff-code {
122
+ padding: 0 6px;
123
+ }
124
+
125
+ .diff-compact .diff-marker {
126
+ width: 12px;
127
+ min-width: 12px;
128
+ }