claude-relay 2.1.3 → 2.2.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,14 +1,14 @@
1
1
  import { 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
- import { initSidebar, renderSessionList, updatePageTitle } from './modules/sidebar.js';
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 } from './modules/input.js';
7
+ import { initInput, clearPendingImages, handleInputSync, autoResize } from './modules/input.js';
8
8
  import { initQrCode } from './modules/qrcode.js';
9
- import { initFileBrowser, loadRootDirectory, handleFsList, handleFsRead, refreshIfOpen } from './modules/filebrowser.js';
10
- import { initTerminal, openTerminal, handleTermOutput, handleTermExited, closeTerminal } from './modules/terminal.js';
11
- import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUserQuestion, 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';
9
+ import { initFileBrowser, loadRootDirectory, handleFsList, handleFsRead, refreshIfOpen, handleFileChanged, closeFileViewer } from './modules/filebrowser.js';
10
+ import { initTerminal, openTerminal, closeTerminal, resetTerminals, handleTermList, handleTermCreated, handleTermOutput, handleTermExited, handleTermClosed } from './modules/terminal.js';
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
 
13
13
  // --- Base path for multi-project routing ---
14
14
  var slugMatch = location.pathname.match(/^\/p\/([a-z0-9_-]+)/);
@@ -172,6 +172,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
172
172
  // tools, currentThinking -> modules/tools.js
173
173
  var highlightTimer = null;
174
174
  var activeSessionId = null;
175
+ var sessionDrafts = {};
175
176
  var slashCommands = [];
176
177
  // slashActiveIdx, slashFiltered, pendingImages, pendingPastes -> modules/input.js
177
178
  // pendingPermissions -> modules/tools.js
@@ -189,6 +190,10 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
189
190
  var loadingMore = false;
190
191
  var historySentinelObserver = null;
191
192
 
193
+ // --- Scroll lock ---
194
+ var isUserScrolledUp = false;
195
+ var scrollThreshold = 150;
196
+
192
197
  // builtinCommands -> modules/input.js
193
198
 
194
199
  // --- Confirm modal ---
@@ -247,6 +252,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
247
252
  get ws() { return ws; },
248
253
  get connected() { return connected; },
249
254
  get projectName() { return projectName; },
255
+ messagesEl: messagesEl,
250
256
  sessionListEl: sessionListEl,
251
257
  sidebar: sidebar,
252
258
  sidebarOverlay: sidebarOverlay,
@@ -496,6 +502,8 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
496
502
  connectOverlay.classList.add("hidden");
497
503
  stopVerbCycle();
498
504
  updateFavicon("#57AB5A");
505
+ if (usageFab) usageFab.classList.remove("hidden");
506
+ if (usageHeaderBtn) usageHeaderBtn.classList.remove("hidden");
499
507
  } else if (status === "processing") {
500
508
  statusDot.classList.add("processing");
501
509
  processing = true;
@@ -533,13 +541,307 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
533
541
  }
534
542
  }
535
543
 
544
+ // --- Model selector ---
545
+ var modelMenuWrap = $("model-menu-wrap");
546
+ var modelBtn = $("model-btn");
547
+ var modelLabel = $("model-label");
548
+ var modelMenu = $("model-menu");
549
+
550
+ function modelDisplayName(value, models) {
551
+ if (!value) return "";
552
+ // Look up displayName from models list
553
+ if (models) {
554
+ for (var i = 0; i < models.length; i++) {
555
+ if (models[i].value === value && models[i].displayName) return models[i].displayName;
556
+ }
557
+ }
558
+ return value;
559
+ }
560
+
561
+ var currentModels = [];
562
+
563
+ function updateModelSelector(current, models) {
564
+ if (!modelMenuWrap || !modelBtn || !modelMenu) return;
565
+ currentModels = models;
566
+ modelLabel.textContent = modelDisplayName(current, models);
567
+ modelMenuWrap.classList.remove("hidden");
568
+
569
+ modelMenu.innerHTML = "";
570
+ var list = models.length > 0 ? models : (current ? [{ value: current, displayName: current }] : []);
571
+ for (var i = 0; i < list.length; i++) {
572
+ var item = list[i];
573
+ var value = item.value || "";
574
+ var label = item.displayName || value;
575
+ var btn = document.createElement("button");
576
+ btn.className = "model-menu-item";
577
+ if (value === current) btn.classList.add("active");
578
+ btn.dataset.model = value;
579
+ btn.textContent = label;
580
+ btn.addEventListener("click", function () {
581
+ var model = this.dataset.model;
582
+ if (ws && ws.readyState === 1) {
583
+ ws.send(JSON.stringify({ type: "set_model", model: model }));
584
+ }
585
+ modelMenu.classList.add("hidden");
586
+ modelBtn.classList.remove("active");
587
+ });
588
+ modelMenu.appendChild(btn);
589
+ }
590
+ }
591
+
592
+ modelBtn.addEventListener("click", function (e) {
593
+ e.stopPropagation();
594
+ var open = modelMenu.classList.toggle("hidden");
595
+ modelBtn.classList.toggle("active", !open);
596
+ });
597
+
598
+ document.addEventListener("click", function (e) {
599
+ if (!modelMenu.contains(e.target) && e.target !== modelBtn) {
600
+ modelMenu.classList.add("hidden");
601
+ modelBtn.classList.remove("active");
602
+ }
603
+ });
604
+
605
+ // --- Usage panel ---
606
+ var usagePanel = $("usage-panel");
607
+ var usagePanelClose = $("usage-panel-close");
608
+ var usageFab = $("usage-fab");
609
+ var usageHeaderBtn = $("usage-header-btn");
610
+ var usageCostEl = $("usage-cost");
611
+ var usageInputEl = $("usage-input");
612
+ var usageOutputEl = $("usage-output");
613
+ var usageCacheReadEl = $("usage-cache-read");
614
+ var usageCacheWriteEl = $("usage-cache-write");
615
+ var usageTurnsEl = $("usage-turns");
616
+ var sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
617
+
618
+ // Rate limit bar elements
619
+ var usageLoading = $("usage-loading");
620
+ var usageError = $("usage-error");
621
+ var usageBars = $("usage-bars");
622
+ var usageRateLimitFetched = false;
623
+
624
+ function formatTokens(n) {
625
+ if (n >= 1000000) return (n / 1000000).toFixed(1) + "M";
626
+ if (n >= 1000) return (n / 1000).toFixed(1) + "K";
627
+ return String(n);
628
+ }
629
+
630
+ function formatTimeUntil(isoStr) {
631
+ if (!isoStr) return "";
632
+ var d = new Date(isoStr);
633
+ var now = Date.now();
634
+ var diff = d.getTime() - now;
635
+ if (diff <= 0) return "Resets soon";
636
+ var hours = Math.floor(diff / 3600000);
637
+ var minutes = Math.floor((diff % 3600000) / 60000);
638
+ var relative;
639
+ if (hours > 24) {
640
+ var days = Math.floor(hours / 24);
641
+ relative = days + "d " + (hours % 24) + "h";
642
+ } else if (hours > 0) {
643
+ relative = hours + "h " + minutes + "m";
644
+ } else {
645
+ relative = minutes + "m";
646
+ }
647
+ // Absolute time: "Mon 2/17 15:00" or "15:00" if today
648
+ var nowDate = new Date(now);
649
+ var month = d.getMonth() + 1;
650
+ var day = d.getDate();
651
+ var hh = String(d.getHours()).padStart(2, "0");
652
+ var mm = String(d.getMinutes()).padStart(2, "0");
653
+ var sameDay = d.getFullYear() === nowDate.getFullYear() && d.getMonth() === nowDate.getMonth() && d.getDate() === nowDate.getDate();
654
+ var abs = sameDay ? hh + ":" + mm : month + "/" + day + " " + hh + ":" + mm;
655
+ return "Resets in " + relative + " (" + abs + ")";
656
+ }
657
+
658
+ function updateRateLimitBar(prefix, utilization, resetsAt) {
659
+ var pctEl = $("usage-pct-" + prefix);
660
+ var fillEl = $("usage-fill-" + prefix);
661
+ var resetEl = $("usage-reset-" + prefix);
662
+ var groupEl = $("usage-bar-" + prefix);
663
+ if (!pctEl || !fillEl || !resetEl) return;
664
+
665
+ if (utilization == null) {
666
+ if (groupEl) groupEl.classList.add("hidden");
667
+ return;
668
+ }
669
+ if (groupEl) groupEl.classList.remove("hidden");
670
+
671
+ var pct = Math.max(0, Math.min(100, Math.round(utilization)));
672
+ pctEl.textContent = pct + "%";
673
+ fillEl.style.width = pct + "%";
674
+ fillEl.className = "usage-bar-fill";
675
+ if (prefix === "extra") fillEl.classList.add("usage-bar-fill-extra");
676
+ if (pct >= 90) fillEl.classList.add("critical");
677
+ else if (pct >= 70) fillEl.classList.add("warn");
678
+
679
+ resetEl.textContent = formatTimeUntil(resetsAt);
680
+ }
681
+
682
+ function handleUsageData(msg) {
683
+ if (!usageLoading || !usageError || !usageBars) return;
684
+ usageLoading.style.display = "none";
685
+
686
+ if (msg.error) {
687
+ usageError.textContent = msg.error;
688
+ usageError.classList.remove("hidden");
689
+ usageBars.classList.add("hidden");
690
+ return;
691
+ }
692
+
693
+ usageError.classList.add("hidden");
694
+ usageBars.classList.remove("hidden");
695
+ var data = msg.data || {};
696
+
697
+ // Session (five_hour)
698
+ var session = data.five_hour || {};
699
+ updateRateLimitBar("session", session.utilization, session.resets_at);
700
+
701
+ // Weekly all models (seven_day)
702
+ var weekly = data.seven_day || {};
703
+ updateRateLimitBar("weekly", weekly.utilization, weekly.resets_at);
704
+
705
+ // Weekly Sonnet only (seven_day_sonnet)
706
+ var sonnet = data.seven_day_sonnet || {};
707
+ updateRateLimitBar("sonnet", sonnet.utilization, sonnet.resets_at);
708
+
709
+ // Extra usage
710
+ var extra = data.extra_usage || {};
711
+ var extraGroup = $("usage-bar-extra");
712
+ if (extra.is_enabled) {
713
+ if (extraGroup) extraGroup.classList.remove("hidden");
714
+ // Compute reset time as first of next month
715
+ var now = new Date();
716
+ var nextMonth = new Date(now.getFullYear(), now.getMonth() + 1, 1);
717
+ updateRateLimitBar("extra", extra.utilization, nextMonth.toISOString());
718
+ var extraResetEl = $("usage-reset-extra");
719
+ if (extraResetEl && extra.monthly_limit != null) {
720
+ var usedDollars = (extra.used_credits / 100).toFixed(2);
721
+ var limitDollars = (extra.monthly_limit / 100).toFixed(2);
722
+ extraResetEl.textContent = "$" + usedDollars + " / $" + limitDollars;
723
+ }
724
+ } else {
725
+ if (extraGroup) extraGroup.classList.add("hidden");
726
+ }
727
+
728
+ usageRateLimitFetched = true;
729
+ }
730
+
731
+ function requestUsageData() {
732
+ if (!ws || ws.readyState !== 1) return;
733
+ if (usageLoading) usageLoading.style.display = "";
734
+ if (usageError) usageError.classList.add("hidden");
735
+ ws.send(JSON.stringify({ type: "get_usage" }));
736
+ }
737
+
738
+ function updateUsagePanel() {
739
+ if (!usageCostEl) return;
740
+ usageCostEl.textContent = "$" + sessionUsage.cost.toFixed(4);
741
+ usageInputEl.textContent = formatTokens(sessionUsage.input);
742
+ usageOutputEl.textContent = formatTokens(sessionUsage.output);
743
+ usageCacheReadEl.textContent = formatTokens(sessionUsage.cacheRead);
744
+ usageCacheWriteEl.textContent = formatTokens(sessionUsage.cacheWrite);
745
+ usageTurnsEl.textContent = String(sessionUsage.turns);
746
+ }
747
+
748
+ function accumulateUsage(cost, usage) {
749
+ if (cost != null) sessionUsage.cost += cost;
750
+ if (usage) {
751
+ sessionUsage.input += usage.input_tokens || usage.inputTokens || 0;
752
+ sessionUsage.output += usage.output_tokens || usage.outputTokens || 0;
753
+ sessionUsage.cacheRead += usage.cache_read_input_tokens || usage.cacheReadInputTokens || 0;
754
+ sessionUsage.cacheWrite += usage.cache_creation_input_tokens || usage.cacheCreationInputTokens || 0;
755
+ }
756
+ sessionUsage.turns++;
757
+ updateUsagePanel();
758
+ }
759
+
760
+ function resetUsage() {
761
+ sessionUsage = { cost: 0, input: 0, output: 0, cacheRead: 0, cacheWrite: 0, turns: 0 };
762
+ updateUsagePanel();
763
+ if (usagePanel) usagePanel.classList.add("hidden");
764
+ if (usageFab) usageFab.classList.remove("active");
765
+ if (usageHeaderBtn) usageHeaderBtn.classList.remove("active");
766
+ usageRateLimitFetched = false;
767
+ }
768
+
769
+ function toggleUsagePanel() {
770
+ if (!usagePanel) return;
771
+ var isHidden = usagePanel.classList.toggle("hidden");
772
+ if (usageFab) usageFab.classList.toggle("active", !isHidden);
773
+ if (usageHeaderBtn) usageHeaderBtn.classList.toggle("active", !isHidden);
774
+ // Fetch rate limit data when opening
775
+ if (!isHidden) {
776
+ requestUsageData();
777
+ }
778
+ refreshIcons();
779
+ }
780
+
781
+ if (usagePanelClose) {
782
+ usagePanelClose.addEventListener("click", function () {
783
+ usagePanel.classList.add("hidden");
784
+ if (usageFab) usageFab.classList.remove("active");
785
+ if (usageHeaderBtn) usageHeaderBtn.classList.remove("active");
786
+ });
787
+ }
788
+
789
+ if (usageFab) {
790
+ usageFab.addEventListener("click", function () {
791
+ toggleUsagePanel();
792
+ });
793
+ }
794
+
795
+ if (usageHeaderBtn) {
796
+ usageHeaderBtn.addEventListener("click", function () {
797
+ toggleUsagePanel();
798
+ });
799
+ }
800
+
536
801
  function addToMessages(el) {
537
802
  if (prependAnchor) messagesEl.insertBefore(el, prependAnchor);
538
803
  else messagesEl.appendChild(el);
539
804
  }
540
805
 
806
+ var newMsgBtn = $("new-msg-btn");
807
+ var newMsgBtnDefault = "\u2193 Latest";
808
+ var newMsgBtnActivity = "\u2193 New activity";
809
+
810
+ messagesEl.addEventListener("scroll", function () {
811
+ var distFromBottom = messagesEl.scrollHeight - messagesEl.scrollTop - messagesEl.clientHeight;
812
+ isUserScrolledUp = distFromBottom > scrollThreshold;
813
+ if (isUserScrolledUp) {
814
+ if (newMsgBtn.classList.contains("hidden")) {
815
+ newMsgBtn.textContent = newMsgBtnDefault;
816
+ }
817
+ newMsgBtn.classList.remove("hidden");
818
+ } else {
819
+ newMsgBtn.classList.add("hidden");
820
+ newMsgBtn.textContent = newMsgBtnDefault;
821
+ }
822
+ });
823
+
824
+ newMsgBtn.addEventListener("click", function () {
825
+ forceScrollToBottom();
826
+ });
827
+
541
828
  function scrollToBottom() {
542
829
  if (prependAnchor) return;
830
+ if (isUserScrolledUp) {
831
+ newMsgBtn.textContent = newMsgBtnActivity;
832
+ newMsgBtn.classList.remove("hidden");
833
+ return;
834
+ }
835
+ requestAnimationFrame(function () {
836
+ messagesEl.scrollTop = messagesEl.scrollHeight;
837
+ });
838
+ }
839
+
840
+ function forceScrollToBottom() {
841
+ if (prependAnchor) return;
842
+ isUserScrolledUp = false;
843
+ newMsgBtn.classList.add("hidden");
844
+ newMsgBtn.textContent = newMsgBtnDefault;
543
845
  requestAnimationFrame(function () {
544
846
  messagesEl.scrollTop = messagesEl.scrollHeight;
545
847
  });
@@ -612,7 +914,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
612
914
 
613
915
  div.appendChild(bubble);
614
916
  addToMessages(div);
615
- scrollToBottom();
917
+ forceScrollToBottom();
616
918
  }
617
919
 
618
920
  function ensureAssistantBlock() {
@@ -725,10 +1027,14 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
725
1027
  historyTotal = 0;
726
1028
  prependAnchor = null;
727
1029
  loadingMore = false;
1030
+ isUserScrolledUp = false;
1031
+ newMsgBtn.classList.add("hidden");
728
1032
  setRewindMode(false);
1033
+ removeSearchTimeline();
729
1034
  setActivity(null);
730
1035
  setStatus("connected");
731
1036
  enableMainInput();
1037
+ resetUsage();
732
1038
  }
733
1039
 
734
1040
  // --- WebSocket ---
@@ -769,6 +1075,9 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
769
1075
  reconnectDelay = 1000;
770
1076
  if (reconnectTimer) { clearTimeout(reconnectTimer); reconnectTimer = null; }
771
1077
 
1078
+ // Reset terminal xterm instances (server will send fresh term_list)
1079
+ resetTerminals();
1080
+
772
1081
  // Re-send push subscription on reconnect
773
1082
  if (window._pushSubscription) {
774
1083
  try {
@@ -829,6 +1138,13 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
829
1138
  prependOlderHistory(msg.items, msg.meta);
830
1139
  break;
831
1140
 
1141
+ case "history_done":
1142
+ var pendingQuery = getActiveSearchQuery();
1143
+ if (pendingQuery) {
1144
+ requestAnimationFrame(function() { buildSearchTimeline(pendingQuery); });
1145
+ }
1146
+ break;
1147
+
832
1148
  case "info":
833
1149
  projectName = msg.project || msg.cwd;
834
1150
  if (msg.slug) currentSlug = msg.slug;
@@ -875,6 +1191,14 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
875
1191
  });
876
1192
  break;
877
1193
 
1194
+ case "model_info":
1195
+ updateModelSelector(msg.model, msg.models || []);
1196
+ break;
1197
+
1198
+ case "usage_data":
1199
+ handleUsageData(msg);
1200
+ break;
1201
+
878
1202
  case "client_count":
879
1203
  var countEl = document.getElementById("client-count");
880
1204
  if (countEl) {
@@ -896,10 +1220,24 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
896
1220
  renderSessionList(msg.sessions || []);
897
1221
  break;
898
1222
 
1223
+ case "search_results":
1224
+ handleSearchResults(msg);
1225
+ break;
1226
+
899
1227
  case "session_switched":
1228
+ // Save draft from outgoing session
1229
+ if (activeSessionId && inputEl.value) {
1230
+ sessionDrafts[activeSessionId] = inputEl.value;
1231
+ } else if (activeSessionId) {
1232
+ delete sessionDrafts[activeSessionId];
1233
+ }
900
1234
  activeSessionId = msg.id;
901
1235
  cliSessionId = msg.cliSessionId || null;
902
1236
  resetClientState();
1237
+ // Restore draft for incoming session
1238
+ var draft = sessionDrafts[activeSessionId] || "";
1239
+ inputEl.value = draft;
1240
+ autoResize();
903
1241
  break;
904
1242
 
905
1243
  case "session_id":
@@ -932,6 +1270,14 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
932
1270
  }
933
1271
  break;
934
1272
 
1273
+ case "compacting":
1274
+ if (msg.active) {
1275
+ setActivity("Compacting conversation...");
1276
+ } else {
1277
+ setActivity(randomThinkingVerb() + "...");
1278
+ }
1279
+ break;
1280
+
935
1281
  case "thinking_start":
936
1282
  startThinking();
937
1283
  break;
@@ -998,11 +1344,13 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
998
1344
  }
999
1345
  break;
1000
1346
 
1001
- case "tool_result":
1002
- if (msg.content != null) {
1347
+ case "tool_result": {
1003
1348
  var tr = getTools()[msg.id];
1004
1349
  if (tr && tr.hidden) break; // skip hidden plan tools
1005
- updateToolResult(msg.id, msg.content, msg.is_error || false);
1350
+ // Always call updateToolResult for Edit (to show diff from input), or when content exists
1351
+ if (msg.content != null || (tr && tr.name === "Edit" && tr.input && tr.input.old_string)) {
1352
+ updateToolResult(msg.id, msg.content || "", msg.is_error || false);
1353
+ }
1006
1354
  // Refresh file browser if an Edit/Write tool modified the open file
1007
1355
  if (!msg.is_error && tr && (tr.name === "Edit" || tr.name === "Write") && tr.input && tr.input.file_path) {
1008
1356
  refreshIfOpen(tr.input.file_path);
@@ -1010,6 +1358,11 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1010
1358
  }
1011
1359
  break;
1012
1360
 
1361
+ case "ask_user_answered":
1362
+ markAskUserAnswered(msg.toolId);
1363
+ stopUrgentBlink();
1364
+ break;
1365
+
1013
1366
  case "permission_request":
1014
1367
  renderPermissionRequest(msg.requestId, msg.toolName, msg.toolInput, msg.decisionReason);
1015
1368
  startUrgentBlink();
@@ -1051,6 +1404,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1051
1404
  markAllToolsDone();
1052
1405
  finalizeAssistantBlock();
1053
1406
  addTurnMeta(msg.cost, msg.duration);
1407
+ accumulateUsage(msg.cost, msg.usage);
1054
1408
  break;
1055
1409
 
1056
1410
  case "done":
@@ -1104,12 +1458,28 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1104
1458
  handleFsRead(msg);
1105
1459
  break;
1106
1460
 
1461
+ case "fs_file_changed":
1462
+ handleFileChanged(msg);
1463
+ break;
1464
+
1465
+ case "term_list":
1466
+ handleTermList(msg);
1467
+ break;
1468
+
1469
+ case "term_created":
1470
+ handleTermCreated(msg);
1471
+ break;
1472
+
1107
1473
  case "term_output":
1108
1474
  handleTermOutput(msg);
1109
1475
  break;
1110
1476
 
1111
1477
  case "term_exited":
1112
- handleTermExited();
1478
+ handleTermExited(msg);
1479
+ break;
1480
+
1481
+ case "term_closed":
1482
+ handleTermClosed(msg);
1113
1483
  break;
1114
1484
  }
1115
1485
  }
@@ -1241,6 +1611,7 @@ import { initTools, resetToolState, saveToolState, restoreToolState, renderAskUs
1241
1611
  messageUuidMap: function() { return messageUuidMap; },
1242
1612
  addUserMessage: addUserMessage,
1243
1613
  addSystemMessage: addSystemMessage,
1614
+ toggleUsagePanel: toggleUsagePanel,
1244
1615
  });
1245
1616
 
1246
1617
  // --- Notifications module (viewport, banners, notifications, debug, service worker) ---
@@ -8,8 +8,15 @@
8
8
  padding: 0;
9
9
  box-sizing: border-box;
10
10
  -webkit-tap-highlight-color: transparent;
11
+ scrollbar-width: thin;
12
+ scrollbar-color: rgba(255,255,255,0.15) transparent;
11
13
  }
12
14
 
15
+ ::-webkit-scrollbar { width: 6px; height: 6px; }
16
+ ::-webkit-scrollbar-track { background: transparent; }
17
+ ::-webkit-scrollbar-thumb { background: rgba(255,255,255,0.15); border-radius: 3px; }
18
+ ::-webkit-scrollbar-thumb:hover { background: rgba(255,255,255,0.25); }
19
+
13
20
  :root {
14
21
  --bg: #2F2E2B;
15
22
  --bg-alt: #35332F;