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/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;
@@ -166,6 +185,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
166
185
  // isComposing -> modules/input.js
167
186
  var reconnectTimer = null;
168
187
  var reconnectDelay = 1000;
188
+ var disconnectNotifTimer = null;
189
+ var disconnectNotifShown = false;
169
190
  var activityEl = null;
170
191
  var currentMsgEl = null;
171
192
  var currentFullText = "";
@@ -657,6 +678,229 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
657
678
  });
658
679
  }
659
680
 
681
+ // --- Status panel ---
682
+ var statusPanel = $("status-panel");
683
+ var statusPanelClose = $("status-panel-close");
684
+ var statusPidEl = $("status-pid");
685
+ var statusUptimeEl = $("status-uptime");
686
+ var statusRssEl = $("status-rss");
687
+ var statusHeapUsedEl = $("status-heap-used");
688
+ var statusHeapTotalEl = $("status-heap-total");
689
+ var statusExternalEl = $("status-external");
690
+ var statusSessionsEl = $("status-sessions");
691
+ var statusProcessingEl = $("status-processing");
692
+ var statusClientsEl = $("status-clients");
693
+ var statusTerminalsEl = $("status-terminals");
694
+ var statusRefreshTimer = null;
695
+
696
+ function formatBytes(n) {
697
+ if (n >= 1073741824) return (n / 1073741824).toFixed(1) + " GB";
698
+ if (n >= 1048576) return (n / 1048576).toFixed(1) + " MB";
699
+ if (n >= 1024) return (n / 1024).toFixed(1) + " KB";
700
+ return n + " B";
701
+ }
702
+
703
+ function formatUptime(seconds) {
704
+ var d = Math.floor(seconds / 86400);
705
+ var h = Math.floor((seconds % 86400) / 3600);
706
+ var m = Math.floor((seconds % 3600) / 60);
707
+ var s = Math.floor(seconds % 60);
708
+ if (d > 0) return d + "d " + h + "h " + m + "m";
709
+ if (h > 0) return h + "h " + m + "m " + s + "s";
710
+ return m + "m " + s + "s";
711
+ }
712
+
713
+ function updateStatusPanel(data) {
714
+ if (!statusPidEl) return;
715
+ statusPidEl.textContent = String(data.pid);
716
+ statusUptimeEl.textContent = formatUptime(data.uptime);
717
+ statusRssEl.textContent = formatBytes(data.memory.rss);
718
+ statusHeapUsedEl.textContent = formatBytes(data.memory.heapUsed);
719
+ statusHeapTotalEl.textContent = formatBytes(data.memory.heapTotal);
720
+ statusExternalEl.textContent = formatBytes(data.memory.external);
721
+ statusSessionsEl.textContent = String(data.sessions);
722
+ statusProcessingEl.textContent = String(data.processing);
723
+ statusClientsEl.textContent = String(data.clients);
724
+ statusTerminalsEl.textContent = String(data.terminals);
725
+ }
726
+
727
+ function requestProcessStats() {
728
+ if (ws && ws.readyState === 1) {
729
+ ws.send(JSON.stringify({ type: "process_stats" }));
730
+ }
731
+ }
732
+
733
+ function toggleStatusPanel() {
734
+ if (!statusPanel) return;
735
+ var opening = statusPanel.classList.contains("hidden");
736
+ statusPanel.classList.toggle("hidden");
737
+ if (opening) {
738
+ requestProcessStats();
739
+ statusRefreshTimer = setInterval(requestProcessStats, 5000);
740
+ } else {
741
+ if (statusRefreshTimer) {
742
+ clearInterval(statusRefreshTimer);
743
+ statusRefreshTimer = null;
744
+ }
745
+ }
746
+ refreshIcons();
747
+ }
748
+
749
+ if (statusPanelClose) {
750
+ statusPanelClose.addEventListener("click", function () {
751
+ statusPanel.classList.add("hidden");
752
+ if (statusRefreshTimer) {
753
+ clearInterval(statusRefreshTimer);
754
+ statusRefreshTimer = null;
755
+ }
756
+ });
757
+ }
758
+
759
+ // --- Context panel ---
760
+ var contextPanel = $("context-panel");
761
+ var contextPanelClose = $("context-panel-close");
762
+ var contextPanelMinimize = $("context-panel-minimize");
763
+ var contextBarFill = $("context-bar-fill");
764
+ var contextBarPct = $("context-bar-pct");
765
+ var contextUsedEl = $("context-used");
766
+ var contextWindowEl = $("context-window");
767
+ var contextMaxOutputEl = $("context-max-output");
768
+ var contextInputEl = $("context-input");
769
+ var contextOutputEl = $("context-output");
770
+ var contextCacheReadEl = $("context-cache-read");
771
+ var contextCacheWriteEl = $("context-cache-write");
772
+ var contextModelEl = $("context-model");
773
+ var contextCostEl = $("context-cost");
774
+ var contextTurnsEl = $("context-turns");
775
+ var contextMini = $("context-mini");
776
+ var contextMiniFill = $("context-mini-fill");
777
+ var contextMiniLabel = $("context-mini-label");
778
+ var contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
779
+
780
+ function contextPctClass(pct) {
781
+ return pct >= 85 ? " danger" : pct >= 60 ? " warn" : "";
782
+ }
783
+
784
+ function updateContextPanel() {
785
+ if (!contextUsedEl) return;
786
+ // Context window usage = input tokens (includes cache read/write) + output tokens
787
+ var used = contextData.input + contextData.output;
788
+ var win = contextData.contextWindow;
789
+ var pct = win > 0 ? Math.min(100, (used / win) * 100) : 0;
790
+ var cls = contextPctClass(pct);
791
+ // Panel bar
792
+ contextBarFill.style.width = pct.toFixed(1) + "%";
793
+ contextBarFill.className = "context-bar-fill" + cls;
794
+ contextBarPct.textContent = pct.toFixed(0) + "%";
795
+ // Mini bar
796
+ if (contextMiniFill) {
797
+ contextMiniFill.style.width = pct.toFixed(1) + "%";
798
+ contextMiniFill.className = "context-mini-fill" + cls;
799
+ }
800
+ if (contextMiniLabel) {
801
+ contextMiniLabel.textContent = (win > 0 ? formatTokens(used) + "/" + formatTokens(win) : "0%");
802
+ }
803
+ contextUsedEl.textContent = formatTokens(used);
804
+ contextWindowEl.textContent = win > 0 ? formatTokens(win) : "-";
805
+ contextMaxOutputEl.textContent = contextData.maxOutputTokens > 0 ? formatTokens(contextData.maxOutputTokens) : "-";
806
+ contextInputEl.textContent = formatTokens(contextData.input);
807
+ contextOutputEl.textContent = formatTokens(contextData.output);
808
+ contextCacheReadEl.textContent = formatTokens(contextData.cacheRead);
809
+ contextCacheWriteEl.textContent = formatTokens(contextData.cacheWrite);
810
+ contextModelEl.textContent = contextData.model;
811
+ contextCostEl.textContent = "$" + contextData.cost.toFixed(4);
812
+ contextTurnsEl.textContent = String(contextData.turns);
813
+ }
814
+
815
+ function accumulateContext(cost, usage, modelUsage) {
816
+ if (cost != null) contextData.cost += cost;
817
+ // Use latest turn values (not cumulative) since each turn's input_tokens
818
+ // already includes the full conversation context up to that point
819
+ if (usage) {
820
+ contextData.input = usage.input_tokens || usage.inputTokens || 0;
821
+ contextData.output = usage.output_tokens || usage.outputTokens || 0;
822
+ contextData.cacheRead = usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
823
+ contextData.cacheWrite = usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
824
+ }
825
+ contextData.turns++;
826
+ if (modelUsage) {
827
+ var models = Object.keys(modelUsage);
828
+ if (models.length > 0) {
829
+ var m = models[0];
830
+ var mu = modelUsage[m];
831
+ contextData.model = m;
832
+ if (mu.contextWindow) contextData.contextWindow = mu.contextWindow;
833
+ if (mu.maxOutputTokens) contextData.maxOutputTokens = mu.maxOutputTokens;
834
+ }
835
+ }
836
+ updateContextPanel();
837
+ }
838
+
839
+ // contextView: "off" | "mini" | "panel"
840
+ function getContextView() {
841
+ try { return localStorage.getItem("claude-relay-context-view") || "off"; } catch (e) { return "off"; }
842
+ }
843
+ function setContextView(v) {
844
+ try { localStorage.setItem("claude-relay-context-view", v); } catch (e) {}
845
+ }
846
+
847
+ function applyContextView(view) {
848
+ if (contextPanel) contextPanel.classList.toggle("hidden", view !== "panel");
849
+ if (contextMini) contextMini.classList.toggle("hidden", view !== "mini");
850
+ if (view === "panel") refreshIcons();
851
+ }
852
+
853
+ function resetContextData() {
854
+ contextData = { contextWindow: 0, maxOutputTokens: 0, model: "-", cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
855
+ updateContextPanel();
856
+ }
857
+
858
+ function resetContext() {
859
+ resetContextData();
860
+ // Keep view state, just reset data
861
+ applyContextView(getContextView());
862
+ }
863
+
864
+ function minimizeContext() {
865
+ setContextView("mini");
866
+ applyContextView("mini");
867
+ }
868
+
869
+ function expandContext() {
870
+ setContextView("panel");
871
+ applyContextView("panel");
872
+ }
873
+
874
+ function toggleContextPanel() {
875
+ if (!contextPanel) return;
876
+ var view = getContextView();
877
+ if (view === "panel") {
878
+ setContextView("mini");
879
+ applyContextView("mini");
880
+ } else {
881
+ setContextView("panel");
882
+ applyContextView("panel");
883
+ }
884
+ }
885
+
886
+ if (contextPanelClose) {
887
+ contextPanelClose.addEventListener("click", function () {
888
+ setContextView("off");
889
+ applyContextView("off");
890
+ });
891
+ }
892
+
893
+ if (contextPanelMinimize) {
894
+ contextPanelMinimize.addEventListener("click", minimizeContext);
895
+ }
896
+
897
+ // Restore context view on load
898
+ applyContextView(getContextView());
899
+
900
+ if (contextMini) {
901
+ contextMini.addEventListener("click", expandContext);
902
+ }
903
+
660
904
  function addToMessages(el) {
661
905
  if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
662
906
  else messagesEl.appendChild(el);
@@ -732,6 +976,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
732
976
  div.dataset.turn = ++turnCounter;
733
977
  var bubble = document.createElement("div");
734
978
  bubble.className = "bubble";
979
+ bubble.dir = "auto";
735
980
 
736
981
  if (images && images.length > 0) {
737
982
  var imgRow = document.createElement("div");
@@ -740,6 +985,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
740
985
  var img = document.createElement("img");
741
986
  img.src = "data:" + images[i].mediaType + ";base64," + images[i].data;
742
987
  img.className = "bubble-img";
988
+ img.addEventListener("click", function () { showImageModal(this.src); });
743
989
  imgRow.appendChild(img);
744
990
  }
745
991
  bubble.appendChild(imgRow);
@@ -781,7 +1027,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
781
1027
  currentMsgEl = document.createElement("div");
782
1028
  currentMsgEl.className = "msg-assistant";
783
1029
  currentMsgEl.dataset.turn = turnCounter;
784
- currentMsgEl.innerHTML = '<div class="md-content"></div>';
1030
+ currentMsgEl.innerHTML = '<div class="md-content" dir="auto"></div>';
785
1031
  addToMessages(currentMsgEl);
786
1032
  currentFullText = "";
787
1033
  }
@@ -894,6 +1140,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
894
1140
  setStatus("connected");
895
1141
  enableMainInput();
896
1142
  resetUsage();
1143
+ resetContext();
897
1144
  }
898
1145
 
899
1146
  // --- WebSocket ---
@@ -920,8 +1167,13 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
920
1167
 
921
1168
  ws.onopen = function () {
922
1169
  if (connectTimeoutId) { clearTimeout(connectTimeoutId); connectTimeoutId = null; }
923
- // Local notification on reconnect (only if not focused)
924
- if (wasConnected && !document.hasFocus() && "serviceWorker" in navigator) {
1170
+ // Cancel pending "connection lost" notification if reconnected quickly
1171
+ if (disconnectNotifTimer) {
1172
+ clearTimeout(disconnectNotifTimer);
1173
+ disconnectNotifTimer = null;
1174
+ }
1175
+ // Only show "restored" notification if "lost" was actually shown
1176
+ if (wasConnected && disconnectNotifShown && !document.hasFocus() && "serviceWorker" in navigator) {
925
1177
  navigator.serviceWorker.ready.then(function (reg) {
926
1178
  reg.showNotification("Claude Relay", {
927
1179
  body: "Server connection restored",
@@ -929,6 +1181,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
929
1181
  });
930
1182
  }).catch(function () {});
931
1183
  }
1184
+ disconnectNotifShown = false;
932
1185
  wasConnected = true;
933
1186
  setStatus("connected");
934
1187
  reconnectDelay = 1000;
@@ -954,14 +1207,20 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
954
1207
  setStatus("disconnected");
955
1208
  processing = false;
956
1209
  setActivity(null);
957
- // Local notification when connection drops (only if not focused)
958
- if (!document.hasFocus() && "serviceWorker" in navigator) {
959
- navigator.serviceWorker.ready.then(function (reg) {
960
- reg.showNotification("Claude Relay", {
961
- body: "Server connection lost",
962
- tag: "claude-disconnect",
963
- });
964
- }).catch(function () {});
1210
+ // Delay "connection lost" notification by 5s to suppress brief disconnects
1211
+ if (!disconnectNotifTimer) {
1212
+ disconnectNotifTimer = setTimeout(function () {
1213
+ disconnectNotifTimer = null;
1214
+ disconnectNotifShown = true;
1215
+ if (!document.hasFocus() && "serviceWorker" in navigator) {
1216
+ navigator.serviceWorker.ready.then(function (reg) {
1217
+ reg.showNotification("Claude Relay", {
1218
+ body: "Server connection lost",
1219
+ tag: "claude-disconnect",
1220
+ });
1221
+ }).catch(function () {});
1222
+ }
1223
+ }, 5000);
965
1224
  }
966
1225
  scheduleReconnect();
967
1226
  };
@@ -1002,6 +1261,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1002
1261
  if (pendingQuery) {
1003
1262
  requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
1004
1263
  }
1264
+ // Scroll to tool element if navigating from file edit history
1265
+ var nav = getPendingNavigate();
1266
+ if (nav && (nav.toolId || nav.assistantUuid)) {
1267
+ requestAnimationFrame(function() {
1268
+ // Prefer scrolling to the exact tool element
1269
+ var target = nav.toolId ? messagesEl.querySelector('[data-tool-id="' + nav.toolId + '"]') : null;
1270
+ if (!target && nav.assistantUuid) {
1271
+ target = messagesEl.querySelector('[data-uuid="' + nav.assistantUuid + '"]');
1272
+ }
1273
+ if (target) {
1274
+ target.scrollIntoView({ behavior: "smooth", block: "center" });
1275
+ target.classList.add("message-blink");
1276
+ setTimeout(function() { target.classList.remove("message-blink"); }, 2000);
1277
+ }
1278
+ });
1279
+ }
1005
1280
  break;
1006
1281
 
1007
1282
  case "info":
@@ -1017,6 +1292,11 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1017
1292
  var debugWrap = $("debug-menu-wrap");
1018
1293
  if (debugWrap) debugWrap.classList.remove("hidden");
1019
1294
  }
1295
+ if (msg.lanHost) window.__lanHost = msg.lanHost;
1296
+ if (msg.dangerouslySkipPermissions) {
1297
+ var spBanner = $("skip-perms-banner");
1298
+ if (spBanner) spBanner.classList.remove("hidden");
1299
+ }
1020
1300
  updateProjectSwitcher(msg);
1021
1301
  break;
1022
1302
 
@@ -1045,7 +1325,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1045
1325
  break;
1046
1326
 
1047
1327
  case "slash_commands":
1048
- slashCommands = (msg.commands || []).map(function (name) {
1328
+ var reserved = new Set(builtinCommands.map(function (c) { return c.name; }));
1329
+ slashCommands = (msg.commands || []).filter(function (name) {
1330
+ return !reserved.has(name);
1331
+ }).map(function (name) {
1049
1332
  return { name: name, desc: "Skill" };
1050
1333
  });
1051
1334
  break;
@@ -1067,6 +1350,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1067
1350
  }
1068
1351
  break;
1069
1352
 
1353
+ case "toast":
1354
+ showToast(msg.message, msg.level, msg.detail);
1355
+ break;
1356
+
1070
1357
  case "input_sync":
1071
1358
  handleInputSync(msg.text);
1072
1359
  break;
@@ -1093,6 +1380,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1093
1380
  var draft = sessionDrafts[activeSessionId] || "";
1094
1381
  inputEl.value = draft;
1095
1382
  autoResize();
1383
+ if (!("ontouchstart" in window)) {
1384
+ inputEl.focus();
1385
+ }
1096
1386
  break;
1097
1387
 
1098
1388
  case "session_id":
@@ -1260,6 +1550,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1260
1550
  finalizeAssistantBlock();
1261
1551
  addTurnMeta(msg.cost, msg.duration);
1262
1552
  accumulateUsage(msg.cost, msg.usage);
1553
+ accumulateContext(msg.cost, msg.usage, msg.modelUsage);
1263
1554
  break;
1264
1555
 
1265
1556
  case "done":
@@ -1272,7 +1563,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1272
1563
  enableMainInput();
1273
1564
  resetToolState();
1274
1565
  if (document.hidden) {
1275
- if (isNotifAlertEnabled()) showDoneNotification();
1566
+ if (isNotifAlertEnabled() && !window._pushSubscription) showDoneNotification();
1276
1567
  if (isNotifSoundEnabled()) playDoneSound();
1277
1568
  }
1278
1569
  break;
@@ -1297,7 +1588,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1297
1588
 
1298
1589
  case "rewind_complete":
1299
1590
  setRewindMode(false);
1300
- addSystemMessage("Rewound to earlier point. Files have been restored.", false);
1591
+ var rewindText = "Rewound to earlier point. Files have been restored.";
1592
+ if (msg.mode === "chat") rewindText = "Conversation rewound to earlier point.";
1593
+ else if (msg.mode === "files") rewindText = "Files restored to earlier point.";
1594
+ addSystemMessage(rewindText, false);
1301
1595
  break;
1302
1596
 
1303
1597
  case "rewind_error":
@@ -1317,6 +1611,22 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1317
1611
  handleFileChanged(msg);
1318
1612
  break;
1319
1613
 
1614
+ case "fs_dir_changed":
1615
+ handleDirChanged(msg);
1616
+ break;
1617
+
1618
+ case "fs_file_history_result":
1619
+ handleFileHistory(msg);
1620
+ break;
1621
+
1622
+ case "fs_git_diff_result":
1623
+ handleGitDiff(msg);
1624
+ break;
1625
+
1626
+ case "fs_file_at_result":
1627
+ handleFileAt(msg);
1628
+ break;
1629
+
1320
1630
  case "term_list":
1321
1631
  handleTermList(msg);
1322
1632
  break;
@@ -1336,6 +1646,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1336
1646
  case "term_closed":
1337
1647
  handleTermClosed(msg);
1338
1648
  break;
1649
+
1650
+ case "process_stats":
1651
+ updateStatusPanel(msg);
1652
+ break;
1339
1653
  }
1340
1654
  }
1341
1655
 
@@ -1467,6 +1781,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1467
1781
  addUserMessage: addUserMessage,
1468
1782
  addSystemMessage: addSystemMessage,
1469
1783
  toggleUsagePanel: toggleUsagePanel,
1784
+ toggleStatusPanel: toggleStatusPanel,
1785
+ toggleContextPanel: toggleContextPanel,
1786
+ resetContextData: resetContextData,
1787
+ showImageModal: showImageModal,
1470
1788
  });
1471
1789
 
1472
1790
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -1479,6 +1797,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1479
1797
  scrollToBottom: scrollToBottom,
1480
1798
  basePath: basePath,
1481
1799
  toggleUsagePanel: toggleUsagePanel,
1800
+ toggleStatusPanel: toggleStatusPanel,
1482
1801
  });
1483
1802
 
1484
1803
  // --- QR code ---
@@ -1488,6 +1807,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1488
1807
  initFileBrowser({
1489
1808
  get ws() { return ws; },
1490
1809
  get connected() { return connected; },
1810
+ get activeSessionId() { return activeSessionId; },
1811
+ messagesEl: messagesEl,
1491
1812
  fileTreeEl: $("file-tree"),
1492
1813
  fileViewerEl: $("file-viewer"),
1493
1814
  });
@@ -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
+ }