farming-code 2.2.8 → 2.2.12

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.
Files changed (107) hide show
  1. package/README.md +42 -28
  2. package/README.zh_cn.md +42 -28
  3. package/THIRD_PARTY_NOTICES.md +11 -2
  4. package/backend/acp-checkpoint-store.js +198 -0
  5. package/backend/acp-runtime.js +306 -83
  6. package/backend/acp-session-state.js +202 -6
  7. package/backend/acp-transcript.js +112 -0
  8. package/backend/agent-activity.js +6 -157
  9. package/backend/agent-manager.js +1592 -741
  10. package/backend/agent-provider-session.js +17 -242
  11. package/backend/agent-runtime-binding.js +219 -0
  12. package/backend/agent-session-history.js +66 -1
  13. package/backend/auth.js +79 -6
  14. package/backend/codex-models.js +81 -84
  15. package/backend/codex-session-archive.js +54 -0
  16. package/backend/codex-terminal-profile.js +500 -0
  17. package/backend/codex-transcript-sanitizer.js +12 -0
  18. package/backend/codex-transcript.js +230 -8
  19. package/backend/config-manager.js +35 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/executable-discovery.js +2 -0
  22. package/backend/farming-net-pass.js +285 -0
  23. package/backend/farming-net-registry.js +112 -0
  24. package/backend/farming-net-server.js +298 -0
  25. package/backend/farming-session-store.js +5 -13
  26. package/backend/git-worktree-info.js +181 -0
  27. package/backend/local-session-engine.js +411 -186
  28. package/backend/main-page-session.js +5 -2
  29. package/backend/native-pty-controller-generation.js +106 -0
  30. package/backend/native-pty-host-client.js +275 -7
  31. package/backend/native-pty-host-identity.js +86 -0
  32. package/backend/native-pty-host.js +813 -114
  33. package/backend/native-session-engine.js +100 -28
  34. package/backend/packaged-node-pty.js +22 -2
  35. package/backend/provider-adapters.js +253 -0
  36. package/backend/provider-session-service.js +241 -0
  37. package/backend/runtime-observation.js +81 -0
  38. package/backend/server.js +354 -84
  39. package/backend/session-engine-bridge.js +21 -2
  40. package/backend/session-engine-router.js +1 -1
  41. package/backend/session-engine.js +1 -1
  42. package/backend/session-stream-protocol.js +185 -0
  43. package/backend/storage-layout.js +55 -0
  44. package/backend/terminal-attach-checkpoint.js +74 -0
  45. package/backend/terminal-exit-quiescence.js +39 -0
  46. package/backend/terminal-reducer-flow-control.js +97 -0
  47. package/backend/terminal-screen-state.js +11 -2
  48. package/backend/terminal-screen-worker-pool.js +59 -6
  49. package/backend/terminal-screen-worker-thread.js +97 -57
  50. package/backend/terminal-screen-worker.js +133 -51
  51. package/backend/terminal-state-serialization.js +127 -0
  52. package/backend/terminal-status.js +23 -4
  53. package/backend/usage-monitor.js +176 -17
  54. package/backend/workspace-directory.js +232 -0
  55. package/backend/workspace-file-router.js +182 -76
  56. package/backend/workspace-file-service.js +319 -4
  57. package/backend/workspace-root-registry.js +164 -0
  58. package/dist/assets/App-DWsa7DXG.js +206 -0
  59. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview-UlVd0O4D.js} +92 -92
  60. package/dist/assets/FileEditorPane-Dxx4rKOg.js +2 -0
  61. package/dist/assets/IconGlyphs-AU22gPDY.js +1 -0
  62. package/dist/assets/ProjectFilesSection-DE7Nb9Jk.js +12 -0
  63. package/dist/assets/ReviewPage-CeDKDyZh.js +3 -0
  64. package/dist/assets/code-dark-DPiuyeSp.css +1 -0
  65. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  66. package/dist/assets/{index-BrbljRqn.js → index-CUFPJoa5.js} +3 -3
  67. package/dist/assets/main-Bqtb5kuz.css +1 -0
  68. package/dist/assets/{monaco.contribution-CyLosfZI.js → monaco.contribution-BXz-lfFB.js} +2 -2
  69. package/dist/assets/{tsMode-DGZTlTj8.js → tsMode-B0oUTcXQ.js} +1 -1
  70. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  71. package/dist/assets/workspace-editor-monaco-BICJESG0.js +4 -0
  72. package/dist/assets/workspace-editor-monaco-DRJ7IaXk.js +1 -0
  73. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  74. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  75. package/dist/farming-2/site.webmanifest +2 -0
  76. package/dist/index.html +1 -1
  77. package/frontend/farming-net/app.css +625 -0
  78. package/frontend/farming-net/app.js +268 -0
  79. package/frontend/farming-net/index.html +86 -0
  80. package/frontend/reading-anchor.js +198 -0
  81. package/frontend/session-bridge.js +12 -3
  82. package/frontend/session-modal-bridge.js +5 -12
  83. package/frontend/skins/crt/app.js +1978 -793
  84. package/frontend/skins/crt/index.html +313 -23
  85. package/frontend/skins/crt/styles/billing.css +294 -223
  86. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  87. package/frontend/terminal-replay.js +372 -0
  88. package/package.json +10 -3
  89. package/shared/browser-protocol.d.ts +5 -0
  90. package/shared/browser-protocol.js +130 -0
  91. package/dist/assets/App-8dYAM6ql.js +0 -124
  92. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  93. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  94. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  95. package/dist/assets/ReviewPage-BaXu1ZdX.js +0 -3
  96. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  97. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  98. package/dist/assets/main-D073SnW4.css +0 -1
  99. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  100. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  101. package/dist/assets/qoder-gHCinseV.svg +0 -1
  102. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  103. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  104. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  105. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  106. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  107. /package/dist/assets/{useWorkspaceMenuKeyboard-CneKAZUJ.js → useWorkspaceMenuKeyboard-Brws6Ar9.js} +0 -0
@@ -5,6 +5,7 @@ let focusedAgentId = null;
5
5
  let keyMap = {};
6
6
  let agents = [];
7
7
  let waitingForAgent = false;
8
+ let workspaceDirectoryPrompt = null;
8
9
  let selectedAgentIndex = null;
9
10
  let terminal = null;
10
11
  let fitAddon = null;
@@ -53,15 +54,23 @@ let billingError = '';
53
54
  let billingRequestSequence = 0;
54
55
  let billingAbortController = null;
55
56
  let billingRefreshTimer = null;
57
+ let billingLiveDayRefreshTimer = null;
56
58
  let billingCanvasFrame = null;
57
59
  let billingMode = 'days';
58
60
  let billingSelectedDate = '';
61
+ let billingSelectedHour = null;
59
62
  let billingDailyRenderSignature = '';
60
63
  let billingDayDetail = null;
61
64
  let billingDayDetailLoading = false;
62
65
  let billingDayDetailError = '';
63
66
  let billingDayDetailRequestSequence = 0;
64
67
  let billingDayDetailAbortController = null;
68
+ let billingDayDetailRetryTimer = null;
69
+ let billingDisplayedTotalDate = '';
70
+ let billingDisplayedTotalValue = null;
71
+ let billingTotalAnimationFrame = null;
72
+ let billingTotalAnimationTarget = null;
73
+ const billingAnimatedMetrics = new Map();
65
74
  const billingDayDetailCache = new Map();
66
75
  let crtAgentPage = 0;
67
76
  let crtAgentPageSize = 1;
@@ -71,8 +80,9 @@ let dashboardRenderDeferred = false;
71
80
  let lastCrtDashboardSignature = '';
72
81
  let crtPreviewRenderTimer = null;
73
82
  const pendingCrtPreviewRenders = new Map();
83
+ const crtStructuredPreviewCache = new Map();
84
+ const crtStructuredPreviewTimers = new Map();
74
85
  let sessionRuntime = null;
75
- let legacySessionPoller = null;
76
86
  let structuredSessionPoller = null;
77
87
  let structuredSessionLoading = false;
78
88
  let structuredSessionRenderedAt = '';
@@ -92,19 +102,13 @@ let structuredComposerRestoreFocusAfterInterrupt = false;
92
102
  let runtimeSwitchPending = false;
93
103
  let pendingRuntimeSwitchAgentId = '';
94
104
  let runtimeSwitchRequestSequence = 0;
95
- let terminalInputBridge = null;
96
- let terminalInputComposing = false;
97
- let terminalInputLastBackspaceAt = 0;
98
- let terminalInputLastDeleteAt = 0;
99
- let terminalInputPendingTexts = [];
105
+ let crtTerminalReplication = null;
100
106
  const terminalPreviewSnapshots = new Map();
101
107
  const crtBrandPulseTimers = new Map();
102
- const SESSION_INPUT_SETTINGS = {
103
- // xterm's native textarea is the same low-latency input path used by Farming Code.
104
- imeEnabled: false
105
- };
106
108
  const SESSION_LINK_LIMIT = 6;
109
+ const CRT_PROTOCOL_VERSION = 2;
107
110
  const CRT_PREVIEW_RENDER_INTERVAL_MS = 1000;
111
+ const CRT_STRUCTURED_PREVIEW_REFRESH_MS = 240;
108
112
  const CRT_AGENT_CARD_MIN_WIDTH = 200;
109
113
  const CRT_AGENT_CARD_MIN_HEIGHT = 160;
110
114
  const CRT_AGENT_GRID_GAP = 15;
@@ -112,7 +116,15 @@ const CRT_AGENT_GRID_PADDING = 20;
112
116
  const CRT_SEARCH_DEBOUNCE_MS = 180;
113
117
  const CRT_SEARCH_RESULT_LIMIT = 100;
114
118
  const CRT_BILLING_REFRESH_MS = 30_000;
119
+ const CRT_BILLING_LIVE_DAY_REFRESH_MS = 5_000;
115
120
  const CRT_BILLING_DAY_DETAIL_CACHE_MS = 30_000;
121
+ const CRT_BILLING_TOTAL_ANIMATION_MS = 900;
122
+ const CRT_BILLING_DAY_DETAIL_RETRY_MS = 750;
123
+ const CRT_BILLING_DAY_DETAIL_MAX_RETRIES = 4;
124
+ const CRT_TERMINAL_CHECKPOINT_REQUEST_TIMEOUT_MS = 5_000;
125
+ const CRT_TERMINAL_RESIZE_SETTLE_MS = 250;
126
+ const CRT_TERMINAL_MIN_COLS = 40;
127
+ const CRT_TERMINAL_MIN_ROWS = 10;
116
128
  const CRT_AGENT_DISPLAY_NAMES = {
117
129
  qwen: 'Qwen Code',
118
130
  codex: 'Codex',
@@ -129,6 +141,7 @@ const CRT_AGENT_DISPLAY_NAMES = {
129
141
  const CRT_TITLE_STATUS_PREFIX_PATTERN = /^[\s**✳✱✲✶·•◇✋✦⏲\u2800-\u28FF]+/u;
130
142
  const CRT_QODER_RUNTIME_TITLE_PATTERN = /^[◇✋✦⏲]/u;
131
143
  const RUNTIME_PATHS = typeof window !== 'undefined' ? window.FarmingRuntimePaths : null;
144
+ const TERMINAL_REPLAY = typeof window !== 'undefined' ? window.FarmingTerminalReplay : null;
132
145
 
133
146
  function farmingApiPath(path) {
134
147
  return RUNTIME_PATHS ? RUNTIME_PATHS.apiPath(path) : `/api${path.startsWith('/') ? path : `/${path}`}`;
@@ -362,9 +375,7 @@ function crtHistorySessionDisplayKey(item) {
362
375
  function buildCrtHistoryItems({ taskHistory = [], agents: agentRecords = [], sessions = [], mainPageSessionKeys = [] } = {}) {
363
376
  const liveAgents = agentRecords.filter((agent) => (
364
377
  agent.isMain !== true
365
- && agent.archived !== true
366
- && agent.status !== 'dead'
367
- && agent.status !== 'stopped'
378
+ && isCrtLiveAgent(agent)
368
379
  ));
369
380
  const claimedSessionKeys = new Set(liveAgents.map((agent) => (
370
381
  agent.providerSessionKey || (() => {
@@ -427,9 +438,7 @@ function buildCrtSearchResults({ query = '', agents: agentRecords = [], sessions
427
438
  agent
428
439
  && agent.id !== mainAgentId
429
440
  && agent.isMain !== true
430
- && agent.archived !== true
431
- && agent.status !== 'dead'
432
- && agent.status !== 'stopped'
441
+ && isCrtLiveAgent(agent)
433
442
  ));
434
443
  const claimedSessionKeys = new Set(liveAgents.map((agent) => {
435
444
  if (agent.providerSessionKey) return agent.providerSessionKey;
@@ -478,6 +487,8 @@ function buildCrtSearchResults({ query = '', agents: agentRecords = [], sessions
478
487
  }
479
488
 
480
489
  function getCrtNavigationScope() {
490
+ const sessionModal = document.getElementById('session-modal');
491
+ if (sessionModal && sessionModal.classList.contains('active')) return sessionModal;
481
492
  const settingsModal = document.getElementById('settings-modal');
482
493
  if (settingsModal && settingsModal.classList.contains('active')) return settingsModal;
483
494
  const inputDialog = document.getElementById('input-dialog');
@@ -669,72 +680,10 @@ function disposeTerminal() {
669
680
  fitAddon = null;
670
681
  }
671
682
 
672
- function destroyTerminalInputBridge() {
673
- if (terminalInputBridge) {
674
- terminalInputBridge.remove();
675
- terminalInputBridge = null;
676
- }
677
- clearPendingPrintableInput();
678
- terminalInputComposing = false;
679
- document.body.removeAttribute('data-ime-input-focused');
680
- document.body.removeAttribute('data-ime-composing');
681
- }
682
-
683
- function clearPendingPrintableInput() {
684
- terminalInputPendingTexts.forEach((pending) => {
685
- clearTimeout(pending.timeoutId);
686
- });
687
- terminalInputPendingTexts = [];
688
- }
689
-
690
- function resetTerminalInputBridgeValue() {
691
- if (!terminalInputBridge) return;
692
- terminalInputBridge.value = ' ';
693
- terminalInputBridge.setSelectionRange(1, 1);
694
- }
695
-
696
- function schedulePrintableInput(text) {
697
- const pending = {
698
- text,
699
- timeoutId: setTimeout(() => {
700
- if (!terminalInputComposing) {
701
- sendTerminalInput(text);
702
- resetTerminalInputBridgeValue();
703
- }
704
- terminalInputPendingTexts = terminalInputPendingTexts.filter((item) => item !== pending);
705
- }, 0)
706
- };
707
- terminalInputPendingTexts.push(pending);
708
- }
709
-
710
- function focusTerminalInputBridge() {
711
- if (!SESSION_INPUT_SETTINGS.imeEnabled) return;
712
- if (!terminalInputBridge) return;
713
- if (isOverlayBlockingTerminalInput()) return;
714
- syncTerminalInputBridgePosition();
715
- terminalInputBridge.focus();
716
- resetTerminalInputBridgeValue();
717
- }
718
-
719
- function isOverlayBlockingTerminalInput() {
720
- const inputDialog = document.getElementById('input-dialog');
721
- if (inputDialog && inputDialog.classList.contains('active')) {
722
- return true;
723
- }
724
-
725
- const settingsModal = document.getElementById('settings-modal');
726
- if (settingsModal && settingsModal.classList.contains('active')) {
727
- return true;
728
- }
729
-
730
- return false;
731
- }
732
-
733
683
  function focusSessionTerminal() {
734
684
  if (terminal && typeof terminal.focus === 'function') {
735
685
  terminal.focus();
736
686
  }
737
- focusTerminalInputBridge();
738
687
  }
739
688
 
740
689
  function getDocumentSelectionText() {
@@ -899,11 +848,24 @@ async function pasteTerminalText(text) {
899
848
  return false;
900
849
  }
901
850
 
902
- sendTerminalInput(text.replace(/\r\n/g, '\n'));
851
+ const normalized = text.replace(/\r\n/g, '\n');
852
+ if (terminal && typeof terminal.paste === 'function') {
853
+ terminal.paste(normalized);
854
+ } else {
855
+ sendTerminalInput(normalized);
856
+ }
903
857
  focusSessionTerminal();
904
858
  return true;
905
859
  }
906
860
 
861
+ function isCrtNativeTerminalPasteTarget(target) {
862
+ return Boolean(
863
+ target &&
864
+ typeof target.closest === 'function' &&
865
+ target.closest('#terminal-output .xterm')
866
+ );
867
+ }
868
+
907
869
  async function pasteFromClipboard() {
908
870
  if (!navigator.clipboard || !navigator.clipboard.readText) {
909
871
  return false;
@@ -918,34 +880,6 @@ async function pasteFromClipboard() {
918
880
  }
919
881
  }
920
882
 
921
- function routeSessionKey(event) {
922
- if (!focusedAgentId || terminalInputComposing || isBrowserShortcut(event)) {
923
- return false;
924
- }
925
-
926
- if (event.ctrlKey && !event.metaKey && !event.altKey && event.key.length === 1 && event.key !== 'Enter') {
927
- const controlChar = getControlChar(event.key);
928
- if (controlChar) {
929
- sendTerminalInput(controlChar);
930
- return true;
931
- }
932
- }
933
-
934
- const sequence = getTerminalSequenceForKey(event);
935
- if (sequence) {
936
- sendTerminalInput(sequence);
937
- return true;
938
- }
939
-
940
- return false;
941
- }
942
-
943
- function getControlChar(key) {
944
- const lower = key.toLowerCase();
945
- if (!/^[a-z]$/.test(lower)) return null;
946
- return String.fromCharCode(lower.charCodeAt(0) - 96);
947
- }
948
-
949
883
  function getSessionClient() {
950
884
  if (!window.FarmingSessionBridge || !window.FarmingSessionBridge.createClient) {
951
885
  return null;
@@ -961,291 +895,6 @@ function getSessionClient() {
961
895
  return sessionClient;
962
896
  }
963
897
 
964
- function getTerminalSequenceForKey(event) {
965
- const { key, shiftKey, altKey, ctrlKey, metaKey } = event;
966
-
967
- if (metaKey) return null;
968
-
969
- if (altKey && !ctrlKey) {
970
- if (key === 'ArrowLeft') return '\x1bb';
971
- if (key === 'ArrowRight') return '\x1bf';
972
- if (key === 'Backspace') return '\x17';
973
- }
974
-
975
- switch (key) {
976
- case 'Enter':
977
- return '\r';
978
- case 'Backspace':
979
- return '\x7f';
980
- case 'Tab':
981
- return shiftKey ? '\x1b[Z' : '\t';
982
- case 'Delete':
983
- return '\x1b[3~';
984
- case 'ArrowUp':
985
- return '\x1b[A';
986
- case 'ArrowDown':
987
- return '\x1b[B';
988
- case 'ArrowRight':
989
- return '\x1b[C';
990
- case 'ArrowLeft':
991
- return '\x1b[D';
992
- case 'Home':
993
- return '\x1b[H';
994
- case 'End':
995
- return '\x1b[F';
996
- case 'PageUp':
997
- return '\x1b[5~';
998
- case 'PageDown':
999
- return '\x1b[6~';
1000
- default:
1001
- return null;
1002
- }
1003
- }
1004
-
1005
- function setupTerminalInputBridge() {
1006
- destroyTerminalInputBridge();
1007
-
1008
- if (!SESSION_INPUT_SETTINGS.imeEnabled) {
1009
- return;
1010
- }
1011
-
1012
- const input = document.createElement('input');
1013
- input.type = 'text';
1014
- input.setAttribute('autocomplete', 'off');
1015
- input.setAttribute('autocorrect', 'off');
1016
- input.setAttribute('autocapitalize', 'off');
1017
- input.setAttribute('spellcheck', 'false');
1018
- input.setAttribute('inputmode', 'text');
1019
- input.setAttribute('aria-hidden', 'true');
1020
- input.style.position = 'absolute';
1021
- input.style.top = '0';
1022
- input.style.left = '0';
1023
- input.style.width = '200px';
1024
- input.style.height = '24px';
1025
- input.style.opacity = '0.01';
1026
- input.style.background = 'transparent';
1027
- input.style.color = 'transparent';
1028
- input.style.caretColor = 'transparent';
1029
- input.style.border = 'none';
1030
- input.style.outline = 'none';
1031
- input.style.fontSize = `${getCrtTerminalFontSize()}px`;
1032
- input.style.fontFamily = TERMINAL_FONT_FAMILY;
1033
- input.style.pointerEvents = 'none';
1034
- input.style.zIndex = '2';
1035
-
1036
- input.addEventListener('compositionstart', () => {
1037
- syncTerminalInputBridgePosition();
1038
- clearPendingPrintableInput();
1039
- terminalInputComposing = true;
1040
- document.body.setAttribute('data-ime-composing', 'true');
1041
- });
1042
-
1043
- input.addEventListener('compositionend', (event) => {
1044
- terminalInputComposing = false;
1045
- document.body.removeAttribute('data-ime-composing');
1046
- clearPendingPrintableInput();
1047
- if (event.data) {
1048
- sendTerminalInput(event.data);
1049
- }
1050
- resetTerminalInputBridgeValue();
1051
- });
1052
-
1053
- input.addEventListener('compositionupdate', syncTerminalInputBridgePosition);
1054
-
1055
- input.addEventListener('beforeinput', (event) => {
1056
- if (terminalInputComposing) {
1057
- return;
1058
- }
1059
-
1060
- const inputEvent = event;
1061
- if (inputEvent.inputType === 'insertText' && inputEvent.data) {
1062
- clearPendingPrintableInput();
1063
- event.preventDefault();
1064
- sendTerminalInput(inputEvent.data);
1065
- resetTerminalInputBridgeValue();
1066
- }
1067
- });
1068
-
1069
- input.addEventListener('input', (event) => {
1070
- if (terminalInputComposing) {
1071
- return;
1072
- }
1073
-
1074
- const inputEvent = event;
1075
- if (inputEvent.inputType === 'deleteContentBackward') {
1076
- clearPendingPrintableInput();
1077
- const now = Date.now();
1078
- if (now - terminalInputLastBackspaceAt > 50) {
1079
- sendTerminalInput('\x7f');
1080
- }
1081
- terminalInputLastBackspaceAt = now;
1082
- requestAnimationFrame(() => {
1083
- if (document.activeElement === input) {
1084
- resetTerminalInputBridgeValue();
1085
- }
1086
- });
1087
- return;
1088
- }
1089
-
1090
- if (inputEvent.inputType === 'deleteContentForward') {
1091
- clearPendingPrintableInput();
1092
- const now = Date.now();
1093
- if (now - terminalInputLastDeleteAt > 50) {
1094
- sendTerminalInput('\x1b[3~');
1095
- }
1096
- terminalInputLastDeleteAt = now;
1097
- requestAnimationFrame(() => {
1098
- if (document.activeElement === input) {
1099
- resetTerminalInputBridgeValue();
1100
- }
1101
- });
1102
- return;
1103
- }
1104
-
1105
- clearPendingPrintableInput();
1106
- resetTerminalInputBridgeValue();
1107
- });
1108
-
1109
- input.addEventListener('keydown', (event) => {
1110
- if (event.defaultPrevented) {
1111
- return;
1112
- }
1113
-
1114
- if (isOverlayBlockingTerminalInput()) {
1115
- return;
1116
- }
1117
-
1118
- if (isBrowserShortcut(event)) {
1119
- return;
1120
- }
1121
-
1122
- if ((event.ctrlKey || event.metaKey) && event.key === 'Escape') {
1123
- return;
1124
- }
1125
-
1126
- if ((event.ctrlKey || event.metaKey) && (event.key === 'k' || event.key === 'K')) {
1127
- return;
1128
- }
1129
-
1130
- if (terminalInputComposing) {
1131
- return;
1132
- }
1133
-
1134
- if (['Enter', 'Tab', 'Escape'].includes(event.key)) {
1135
- event.preventDefault();
1136
- }
1137
-
1138
- if (event.ctrlKey && !event.metaKey && !event.altKey && event.key.length === 1 && event.key !== 'Enter') {
1139
- clearPendingPrintableInput();
1140
- const controlChar = getControlChar(event.key);
1141
- if (controlChar) {
1142
- event.preventDefault();
1143
- sendTerminalInput(controlChar);
1144
- resetTerminalInputBridgeValue();
1145
- }
1146
- return;
1147
- }
1148
-
1149
- if (!event.ctrlKey && !event.metaKey && !event.altKey && event.key.length === 1) {
1150
- schedulePrintableInput(event.key);
1151
- return;
1152
- }
1153
-
1154
- if (event.key === 'Backspace') {
1155
- clearPendingPrintableInput();
1156
- terminalInputLastBackspaceAt = Date.now();
1157
- sendTerminalInput('\x7f');
1158
- requestAnimationFrame(() => {
1159
- if (document.activeElement === input) {
1160
- resetTerminalInputBridgeValue();
1161
- }
1162
- });
1163
- return;
1164
- }
1165
-
1166
- const sequence = getTerminalSequenceForKey(event);
1167
- if (!sequence) {
1168
- return;
1169
- }
1170
-
1171
- if (event.key === 'Delete') {
1172
- terminalInputLastDeleteAt = Date.now();
1173
- }
1174
-
1175
- event.preventDefault();
1176
- sendTerminalInput(sequence);
1177
- if (event.key !== 'Tab') {
1178
- resetTerminalInputBridgeValue();
1179
- }
1180
- });
1181
-
1182
- input.addEventListener('focus', () => {
1183
- document.body.setAttribute('data-ime-input-focused', 'true');
1184
- requestAnimationFrame(() => {
1185
- if (document.activeElement === input) {
1186
- resetTerminalInputBridgeValue();
1187
- }
1188
- });
1189
- });
1190
-
1191
- input.addEventListener('blur', () => {
1192
- document.body.removeAttribute('data-ime-input-focused');
1193
- setTimeout(() => {
1194
- const sessionActive = document.getElementById('session-modal')?.classList.contains('active');
1195
- if (!sessionActive || terminalInputComposing || !terminalInputBridge || isOverlayBlockingTerminalInput()) {
1196
- return;
1197
- }
1198
- focusTerminalInputBridge();
1199
- }, 0);
1200
- });
1201
-
1202
- const sessionModal = document.getElementById('session-modal');
1203
- if (sessionModal) {
1204
- sessionModal.appendChild(input);
1205
- } else {
1206
- document.body.appendChild(input);
1207
- }
1208
- terminalInputBridge = input;
1209
- syncTerminalInputBridgePosition();
1210
- }
1211
-
1212
- function calculateTerminalInputBridgePosition(cursor, dimensions, screenRect, parentRect) {
1213
- if (!cursor || !dimensions || !screenRect || !parentRect) return null;
1214
- if (dimensions.cols <= 0 || dimensions.rows <= 0) return null;
1215
-
1216
- const cellWidth = screenRect.width / dimensions.cols;
1217
- const cellHeight = screenRect.height / dimensions.rows;
1218
- return {
1219
- left: Math.max(0, screenRect.left - parentRect.left + cursor.x * cellWidth),
1220
- top: Math.max(0, screenRect.top - parentRect.top + cursor.y * cellHeight),
1221
- height: Math.max(getCrtTerminalFontSize() + 2, cellHeight)
1222
- };
1223
- }
1224
-
1225
- function syncTerminalInputBridgePosition() {
1226
- if (!terminalInputBridge || !terminal) return;
1227
- const sessionModal = document.getElementById('session-modal');
1228
- const terminalElement = terminal.element;
1229
- const screen = terminalElement && terminalElement.querySelector
1230
- ? terminalElement.querySelector('.xterm-screen')
1231
- : null;
1232
- const activeBuffer = terminal.buffer && terminal.buffer.active;
1233
- if (!sessionModal || !(screen instanceof HTMLElement) || !activeBuffer) return;
1234
-
1235
- const position = calculateTerminalInputBridgePosition(
1236
- { x: activeBuffer.cursorX, y: activeBuffer.cursorY },
1237
- { cols: terminal.cols, rows: terminal.rows },
1238
- screen.getBoundingClientRect(),
1239
- sessionModal.getBoundingClientRect()
1240
- );
1241
- if (!position) return;
1242
-
1243
- terminalInputBridge.style.left = `${position.left}px`;
1244
- terminalInputBridge.style.top = `${position.top}px`;
1245
- terminalInputBridge.style.height = `${position.height}px`;
1246
- terminalInputBridge.style.lineHeight = `${position.height}px`;
1247
- }
1248
-
1249
898
  function normalizeCrtTerminalFontSize(value) {
1250
899
  const fontSize = Number(value);
1251
900
  if (!Number.isFinite(fontSize)) return DEFAULT_TERMINAL_FONT_SIZE;
@@ -1281,24 +930,32 @@ async function createTerminalInstance(options = {}) {
1281
930
  return null;
1282
931
  }
1283
932
 
1284
- function showCrtWebglFailure(error) {
933
+ function showCrtTerminalFailure(titleText, error, detailText) {
1285
934
  const terminalContainer = document.getElementById('terminal-output');
1286
935
  if (!terminalContainer) return;
1287
936
  terminalContainer.querySelector('.crt-webgl-error')?.remove();
1288
937
  const panel = document.createElement('div');
1289
938
  panel.className = 'crt-webgl-error';
1290
939
  const title = document.createElement('strong');
1291
- title.textContent = 'CRT WEBGL ERROR';
940
+ title.textContent = titleText;
1292
941
  const message = document.createElement('span');
1293
942
  message.textContent = error && error.message
1294
943
  ? error.message
1295
- : 'Farming CRT requires WebGL2 hardware acceleration.';
944
+ : String(error || 'Terminal unavailable');
1296
945
  const detail = document.createElement('small');
1297
- detail.textContent = 'The Agent is still running. Close and reopen this terminal after WebGL is available.';
946
+ detail.textContent = detailText;
1298
947
  panel.append(title, message, detail);
1299
948
  terminalContainer.appendChild(panel);
1300
949
  }
1301
950
 
951
+ function showCrtWebglFailure(error) {
952
+ showCrtTerminalFailure(
953
+ 'CRT WEBGL ERROR',
954
+ error,
955
+ 'The Agent is still running. Close and reopen this terminal after WebGL is available.'
956
+ );
957
+ }
958
+
1302
959
  function shouldUseLiveSessionText(agent) {
1303
960
  return Boolean(agent && agent.sessionSource === 'live-text');
1304
961
  }
@@ -1353,10 +1010,32 @@ function getCrtPreviewCellStyle(cell) {
1353
1010
  };
1354
1011
  }
1355
1012
 
1013
+ function getCrtTerminalSnapshotRows(snapshot) {
1014
+ if (!snapshot || !Array.isArray(snapshot.cells)) return [];
1015
+ let lastMeaningfulRow = -1;
1016
+ snapshot.cells.forEach((cells, rowIndex) => {
1017
+ if (!Array.isArray(cells)) return;
1018
+ const meaningful = cells.some((cell) => {
1019
+ if (!cell || cell.width === 0) return false;
1020
+ const attributes = cell.attributes || 0;
1021
+ const hasBackground = Number.isFinite(cell.bg) && cell.bg >= 0;
1022
+ return String(cell.char || '').trim() !== '' || hasBackground || Boolean(attributes & 0x10);
1023
+ });
1024
+ if (meaningful) lastMeaningfulRow = rowIndex;
1025
+ });
1026
+ const cursorRow = snapshot.cursorVisible === true && Number.isInteger(snapshot.cursorY)
1027
+ ? Math.min(snapshot.cells.length - 1, Math.max(0, snapshot.cursorY))
1028
+ : -1;
1029
+ const lastVisibleRow = Math.max(lastMeaningfulRow, cursorRow);
1030
+ return lastVisibleRow >= 0 ? snapshot.cells.slice(0, lastVisibleRow + 1) : [];
1031
+ }
1032
+
1356
1033
  function renderCrtTerminalSnapshot(container, snapshot) {
1357
- if (!container || !snapshot || !Array.isArray(snapshot.cells)) return false;
1034
+ if (!container) return false;
1035
+ const rows = getCrtTerminalSnapshotRows(snapshot);
1036
+ if (rows.length === 0) return false;
1358
1037
  container.classList.add('terminal-snapshot');
1359
- snapshot.cells.forEach((cells) => {
1038
+ rows.forEach((cells) => {
1360
1039
  const row = document.createElement('div');
1361
1040
  row.className = 'terminal-snapshot-row';
1362
1041
  let currentSpan = null;
@@ -1378,6 +1057,104 @@ function renderCrtTerminalSnapshot(container, snapshot) {
1378
1057
  return true;
1379
1058
  }
1380
1059
 
1060
+ function crtStructuredPreviewStatus(agent, cached) {
1061
+ if (cached && cached.error && !cached.preview) return 'PREVIEW OFFLINE';
1062
+ if ((!cached || !cached.preview) && (!cached || cached.loading)) return 'SYNCING';
1063
+ const status = String(structuredRuntimeStatus(agent) || 'idle').toUpperCase().replaceAll('-', ' ');
1064
+ if (status === 'WAITING FOR PERMISSION') return 'PERMISSION NEEDED';
1065
+ if (status === 'WAITING FOR INPUT') return 'INPUT NEEDED';
1066
+ return status;
1067
+ }
1068
+
1069
+ function appendCrtStructuredPreviewLine(container, role, label, text) {
1070
+ if (!text) return;
1071
+ const line = document.createElement('div');
1072
+ line.className = `agent-chat-preview-line ${role}`;
1073
+ line.dataset.previewRole = role;
1074
+ const roleLabel = document.createElement('span');
1075
+ roleLabel.className = 'agent-chat-preview-role';
1076
+ roleLabel.textContent = label;
1077
+ const content = document.createElement('span');
1078
+ content.className = 'agent-chat-preview-text';
1079
+ content.textContent = text;
1080
+ line.append(roleLabel, content);
1081
+ container.appendChild(line);
1082
+ }
1083
+
1084
+ function renderCrtStructuredPreview(output, agent) {
1085
+ output.classList.add('structured-preview');
1086
+ const cached = crtStructuredPreviewCache.get(agent.id) || null;
1087
+ const preview = cached && cached.preview;
1088
+ const runtimeStatus = String(structuredRuntimeStatus(agent) || '').toLowerCase();
1089
+ const active = ['connecting', 'working', 'waiting-for-permission', 'waiting-for-input', 'interrupting']
1090
+ .includes(runtimeStatus);
1091
+ const panel = document.createElement('div');
1092
+ panel.className = `agent-chat-preview${active ? ' active' : ''}`;
1093
+ panel.dataset.previewKind = 'chat';
1094
+
1095
+ const meta = document.createElement('div');
1096
+ meta.className = 'agent-chat-preview-meta';
1097
+ const channel = document.createElement('span');
1098
+ channel.textContent = `CHAT / ${structuredRuntimeKind(agent)}`;
1099
+ const stateLabel = document.createElement('span');
1100
+ stateLabel.className = 'agent-chat-preview-state';
1101
+ const signal = document.createElement('i');
1102
+ signal.setAttribute('aria-hidden', 'true');
1103
+ const stateText = document.createElement('span');
1104
+ stateText.textContent = crtStructuredPreviewStatus(agent, cached);
1105
+ stateLabel.append(signal, stateText);
1106
+ meta.append(channel, stateLabel);
1107
+ panel.appendChild(meta);
1108
+
1109
+ const trail = document.createElement('div');
1110
+ trail.className = 'agent-chat-preview-trail';
1111
+ panel.appendChild(trail);
1112
+
1113
+ if (preview) {
1114
+ const messageLines = Array.isArray(preview.messageLines) && preview.messageLines.length > 0
1115
+ ? preview.messageLines
1116
+ : [
1117
+ { role: 'user', label: 'YOU', text: preview.userText },
1118
+ { role: 'assistant', label: 'AGENT', text: preview.assistantText },
1119
+ ];
1120
+ messageLines.forEach((line) => {
1121
+ appendCrtStructuredPreviewLine(trail, line.role, line.label, line.text);
1122
+ });
1123
+ if (active && preview.activityText && preview.activityText !== preview.assistantText) {
1124
+ appendCrtStructuredPreviewLine(trail, 'activity', 'NOW', preview.activityText);
1125
+ }
1126
+ }
1127
+
1128
+ if (!trail.querySelector('.agent-chat-preview-line')) {
1129
+ const empty = document.createElement('div');
1130
+ empty.className = 'agent-chat-preview-empty';
1131
+ empty.textContent = cached && cached.error
1132
+ ? 'Conversation preview unavailable'
1133
+ : active ? 'Establishing conversation stream…' : 'Ready for the first message';
1134
+ trail.appendChild(empty);
1135
+ }
1136
+
1137
+ output.replaceChildren(panel);
1138
+ scheduleCrtStructuredPreviewRefresh(agent);
1139
+ }
1140
+
1141
+ function renderCrtAgentOutput(output, agent, { main = false } = {}) {
1142
+ output.classList.remove('structured-preview');
1143
+ if (isStructuredRuntimeAgent(agent)) {
1144
+ renderCrtStructuredPreview(output, agent);
1145
+ return;
1146
+ }
1147
+ const outputTail = document.createElement('div');
1148
+ outputTail.className = 'agent-output-tail';
1149
+ const cleanOutput = getAgentDisplayText(agent);
1150
+ if (!renderCrtTerminalSnapshot(outputTail, agent.previewSnapshot)) {
1151
+ outputTail.textContent = main
1152
+ ? cleanOutput.slice(-150) || 'No output yet...'
1153
+ : cleanOutput || 'No output yet...';
1154
+ }
1155
+ output.replaceChildren(outputTail);
1156
+ }
1157
+
1381
1158
  function crtDashboardStateSignature(value) {
1382
1159
  if (!value || !Array.isArray(value.agents)) return '';
1383
1160
  return JSON.stringify([
@@ -1417,8 +1194,17 @@ function renderCrtDashboardIfNeeded(force = false) {
1417
1194
  return true;
1418
1195
  }
1419
1196
 
1197
+ function scheduleCrtRenderedStructuredPreviews() {
1198
+ if (typeof document === 'undefined' || !state) return;
1199
+ document.querySelectorAll('#map-area .agent-block[data-agent-id], #main-agent-block[data-agent-id]')
1200
+ .forEach((block) => {
1201
+ const agent = state.agents.find((candidate) => candidate.id === block.dataset.agentId);
1202
+ if (agent) scheduleCrtStructuredPreviewRefresh(agent);
1203
+ });
1204
+ }
1205
+
1420
1206
  function updateCrtAgentPreviewCard(agent) {
1421
- if (typeof document === 'undefined' || !agent) return false;
1207
+ if (typeof document === 'undefined' || !isCrtLiveAgent(agent)) return false;
1422
1208
  const block = Array.from(document.querySelectorAll('[data-agent-id]'))
1423
1209
  .find((candidate) => candidate.dataset.agentId === agent.id);
1424
1210
  if (!block) return false;
@@ -1441,15 +1227,7 @@ function updateCrtAgentPreviewCard(agent) {
1441
1227
  status.textContent = [agent.status, agent.activityLevel, projectName].filter(Boolean).join(' | ');
1442
1228
  }
1443
1229
 
1444
- const outputTail = document.createElement('div');
1445
- outputTail.className = 'agent-output-tail';
1446
- const cleanOutput = getAgentDisplayText(agent);
1447
- if (!renderCrtTerminalSnapshot(outputTail, agent.previewSnapshot)) {
1448
- outputTail.textContent = isMain
1449
- ? cleanOutput.slice(-150) || 'No output yet...'
1450
- : cleanOutput || 'No output yet...';
1451
- }
1452
- output.replaceChildren(outputTail);
1230
+ renderCrtAgentOutput(output, agent, { main: isMain });
1453
1231
  lastCrtDashboardSignature = crtDashboardStateSignature(state);
1454
1232
  return true;
1455
1233
  }
@@ -1465,8 +1243,10 @@ function flushCrtPreviewCardRenders() {
1465
1243
  }
1466
1244
 
1467
1245
  pending.forEach(({ agent, previousSnapshot, previousText, previewChanged }) => {
1468
- if (!updateCrtAgentPreviewCard(agent)) {
1469
- if (agent.id !== state.mainAgentId && !isCrtAgentOnCurrentPage(agent.id)) {
1246
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
1247
+ if (!isCrtLiveAgent(currentAgent)) return;
1248
+ if (!updateCrtAgentPreviewCard(currentAgent)) {
1249
+ if (currentAgent.id !== state.mainAgentId && !isCrtAgentOnCurrentPage(currentAgent.id)) {
1470
1250
  if (previewChanged) pulseCrtBrandForAgent(agent.id);
1471
1251
  return;
1472
1252
  }
@@ -1481,6 +1261,7 @@ function flushCrtPreviewCardRenders() {
1481
1261
  }
1482
1262
 
1483
1263
  function scheduleCrtPreviewCardRender(agent, previousSnapshot, previousText, previewChanged) {
1264
+ if (!isCrtLiveAgent(agent)) return;
1484
1265
  const existing = pendingCrtPreviewRenders.get(agent.id);
1485
1266
  pendingCrtPreviewRenders.set(agent.id, {
1486
1267
  agent,
@@ -1617,10 +1398,10 @@ function isCrtAgentWorking(agent) {
1617
1398
  if (activity === 'busy') return true;
1618
1399
  if (activity === 'idle' || activity === 'exited') return false;
1619
1400
  if (agent.terminalStatus && agent.terminalStatus.busy === true) return true;
1401
+ const runtime = agent.runtimeBinding || { kind: 'terminal' };
1620
1402
  if (
1621
- agent.providerSessionProvider === 'codex' &&
1622
- agent.codexRuntimeMode === 'app-server' &&
1623
- ['working', 'waiting-for-input', 'interrupting'].includes(agent.codexAppServerState || '')
1403
+ runtime.kind === 'app-server' &&
1404
+ ['working', 'waiting-for-input', 'interrupting'].includes(runtime.state || '')
1624
1405
  ) {
1625
1406
  return true;
1626
1407
  }
@@ -1850,6 +1631,150 @@ function buildTerminalLineProjection(line) {
1850
1631
  return { text, offsetToCell };
1851
1632
  }
1852
1633
 
1634
+ const CRT_READING_ANCHOR_LINE_COUNT = 3;
1635
+
1636
+ function crtReadingAnchorApi() {
1637
+ return window.FarmingReadingAnchors || null;
1638
+ }
1639
+
1640
+ function crtTerminalVisibleBufferBase(currentTerminal) {
1641
+ if (!currentTerminal) return 0;
1642
+ if (typeof currentTerminal.getVisibleBufferBase === 'function') {
1643
+ return Math.max(0, Number(currentTerminal.getVisibleBufferBase()) || 0);
1644
+ }
1645
+ return Math.max(0, Number(currentTerminal.buffer && currentTerminal.buffer.active && currentTerminal.buffer.active.viewportY) || 0);
1646
+ }
1647
+
1648
+ function crtLogicalTerminalLineAtRow(currentTerminal, row) {
1649
+ const buffer = currentTerminal && currentTerminal.buffer && currentTerminal.buffer.active;
1650
+ if (!buffer || typeof buffer.getLine !== 'function' || !Number.isFinite(row) || row < 0) return null;
1651
+ let startRow = row;
1652
+ while (startRow > 0 && buffer.getLine(startRow)?.isWrapped) startRow -= 1;
1653
+ let endRow = row;
1654
+ while (buffer.getLine(endRow + 1)?.isWrapped) endRow += 1;
1655
+ const lines = [];
1656
+ for (let index = startRow; index <= endRow; index += 1) {
1657
+ lines.push(buildTerminalLineProjection(buffer.getLine(index)).text.trimEnd());
1658
+ }
1659
+ return { startRow, endRow, text: lines.join('') };
1660
+ }
1661
+
1662
+ function saveCrtTerminalReadingAnchor(agentId, currentTerminal = terminal) {
1663
+ const api = crtReadingAnchorApi();
1664
+ if (!api || !agentId || !currentTerminal) return;
1665
+ const key = api.agentKey(agentId, 'terminal');
1666
+ const buffer = currentTerminal.buffer && currentTerminal.buffer.active;
1667
+ const base = crtTerminalVisibleBufferBase(currentTerminal);
1668
+ const maxRow = Math.max(0, Number(buffer && buffer.length) || 0);
1669
+ if (!buffer || base >= maxRow - Math.max(1, Number(currentTerminal.rows) || 1)) {
1670
+ api.remove(key);
1671
+ return;
1672
+ }
1673
+ const firstLine = crtLogicalTerminalLineAtRow(currentTerminal, base);
1674
+ if (!firstLine) return;
1675
+ const lines = [];
1676
+ let row = firstLine.startRow;
1677
+ for (let index = 0; index < CRT_READING_ANCHOR_LINE_COUNT; index += 1) {
1678
+ const line = crtLogicalTerminalLineAtRow(currentTerminal, row);
1679
+ if (!line) break;
1680
+ lines.push(line.text);
1681
+ row = line.endRow + 1;
1682
+ }
1683
+ if (!lines.length) return;
1684
+ api.save({
1685
+ version: 1,
1686
+ surface: 'terminal',
1687
+ resource: { kind: 'agent', id: agentId },
1688
+ locator: { kind: 'terminal-lines', id: api.fingerprint(lines), lineCount: lines.length },
1689
+ position: { unit: 'row', value: Math.max(0, base - firstLine.startRow) },
1690
+ });
1691
+ }
1692
+
1693
+ function restoreCrtTerminalReadingAnchor(agentId, currentTerminal = terminal) {
1694
+ const api = crtReadingAnchorApi();
1695
+ if (!api || !agentId || !currentTerminal) return false;
1696
+ const key = api.agentKey(agentId, 'terminal');
1697
+ const anchor = api.read(key);
1698
+ if (!anchor || anchor.surface !== 'terminal') return false;
1699
+ const buffer = currentTerminal.buffer && currentTerminal.buffer.active;
1700
+ const lineCount = Math.max(1, Number(anchor.locator && anchor.locator.lineCount) || 1);
1701
+ const lastRow = Math.max(0, Number(buffer && buffer.length) || 0);
1702
+ let closest = null;
1703
+ let closestDistance = Number.POSITIVE_INFINITY;
1704
+ for (let row = 0; row <= lastRow;) {
1705
+ const firstLine = crtLogicalTerminalLineAtRow(currentTerminal, row);
1706
+ if (!firstLine) {
1707
+ row += 1;
1708
+ continue;
1709
+ }
1710
+ const lines = [firstLine.text];
1711
+ let nextRow = firstLine.endRow + 1;
1712
+ for (let index = 1; index < lineCount; index += 1) {
1713
+ const line = crtLogicalTerminalLineAtRow(currentTerminal, nextRow);
1714
+ if (!line) break;
1715
+ lines.push(line.text);
1716
+ nextRow = line.endRow + 1;
1717
+ }
1718
+ if (lines.length === lineCount && api.fingerprint(lines) === anchor.locator.id) {
1719
+ const distance = Math.abs(firstLine.startRow - crtTerminalVisibleBufferBase(currentTerminal));
1720
+ if (distance < closestDistance) {
1721
+ closest = firstLine;
1722
+ closestDistance = distance;
1723
+ }
1724
+ }
1725
+ row = Math.max(row + 1, firstLine.endRow + 1);
1726
+ }
1727
+ if (!closest || typeof currentTerminal.scrollToLine !== 'function') {
1728
+ api.remove(key);
1729
+ currentTerminal.scrollToBottom?.();
1730
+ return false;
1731
+ }
1732
+ currentTerminal.scrollToLine(Math.min(closest.endRow, closest.startRow + Math.max(0, Number(anchor.position.value) || 0)));
1733
+ return true;
1734
+ }
1735
+
1736
+ function saveCrtStructuredReadingAnchor(agentId) {
1737
+ const api = crtReadingAnchorApi();
1738
+ const container = document.getElementById('terminal-output');
1739
+ if (!api || !agentId || !container) return;
1740
+ const key = api.agentKey(agentId, 'chat');
1741
+ if (container.scrollHeight - container.scrollTop - container.clientHeight < 80) {
1742
+ api.remove(key);
1743
+ return;
1744
+ }
1745
+ const containerRect = container.getBoundingClientRect();
1746
+ const turn = Array.from(container.querySelectorAll('[data-reading-anchor-id]'))
1747
+ .find((candidate) => candidate.getBoundingClientRect().bottom > containerRect.top);
1748
+ if (!turn) return;
1749
+ const turnRect = turn.getBoundingClientRect();
1750
+ api.save({
1751
+ version: 1,
1752
+ surface: 'chat',
1753
+ resource: { kind: 'agent', id: agentId },
1754
+ locator: { kind: 'message', id: turn.dataset.readingAnchorId },
1755
+ position: { unit: 'fraction', value: Math.max(0, Math.min(1, (containerRect.top - turnRect.top) / Math.max(1, turnRect.height))) },
1756
+ });
1757
+ }
1758
+
1759
+ function restoreCrtStructuredReadingAnchor(agentId) {
1760
+ const api = crtReadingAnchorApi();
1761
+ const container = document.getElementById('terminal-output');
1762
+ if (!api || !agentId || !container) return false;
1763
+ const key = api.agentKey(agentId, 'chat');
1764
+ const anchor = api.read(key);
1765
+ if (!anchor || anchor.surface !== 'chat') return false;
1766
+ const target = container.querySelector(`[data-reading-anchor-id="${CSS.escape(anchor.locator.id)}"]`);
1767
+ if (!target) {
1768
+ api.remove(key);
1769
+ container.scrollTop = container.scrollHeight;
1770
+ return false;
1771
+ }
1772
+ const containerRect = container.getBoundingClientRect();
1773
+ const targetRect = target.getBoundingClientRect();
1774
+ container.scrollTop += targetRect.top + targetRect.height * anchor.position.value - containerRect.top;
1775
+ return true;
1776
+ }
1777
+
1853
1778
  function collectWrappedLinkContext(buffer, row) {
1854
1779
  if (!buffer || typeof buffer.getLine !== 'function') {
1855
1780
  return null;
@@ -2267,7 +2192,9 @@ function normalizeSessionViewPayload(payload, fallbackAgent = null) {
2267
2192
  sessionSource: session.sessionSource || (fallbackAgent && fallbackAgent.sessionSource) || 'buffer',
2268
2193
  output: typeof session.output === 'string' ? session.output : ((fallbackAgent && fallbackAgent.output) || ''),
2269
2194
  renderOutput: typeof session.renderOutput === 'string' ? session.renderOutput : '',
2195
+ runtimeEpoch: typeof session.runtimeEpoch === 'string' ? session.runtimeEpoch : '',
2270
2196
  outputSeq: Number.isFinite(session.outputSeq) ? session.outputSeq : null,
2197
+ stateRevision: Number.isFinite(session.stateRevision) ? session.stateRevision : null,
2271
2198
  previewCols: Number.isFinite(session.previewCols) ? session.previewCols : null,
2272
2199
  previewRows: Number.isFinite(session.previewRows) ? session.previewRows : null,
2273
2200
  previewText: typeof session.previewText === 'string' ? session.previewText : ((fallbackAgent && fallbackAgent.previewText) || ''),
@@ -2282,25 +2209,440 @@ function normalizeSessionViewPayload(payload, fallbackAgent = null) {
2282
2209
  };
2283
2210
  }
2284
2211
 
2285
- function applySessionReplayCursorVisibility(text, sessionView, agent) {
2286
- if (!text) return text;
2287
- if (/\x1b\[\?25[hl]/.test(text)) return text;
2212
+ function createCrtTerminalReplication(agentId) {
2213
+ return {
2214
+ agentId,
2215
+ lastResizeCols: null,
2216
+ lastResizeRows: null,
2217
+ pendingFitResize: null,
2218
+ fitResizeTimer: null,
2219
+ applyingLocalResize: false,
2220
+ replayState: TERMINAL_REPLAY.createState(),
2221
+ checkpointInFlight: false,
2222
+ checkpointSeq: 0,
2223
+ checkpointAbortController: null,
2224
+ checkpointRetryTimer: null,
2225
+ installSeq: 0,
2226
+ installInProgress: false,
2227
+ pendingCheckpoint: null,
2228
+ writeInProgress: false,
2229
+ disposed: false
2230
+ };
2231
+ }
2232
+
2233
+ function installCrtTerminalTestApi() {
2234
+ if (
2235
+ typeof window === 'undefined' ||
2236
+ !window.__FARMING_E2E__ ||
2237
+ window.__farmingCrtTerminalTest
2238
+ ) return;
2239
+ window.__farmingCrtTerminalTest = {
2240
+ getState() {
2241
+ const replication = crtTerminalReplication;
2242
+ if (!replication || replication.disposed) return null;
2243
+ return {
2244
+ runtimeEpoch: replication.replayState.runtimeEpoch,
2245
+ outputSeq: replication.replayState.outputSeq,
2246
+ stateRevision: replication.replayState.stateRevision,
2247
+ cols: terminal?.cols || 0,
2248
+ rows: terminal?.rows || 0,
2249
+ replaying: replication.replayState.recovering,
2250
+ checkpointHalted: replication.replayState.halted,
2251
+ writeInProgress: replication.writeInProgress,
2252
+ checkpointInFlight: replication.checkpointInFlight,
2253
+ checkpointInstallInProgress: replication.installInProgress,
2254
+ pendingFitResize: replication.pendingFitResize,
2255
+ fitResizeTimerPending: replication.fitResizeTimer !== null
2256
+ };
2257
+ },
2258
+ getRows() {
2259
+ const buffer = terminal && terminal.buffer && terminal.buffer.active;
2260
+ if (!buffer || typeof buffer.getLine !== 'function') return [];
2261
+ const rows = [];
2262
+ for (let index = 0; index < buffer.length; index += 1) {
2263
+ rows.push(buffer.getLine(index)?.translateToString(true) || '');
2264
+ }
2265
+ return rows;
2266
+ },
2267
+ notifyResizeForTest(cols, rows) {
2268
+ sendSessionResize(focusedAgentId, { cols, rows });
2269
+ },
2270
+ streamSequenced(data, outputSeq, runtimeEpoch, stateRevision) {
2271
+ const replication = crtTerminalReplication;
2272
+ if (!replication || replication.disposed) return false;
2273
+ handleCrtTerminalStream({
2274
+ agentId: replication.agentId,
2275
+ kind: 'output',
2276
+ data: String(data || ''),
2277
+ outputSeq,
2278
+ runtimeEpoch,
2279
+ stateRevision
2280
+ });
2281
+ return true;
2282
+ },
2283
+ replaceStream(stream) {
2284
+ const replication = crtTerminalReplication;
2285
+ if (!replication || replication.disposed || !stream) return false;
2286
+ handleCrtTerminalStream({
2287
+ ...stream,
2288
+ agentId: replication.agentId,
2289
+ replace: true
2290
+ });
2291
+ return true;
2292
+ }
2293
+ };
2294
+ }
2295
+
2296
+ function disposeCrtTerminalReplication() {
2297
+ const replication = crtTerminalReplication;
2298
+ if (!replication) return;
2299
+ replication.disposed = true;
2300
+ if (replication.fitResizeTimer) clearTimeout(replication.fitResizeTimer);
2301
+ replication.fitResizeTimer = null;
2302
+ replication.pendingFitResize = null;
2303
+ if (replication.checkpointRetryTimer) clearTimeout(replication.checkpointRetryTimer);
2304
+ replication.checkpointRetryTimer = null;
2305
+ replication.checkpointAbortController?.abort();
2306
+ replication.checkpointAbortController = null;
2307
+ document.getElementById('terminal-output')?.classList.remove('crt-terminal-checkpoint-installing');
2308
+ crtTerminalReplication = null;
2309
+ }
2310
+
2311
+ function queueCrtTerminalInput(input) {
2312
+ const replication = crtTerminalReplication;
2313
+ if (!replication || replication.disposed) return false;
2314
+ const text = String(input || '');
2315
+ if (!text) return false;
2316
+ return Boolean(getSessionClient()?.sendTerminalInput(replication.agentId, text));
2317
+ }
2318
+
2319
+ function requestCrtTerminalReplay() {
2320
+ const replication = crtTerminalReplication;
2321
+ if (
2322
+ !replication ||
2323
+ replication.disposed ||
2324
+ replication.replayState.halted ||
2325
+ replication.checkpointRetryTimer ||
2326
+ replication.checkpointInFlight ||
2327
+ replication.installInProgress
2328
+ ) return;
2329
+ TERMINAL_REPLAY.beginRecovery(replication.replayState);
2330
+ void refreshSessionView(true, replication.agentId, getCurrentSessionToken());
2331
+ }
2332
+
2333
+ function queueCrtTerminalTransition(event) {
2334
+ const replication = crtTerminalReplication;
2335
+ if (!replication || replication.disposed) return;
2336
+ const result = TERMINAL_REPLAY.queueTransition(replication.replayState, event);
2337
+ if (!result.queued) {
2338
+ requestCrtTerminalReplay();
2339
+ }
2340
+ }
2341
+
2342
+ function scheduleCrtTerminalCheckpointRetry(replication, delay) {
2343
+ if (
2344
+ !replication ||
2345
+ replication.disposed ||
2346
+ replication.replayState.halted ||
2347
+ replication.checkpointRetryTimer
2348
+ ) return;
2349
+ replication.checkpointRetryTimer = setTimeout(() => {
2350
+ replication.checkpointRetryTimer = null;
2351
+ if (!crtTerminalReplication || crtTerminalReplication !== replication || replication.disposed) return;
2352
+ requestCrtTerminalReplay();
2353
+ }, delay);
2354
+ }
2355
+
2356
+ function retryCrtTerminalReplayAfterFailure(replication, failure, error = null) {
2357
+ if (!replication || replication.disposed) return;
2358
+ replication.checkpointInFlight = false;
2359
+ if (failure.halted) {
2360
+ stopCrtTerminalReplay(replication, failure.message);
2361
+ return;
2362
+ }
2363
+ if (error) console.warn('Terminal replay request failed; retrying:', error);
2364
+ scheduleCrtTerminalCheckpointRetry(replication, failure.delay);
2365
+ }
2366
+
2367
+ function finishCrtTerminalReplay(replication = crtTerminalReplication) {
2368
+ if (
2369
+ !replication ||
2370
+ replication.disposed ||
2371
+ replication.checkpointInFlight ||
2372
+ replication.installInProgress ||
2373
+ replication.pendingCheckpoint ||
2374
+ replication.writeInProgress
2375
+ ) return;
2376
+ if (replication.replayState.queuedTransitions.length > 0 && !replication.replayState.recovering) {
2377
+ flushCrtTerminalTransitions();
2378
+ if (
2379
+ replication.replayState.queuedTransitions.length > 0 ||
2380
+ replication.writeInProgress ||
2381
+ replication.checkpointInFlight ||
2382
+ replication.replayState.recovering
2383
+ ) return;
2384
+ }
2385
+ if (
2386
+ replication.replayState.recovering ||
2387
+ TERMINAL_REPLAY.isReplayTargetPending(replication.replayState)
2388
+ ) {
2389
+ requestCrtTerminalReplay();
2390
+ return;
2391
+ }
2392
+ requestAnimationFrame(() => {
2393
+ if (!crtTerminalReplication || crtTerminalReplication !== replication || replication.disposed) return;
2394
+ document.getElementById('terminal-output')?.classList.remove('crt-terminal-checkpoint-installing');
2395
+ sendSessionResize(replication.agentId);
2396
+ });
2397
+ }
2398
+
2399
+ function stopCrtTerminalReplay(replication, error) {
2400
+ if (!replication || replication.disposed) return;
2401
+ replication.checkpointInFlight = false;
2402
+ replication.installInProgress = false;
2403
+ replication.pendingCheckpoint = null;
2404
+ TERMINAL_REPLAY.clearQueuedTransitions(replication.replayState);
2405
+ document.getElementById('terminal-output')?.classList.add('crt-terminal-checkpoint-installing');
2406
+ console.error('Terminal replay failed:', error);
2407
+ showCrtTerminalFailure(
2408
+ 'CRT TERMINAL SYNC ERROR',
2409
+ error,
2410
+ 'The Agent is still running. Close and reopen this terminal to retry recovery.'
2411
+ );
2412
+ }
2413
+
2414
+ function applyCrtTerminalTransition(event) {
2415
+ const replication = crtTerminalReplication;
2416
+ if (!replication || !terminal || replication.disposed) return;
2417
+ const decision = TERMINAL_REPLAY.classifyTransition(replication.replayState, event);
2418
+ if (decision.action === 'drop') return;
2419
+ if (decision.action === 'recover') {
2420
+ queueCrtTerminalTransition(event);
2421
+ requestCrtTerminalReplay();
2422
+ return;
2423
+ }
2424
+
2425
+ if (event.kind === 'resize') {
2426
+ replication.applyingLocalResize = true;
2427
+ try {
2428
+ terminal.resize(Math.floor(event.cols), Math.floor(event.rows));
2429
+ } finally {
2430
+ replication.applyingLocalResize = false;
2431
+ }
2432
+ TERMINAL_REPLAY.commitTransition(replication.replayState, event);
2433
+ refreshSessionTerminalUi({ preserveSearchIndex: true });
2434
+ flushCrtTerminalTransitions();
2435
+ return;
2436
+ }
2437
+
2438
+ const transitionData = event.kind === 'clear' ? '\x1b[2J\x1b[3J\x1b[H' : event.data;
2439
+ if (!transitionData) {
2440
+ TERMINAL_REPLAY.commitTransition(replication.replayState, event);
2441
+ flushCrtTerminalTransitions();
2442
+ return;
2443
+ }
2444
+
2445
+ replication.writeInProgress = true;
2446
+ terminal.write(transitionData, () => {
2447
+ if (!crtTerminalReplication || crtTerminalReplication !== replication || replication.disposed) return;
2448
+ TERMINAL_REPLAY.commitTransition(replication.replayState, event);
2449
+ if (event.kind === 'clear') terminal.clearSelection?.();
2450
+ replication.writeInProgress = false;
2451
+ refreshSessionTerminalUi({ preserveSearchIndex: true });
2452
+ if (drainCrtTerminalCheckpointInstall(replication)) return;
2453
+ flushCrtTerminalTransitions();
2454
+ finishCrtTerminalReplay(replication);
2455
+ });
2456
+ }
2457
+
2458
+ function flushCrtTerminalTransitions() {
2459
+ const replication = crtTerminalReplication;
2460
+ if (
2461
+ !replication ||
2462
+ replication.disposed ||
2463
+ replication.replayState.recovering ||
2464
+ replication.checkpointInFlight ||
2465
+ replication.installInProgress ||
2466
+ replication.writeInProgress
2467
+ ) return;
2468
+
2469
+ while (
2470
+ !replication.replayState.recovering &&
2471
+ !replication.checkpointInFlight &&
2472
+ !replication.installInProgress &&
2473
+ !replication.writeInProgress
2474
+ ) {
2475
+ const next = TERMINAL_REPLAY.takeQueuedTransition(replication.replayState);
2476
+ if (!next) break;
2477
+ applyCrtTerminalTransition(next);
2478
+ }
2479
+ }
2480
+
2481
+ function performCrtTerminalCheckpointInstall(replication, sessionView) {
2482
+ if (
2483
+ !crtTerminalReplication ||
2484
+ crtTerminalReplication !== replication ||
2485
+ !terminal ||
2486
+ replication.disposed
2487
+ ) return false;
2488
+
2489
+ const installSeq = replication.installSeq + 1;
2490
+ replication.installSeq = installSeq;
2491
+ replication.installInProgress = true;
2492
+ TERMINAL_REPLAY.beginRecovery(replication.replayState, sessionView);
2493
+ const container = document.getElementById('terminal-output');
2494
+ container?.classList.add('crt-terminal-checkpoint-installing');
2495
+
2496
+ replication.applyingLocalResize = true;
2497
+ try {
2498
+ terminal.resize(sessionView.previewCols, sessionView.previewRows);
2499
+ } finally {
2500
+ replication.applyingLocalResize = false;
2501
+ }
2502
+ terminal.reset();
2503
+
2504
+ const finishInstall = () => {
2505
+ if (
2506
+ !crtTerminalReplication ||
2507
+ crtTerminalReplication !== replication ||
2508
+ replication.disposed ||
2509
+ replication.installSeq !== installSeq
2510
+ ) return;
2511
+
2512
+ TERMINAL_REPLAY.commitCheckpoint(replication.replayState, {
2513
+ runtimeEpoch: sessionView.runtimeEpoch,
2514
+ outputSeq: sessionView.outputSeq,
2515
+ stateRevision: sessionView.stateRevision,
2516
+ cols: sessionView.previewCols,
2517
+ rows: sessionView.previewRows
2518
+ });
2519
+ replication.installInProgress = false;
2520
+ refreshSessionTerminalUi({ preserveSearchIndex: true });
2521
+ const runtime = getSessionRuntime();
2522
+ if (runtime) {
2523
+ runtime.markHydrated(sessionView.renderOutput.length);
2524
+ syncSessionRuntimeState();
2525
+ }
2526
+ if (drainCrtTerminalCheckpointInstall(replication)) return;
2527
+ flushCrtTerminalTransitions();
2528
+ finishCrtTerminalReplay(replication);
2529
+ };
2530
+
2531
+ if (sessionView.renderOutput) {
2532
+ terminal.write(sessionView.renderOutput, finishInstall);
2533
+ } else {
2534
+ finishInstall();
2535
+ }
2536
+ return true;
2537
+ }
2288
2538
 
2289
- const snapshotVisibility = sessionView
2290
- && sessionView.previewSnapshot
2291
- && sessionView.previewSnapshot.cursorVisible;
2292
- if (typeof snapshotVisibility === 'boolean') {
2293
- return `${text}\x1b[?25${snapshotVisibility ? 'h' : 'l'}`;
2539
+ function drainCrtTerminalCheckpointInstall(replication = crtTerminalReplication) {
2540
+ if (
2541
+ !replication ||
2542
+ replication.disposed ||
2543
+ replication.installInProgress ||
2544
+ replication.writeInProgress ||
2545
+ !replication.pendingCheckpoint
2546
+ ) return false;
2547
+ const sessionView = replication.pendingCheckpoint;
2548
+ replication.pendingCheckpoint = null;
2549
+ return performCrtTerminalCheckpointInstall(replication, sessionView);
2550
+ }
2551
+
2552
+ function installCrtTerminalCheckpoint(sessionView) {
2553
+ const replication = crtTerminalReplication;
2554
+ if (!replication || !terminal || replication.disposed) return false;
2555
+ const checkpoint = {
2556
+ runtimeEpoch: sessionView.runtimeEpoch,
2557
+ outputSeq: sessionView.outputSeq,
2558
+ stateRevision: sessionView.stateRevision,
2559
+ cols: sessionView.previewCols,
2560
+ rows: sessionView.previewRows
2561
+ };
2562
+ const decision = TERMINAL_REPLAY.evaluateCheckpoint(replication.replayState, checkpoint);
2563
+ if (decision.action === 'reject') {
2564
+ retryCrtTerminalReplayAfterFailure(
2565
+ replication,
2566
+ TERMINAL_REPLAY.recordInvariantFailure(
2567
+ replication.replayState,
2568
+ decision.signature || 'invalid-checkpoint',
2569
+ decision.message || 'Terminal replay returned an invalid screen state'
2570
+ )
2571
+ );
2572
+ return false;
2573
+ }
2574
+ if (
2575
+ decision.action === 'current' &&
2576
+ terminal.cols === sessionView.previewCols &&
2577
+ terminal.rows === sessionView.previewRows
2578
+ ) {
2579
+ TERMINAL_REPLAY.commitCheckpoint(replication.replayState, checkpoint);
2580
+ flushCrtTerminalTransitions();
2581
+ finishCrtTerminalReplay(replication);
2582
+ return true;
2294
2583
  }
2295
2584
 
2296
- // Compatibility for a live native PTY host started before cursor visibility
2297
- // was added to snapshots. Claude's TUI paints its own input cursor and hides
2298
- // the hardware cursor, so its replay must not expose xterm's default cursor.
2299
- return crtCommandProgram(agent && agent.command) === 'claude'
2300
- ? `${text}\x1b[?25l`
2301
- : text;
2585
+ replication.pendingCheckpoint = sessionView;
2586
+ TERMINAL_REPLAY.beginRecovery(replication.replayState, sessionView);
2587
+ drainCrtTerminalCheckpointInstall(replication);
2588
+ return true;
2302
2589
  }
2303
2590
 
2591
+ function handleCrtTerminalStream(stream) {
2592
+ const replication = crtTerminalReplication;
2593
+ if (!replication || !stream || stream.agentId !== replication.agentId) return;
2594
+ if (stream.replace === true) {
2595
+ replication.checkpointSeq += 1;
2596
+ replication.checkpointAbortController?.abort();
2597
+ replication.checkpointAbortController = null;
2598
+ replication.checkpointInFlight = false;
2599
+ installCrtTerminalCheckpoint({
2600
+ runtimeEpoch: stream.runtimeEpoch,
2601
+ outputSeq: stream.outputSeq,
2602
+ stateRevision: stream.stateRevision,
2603
+ previewCols: stream.cols,
2604
+ previewRows: stream.rows,
2605
+ renderOutput: typeof stream.data === 'string' ? stream.data : ''
2606
+ });
2607
+ if (Array.isArray(stream.chunks)) {
2608
+ stream.chunks.forEach((chunk) => queueCrtTerminalTransition({
2609
+ kind: chunk.kind || 'output',
2610
+ data: typeof chunk.data === 'string' ? chunk.data : '',
2611
+ runtimeEpoch: chunk.runtimeEpoch || stream.runtimeEpoch,
2612
+ outputSeq: chunk.outputSeq,
2613
+ stateRevision: chunk.stateRevision,
2614
+ cols: chunk.cols,
2615
+ rows: chunk.rows
2616
+ }));
2617
+ flushCrtTerminalTransitions();
2618
+ finishCrtTerminalReplay(replication);
2619
+ }
2620
+ return;
2621
+ }
2622
+
2623
+ const chunks = Array.isArray(stream.chunks) ? stream.chunks : [stream];
2624
+ chunks.forEach((chunk) => {
2625
+ const event = {
2626
+ kind: chunk.kind || 'output',
2627
+ data: typeof chunk.data === 'string' ? chunk.data : '',
2628
+ runtimeEpoch: chunk.runtimeEpoch || stream.runtimeEpoch,
2629
+ outputSeq: chunk.outputSeq,
2630
+ stateRevision: chunk.stateRevision,
2631
+ cols: chunk.cols,
2632
+ rows: chunk.rows
2633
+ };
2634
+ if (
2635
+ replication.replayState.recovering ||
2636
+ replication.checkpointInFlight ||
2637
+ replication.installInProgress ||
2638
+ replication.writeInProgress
2639
+ ) {
2640
+ queueCrtTerminalTransition(event);
2641
+ } else {
2642
+ applyCrtTerminalTransition(event);
2643
+ }
2644
+ });
2645
+ }
2304
2646
  function deriveSessionStreamPatch(stream, currentFocusedAgentId, currentSessionSource) {
2305
2647
  if (!stream) return null;
2306
2648
  if (stream.agentId !== currentFocusedAgentId) return null;
@@ -2335,7 +2677,7 @@ function shouldPollSessionView(sessionSource) {
2335
2677
  if (SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.shouldPollSessionView) {
2336
2678
  return SESSION_MODAL_BRIDGE.shouldPollSessionView(sessionSource);
2337
2679
  }
2338
- return sessionSource === 'live-text';
2680
+ return false;
2339
2681
  }
2340
2682
 
2341
2683
  function getSessionModalDomState(documentRef) {
@@ -2354,8 +2696,6 @@ function getSessionRuntime() {
2354
2696
  sessionRuntime = SESSION_MODAL_BRIDGE.createRuntime({
2355
2697
  deriveSessionStreamPatch,
2356
2698
  refreshSessionView,
2357
- schedulePoll: (handler) => setInterval(handler, 350),
2358
- clearPoll: (poller) => clearInterval(poller)
2359
2699
  });
2360
2700
  }
2361
2701
 
@@ -2384,11 +2724,6 @@ function getCurrentSessionToken() {
2384
2724
  return runtime ? runtime.getSessionToken() : 0;
2385
2725
  }
2386
2726
 
2387
- function isAwaitingInitialSessionSync() {
2388
- const runtime = getSessionRuntime();
2389
- return runtime ? runtime.isAwaitingInitialSync() : false;
2390
- }
2391
-
2392
2727
  function setSessionOutputLength(length) {
2393
2728
  const runtime = getSessionRuntime();
2394
2729
  if (runtime) {
@@ -2550,12 +2885,9 @@ function applyCrtTerminalFontSize(value) {
2550
2885
  if (input) input.value = String(fontSize);
2551
2886
  if (output) output.textContent = `${fontSize} px`;
2552
2887
  if (terminal && terminal.options) terminal.options.fontSize = fontSize;
2553
- if (terminalInputBridge) terminalInputBridge.style.fontSize = `${fontSize}px`;
2554
2888
  if (terminal && fitAddon && focusedAgentId && typeof requestAnimationFrame === 'function') {
2555
2889
  requestAnimationFrame(() => {
2556
2890
  if (!terminal || !fitAddon || !focusedAgentId) return;
2557
- fitAddon.fit();
2558
- syncTerminalInputBridgePosition();
2559
2891
  sendSessionResize();
2560
2892
  });
2561
2893
  }
@@ -3064,6 +3396,7 @@ function formatCrtExactUsageValue(value) {
3064
3396
  }
3065
3397
 
3066
3398
  function formatCrtCompactTotalValue(value) {
3399
+ if (value === null || value === undefined || value === '') return '--';
3067
3400
  const numberValue = Number(value);
3068
3401
  if (!Number.isFinite(numberValue) || numberValue < 0) return '--';
3069
3402
  const units = [
@@ -3098,26 +3431,199 @@ function crtBillingDayPoint(dateValue = billingSelectedDate) {
3098
3431
  return points.find(point => point && point.date === dateValue) || null;
3099
3432
  }
3100
3433
 
3101
- function crtBillingLogPosition(value, minimum, maximum) {
3434
+ function crtBillingSelectedDayIsCurrent(point = crtBillingDayPoint()) {
3435
+ const daily = billingSummary && billingSummary.daily;
3436
+ return Boolean(daily && point && point.date === daily.endDate);
3437
+ }
3438
+
3439
+ function cancelCrtBillingTotalAnimation() {
3440
+ if (billingTotalAnimationFrame !== null) {
3441
+ window.cancelAnimationFrame(billingTotalAnimationFrame);
3442
+ billingTotalAnimationFrame = null;
3443
+ }
3444
+ billingTotalAnimationTarget = null;
3445
+ }
3446
+
3447
+ function cancelCrtBillingMetricAnimations() {
3448
+ billingAnimatedMetrics.forEach((metric) => {
3449
+ if (metric.frame !== null) window.cancelAnimationFrame(metric.frame);
3450
+ });
3451
+ billingAnimatedMetrics.clear();
3452
+ }
3453
+
3454
+ function updateCrtBillingAnimatedMetric(key, value, {
3455
+ date = '',
3456
+ live = false,
3457
+ write,
3458
+ } = {}) {
3459
+ const target = Number(value);
3460
+ const existing = billingAnimatedMetrics.get(key);
3461
+ if (!Number.isFinite(target) || target < 0 || typeof write !== 'function') {
3462
+ if (existing && existing.frame !== null) window.cancelAnimationFrame(existing.frame);
3463
+ billingAnimatedMetrics.delete(key);
3464
+ write?.(null, null);
3465
+ return;
3466
+ }
3467
+
3468
+ const roundedTarget = Math.round(target);
3469
+ if (existing && existing.frame !== null && existing.target === roundedTarget && existing.date === date) {
3470
+ existing.write = write;
3471
+ return;
3472
+ }
3473
+ if (existing && existing.frame !== null) window.cancelAnimationFrame(existing.frame);
3474
+ const shouldSnap = !live
3475
+ || !existing
3476
+ || existing.date !== date
3477
+ || !Number.isFinite(existing.value)
3478
+ || roundedTarget <= existing.value;
3479
+ if (shouldSnap) {
3480
+ const metric = { date, value: roundedTarget, target: roundedTarget, frame: null, write };
3481
+ billingAnimatedMetrics.set(key, metric);
3482
+ write(roundedTarget, roundedTarget);
3483
+ return;
3484
+ }
3485
+
3486
+ const metric = {
3487
+ date,
3488
+ value: existing.value,
3489
+ target: roundedTarget,
3490
+ frame: null,
3491
+ write,
3492
+ };
3493
+ billingAnimatedMetrics.set(key, metric);
3494
+ const startValue = existing.value;
3495
+ const startedAt = window.performance.now();
3496
+ const step = (now) => {
3497
+ const current = billingAnimatedMetrics.get(key);
3498
+ if (current !== metric) return;
3499
+ const progress = Math.min(1, Math.max(0, (now - startedAt) / CRT_BILLING_TOTAL_ANIMATION_MS));
3500
+ const steppedProgress = Math.min(1, Math.floor(progress * 18) / 18);
3501
+ const easedProgress = 1 - ((1 - steppedProgress) ** 3);
3502
+ metric.value = Math.round(startValue + (roundedTarget - startValue) * easedProgress);
3503
+ metric.write(metric.value, roundedTarget);
3504
+ if (progress < 1) {
3505
+ metric.frame = window.requestAnimationFrame(step);
3506
+ return;
3507
+ }
3508
+ metric.value = roundedTarget;
3509
+ metric.frame = null;
3510
+ metric.write(roundedTarget, roundedTarget);
3511
+ };
3512
+ metric.frame = window.requestAnimationFrame(step);
3513
+ }
3514
+
3515
+ function updateCrtBillingExactMetric(id, value, { date = '', live = false } = {}) {
3516
+ const element = document.getElementById(id);
3517
+ if (!element) return;
3518
+ updateCrtBillingAnimatedMetric(id, value, {
3519
+ date,
3520
+ live,
3521
+ write: (displayed, target) => {
3522
+ element.textContent = formatCrtExactUsageValue(displayed);
3523
+ element.dataset.displayedValue = displayed === null ? '' : String(displayed);
3524
+ element.dataset.targetValue = target === null ? '' : String(target);
3525
+ },
3526
+ });
3527
+ }
3528
+
3529
+ function writeCrtBillingTotalDisplay(value, target, { live = false } = {}) {
3530
+ const total = document.getElementById('billing-day-total');
3531
+ const compact = document.getElementById('billing-day-total-compact');
3532
+ const meter = document.getElementById('billing-day-total-meter');
3533
+ const numericValue = Number.isFinite(Number(value)) ? Math.max(0, Math.round(Number(value))) : null;
3534
+ const numericTarget = Number.isFinite(Number(target)) ? Math.max(0, Math.round(Number(target))) : null;
3535
+ if (total) total.textContent = formatCrtExactUsageValue(numericValue);
3536
+ if (compact) compact.textContent = formatCrtCompactTotalValue(numericValue);
3537
+ if (meter) {
3538
+ meter.classList.toggle('is-live', live);
3539
+ meter.dataset.displayedTotal = numericValue === null ? '' : String(numericValue);
3540
+ meter.dataset.targetTotal = numericTarget === null ? '' : String(numericTarget);
3541
+ meter.setAttribute('aria-label', numericTarget === null
3542
+ ? 'Total tokens unavailable'
3543
+ : `${formatCrtExactUsageValue(numericTarget)} total tokens${live ? ', live refresh every 5 seconds' : ''}`);
3544
+ }
3545
+ }
3546
+
3547
+ function updateCrtBillingTotalDisplay(value, { date = '', live = false } = {}) {
3548
+ const target = Number(value);
3549
+ if (!Number.isFinite(target) || target < 0) {
3550
+ cancelCrtBillingTotalAnimation();
3551
+ billingDisplayedTotalDate = date;
3552
+ billingDisplayedTotalValue = null;
3553
+ writeCrtBillingTotalDisplay(null, null, { live });
3554
+ return;
3555
+ }
3556
+
3557
+ const roundedTarget = Math.round(target);
3558
+ const shouldSnap = !live
3559
+ || billingDisplayedTotalDate !== date
3560
+ || !Number.isFinite(billingDisplayedTotalValue)
3561
+ || roundedTarget <= billingDisplayedTotalValue;
3562
+ if (shouldSnap) {
3563
+ cancelCrtBillingTotalAnimation();
3564
+ billingDisplayedTotalDate = date;
3565
+ billingDisplayedTotalValue = roundedTarget;
3566
+ writeCrtBillingTotalDisplay(roundedTarget, roundedTarget, { live });
3567
+ return;
3568
+ }
3569
+ if (billingTotalAnimationFrame !== null && billingTotalAnimationTarget === roundedTarget) return;
3570
+
3571
+ cancelCrtBillingTotalAnimation();
3572
+ const startValue = billingDisplayedTotalValue;
3573
+ const startedAt = window.performance.now();
3574
+ billingTotalAnimationTarget = roundedTarget;
3575
+ const step = (now) => {
3576
+ const progress = Math.min(1, Math.max(0, (now - startedAt) / CRT_BILLING_TOTAL_ANIMATION_MS));
3577
+ const steppedProgress = Math.min(1, Math.floor(progress * 18) / 18);
3578
+ const easedProgress = 1 - ((1 - steppedProgress) ** 3);
3579
+ billingDisplayedTotalValue = Math.round(startValue + (roundedTarget - startValue) * easedProgress);
3580
+ writeCrtBillingTotalDisplay(billingDisplayedTotalValue, roundedTarget, { live: true });
3581
+ if (progress < 1) {
3582
+ billingTotalAnimationFrame = window.requestAnimationFrame(step);
3583
+ return;
3584
+ }
3585
+ billingDisplayedTotalValue = roundedTarget;
3586
+ billingTotalAnimationFrame = null;
3587
+ billingTotalAnimationTarget = null;
3588
+ writeCrtBillingTotalDisplay(roundedTarget, roundedTarget, { live: true });
3589
+ };
3590
+ billingTotalAnimationFrame = window.requestAnimationFrame(step);
3591
+ }
3592
+
3593
+ const CRT_BILLING_OVERRANGE_BASE = 1_000_000_000;
3594
+
3595
+ function crtBillingOverrangeTier(value) {
3596
+ const total = Math.max(0, Number(value) || 0);
3597
+ if (total < CRT_BILLING_OVERRANGE_BASE) return 0;
3598
+ return Math.min(4, Math.floor(Math.log2(total / CRT_BILLING_OVERRANGE_BASE)) + 1);
3599
+ }
3600
+
3601
+ function crtBillingOverrangeLabel(tier) {
3602
+ return tier > 0 ? `${2 ** (tier - 1)}B+ OVERRANGE` : '';
3603
+ }
3604
+
3605
+ function crtBillingHeatThresholds(values) {
3606
+ const activeValues = (Array.isArray(values) ? values : [])
3607
+ .map(value => Math.max(0, Number(value) || 0))
3608
+ .filter(value => value > 0 && value < CRT_BILLING_OVERRANGE_BASE)
3609
+ .sort((left, right) => left - right);
3610
+ if (activeValues.length === 0) return [];
3611
+ return [0.2, 0.4, 0.6, 0.8].map(quantile => (
3612
+ activeValues[Math.min(activeValues.length - 1, Math.ceil(activeValues.length * quantile) - 1)]
3613
+ ));
3614
+ }
3615
+
3616
+ function crtBillingHeatLevel(value, thresholds) {
3102
3617
  const total = Math.max(0, Number(value) || 0);
3103
3618
  if (total <= 0) return 0;
3104
- if (total <= minimum || maximum <= minimum) return 2;
3105
- const position = (Math.log10(total) - Math.log10(minimum))
3106
- / (Math.log10(maximum) - Math.log10(minimum)) * 100;
3107
- return Math.max(2, Math.min(100, position));
3619
+ const bands = Array.isArray(thresholds) ? thresholds : [];
3620
+ return Math.max(1, Math.min(5, 1 + bands.filter(threshold => total > threshold).length));
3108
3621
  }
3109
3622
 
3110
- function crtBillingLogGuideValues(minimum, maximum) {
3111
- const values = [];
3112
- const firstExponent = Math.ceil(Math.log10(Math.max(1, minimum)));
3113
- const lastExponent = Math.floor(Math.log10(Math.max(1, maximum)));
3114
- for (let exponent = firstExponent; exponent <= lastExponent; exponent += 1) {
3115
- const value = 10 ** exponent;
3116
- if (value > minimum && value < maximum) values.push(value);
3117
- }
3118
- const upperThird = 3 * (10 ** lastExponent);
3119
- if (upperThird > minimum && upperThird < maximum) values.push(upperThird);
3120
- return Array.from(new Set(values)).sort((left, right) => left - right);
3623
+ function crtBillingDayDetailHasHourlyActivity(detail) {
3624
+ return Boolean(detail && Array.isArray(detail.hours) && detail.hours.some(hour => (
3625
+ Math.max(0, Number(hour && hour.totalTokens) || 0) > 0
3626
+ )));
3121
3627
  }
3122
3628
 
3123
3629
  function crtBillingHourlyPath(hours, valueForHour, maximum) {
@@ -3126,10 +3632,11 @@ function crtBillingHourlyPath(hours, valueForHour, maximum) {
3126
3632
  const points = Array.isArray(hours) ? hours : [];
3127
3633
  if (points.length === 0 || maximum <= 0) return '';
3128
3634
  return points.map((hour, index) => {
3129
- const x = points.length === 1 ? 0 : index / (points.length - 1) * width;
3635
+ const startX = index / points.length * width;
3636
+ const endX = (index + 1) / points.length * width;
3130
3637
  const value = Math.max(0, Number(valueForHour(hour)) || 0);
3131
3638
  const y = height - Math.min(1, value / maximum) * height;
3132
- return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`;
3639
+ return `${index === 0 ? 'M' : 'L'}${startX.toFixed(1)} ${y.toFixed(1)} L${endX.toFixed(1)} ${y.toFixed(1)}`;
3133
3640
  }).join(' ');
3134
3641
  }
3135
3642
 
@@ -3145,6 +3652,8 @@ function renderCrtBillingDayInsight() {
3145
3652
  const cachePath = document.getElementById('billing-day-cache-path');
3146
3653
  const scale = document.getElementById('billing-day-curve-scale');
3147
3654
  const maximumLabel = document.getElementById('billing-day-curve-max');
3655
+ const hourStrip = document.getElementById('billing-day-hour-strip');
3656
+ const hourReadout = document.getElementById('billing-day-hour-readout');
3148
3657
  const shares = document.getElementById('billing-day-provider-shares');
3149
3658
  const maximum = Math.max(0, ...hours.map(hour => Math.max(0, Number(hour && hour.totalTokens) || 0)));
3150
3659
 
@@ -3154,19 +3663,94 @@ function renderCrtBillingDayInsight() {
3154
3663
  hour => (Number(hour.cacheReadTokens) || 0) + (Number(hour.cacheWriteTokens) || 0),
3155
3664
  maximum,
3156
3665
  ));
3157
- if (scale) scale.textContent = maximum > 0 ? `${formatCrtUsageValue(maximum)} TOK/H PEAK` : '-- TOK/H PEAK';
3158
- if (maximumLabel) maximumLabel.textContent = maximum > 0 ? formatCrtUsageValue(maximum) : '--';
3666
+ const isToday = crtBillingSelectedDayIsCurrent(point);
3667
+ updateCrtBillingAnimatedMetric('billing-day-curve-scale', maximum > 0 ? maximum : null, {
3668
+ date: point && point.date || '',
3669
+ live: isToday,
3670
+ write: displayed => {
3671
+ if (scale) scale.textContent = displayed === null ? '-- TOK/H PEAK' : `${formatCrtUsageValue(displayed)} TOK/H PEAK`;
3672
+ },
3673
+ });
3674
+ updateCrtBillingAnimatedMetric('billing-day-curve-max', maximum > 0 ? maximum : null, {
3675
+ date: point && point.date || '',
3676
+ live: isToday,
3677
+ write: displayed => {
3678
+ if (maximumLabel) maximumLabel.textContent = displayed === null ? '--' : formatCrtUsageValue(displayed);
3679
+ },
3680
+ });
3681
+ if (hourStrip) {
3682
+ hourStrip.replaceChildren();
3683
+ if (hours.length > 0) {
3684
+ const heatThresholds = crtBillingHeatThresholds(hours.map(hour => hour && hour.totalTokens));
3685
+ if (!Number.isInteger(billingSelectedHour) || billingSelectedHour < 0 || billingSelectedHour >= hours.length) {
3686
+ billingSelectedHour = hours.reduce((peakIndex, hour, index) => (
3687
+ Number(hour && hour.totalTokens) > Number(hours[peakIndex] && hours[peakIndex].totalTokens) ? index : peakIndex
3688
+ ), 0);
3689
+ }
3690
+ const selectHour = (index, { focus = false } = {}) => {
3691
+ billingSelectedHour = index;
3692
+ hourStrip.querySelectorAll('.billing-day-hour-cell').forEach((cell, cellIndex) => {
3693
+ const selected = cellIndex === index;
3694
+ cell.classList.toggle('selected', selected);
3695
+ cell.setAttribute('aria-selected', selected ? 'true' : 'false');
3696
+ cell.tabIndex = selected ? 0 : -1;
3697
+ });
3698
+ const hour = hours[index] || {};
3699
+ const hourValue = Number.isFinite(Number(hour.hour)) ? Number(hour.hour) : index;
3700
+ const total = Math.max(0, Number(hour.totalTokens) || 0);
3701
+ const cache = Math.max(0, (Number(hour.cacheReadTokens) || 0) + (Number(hour.cacheWriteTokens) || 0));
3702
+ if (hourReadout) {
3703
+ const endHour = hourValue + 1;
3704
+ const cacheShare = total > 0 ? `${(cache / total * 100).toFixed(1)}% CACHE` : 'NO ACTIVITY';
3705
+ hourReadout.textContent = `[${String(hourValue).padStart(2, '0')}:00—${String(endHour).padStart(2, '0')}:00] TOTAL ${formatCrtUsageValue(total)} // CACHE ${formatCrtUsageValue(cache)} // ${cacheShare}`;
3706
+ hourReadout.title = `${String(hourValue).padStart(2, '0')}:00—${String(endHour).padStart(2, '0')}:00 · ${formatCrtExactUsageValue(total)} total tokens · ${formatCrtExactUsageValue(cache)} cache tokens`;
3707
+ }
3708
+ if (focus) hourStrip.children[index]?.focus();
3709
+ };
3710
+ hours.forEach((hour, index) => {
3711
+ const total = Math.max(0, Number(hour && hour.totalTokens) || 0);
3712
+ const cache = Math.max(0, (Number(hour && hour.cacheReadTokens) || 0) + (Number(hour && hour.cacheWriteTokens) || 0));
3713
+ const hourValue = Number.isFinite(Number(hour && hour.hour)) ? Number(hour.hour) : index;
3714
+ const overrangeTier = crtBillingOverrangeTier(total);
3715
+ const cell = document.createElement('button');
3716
+ cell.type = 'button';
3717
+ cell.className = 'billing-day-hour-cell';
3718
+ cell.dataset.level = overrangeTier ? 'overrange' : String(crtBillingHeatLevel(total, heatThresholds));
3719
+ if (overrangeTier) cell.dataset.overrange = String(overrangeTier);
3720
+ cell.dataset.hour = String(hourValue);
3721
+ cell.setAttribute('role', 'gridcell');
3722
+ cell.setAttribute('aria-label', `${String(hourValue).padStart(2, '0')}:00 to ${String(hourValue + 1).padStart(2, '0')}:00, ${formatCrtExactUsageValue(total)} total tokens, ${formatCrtExactUsageValue(cache)} cache tokens`);
3723
+ cell.tabIndex = index === billingSelectedHour ? 0 : -1;
3724
+ cell.addEventListener('click', () => selectHour(index));
3725
+ cell.addEventListener('mouseenter', () => selectHour(index));
3726
+ cell.addEventListener('focus', () => selectHour(index));
3727
+ cell.addEventListener('keydown', (event) => {
3728
+ if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
3729
+ event.preventDefault();
3730
+ event.stopPropagation();
3731
+ selectHour(Math.max(0, Math.min(hours.length - 1, index + (event.key === 'ArrowLeft' ? -1 : 1))), { focus: true });
3732
+ });
3733
+ hourStrip.appendChild(cell);
3734
+ });
3735
+ selectHour(billingSelectedHour);
3736
+ } else if (hourReadout) {
3737
+ hourReadout.textContent = billingDayDetailLoading ? 'READING HOURLY COORDINATES' : 'NO HOURLY ACTIVITY';
3738
+ hourReadout.removeAttribute('title');
3739
+ }
3740
+ }
3159
3741
  if (state) {
3160
- state.classList.toggle('is-error', Boolean(billingDayDetailError));
3161
- state.textContent = billingDayDetailError
3162
- ? 'DAY SIGNAL LOST'
3163
- : billingDayDetailLoading && !selectedDetail
3164
- ? 'READING 24 HOURLY BINS'
3165
- : selectedDetail && maximum > 0
3166
- ? '24 HOURLY BINS READY'
3167
- : selectedDetail
3168
- ? 'NO HOURLY ACTIVITY'
3169
- : 'SELECTED DAY DETAIL';
3742
+ state.classList.toggle('is-error', Boolean(billingDayDetailError && !selectedDetail));
3743
+ state.textContent = billingDayDetailError && selectedDetail
3744
+ ? '24 HOURLY BINS READY · STALE'
3745
+ : billingDayDetailError
3746
+ ? 'DAY SIGNAL LOST'
3747
+ : billingDayDetailLoading && !selectedDetail
3748
+ ? 'READING 24 HOURLY BINS'
3749
+ : selectedDetail && maximum > 0
3750
+ ? '24 HOURLY BINS READY'
3751
+ : selectedDetail
3752
+ ? 'NO HOURLY ACTIVITY'
3753
+ : 'SELECTED DAY DETAIL';
3170
3754
  }
3171
3755
 
3172
3756
  if (!shares) return;
@@ -3209,42 +3793,46 @@ function renderCrtBillingDayInsight() {
3209
3793
  });
3210
3794
  }
3211
3795
 
3212
- function renderCrtBillingSelectedDay() {
3796
+ function renderCrtBillingSelectedDay({ preferSummary = false } = {}) {
3213
3797
  const daily = billingSummary && billingSummary.daily;
3214
3798
  const point = crtBillingDayPoint();
3799
+ const cachedEntry = billingDayDetailCache.get(billingSelectedDate);
3800
+ const selectedDetail = billingDayDetail && billingDayDetail.date === billingSelectedDate
3801
+ ? billingDayDetail
3802
+ : cachedEntry && cachedEntry.detail || null;
3803
+ const displayDetail = preferSummary ? null : selectedDetail;
3804
+ const displayPoint = displayDetail && displayDetail.total || point;
3805
+ const displayProviders = displayDetail && displayDetail.providers || point && point.providers;
3806
+ const isToday = crtBillingSelectedDayIsCurrent(point);
3215
3807
  const date = document.getElementById('billing-day-date');
3216
3808
  const stateLabel = document.getElementById('billing-day-state');
3217
- const total = document.getElementById('billing-day-total');
3218
- const compactTotal = document.getElementById('billing-day-total-compact');
3219
- const input = document.getElementById('billing-day-input');
3220
- const output = document.getElementById('billing-day-output');
3221
- const cacheRead = document.getElementById('billing-day-cache-read');
3222
- const cacheWrite = document.getElementById('billing-day-cache-write');
3223
3809
  const providers = document.getElementById('billing-day-providers');
3224
3810
  if (date) date.textContent = crtBillingDayLabel(point && point.date);
3225
- if (total) total.textContent = formatCrtExactUsageValue(point && point.totalTokens);
3226
- if (compactTotal) compactTotal.textContent = formatCrtCompactTotalValue(point && point.totalTokens);
3227
- if (input) input.textContent = formatCrtExactUsageValue(point && point.inputTokens);
3228
- if (output) output.textContent = formatCrtExactUsageValue(point && point.outputTokens);
3229
- if (cacheRead) cacheRead.textContent = formatCrtExactUsageValue(point && point.cacheReadTokens);
3230
- if (cacheWrite) cacheWrite.textContent = formatCrtExactUsageValue(point && point.cacheWriteTokens);
3811
+ updateCrtBillingTotalDisplay(displayPoint && displayPoint.totalTokens, {
3812
+ date: point && point.date || '',
3813
+ live: isToday,
3814
+ });
3815
+ const selectedDate = point && point.date || '';
3816
+ updateCrtBillingExactMetric('billing-day-input', displayPoint && displayPoint.inputTokens, { date: selectedDate, live: isToday });
3817
+ updateCrtBillingExactMetric('billing-day-output', displayPoint && displayPoint.outputTokens, { date: selectedDate, live: isToday });
3818
+ updateCrtBillingExactMetric('billing-day-cache-read', displayPoint && displayPoint.cacheReadTokens, { date: selectedDate, live: isToday });
3819
+ updateCrtBillingExactMetric('billing-day-cache-write', displayPoint && displayPoint.cacheWriteTokens, { date: selectedDate, live: isToday });
3231
3820
  if (providers) {
3232
- const providerTotals = point && point.providers ? Object.entries(point.providers) : [];
3821
+ const providerTotals = displayProviders ? Object.entries(displayProviders) : [];
3233
3822
  providers.textContent = providerTotals
3234
3823
  .map(([provider, usage]) => `${provider.toUpperCase()} ${formatCrtExactUsageValue(usage && usage.totalTokens)}`)
3235
3824
  .join(' · ') || '--';
3236
3825
  providers.title = providers.textContent;
3237
3826
  }
3238
3827
  if (stateLabel) {
3239
- const isToday = Boolean(daily && point && point.date === daily.endDate);
3240
- const notes = [isToday ? 'PARTIAL DAY' : 'COMPLETE DAY', 'INCL CACHE'];
3828
+ const notes = isToday ? ['LIVE 5S', 'PARTIAL DAY', 'INCL CACHE'] : ['COMPLETE DAY', 'INCL CACHE'];
3241
3829
  if (daily && daily.partial) notes.push('PARTIAL SOURCE');
3242
3830
  if (point && Number(point.unattributedTokens) > 0) {
3243
3831
  notes.push(`${formatCrtUsageValue(point.unattributedTokens)} UNCLASSIFIED`);
3244
3832
  }
3245
3833
  stateLabel.textContent = point ? notes.join(' · ') : 'LOCAL HISTORY';
3246
3834
  }
3247
- document.querySelectorAll('#billing-daily-bars .billing-daily-column').forEach((cell) => {
3835
+ document.querySelectorAll('#billing-calendar-grid .billing-calendar-day').forEach((cell) => {
3248
3836
  const selected = cell.dataset.date === billingSelectedDate;
3249
3837
  cell.classList.toggle('selected', selected);
3250
3838
  cell.setAttribute('aria-selected', selected ? 'true' : 'false');
@@ -3254,13 +3842,16 @@ function renderCrtBillingSelectedDay() {
3254
3842
 
3255
3843
  function selectCrtBillingDay(dateValue, { focus = false } = {}) {
3256
3844
  if (!crtBillingDayPoint(dateValue)) return false;
3845
+ cancelCrtBillingDayDetailRetry();
3257
3846
  billingSelectedDate = dateValue;
3847
+ billingSelectedHour = null;
3848
+ const isToday = crtBillingSelectedDayIsCurrent(crtBillingDayPoint(dateValue));
3258
3849
  billingDayDetail = billingDayDetailCache.get(dateValue)?.detail || null;
3259
3850
  billingDayDetailError = '';
3260
- renderCrtBillingSelectedDay();
3261
- void loadCrtBillingDayDetail(dateValue);
3851
+ renderCrtBillingSelectedDay({ preferSummary: isToday });
3852
+ void loadCrtBillingDayDetail(dateValue, { force: isToday, live: isToday });
3262
3853
  if (focus) {
3263
- const cell = document.querySelector(`#billing-daily-bars .billing-daily-column[data-date="${dateValue}"]`);
3854
+ const cell = document.querySelector(`#billing-calendar-grid .billing-calendar-day[data-date="${dateValue}"]`);
3264
3855
  if (cell) {
3265
3856
  cell.focus({ preventScroll: true });
3266
3857
  scrollCrtBillingSelectedDayIntoView();
@@ -3269,18 +3860,27 @@ function selectCrtBillingDay(dateValue, { focus = false } = {}) {
3269
3860
  return true;
3270
3861
  }
3271
3862
 
3272
- async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3863
+ function cancelCrtBillingDayDetailRetry() {
3864
+ if (billingDayDetailRetryTimer !== null) {
3865
+ clearTimeout(billingDayDetailRetryTimer);
3866
+ billingDayDetailRetryTimer = null;
3867
+ }
3868
+ }
3869
+
3870
+ async function loadCrtBillingDayDetail(dateValue, { force = false, live = false, retryCount = 0 } = {}) {
3273
3871
  const date = String(dateValue || '').trim();
3274
3872
  if (!crtBillingDayPoint(date)) return;
3873
+ if (retryCount === 0) cancelCrtBillingDayDetailRetry();
3275
3874
  const cachedEntry = billingDayDetailCache.get(date);
3276
3875
  const cached = cachedEntry && cachedEntry.detail;
3277
- const cacheFresh = cachedEntry && Date.now() - cachedEntry.fetchedAt <= CRT_BILLING_DAY_DETAIL_CACHE_MS;
3876
+ const cacheMaxAge = live ? CRT_BILLING_LIVE_DAY_REFRESH_MS : CRT_BILLING_DAY_DETAIL_CACHE_MS;
3877
+ const cacheFresh = cachedEntry && Date.now() - cachedEntry.fetchedAt <= cacheMaxAge;
3278
3878
  if (cached && cacheFresh && !force) {
3279
3879
  if (billingSelectedDate === date) {
3280
3880
  billingDayDetail = cached;
3281
3881
  billingDayDetailLoading = false;
3282
3882
  billingDayDetailError = '';
3283
- renderCrtBillingDayInsight();
3883
+ renderCrtBillingSelectedDay();
3284
3884
  }
3285
3885
  return;
3286
3886
  }
@@ -3293,11 +3893,12 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3293
3893
  billingDayDetailLoading = true;
3294
3894
  billingDayDetailError = '';
3295
3895
  if (billingSelectedDate === date) {
3296
- billingDayDetail = null;
3896
+ if (!cached) billingDayDetail = null;
3297
3897
  renderCrtBillingDayInsight();
3298
3898
  }
3899
+ let shouldRetry = false;
3299
3900
  try {
3300
- const response = await fetch(farmingApiPath(`/usage/day?date=${encodeURIComponent(date)}`), {
3901
+ const response = await fetch(farmingApiPath(`/usage/day?date=${encodeURIComponent(date)}${live ? '&live=1' : ''}`), {
3301
3902
  signal: controller.signal,
3302
3903
  cache: 'no-store',
3303
3904
  });
@@ -3306,18 +3907,44 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3306
3907
  throw new Error(data && data.error ? data.error : `Usage day request failed (${response.status})`);
3307
3908
  }
3308
3909
  if (requestSequence !== billingDayDetailRequestSequence) return;
3910
+ const previousDetail = cached || (billingDayDetail && billingDayDetail.date === date ? billingDayDetail : null);
3911
+ const nextTotal = Math.max(0, Number(data.detail.total && data.detail.total.totalTokens) || 0);
3912
+ if (
3913
+ nextTotal > 0
3914
+ && crtBillingDayDetailHasHourlyActivity(previousDetail)
3915
+ && !crtBillingDayDetailHasHourlyActivity(data.detail)
3916
+ ) {
3917
+ throw new Error('Usage day response omitted previously available hourly bins');
3918
+ }
3309
3919
  billingDayDetailCache.set(date, { detail: data.detail, fetchedAt: Date.now() });
3310
3920
  if (billingSelectedDate === date) billingDayDetail = data.detail;
3921
+ if (live) renderCrtBillingDaily();
3311
3922
  } catch (error) {
3312
3923
  if (controller.signal.aborted || requestSequence !== billingDayDetailRequestSequence) return;
3313
- if (billingSelectedDate === date) {
3924
+ if (billingSelectedDate === date && retryCount < CRT_BILLING_DAY_DETAIL_MAX_RETRIES) {
3925
+ shouldRetry = true;
3926
+ } else if (billingSelectedDate === date) {
3314
3927
  billingDayDetailError = error instanceof Error ? error.message : 'Failed to load selected day';
3315
3928
  }
3316
3929
  } finally {
3317
3930
  if (requestSequence === billingDayDetailRequestSequence) {
3318
- billingDayDetailLoading = false;
3319
3931
  billingDayDetailAbortController = null;
3320
- if (billingSelectedDate === date) renderCrtBillingDayInsight();
3932
+ if (shouldRetry) {
3933
+ billingDayDetailLoading = true;
3934
+ cancelCrtBillingDayDetailRetry();
3935
+ const retryDelay = CRT_BILLING_DAY_DETAIL_RETRY_MS * (2 ** retryCount);
3936
+ billingDayDetailRetryTimer = setTimeout(() => {
3937
+ billingDayDetailRetryTimer = null;
3938
+ if (billingSelectedDate !== date || crtMainView !== 'billing') {
3939
+ billingDayDetailLoading = false;
3940
+ return;
3941
+ }
3942
+ void loadCrtBillingDayDetail(date, { force: true, live, retryCount: retryCount + 1 });
3943
+ }, retryDelay);
3944
+ } else {
3945
+ billingDayDetailLoading = false;
3946
+ }
3947
+ if (billingSelectedDate === date) renderCrtBillingSelectedDay();
3321
3948
  }
3322
3949
  }
3323
3950
  }
@@ -3325,7 +3952,7 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3325
3952
  function scrollCrtBillingSelectedDayIntoView() {
3326
3953
  const scroll = document.getElementById('billing-daily-scroll');
3327
3954
  const cell = billingSelectedDate
3328
- ? document.querySelector(`#billing-daily-bars .billing-daily-column[data-date="${billingSelectedDate}"]`)
3955
+ ? document.querySelector(`#billing-calendar-grid .billing-calendar-day[data-date="${billingSelectedDate}"]`)
3329
3956
  : null;
3330
3957
  if (!scroll || !cell) return;
3331
3958
  const left = cell.offsetLeft;
@@ -3355,7 +3982,19 @@ function renderCrtBillingDaily(summary = billingSummary) {
3355
3982
  const element = document.getElementById(id);
3356
3983
  if (element) element.textContent = formatCrtUsageValue(value);
3357
3984
  };
3358
- setValue('billing-today-total', totals.todayTokens);
3985
+ const todayDetail = daily && billingDayDetailCache.get(daily.endDate)?.detail;
3986
+ const todayTokens = todayDetail && todayDetail.total ? todayDetail.total.totalTokens : totals.todayTokens;
3987
+ updateCrtBillingAnimatedMetric('billing-today-total', todayTokens, {
3988
+ date: daily && daily.endDate || '',
3989
+ live: true,
3990
+ write: (displayed, target) => {
3991
+ const element = document.getElementById('billing-today-total');
3992
+ if (!element) return;
3993
+ element.textContent = formatCrtUsageValue(displayed);
3994
+ element.dataset.displayedValue = displayed === null ? '' : String(displayed);
3995
+ element.dataset.targetValue = target === null ? '' : String(target);
3996
+ },
3997
+ });
3359
3998
  setValue('billing-7d-total', totals.sevenDayTokens);
3360
3999
  setValue('billing-30d-total', totals.thirtyDayTokens);
3361
4000
  setValue('billing-period-total', totals.periodTokens);
@@ -3376,12 +4015,9 @@ function renderCrtBillingDaily(summary = billingSummary) {
3376
4015
  : 'LOCAL TIME';
3377
4016
  }
3378
4017
 
3379
- const bars = document.getElementById('billing-daily-bars');
3380
- const xAxis = document.getElementById('billing-daily-x-axis');
3381
- const yAxis = document.getElementById('billing-y-axis');
3382
- const guides = document.getElementById('billing-log-guides');
3383
- const activity = document.getElementById('billing-activity-strip');
3384
- if (!bars || !xAxis || !yAxis || !guides || !activity) return;
4018
+ const calendar = document.getElementById('billing-calendar-grid');
4019
+ const months = document.getElementById('billing-calendar-months');
4020
+ if (!calendar || !months) return;
3385
4021
  const signature = points.map(point => [
3386
4022
  point.date,
3387
4023
  point.totalTokens,
@@ -3390,95 +4026,62 @@ function renderCrtBillingDaily(summary = billingSummary) {
3390
4026
  ].join(':')).join('|');
3391
4027
  if (signature !== billingDailyRenderSignature) {
3392
4028
  billingDailyRenderSignature = signature;
3393
- bars.replaceChildren();
3394
- xAxis.replaceChildren();
3395
- yAxis.replaceChildren();
3396
- guides.replaceChildren();
3397
- activity.replaceChildren();
3398
-
3399
- const chartPoints = points.slice(-120);
3400
- const peakTokens = Math.max(1_000, ...chartPoints.map(point => Math.max(0, Number(point.totalTokens) || 0)));
3401
- const maximum = peakTokens * 1.08;
3402
- const minimum = Math.max(1, maximum / 1_000);
3403
- crtBillingLogGuideValues(minimum, maximum).forEach((value) => {
3404
- const position = crtBillingLogPosition(value, minimum, maximum);
3405
- const guide = document.createElement('div');
3406
- guide.className = 'billing-log-guide';
3407
- guide.style.bottom = `${position}%`;
3408
- guides.appendChild(guide);
3409
-
3410
- const axisLabel = document.createElement('span');
3411
- axisLabel.className = 'billing-y-axis-label';
3412
- axisLabel.style.bottom = `${position}%`;
3413
- axisLabel.textContent = formatCrtUsageValue(value);
3414
- yAxis.appendChild(axisLabel);
4029
+ calendar.replaceChildren();
4030
+ months.replaceChildren();
4031
+
4032
+ const chartPoints = points.slice(-(52 * 7));
4033
+ const heatThresholds = crtBillingHeatThresholds(chartPoints.map(point => point && point.totalTokens));
4034
+ const firstDate = parseCrtBillingDate(chartPoints[0] && chartPoints[0].date);
4035
+ const leadingDays = firstDate ? (firstDate.getDay() + 6) % 7 : 0;
4036
+ const weekCount = Math.max(1, Math.ceil((leadingDays + chartPoints.length) / 7));
4037
+ calendar.style.setProperty('--billing-calendar-weeks', String(weekCount));
4038
+ months.style.setProperty('--billing-calendar-weeks', String(weekCount));
4039
+ Array.from({ length: leadingDays }).forEach(() => {
4040
+ const spacer = document.createElement('span');
4041
+ spacer.className = 'billing-calendar-spacer';
4042
+ spacer.setAttribute('aria-hidden', 'true');
4043
+ calendar.appendChild(spacer);
3415
4044
  });
3416
- const floorLabel = document.createElement('span');
3417
- floorLabel.className = 'billing-y-axis-label is-floor';
3418
- floorLabel.style.bottom = '0';
3419
- floorLabel.textContent = `≤${formatCrtUsageValue(minimum)}`;
3420
- yAxis.appendChild(floorLabel);
3421
-
3422
- bars.style.setProperty('--billing-chart-days', String(Math.max(1, chartPoints.length)));
3423
- xAxis.style.setProperty('--billing-chart-days', String(Math.max(1, chartPoints.length)));
4045
+
4046
+ const monthLabels = Array.from({ length: weekCount }, () => '');
3424
4047
  chartPoints.forEach((point, index) => {
3425
4048
  const pointDate = parseCrtBillingDate(point.date);
3426
- const axisTick = document.createElement('span');
3427
- const isFirst = index === 0;
3428
- const isLast = index === chartPoints.length - 1;
3429
- const isHalfMonth = pointDate && [1, 15].includes(pointDate.getDate());
3430
- if (pointDate && (isFirst || isLast || isHalfMonth)) {
3431
- const month = pointDate.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
3432
- axisTick.textContent = `${month} ${String(pointDate.getDate()).padStart(2, '0')}`;
3433
- axisTick.className = `has-label${isLast ? ' is-end' : ''}`;
4049
+ const weekIndex = Math.floor((leadingDays + index) / 7);
4050
+ if (pointDate && (index === 0 || pointDate.getDate() === 1) && !monthLabels[weekIndex]) {
4051
+ monthLabels[weekIndex] = pointDate.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
3434
4052
  }
3435
- xAxis.appendChild(axisTick);
3436
-
3437
4053
  const total = Math.max(0, Number(point.totalTokens) || 0);
3438
4054
  const cache = Math.min(total, Math.max(0, Number(point.cacheReadTokens) || 0)
3439
4055
  + Math.max(0, Number(point.cacheWriteTokens) || 0));
3440
- const direct = Math.max(0, total - cache);
3441
- const column = document.createElement('button');
3442
- column.type = 'button';
3443
- column.className = 'billing-daily-column';
3444
- column.dataset.date = point.date;
3445
- column.dataset.billion = total >= 1_000_000_000 ? 'true' : 'false';
3446
- column.setAttribute('role', 'gridcell');
3447
- column.setAttribute('aria-label', `${point.date}: ${formatCrtExactUsageValue(total)} tokens, ${formatCrtExactUsageValue(cache)} cache tokens`);
3448
- column.setAttribute('aria-selected', 'false');
3449
- column.tabIndex = -1;
3450
- column.title = `${point.date} · ${formatCrtExactUsageValue(total)} total · ${formatCrtExactUsageValue(cache)} cache`;
3451
- column.addEventListener('click', () => selectCrtBillingDay(point.date));
3452
- if (total > 0) {
3453
- const bar = document.createElement('span');
3454
- bar.className = 'billing-daily-bar';
3455
- bar.style.height = `${crtBillingLogPosition(total, minimum, maximum)}%`;
3456
- if (direct > 0) {
3457
- const directSegment = document.createElement('span');
3458
- directSegment.className = 'billing-daily-direct';
3459
- directSegment.style.height = `${direct / total * 100}%`;
3460
- bar.appendChild(directSegment);
3461
- }
3462
- if (cache > 0) {
3463
- const cacheSegment = document.createElement('span');
3464
- cacheSegment.className = 'billing-daily-cache';
3465
- cacheSegment.style.height = `${cache / total * 100}%`;
3466
- bar.appendChild(cacheSegment);
3467
- }
3468
- column.appendChild(bar);
3469
- }
3470
- bars.appendChild(column);
4056
+ const overrangeTier = crtBillingOverrangeTier(total);
4057
+ const overrangeLabel = crtBillingOverrangeLabel(overrangeTier);
4058
+ const day = document.createElement('button');
4059
+ day.type = 'button';
4060
+ day.className = 'billing-calendar-day';
4061
+ day.dataset.date = point.date;
4062
+ day.dataset.level = overrangeTier ? 'overrange' : String(crtBillingHeatLevel(total, heatThresholds));
4063
+ if (overrangeTier) day.dataset.overrange = String(overrangeTier);
4064
+ day.setAttribute('role', 'gridcell');
4065
+ day.setAttribute('aria-label', `${point.date}: ${formatCrtExactUsageValue(total)} tokens, ${formatCrtExactUsageValue(cache)} cache tokens${overrangeLabel ? `, ${overrangeLabel}` : ''}`);
4066
+ day.setAttribute('aria-selected', 'false');
4067
+ day.tabIndex = -1;
4068
+ day.title = `${point.date} · ${formatCrtExactUsageValue(total)} total · ${formatCrtExactUsageValue(cache)} cache${overrangeLabel ? ` · ${overrangeLabel}` : ''}`;
4069
+ day.addEventListener('click', () => selectCrtBillingDay(point.date));
4070
+ calendar.appendChild(day);
3471
4071
  });
3472
-
3473
- activity.style.setProperty('--billing-activity-days', String(Math.max(1, points.length)));
3474
- activity.setAttribute('aria-label', `${points.length}-day activity: ${activeDays} active days, ${billionDays} days at or above one billion tokens`);
3475
- points.forEach((point) => {
3476
- const tick = document.createElement('span');
3477
- const total = Math.max(0, Number(point.totalTokens) || 0);
3478
- tick.className = `billing-activity-tick${total > 0 ? ' is-active' : ''}${total >= 1_000_000_000 ? ' is-billion' : ''}`;
3479
- tick.title = `${point.date} · ${formatCrtExactUsageValue(total)} tokens`;
3480
- activity.appendChild(tick);
4072
+ const trailingDays = weekCount * 7 - leadingDays - chartPoints.length;
4073
+ Array.from({ length: trailingDays }).forEach(() => {
4074
+ const spacer = document.createElement('span');
4075
+ spacer.className = 'billing-calendar-spacer';
4076
+ spacer.setAttribute('aria-hidden', 'true');
4077
+ calendar.appendChild(spacer);
3481
4078
  });
4079
+ monthLabels.forEach((label) => {
4080
+ const month = document.createElement('span');
4081
+ month.textContent = label;
4082
+ months.appendChild(month);
4083
+ });
4084
+ calendar.setAttribute('aria-label', `${chartPoints.length}-day token activity: ${activeDays} active days, ${billionDays} days at or above one billion tokens`);
3482
4085
 
3483
4086
  if (!billingSelectedDate || !points.some(point => point.date === billingSelectedDate)) {
3484
4087
  billingSelectedDate = daily && daily.endDate || points.at(-1)?.date || '';
@@ -3765,10 +4368,6 @@ async function loadCrtBilling({ fresh = false } = {}) {
3765
4368
  billingAbortController = controller;
3766
4369
  billingLoading = true;
3767
4370
  billingError = '';
3768
- if (fresh) {
3769
- billingDayDetailCache.clear();
3770
- billingDayDetail = null;
3771
- }
3772
4371
  renderCrtBilling();
3773
4372
  try {
3774
4373
  const response = await fetch(farmingApiPath(`/usage${fresh ? '?fresh=1' : ''}`), {
@@ -3788,7 +4387,10 @@ async function loadCrtBilling({ fresh = false } = {}) {
3788
4387
  billingAbortController = null;
3789
4388
  renderCrtBilling();
3790
4389
  if (!billingError && billingSelectedDate) {
3791
- void loadCrtBillingDayDetail(billingSelectedDate, { force: fresh });
4390
+ void loadCrtBillingDayDetail(billingSelectedDate, {
4391
+ force: fresh,
4392
+ live: crtBillingSelectedDayIsCurrent(),
4393
+ });
3792
4394
  }
3793
4395
  }
3794
4396
  }
@@ -3799,6 +4401,10 @@ function stopCrtBillingRefresh({ abort = false } = {}) {
3799
4401
  clearInterval(billingRefreshTimer);
3800
4402
  billingRefreshTimer = null;
3801
4403
  }
4404
+ if (billingLiveDayRefreshTimer !== null) {
4405
+ clearInterval(billingLiveDayRefreshTimer);
4406
+ billingLiveDayRefreshTimer = null;
4407
+ }
3802
4408
  if (abort && billingAbortController) {
3803
4409
  billingRequestSequence += 1;
3804
4410
  billingAbortController.abort();
@@ -3811,6 +4417,11 @@ function stopCrtBillingRefresh({ abort = false } = {}) {
3811
4417
  billingDayDetailAbortController = null;
3812
4418
  billingDayDetailLoading = false;
3813
4419
  }
4420
+ if (abort) {
4421
+ cancelCrtBillingDayDetailRetry();
4422
+ cancelCrtBillingTotalAnimation();
4423
+ cancelCrtBillingMetricAnimations();
4424
+ }
3814
4425
  }
3815
4426
 
3816
4427
  function startCrtBillingRefresh() {
@@ -3818,6 +4429,16 @@ function startCrtBillingRefresh() {
3818
4429
  billingRefreshTimer = setInterval(() => {
3819
4430
  if (crtMainView === 'billing' && document.visibilityState !== 'hidden') void loadCrtBilling();
3820
4431
  }, CRT_BILLING_REFRESH_MS);
4432
+ billingLiveDayRefreshTimer = setInterval(() => {
4433
+ if (
4434
+ crtMainView !== 'billing'
4435
+ || billingMode !== 'days'
4436
+ || document.visibilityState === 'hidden'
4437
+ || billingDayDetailLoading
4438
+ || !crtBillingSelectedDayIsCurrent()
4439
+ ) return;
4440
+ void loadCrtBillingDayDetail(billingSelectedDate, { force: true, live: true });
4441
+ }, CRT_BILLING_LIVE_DAY_REFRESH_MS);
3821
4442
  }
3822
4443
 
3823
4444
  function refreshCrtBilling() {
@@ -4140,7 +4761,7 @@ function needsMainAgent(currentState = state) {
4140
4761
  const mainAgent = currentState && currentState.mainAgentId
4141
4762
  ? currentState.agents.find((agent) => agent.id === currentState.mainAgentId)
4142
4763
  : null;
4143
- return !currentState || !currentState.mainAgentId || (mainAgent && mainAgent.status === 'dead');
4764
+ return !currentState || !currentState.mainAgentId || !isCrtLiveAgent(mainAgent);
4144
4765
  }
4145
4766
 
4146
4767
  function getDefaultWorkspaceForDialog(asMainAgent) {
@@ -4337,6 +4958,7 @@ function refreshWorkspaceMemoryUI() {
4337
4958
  function seedWorkspaceInput() {
4338
4959
  const workspaceInput = document.getElementById('workspace-input');
4339
4960
  if (!workspaceInput) return;
4961
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4340
4962
  workspaceInput.value = '';
4341
4963
  workspaceInput.placeholder = pendingMainAgentLaunch
4342
4964
  ? formatWorkspaceForDisplay(getDefaultWorkspaceForDialog(true))
@@ -4400,13 +5022,129 @@ function setupWorkspaceHistoryControls() {
4400
5022
  }
4401
5023
 
4402
5024
  async function confirmStartAgent() {
4403
- if (waitingForAgent || selectedAgentIndex === null || selectedAgentIndex < 0 || selectedAgentIndex >= agents.length) return;
5025
+ if (waitingForAgent || workspaceDirectoryPrompt || selectedAgentIndex === null || selectedAgentIndex < 0 || selectedAgentIndex >= agents.length) return;
4404
5026
 
4405
5027
  const agent = agents[selectedAgentIndex];
4406
5028
  const workspaceInput = normalizeWorkspaceValue(document.getElementById('workspace-input').value);
4407
5029
  const asMainAgent = pendingMainAgentLaunch;
4408
5030
  const workspaceToUse = resolveWorkspaceToStart(workspaceInput, asMainAgent);
4409
5031
 
5032
+ if (asMainAgent || !workspaceToUse) {
5033
+ await startCrtAgent(agent, workspaceToUse, asMainAgent);
5034
+ return;
5035
+ }
5036
+
5037
+ try {
5038
+ const result = await prepareCrtWorkspaceDirectory(workspaceToUse, false);
5039
+ if (result.status === 'ready') {
5040
+ await startCrtAgent(agent, result.workspace, false);
5041
+ return;
5042
+ }
5043
+ showWorkspaceDirectoryPrompt(result.status === 'missing' ? 'confirm' : 'error', {
5044
+ workspace: result.workspace || workspaceToUse,
5045
+ code: result.code || ''
5046
+ });
5047
+ } catch {
5048
+ showWorkspaceDirectoryPrompt('error', { workspace: workspaceToUse, code: '' });
5049
+ }
5050
+ }
5051
+
5052
+ async function prepareCrtWorkspaceDirectory(workspace, create) {
5053
+ const response = await fetch(farmingApiPath('/workspaces/prepare'), {
5054
+ method: 'POST',
5055
+ headers: { 'Content-Type': 'application/json' },
5056
+ body: JSON.stringify({ workspace, create: create === true })
5057
+ });
5058
+ const result = await response.json().catch(() => null);
5059
+ if (result && typeof result.status === 'string' && typeof result.workspace === 'string') {
5060
+ return result;
5061
+ }
5062
+ throw new Error(result && result.message ? result.message : `Failed to prepare workspace (${response.status})`);
5063
+ }
5064
+
5065
+ function showWorkspaceDirectoryPrompt(kind, { workspace, code = '' }) {
5066
+ const prompt = document.getElementById('workspace-directory-prompt');
5067
+ const input = document.getElementById('workspace-input');
5068
+ const history = document.getElementById('workspace-history');
5069
+ const startActions = document.getElementById('workspace-start-actions');
5070
+ const createButton = document.getElementById('workspace-directory-create');
5071
+ const cancelButton = document.getElementById('workspace-directory-cancel');
5072
+ const errorBackButton = document.getElementById('workspace-directory-error-back');
5073
+ if (!prompt || !input || !createButton || !cancelButton || !errorBackButton) return;
5074
+
5075
+ workspaceDirectoryPrompt = { kind, workspace, code };
5076
+ const isError = kind === 'error';
5077
+ const isCreating = kind === 'creating';
5078
+ prompt.hidden = false;
5079
+ prompt.classList.toggle('error', isError);
5080
+ input.disabled = true;
5081
+ if (history) history.style.display = 'none';
5082
+ if (startActions) startActions.style.display = 'none';
5083
+
5084
+ document.getElementById('workspace-directory-prompt-icon').textContent = isError ? '[!]' : '[+]';
5085
+ document.getElementById('workspace-directory-prompt-title').textContent = isError
5086
+ ? 'Could not create workspace'
5087
+ : isCreating
5088
+ ? 'Creating workspace...'
5089
+ : 'Create this workspace?';
5090
+ document.getElementById('workspace-directory-prompt-description').textContent = isError
5091
+ ? code === 'workspace-create-forbidden'
5092
+ ? 'Farming does not have permission to create this directory. Choose another location or update the parent directory permissions.'
5093
+ : 'Farming could not create this directory. Check the path and try again.'
5094
+ : 'This directory does not exist yet. Farming can create it and start the Agent there.';
5095
+ document.getElementById('workspace-directory-prompt-path').textContent = formatWorkspaceForDisplay(workspace);
5096
+
5097
+ createButton.hidden = isError;
5098
+ cancelButton.hidden = isError;
5099
+ errorBackButton.hidden = !isError;
5100
+ createButton.disabled = isCreating;
5101
+ cancelButton.disabled = isCreating;
5102
+ createButton.textContent = isCreating ? 'Creating...' : 'Create & Start [Enter]';
5103
+ clearCrtNavigationSelection();
5104
+ const primary = isError ? errorBackButton : createButton;
5105
+ if (!isCreating) {
5106
+ setCrtNavigationSelection(primary);
5107
+ primary.focus();
5108
+ }
5109
+ }
5110
+
5111
+ function hideWorkspaceDirectoryPrompt({ focusInput = true } = {}) {
5112
+ const prompt = document.getElementById('workspace-directory-prompt');
5113
+ const input = document.getElementById('workspace-input');
5114
+ const startActions = document.getElementById('workspace-start-actions');
5115
+ workspaceDirectoryPrompt = null;
5116
+ if (prompt) prompt.hidden = true;
5117
+ if (input) input.disabled = false;
5118
+ if (startActions) startActions.style.display = 'block';
5119
+ renderWorkspaceHistoryUI();
5120
+ if (focusInput && input) {
5121
+ input.focus();
5122
+ input.setSelectionRange(input.value.length, input.value.length);
5123
+ }
5124
+ }
5125
+
5126
+ async function createCrtWorkspaceAndStart() {
5127
+ if (!workspaceDirectoryPrompt || workspaceDirectoryPrompt.kind !== 'confirm') return;
5128
+ const target = workspaceDirectoryPrompt.workspace;
5129
+ const agent = selectedAgentIndex === null ? null : agents[selectedAgentIndex];
5130
+ if (!agent) return;
5131
+ showWorkspaceDirectoryPrompt('creating', { workspace: target });
5132
+ try {
5133
+ const result = await prepareCrtWorkspaceDirectory(target, true);
5134
+ if (result.status === 'created' || result.status === 'ready') {
5135
+ await startCrtAgent(agent, result.workspace, false);
5136
+ return;
5137
+ }
5138
+ showWorkspaceDirectoryPrompt('error', {
5139
+ workspace: result.workspace || target,
5140
+ code: result.code || ''
5141
+ });
5142
+ } catch {
5143
+ showWorkspaceDirectoryPrompt('error', { workspace: target, code: '' });
5144
+ }
5145
+ }
5146
+
5147
+ async function startCrtAgent(agent, workspaceToUse, asMainAgent) {
4410
5148
  console.log('Starting agent:', agent.name, 'workspace:', workspaceToUse || 'default');
4411
5149
 
4412
5150
  waitingForAgent = true;
@@ -4436,7 +5174,10 @@ async function confirmStartAgent() {
4436
5174
  }));
4437
5175
  }
4438
5176
 
5177
+ globalThis.createCrtWorkspaceAndStart = createCrtWorkspaceAndStart;
5178
+
4439
5179
  function backToAgentList() {
5180
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4440
5181
  clearCrtNavigationSelection();
4441
5182
  selectedAgentIndex = null;
4442
5183
  document.getElementById('agent-list').style.display = 'block';
@@ -4449,6 +5190,7 @@ function selectAgent(index) {
4449
5190
  if (index < 0 || index >= agents.length) return;
4450
5191
 
4451
5192
  const agent = agents[index];
5193
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4452
5194
 
4453
5195
  console.log('Selected agent:', agent.name);
4454
5196
  clearCrtNavigationSelection();
@@ -4478,10 +5220,11 @@ function requestedCrtAgentId(search = typeof window !== 'undefined' ? window.loc
4478
5220
  function openCrtAgentDeeplinkIfReady() {
4479
5221
  if (didApplyAgentDeeplink || !state) return false;
4480
5222
  didApplyAgentDeeplink = true;
5223
+ crtReadingAnchorApi()?.importFromSearch(window.location.search);
4481
5224
 
4482
5225
  const agentId = requestedCrtAgentId();
4483
5226
  const agent = agentId
4484
- ? state.agents.find((candidate) => candidate.id === agentId && candidate.archived !== true)
5227
+ ? state.agents.find((candidate) => candidate.id === agentId && isCrtLiveAgent(candidate))
4485
5228
  : null;
4486
5229
  if (!agent) return false;
4487
5230
 
@@ -4505,13 +5248,25 @@ function connect() {
4505
5248
  socket.onopen = () => {
4506
5249
  if (ws !== socket) return;
4507
5250
  console.log('Connected to server');
5251
+ socket.send(JSON.stringify({ type: 'protocol-hello', protocolVersion: CRT_PROTOCOL_VERSION }));
4508
5252
  const activeAgentId = isCrtSessionOpen() ? focusedAgentId : null;
4509
5253
  getSessionClient()?.focusAgent(activeAgentId, {
4510
5254
  streamScope: 'focused',
4511
5255
  previewScope: activeAgentId ? 'none' : 'all',
4512
5256
  });
4513
5257
  if (activeAgentId && terminal) {
4514
- void refreshSessionView(true, activeAgentId, getCurrentSessionToken());
5258
+ if (crtTerminalReplication) {
5259
+ clearPendingCrtTerminalFitResize(crtTerminalReplication);
5260
+ crtTerminalReplication.lastResizeCols = null;
5261
+ crtTerminalReplication.lastResizeRows = null;
5262
+ if (crtTerminalReplication.checkpointRetryTimer) {
5263
+ clearTimeout(crtTerminalReplication.checkpointRetryTimer);
5264
+ crtTerminalReplication.checkpointRetryTimer = null;
5265
+ }
5266
+ TERMINAL_REPLAY.resetRecovery(crtTerminalReplication.replayState);
5267
+ TERMINAL_REPLAY.beginRecovery(crtTerminalReplication.replayState);
5268
+ requestCrtTerminalReplay();
5269
+ }
4515
5270
  }
4516
5271
  loadAgents();
4517
5272
  };
@@ -4519,9 +5274,20 @@ function connect() {
4519
5274
  socket.onmessage = (event) => {
4520
5275
  if (ws !== socket) return;
4521
5276
  const data = JSON.parse(event.data);
5277
+ if (data.type === 'protocol-hello') {
5278
+ if (data.protocolVersion !== CRT_PROTOCOL_VERSION) {
5279
+ socket.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
5280
+ }
5281
+ return;
5282
+ }
5283
+ if (data.type === 'protocol-error') {
5284
+ console.error(data.message || 'Farming protocol error');
5285
+ return;
5286
+ }
4522
5287
  if (data.type === 'state') {
4523
5288
  const prevAgentCount = state ? state.agents.length : 0;
4524
5289
  state = data.state;
5290
+ pruneCrtStructuredPreviews(state);
4525
5291
  const activeAgentIds = new Set(state.agents.map((agent) => agent.id));
4526
5292
  terminalPreviewSnapshots.forEach((_snapshot, agentId) => {
4527
5293
  if (!activeAgentIds.has(agentId)) terminalPreviewSnapshots.delete(agentId);
@@ -4532,6 +5298,7 @@ function connect() {
4532
5298
  }
4533
5299
  });
4534
5300
  const dashboardRendered = renderCrtDashboardIfNeeded();
5301
+ scheduleCrtRenderedStructuredPreviews();
4535
5302
  if (dashboardRendered && crtMainView === 'history') renderCrtHistory();
4536
5303
  if (crtMainView === 'search') renderCrtSearch();
4537
5304
  generateKeyMap();
@@ -4558,6 +5325,14 @@ function connect() {
4558
5325
  openCrtAgentDeeplinkIfReady();
4559
5326
  } else if (data.type === 'agent-started') {
4560
5327
  selectCrtStartedAgent(data.agentId);
5328
+ } else if (data.type === 'agent-update') {
5329
+ const update = data.update;
5330
+ const agent = update && state && state.agents.find(candidate => candidate.id === update.agentId);
5331
+ if (agent && update.patch && typeof update.patch === 'object') {
5332
+ Object.assign(agent, update.patch);
5333
+ renderCrtDashboardIfNeeded();
5334
+ if (agent.id === focusedAgentId) updateCrtRuntimeSwitchControl(agent);
5335
+ }
4561
5336
  } else if (data.type === 'session-preview') {
4562
5337
  const preview = data.preview;
4563
5338
  if (preview && preview.agentId) {
@@ -4573,11 +5348,16 @@ function connect() {
4573
5348
  agent.previewRows = preview.rows || agent.previewRows;
4574
5349
  agent.previewSnapshot = preview.previewSnapshot || null;
4575
5350
  if (preview.terminalStatus) agent.terminalStatus = preview.terminalStatus;
5351
+ if (preview.runtimeObservation) agent.runtimeObservation = preview.runtimeObservation;
4576
5352
  if (isCrtSessionOpen()) dashboardRenderDeferred = true;
4577
5353
  else scheduleCrtPreviewCardRender(agent, previousSnapshot, previousText, previewChanged);
4578
5354
  }
4579
5355
  }
4580
5356
  } else if (data.type === 'session-output') {
5357
+ if (crtTerminalReplication) {
5358
+ handleCrtTerminalStream(data.stream);
5359
+ return;
5360
+ }
4581
5361
  const runtime = getSessionRuntime();
4582
5362
  const sessionToken = runtime ? runtime.getSessionToken() : 0;
4583
5363
  const runtimeResult = runtime ? runtime.handleStreamMessage(data.stream) : null;
@@ -4596,10 +5376,20 @@ function connect() {
4596
5376
  setSessionOutputLength(getSessionOutputLength() + patch.nextLengthDelta);
4597
5377
  }
4598
5378
  }
5379
+ } else if (data.type === 'agent-activity') {
5380
+ const activity = data.activity;
5381
+ const agent = activity && state && state.agents.find((candidate) => candidate.id === activity.agentId);
5382
+ if (agent) {
5383
+ Object.assign(agent, activity);
5384
+ renderCrtDashboardIfNeeded();
5385
+ }
4599
5386
  } else if (data.type === 'system-stats') {
4600
5387
  updateSystemStats(data.stats, data.uptime, data.usageRate);
4601
5388
  } else if (data.type === 'error') {
4602
5389
  waitingForAgent = false;
5390
+ if (workspaceDirectoryPrompt?.kind === 'creating') {
5391
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
5392
+ }
4603
5393
  alert('Error: ' + data.message);
4604
5394
  }
4605
5395
  };
@@ -4608,6 +5398,21 @@ function connect() {
4608
5398
  if (ws !== socket) return;
4609
5399
  ws = null;
4610
5400
  console.log('Disconnected from server');
5401
+ if (crtTerminalReplication) {
5402
+ clearPendingCrtTerminalFitResize(crtTerminalReplication);
5403
+ crtTerminalReplication.lastResizeCols = null;
5404
+ crtTerminalReplication.lastResizeRows = null;
5405
+ crtTerminalReplication.checkpointSeq += 1;
5406
+ crtTerminalReplication.checkpointAbortController?.abort();
5407
+ crtTerminalReplication.checkpointAbortController = null;
5408
+ crtTerminalReplication.checkpointInFlight = false;
5409
+ if (crtTerminalReplication.checkpointRetryTimer) {
5410
+ clearTimeout(crtTerminalReplication.checkpointRetryTimer);
5411
+ crtTerminalReplication.checkpointRetryTimer = null;
5412
+ }
5413
+ TERMINAL_REPLAY.resetRecovery(crtTerminalReplication.replayState);
5414
+ TERMINAL_REPLAY.beginRecovery(crtTerminalReplication.replayState);
5415
+ }
4611
5416
  if (typeof document === 'undefined' || document.visibilityState !== 'hidden') {
4612
5417
  wsReconnectTimer = setTimeout(() => {
4613
5418
  wsReconnectTimer = null;
@@ -4651,7 +5456,7 @@ function checkMainAgentStatus() {
4651
5456
  ? state.agents.find(a => a.id === state.mainAgentId)
4652
5457
  : null;
4653
5458
 
4654
- if (!state.mainAgentId || (mainAgent && mainAgent.status === 'dead')) {
5459
+ if (!state.mainAgentId || !isCrtLiveAgent(mainAgent)) {
4655
5460
  showInputDialog();
4656
5461
  }
4657
5462
  }
@@ -4729,9 +5534,34 @@ function updateSystemStats(stats, uptime, usageRate) {
4729
5534
  }
4730
5535
  }
4731
5536
 
5537
+ function isCrtLiveAgent(agent) {
5538
+ return Boolean(
5539
+ agent
5540
+ && agent.archived !== true
5541
+ && agent.status !== 'dead'
5542
+ && agent.status !== 'stopped'
5543
+ );
5544
+ }
5545
+
5546
+ function getCrtLiveAgents(currentState = state) {
5547
+ if (!currentState || !Array.isArray(currentState.agents)) return [];
5548
+ return currentState.agents.filter(isCrtLiveAgent);
5549
+ }
5550
+
4732
5551
  function getCrtRegularAgents(currentState = state) {
4733
5552
  if (!currentState || !Array.isArray(currentState.agents)) return [];
4734
- return currentState.agents.filter((agent) => agent.id !== currentState.mainAgentId);
5553
+ return getCrtLiveAgents(currentState).filter((agent) => (
5554
+ agent.id !== currentState.mainAgentId && agent.isMain !== true
5555
+ ));
5556
+ }
5557
+
5558
+ function getCrtAgentRemovalFallback(currentState, removedAgentId) {
5559
+ const liveAgents = getCrtLiveAgents(currentState);
5560
+ const removedIndex = liveAgents.findIndex((agent) => agent.id === removedAgentId);
5561
+ const remaining = liveAgents.filter((agent) => agent.id !== removedAgentId);
5562
+ if (!remaining.length) return '';
5563
+ if (removedIndex < 0) return remaining[0].id;
5564
+ return remaining[Math.min(removedIndex, remaining.length - 1)].id;
4735
5565
  }
4736
5566
 
4737
5567
  function updateCrtAgentPageStatus(pageState) {
@@ -4774,8 +5604,9 @@ function renderState() {
4774
5604
  lastCrtDashboardSignature = crtDashboardStateSignature(state);
4775
5605
 
4776
5606
  // 更新吊顶的 Agent 数量
4777
- const activeAgents = state.agents.filter(a => a.status === 'running').length;
4778
- const totalAgents = state.agents.length;
5607
+ const visibleAgents = getCrtLiveAgents(state);
5608
+ const activeAgents = visibleAgents.filter(a => a.status === 'running').length;
5609
+ const totalAgents = visibleAgents.length;
4779
5610
  document.getElementById('active-agents').textContent = activeAgents;
4780
5611
  document.getElementById('total-agents').textContent = totalAgents;
4781
5612
  updateCrtBrandState(state);
@@ -4789,18 +5620,21 @@ function renderState() {
4789
5620
  agentBlocks.forEach(block => block.remove());
4790
5621
 
4791
5622
  mainAgentBlock.innerHTML = '';
5623
+ mainAgentBlock.removeAttribute('data-agent-id');
5624
+ mainAgentBlock.removeAttribute('data-crt-nav-key');
4792
5625
 
4793
- if (state.agents.length === 0) {
4794
- emptyState.style.display = 'flex';
5626
+ const regularAgents = getCrtRegularAgents(state);
5627
+ const hasRegularAgents = regularAgents.length > 0;
5628
+ mapArea.classList.toggle('empty', !hasRegularAgents);
5629
+ emptyState.style.display = hasRegularAgents ? 'none' : 'flex';
5630
+
5631
+ if (visibleAgents.length === 0) {
4795
5632
  mainAgentPanel.style.display = 'none';
4796
5633
  updateCrtAgentPageStatus(getCrtAgentPage([], 0, 1));
4797
5634
  showInputDialog();
4798
5635
  return;
4799
5636
  }
4800
5637
 
4801
- emptyState.style.display = 'none';
4802
-
4803
- const regularAgents = getCrtRegularAgents(state);
4804
5638
  const pageLayout = calculateCrtAgentPageLayout(
4805
5639
  mapArea.clientWidth,
4806
5640
  mapArea.clientHeight,
@@ -4823,7 +5657,7 @@ function renderState() {
4823
5657
  // 渲染 Main Agent 到右下角
4824
5658
  if (state.mainAgentId) {
4825
5659
  const mainAgent = state.agents.find(a => a.id === state.mainAgentId);
4826
- if (mainAgent) {
5660
+ if (isCrtLiveAgent(mainAgent)) {
4827
5661
  mainAgentPanel.style.display = 'block';
4828
5662
  mainAgentBlock.dataset.crtNavKey = `agent:${mainAgent.id}`;
4829
5663
  mainAgentBlock.dataset.agentId = mainAgent.id;
@@ -4843,13 +5677,7 @@ function renderState() {
4843
5677
  const output = document.createElement('div');
4844
5678
  output.className = 'agent-output';
4845
5679
  output.style.height = '80px';
4846
- const cleanOutput = getAgentDisplayText(mainAgent);
4847
- const outputTail = document.createElement('div');
4848
- outputTail.className = 'agent-output-tail';
4849
- if (!renderCrtTerminalSnapshot(outputTail, mainAgent.previewSnapshot)) {
4850
- outputTail.textContent = cleanOutput.slice(-150) || 'No output yet...';
4851
- }
4852
- output.appendChild(outputTail);
5680
+ renderCrtAgentOutput(output, mainAgent, { main: true });
4853
5681
  mainAgentBlock.appendChild(output);
4854
5682
 
4855
5683
  mainAgentBlock.onclick = () => openSession(mainAgent.id);
@@ -4893,13 +5721,7 @@ function renderState() {
4893
5721
 
4894
5722
  const output = document.createElement('div');
4895
5723
  output.className = 'agent-output';
4896
- const cleanOutput = getAgentDisplayText(agent);
4897
- const outputTail = document.createElement('div');
4898
- outputTail.className = 'agent-output-tail';
4899
- if (!renderCrtTerminalSnapshot(outputTail, agent.previewSnapshot)) {
4900
- outputTail.textContent = cleanOutput || 'No output yet...';
4901
- }
4902
- output.appendChild(outputTail);
5724
+ renderCrtAgentOutput(output, agent);
4903
5725
  block.appendChild(output);
4904
5726
 
4905
5727
  block.onclick = () => openSession(agent.id);
@@ -4914,14 +5736,14 @@ function generateKeyMap() {
4914
5736
 
4915
5737
  keyMap = {};
4916
5738
  let keyIndex = 1;
4917
- state.agents.forEach((agent) => {
4918
- if (agent.id === state.mainAgentId) return; // 跳过 Main Agent
5739
+ getCrtRegularAgents(state).forEach((agent) => {
4919
5740
  keyMap[keyIndex] = agent.id;
4920
5741
  keyIndex++;
4921
5742
  });
4922
5743
  }
4923
5744
 
4924
5745
  function showInputDialog(prefill = null) {
5746
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4925
5747
  clearCrtNavigationSelection();
4926
5748
  void loadAgents();
4927
5749
  const title = document.getElementById('dialog-title');
@@ -4972,6 +5794,7 @@ function hideInputDialog() {
4972
5794
  pendingMainAgentLaunch = false;
4973
5795
  pendingAgentLaunchPrefill = null;
4974
5796
  waitingForAgent = false;
5797
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4975
5798
  document.getElementById('agent-list').style.display = 'block';
4976
5799
  document.getElementById('workspace-input-container').style.display = 'none';
4977
5800
  document.getElementById('input-dialog').classList.remove('active');
@@ -4993,10 +5816,10 @@ function isCrtAgentInteractive(agent) {
4993
5816
  }
4994
5817
 
4995
5818
  function structuredRuntimeKind(agent) {
4996
- if (!agent) return '';
4997
- if (agent.agentRuntimeMode === 'acp') return 'ACP';
4998
- if (agent.agentRuntimeMode === 'json') return 'JSON';
4999
- if (agent.providerSessionProvider === 'codex' && agent.codexRuntimeMode === 'app-server') return 'APP SERVER';
5819
+ const kind = agent && agent.runtimeBinding && agent.runtimeBinding.kind;
5820
+ if (kind === 'acp') return 'ACP';
5821
+ if (kind === 'json') return 'JSON';
5822
+ if (kind === 'app-server') return 'APP SERVER';
5000
5823
  return '';
5001
5824
  }
5002
5825
 
@@ -5011,7 +5834,7 @@ function crtRuntimeView(agent) {
5011
5834
  function canSwitchCrtAgentRuntime(agent) {
5012
5835
  const freshCodexTerminal = agent
5013
5836
  && agent.providerSessionProvider === 'codex'
5014
- && agent.agentRuntimeMode === 'terminal'
5837
+ && agent.runtimeBinding?.kind === 'terminal'
5015
5838
  && agent.providerSessionTemporary === true
5016
5839
  && agent.terminalInputReceived !== true;
5017
5840
  return Boolean(
@@ -5033,6 +5856,43 @@ function isCrtRuntimeSwitchShortcut(event) {
5033
5856
  );
5034
5857
  }
5035
5858
 
5859
+ function hasCrtStructuredLocalEscapeAction(context = {}) {
5860
+ return Boolean(
5861
+ context.structuredTranscriptFocused
5862
+ || context.structuredToolFocused
5863
+ || context.structuredMenuItemFocused
5864
+ || context.structuredInterruptFocused
5865
+ || (context.structuredInputFocused && context.structuredComposerMenuOpen)
5866
+ );
5867
+ }
5868
+
5869
+ function resolveCrtSessionKeyboardCommand(event, context = {}) {
5870
+ if (!event) return '';
5871
+ const key = String(event.key || '').toLowerCase();
5872
+ const primaryModifier = Boolean(event.ctrlKey || event.metaKey);
5873
+
5874
+ // Session-wide commands must remain reachable from every focus owner,
5875
+ // including the hidden Terminal IME bridge and every structured Chat control.
5876
+ if (primaryModifier && key === 'k') return 'kill';
5877
+ if (primaryModifier && key === 'escape') return 'close';
5878
+
5879
+ // Plain Escape closes an idle Chat only when a more local transition does
5880
+ // not own it. Terminal keeps plain Escape for the running TUI.
5881
+ if (
5882
+ context.structuredSessionActive === true
5883
+ && key === 'escape'
5884
+ && !event.altKey
5885
+ && !event.shiftKey
5886
+ && event.isComposing !== true
5887
+ && context.composing !== true
5888
+ && !hasCrtStructuredLocalEscapeAction(context)
5889
+ ) {
5890
+ return 'close';
5891
+ }
5892
+
5893
+ return '';
5894
+ }
5895
+
5036
5896
  function setCrtRuntimeSwitchStatus(message = '', error = false) {
5037
5897
  const status = document.getElementById('crt-runtime-switch-status');
5038
5898
  const messageNode = document.getElementById('crt-runtime-switch-message');
@@ -5128,23 +5988,19 @@ function toggleCrtSessionRuntimeMode() {
5128
5988
  }
5129
5989
 
5130
5990
  function structuredTranscriptEndpoint(agent) {
5131
- if (agent.agentRuntimeMode === 'acp') return 'acp-transcript';
5132
- if (agent.agentRuntimeMode === 'json') return 'json-cli-transcript';
5991
+ if (agent.runtimeBinding.kind === 'acp') return 'acp-transcript';
5992
+ if (agent.runtimeBinding.kind === 'json') return 'json-cli-transcript';
5133
5993
  return 'codex-app-server-transcript';
5134
5994
  }
5135
5995
 
5136
5996
  function structuredRuntimeStatus(agent) {
5137
5997
  if (!agent) return '';
5138
- if (agent.agentRuntimeMode === 'acp') return agent.acpState || 'idle';
5139
- if (agent.agentRuntimeMode === 'json') return agent.jsonCliState || 'idle';
5140
- return agent.codexAppServerState || 'idle';
5998
+ return agent.runtimeBinding?.state || 'idle';
5141
5999
  }
5142
6000
 
5143
6001
  function structuredRuntimeError(agent) {
5144
6002
  if (!agent) return '';
5145
- if (agent.agentRuntimeMode === 'acp') return agent.acpError || '';
5146
- if (agent.agentRuntimeMode === 'json') return agent.jsonCliError || '';
5147
- return agent.codexAppServerError || '';
6003
+ return agent.runtimeBinding?.error || '';
5148
6004
  }
5149
6005
 
5150
6006
  function structuredComposerAction(agent, draft = '') {
@@ -5153,12 +6009,8 @@ function structuredComposerAction(agent, draft = '') {
5153
6009
  const status = String(structuredRuntimeStatus(agent) || 'idle');
5154
6010
  const working = ['working', 'waiting-for-permission'].includes(status);
5155
6011
  if (working) {
5156
- if (agent.agentRuntimeMode === 'acp' && String(draft || '').trim()) return 'send';
5157
- if (
5158
- agent.providerSessionProvider === 'codex'
5159
- && agent.codexRuntimeMode === 'app-server'
5160
- && String(draft || '').trim()
5161
- ) return 'steer';
6012
+ if (agent.runtimeBinding?.kind === 'acp' && String(draft || '').trim()) return 'send';
6013
+ if (agent.runtimeBinding?.kind === 'app-server' && String(draft || '').trim()) return 'steer';
5162
6014
  return 'interrupt';
5163
6015
  }
5164
6016
  if (['starting', 'interrupting'].includes(status)) return 'disabled';
@@ -5295,6 +6147,37 @@ function focusStructuredComposerMenuButton(current, offset = 0) {
5295
6147
  return true;
5296
6148
  }
5297
6149
 
6150
+ function structuredComposerMenuPath() {
6151
+ return `${structuredComposerMenu}:${structuredComposerConfigId}`;
6152
+ }
6153
+
6154
+ function captureStructuredComposerMenuFocus(menu) {
6155
+ const focused = document.activeElement && document.activeElement.closest
6156
+ ? document.activeElement.closest('.crt-structured-menu-item')
6157
+ : null;
6158
+ if (!focused || !menu.contains(focused)) return null;
6159
+ const buttons = structuredComposerMenuButtons();
6160
+ return {
6161
+ path: structuredComposerMenuPath(),
6162
+ key: focused.dataset.menuKey || '',
6163
+ index: buttons.indexOf(focused)
6164
+ };
6165
+ }
6166
+
6167
+ function restoreStructuredComposerMenuFocus(snapshot) {
6168
+ if (!snapshot || snapshot.path !== structuredComposerMenuPath()) return false;
6169
+ const buttons = structuredComposerMenuButtons();
6170
+ if (!buttons.length) return false;
6171
+ const matching = snapshot.key
6172
+ ? buttons.find((button) => button.dataset.menuKey === snapshot.key)
6173
+ : null;
6174
+ const fallbackIndex = Math.min(Math.max(snapshot.index, 0), buttons.length - 1);
6175
+ const target = matching || buttons[fallbackIndex];
6176
+ target.focus();
6177
+ target.scrollIntoView({ block: 'nearest' });
6178
+ return true;
6179
+ }
6180
+
5298
6181
  function closeStructuredComposerMenu({ restoreFocus = true } = {}) {
5299
6182
  structuredComposerMenu = '';
5300
6183
  structuredComposerConfigId = '';
@@ -5334,10 +6217,11 @@ async function patchStructuredAcpSession(patch) {
5334
6217
  if (opener && !opener.hidden && !opener.disabled) opener.focus();
5335
6218
  }
5336
6219
 
5337
- function structuredMenuButton(label, description, active, onClick) {
6220
+ function structuredMenuButton(label, description, active, onClick, menuKey = '') {
5338
6221
  const button = document.createElement('button');
5339
6222
  button.type = 'button';
5340
6223
  button.className = `crt-structured-menu-item${active ? ' active' : ''}`;
6224
+ button.dataset.menuKey = menuKey;
5341
6225
  const title = document.createElement('span');
5342
6226
  title.textContent = label;
5343
6227
  button.appendChild(title);
@@ -5353,6 +6237,7 @@ function structuredMenuButton(label, description, active, onClick) {
5353
6237
  function renderStructuredComposerMenu() {
5354
6238
  const menu = document.getElementById('crt-structured-composer-menu');
5355
6239
  if (!menu) return;
6240
+ const retainedFocus = captureStructuredComposerMenuFocus(menu);
5356
6241
  menu.replaceChildren();
5357
6242
  const session = structuredSessionSnapshot;
5358
6243
  if (!structuredComposerMenu || !session) {
@@ -5389,7 +6274,7 @@ function renderStructuredComposerMenu() {
5389
6274
  renderStructuredSessionControls();
5390
6275
  updateStructuredComposerState(state && state.agents.find((agent) => agent.id === focusedAgentId));
5391
6276
  input.focus();
5392
- }));
6277
+ }, `command:${command.name}`));
5393
6278
  });
5394
6279
  } else if (structuredComposerMenu === 'mode') {
5395
6280
  const modes = session.modes && Array.isArray(session.modes.availableModes) ? session.modes.availableModes : [];
@@ -5397,7 +6282,7 @@ function renderStructuredComposerMenu() {
5397
6282
  modes.forEach((mode) => {
5398
6283
  items.appendChild(structuredMenuButton(mode.name || mode.id, mode.description || '', mode.id === current, () => {
5399
6284
  void patchStructuredAcpSession({ modeId: mode.id }).catch(showStructuredComposerError);
5400
- }));
6285
+ }, `mode:${mode.id}`));
5401
6286
  });
5402
6287
  } else if (!selectedConfig) {
5403
6288
  structuredVisibleConfigOptions(session).forEach((option) => {
@@ -5409,20 +6294,21 @@ function renderStructuredComposerMenu() {
5409
6294
  structuredComposerConfigId = option.id;
5410
6295
  structuredComposerMenuFocusPending = true;
5411
6296
  renderStructuredSessionControls();
5412
- }
6297
+ },
6298
+ `config:${option.id}`
5413
6299
  ));
5414
6300
  });
5415
6301
  } else if (selectedConfig.type === 'boolean') {
5416
6302
  [false, true].forEach((value) => {
5417
6303
  items.appendChild(structuredMenuButton(value ? 'ON' : 'OFF', selectedConfig.description || '', selectedConfig.currentValue === value, () => {
5418
6304
  void patchStructuredAcpSession({ configId: selectedConfig.id, value }).catch(showStructuredComposerError);
5419
- }));
6305
+ }, `boolean:${value}`));
5420
6306
  });
5421
6307
  } else {
5422
6308
  structuredSelectOptions(selectedConfig).forEach((candidate) => {
5423
6309
  items.appendChild(structuredMenuButton(candidate.name || candidate.value, candidate.description || '', candidate.value === selectedConfig.currentValue, () => {
5424
6310
  void patchStructuredAcpSession({ configId: selectedConfig.id, value: candidate.value }).catch(showStructuredComposerError);
5425
- }));
6311
+ }, `option:${candidate.value}`));
5426
6312
  });
5427
6313
  }
5428
6314
 
@@ -5431,6 +6317,8 @@ function renderStructuredComposerMenu() {
5431
6317
  if (structuredComposerMenuFocusPending) {
5432
6318
  structuredComposerMenuFocusPending = false;
5433
6319
  window.requestAnimationFrame(() => focusStructuredComposerMenuButton(null, 0));
6320
+ } else {
6321
+ restoreStructuredComposerMenuFocus(retainedFocus);
5434
6322
  }
5435
6323
  }
5436
6324
 
@@ -5457,11 +6345,11 @@ function renderStructuredSessionControls() {
5457
6345
  async function refreshStructuredSessionControls(agentId = focusedAgentId, force = false) {
5458
6346
  if (!agentId || structuredSessionControlsLoading) return;
5459
6347
  const agent = state && state.agents.find((candidate) => candidate.id === agentId);
5460
- if (!agent || agent.agentRuntimeMode !== 'acp') {
6348
+ if (!agent || agent.runtimeBinding?.kind !== 'acp') {
5461
6349
  resetStructuredSessionControls();
5462
6350
  return;
5463
6351
  }
5464
- const revision = String(agent.acpSessionUpdatedAt || '');
6352
+ const revision = String(agent.runtimeBinding.sessionUpdatedAt || '');
5465
6353
  if (!force && (
5466
6354
  (revision && revision === structuredSessionControlsRevision)
5467
6355
  || (!revision && structuredSessionSnapshot)
@@ -5579,7 +6467,7 @@ function updateStructuredComposerState(agent) {
5579
6467
  statusNode.textContent = error || `${kind} ${String(runtimeStatus || 'idle').toUpperCase()}`;
5580
6468
  statusNode.classList.toggle('error', Boolean(error));
5581
6469
  renderStructuredPermissions(agent);
5582
- if (agent && agent.agentRuntimeMode === 'acp') void refreshStructuredSessionControls(agent.id);
6470
+ if (agent && agent.runtimeBinding?.kind === 'acp') void refreshStructuredSessionControls(agent.id);
5583
6471
  }
5584
6472
 
5585
6473
  function structuredAttachmentId(file) {
@@ -5716,10 +6604,11 @@ function renderStructuredPermissions(agent) {
5716
6604
  const container = document.getElementById('crt-structured-composer-notices');
5717
6605
  if (!container) return;
5718
6606
  container.replaceChildren();
5719
- if (!agent || agent.agentRuntimeMode !== 'acp') return;
5720
- const requests = Array.isArray(agent.acpPendingPermissions) && agent.acpPendingPermissions.length
5721
- ? agent.acpPendingPermissions
5722
- : (agent.acpPendingPermission ? [agent.acpPendingPermission] : []);
6607
+ if (!agent || agent.runtimeBinding?.kind !== 'acp') return;
6608
+ const runtime = agent.runtimeBinding;
6609
+ const requests = Array.isArray(runtime.pendingPermissions) && runtime.pendingPermissions.length
6610
+ ? runtime.pendingPermissions
6611
+ : (runtime.pendingPermission ? [runtime.pendingPermission] : []);
5723
6612
  requests.forEach((request) => {
5724
6613
  const panel = document.createElement('section');
5725
6614
  panel.className = 'crt-structured-permission';
@@ -5778,13 +6667,13 @@ function structuredTranscriptTurns(transcript) {
5778
6667
  const text = structuredTranscriptContentText(entry.content);
5779
6668
  if (!text) return;
5780
6669
  if (entry.role === 'user') {
5781
- current = { userMessage: text, finalMessage: '' };
6670
+ current = { id: entry.id || `user-${turns.length}`, userMessage: text, finalMessage: '' };
5782
6671
  turns.push(current);
5783
6672
  return;
5784
6673
  }
5785
6674
  if (entry.role !== 'assistant') return;
5786
6675
  if (!current) {
5787
- current = { userMessage: '', finalMessage: '' };
6676
+ current = { id: entry.id || `assistant-${turns.length}`, userMessage: '', finalMessage: '' };
5788
6677
  turns.push(current);
5789
6678
  }
5790
6679
  current.finalMessage = text;
@@ -5792,12 +6681,215 @@ function structuredTranscriptTurns(transcript) {
5792
6681
  return turns;
5793
6682
  }
5794
6683
 
6684
+ function normalizeCrtStructuredPreviewText(value, limit = 240) {
6685
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
6686
+ if (text.length <= limit) return text;
6687
+ return `${text.slice(0, Math.max(1, limit - 1)).trimEnd()}…`;
6688
+ }
6689
+
6690
+ function structuredTranscriptAttachmentLabel(content) {
6691
+ const labels = (Array.isArray(content) ? content : []).flatMap((block) => {
6692
+ const type = String(block && block.type || '').toLowerCase();
6693
+ if (type.includes('image')) return ['Image attachment'];
6694
+ if (type.includes('audio')) return ['Audio attachment'];
6695
+ if (type.includes('file') || type.includes('resource')) return ['File attachment'];
6696
+ return [];
6697
+ });
6698
+ return Array.from(new Set(labels)).join(' + ');
6699
+ }
6700
+
6701
+ function crtStructuredPreviewMessageLines(transcript, limit = 8) {
6702
+ const maxLines = Math.max(1, Number(limit) || 8);
6703
+ const turnLines = structuredTranscriptTurns(transcript).flatMap((turn) => [
6704
+ turn && turn.userMessage
6705
+ ? { role: 'user', label: 'YOU', text: normalizeCrtStructuredPreviewText(turn.userMessage) }
6706
+ : null,
6707
+ turn && turn.finalMessage
6708
+ ? { role: 'assistant', label: 'AGENT', text: normalizeCrtStructuredPreviewText(turn.finalMessage) }
6709
+ : null,
6710
+ ]).filter(Boolean);
6711
+ if (turnLines.length > 0) return turnLines.slice(-maxLines);
6712
+
6713
+ const entries = transcript && Array.isArray(transcript.entries)
6714
+ ? transcript.entries
6715
+ .filter((entry) => (
6716
+ entry
6717
+ && entry.internal !== true
6718
+ && entry.type === 'message'
6719
+ && (entry.role === 'user' || entry.role === 'assistant')
6720
+ ))
6721
+ .map((entry) => ({
6722
+ role: entry.role,
6723
+ label: entry.role === 'user' ? 'YOU' : 'AGENT',
6724
+ text: normalizeCrtStructuredPreviewText(structuredTranscriptContentText(entry.content))
6725
+ || structuredTranscriptAttachmentLabel(entry.content),
6726
+ }))
6727
+ .filter((line) => line.text)
6728
+ : [];
6729
+ return entries.slice(-maxLines);
6730
+ }
6731
+
6732
+ function structuredEntryActivityText(entry) {
6733
+ if (!entry || entry.internal === true) return '';
6734
+ if (entry.type === 'tool') {
6735
+ const title = normalizeCrtStructuredPreviewText(entry.title || entry.kind || 'Tool activity', 160);
6736
+ const status = normalizeCrtStructuredPreviewText(entry.status, 40).replaceAll('_', ' ');
6737
+ return [title, status].filter(Boolean).join(' · ');
6738
+ }
6739
+ if (entry.type === 'thought') {
6740
+ return normalizeCrtStructuredPreviewText(structuredTranscriptContentText(entry.content), 180);
6741
+ }
6742
+ if (entry.type === 'plan') {
6743
+ const steps = Array.isArray(entry.entries) ? entry.entries : [];
6744
+ const current = steps.find((step) => step && step.status === 'in_progress')
6745
+ || [...steps].reverse().find((step) => step && step.status !== 'completed')
6746
+ || steps.at(-1);
6747
+ return normalizeCrtStructuredPreviewText(current && (current.content || current.title), 180);
6748
+ }
6749
+ return '';
6750
+ }
6751
+
6752
+ function buildCrtStructuredPreview(transcript, agent = null) {
6753
+ const entries = transcript && Array.isArray(transcript.entries)
6754
+ ? transcript.entries.filter((entry) => entry && entry.internal !== true)
6755
+ : [];
6756
+ const turns = structuredTranscriptTurns(transcript);
6757
+ const latestTurn = turns.at(-1) || null;
6758
+ const messageLines = crtStructuredPreviewMessageLines(transcript);
6759
+ let userText = normalizeCrtStructuredPreviewText(latestTurn && latestTurn.userMessage);
6760
+ let assistantText = normalizeCrtStructuredPreviewText(latestTurn && latestTurn.finalMessage);
6761
+ let activityText = '';
6762
+
6763
+ if (entries.length > 0) {
6764
+ const latestUserIndex = entries.findLastIndex((entry) => entry.type === 'message' && entry.role === 'user');
6765
+ const latestUser = latestUserIndex >= 0 ? entries[latestUserIndex] : null;
6766
+ if (latestUser) {
6767
+ userText = normalizeCrtStructuredPreviewText(structuredTranscriptContentText(latestUser.content))
6768
+ || structuredTranscriptAttachmentLabel(latestUser.content);
6769
+ }
6770
+ const turnEntries = latestUserIndex >= 0 ? entries.slice(latestUserIndex + 1) : entries;
6771
+ const latestAssistant = [...turnEntries].reverse().find((entry) => (
6772
+ entry.type === 'message' && entry.role === 'assistant'
6773
+ ));
6774
+ if (latestAssistant) {
6775
+ assistantText = normalizeCrtStructuredPreviewText(structuredTranscriptContentText(latestAssistant.content))
6776
+ || structuredTranscriptAttachmentLabel(latestAssistant.content);
6777
+ }
6778
+ activityText = [...turnEntries].reverse().map(structuredEntryActivityText).find(Boolean) || '';
6779
+ } else if (latestTurn && Array.isArray(latestTurn.processItems)) {
6780
+ const processItem = [...latestTurn.processItems].reverse().find(Boolean);
6781
+ activityText = normalizeCrtStructuredPreviewText(
6782
+ processItem && (processItem.detail || processItem.title),
6783
+ 180,
6784
+ );
6785
+ }
6786
+
6787
+ return {
6788
+ messageLines,
6789
+ userText,
6790
+ assistantText,
6791
+ activityText,
6792
+ state: String(structuredRuntimeStatus(agent) || (transcript && transcript.state) || ''),
6793
+ };
6794
+ }
6795
+
6796
+ function crtStructuredPreviewRevision(agent) {
6797
+ if (!agent || !isStructuredRuntimeAgent(agent)) return '';
6798
+ const runtime = agent.runtimeBinding;
6799
+ return JSON.stringify([
6800
+ structuredRuntimeKind(agent),
6801
+ structuredRuntimeStatus(agent),
6802
+ runtime.sessionRevision || 0,
6803
+ runtime.sessionUpdatedAt || '',
6804
+ runtime.transcriptUpdatedAt || '',
6805
+ runtime.turnId || '',
6806
+ agent.lastActivity || 0,
6807
+ ]);
6808
+ }
6809
+
6810
+ function pruneCrtStructuredPreviews(currentState = state) {
6811
+ const visibleIds = new Set(getCrtLiveAgents(currentState).map((agent) => agent.id));
6812
+ crtStructuredPreviewCache.forEach((_value, agentId) => {
6813
+ if (!visibleIds.has(agentId)) crtStructuredPreviewCache.delete(agentId);
6814
+ });
6815
+ crtStructuredPreviewTimers.forEach((timer, agentId) => {
6816
+ if (visibleIds.has(agentId)) return;
6817
+ clearTimeout(timer);
6818
+ crtStructuredPreviewTimers.delete(agentId);
6819
+ });
6820
+ }
6821
+
6822
+ function scheduleCrtStructuredPreviewRefresh(agent) {
6823
+ if (!isCrtLiveAgent(agent) || !isStructuredRuntimeAgent(agent)) return;
6824
+ const revision = crtStructuredPreviewRevision(agent);
6825
+ const cached = crtStructuredPreviewCache.get(agent.id);
6826
+ if (cached && cached.revision === revision && (cached.loading || cached.ready)) return;
6827
+ if (crtStructuredPreviewTimers.has(agent.id)) return;
6828
+ const timer = setTimeout(() => {
6829
+ crtStructuredPreviewTimers.delete(agent.id);
6830
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6831
+ if (isCrtLiveAgent(currentAgent) && isStructuredRuntimeAgent(currentAgent)) {
6832
+ void refreshCrtStructuredPreview(currentAgent);
6833
+ }
6834
+ }, CRT_STRUCTURED_PREVIEW_REFRESH_MS);
6835
+ crtStructuredPreviewTimers.set(agent.id, timer);
6836
+ }
6837
+
6838
+ async function refreshCrtStructuredPreview(agent) {
6839
+ const revision = crtStructuredPreviewRevision(agent);
6840
+ const cached = crtStructuredPreviewCache.get(agent.id);
6841
+ if (cached && cached.revision === revision && (cached.loading || cached.ready)) return;
6842
+ crtStructuredPreviewCache.set(agent.id, {
6843
+ revision,
6844
+ loading: true,
6845
+ ready: false,
6846
+ preview: cached && cached.preview || null,
6847
+ error: '',
6848
+ });
6849
+
6850
+ try {
6851
+ const endpoint = structuredTranscriptEndpoint(agent);
6852
+ const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agent.id)}/${endpoint}?maxTurns=20`));
6853
+ const body = await response.json().catch(() => null);
6854
+ if (!response.ok || !body || !body.transcript) {
6855
+ throw new Error(body && body.error ? body.error : `Conversation preview failed (${response.status})`);
6856
+ }
6857
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6858
+ if (!isCrtLiveAgent(currentAgent) || crtStructuredPreviewRevision(currentAgent) !== revision) {
6859
+ if (isCrtLiveAgent(currentAgent)) scheduleCrtStructuredPreviewRefresh(currentAgent);
6860
+ return;
6861
+ }
6862
+ crtStructuredPreviewCache.set(agent.id, {
6863
+ revision,
6864
+ loading: false,
6865
+ ready: true,
6866
+ preview: buildCrtStructuredPreview(body.transcript, currentAgent),
6867
+ error: '',
6868
+ });
6869
+ if (isCrtSessionOpen()) dashboardRenderDeferred = true;
6870
+ else updateCrtAgentPreviewCard(currentAgent);
6871
+ } catch (error) {
6872
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6873
+ if (!isCrtLiveAgent(currentAgent) || crtStructuredPreviewRevision(currentAgent) !== revision) return;
6874
+ crtStructuredPreviewCache.set(agent.id, {
6875
+ revision,
6876
+ loading: false,
6877
+ ready: true,
6878
+ preview: cached && cached.preview || null,
6879
+ error: error && error.message ? error.message : 'Conversation preview unavailable',
6880
+ });
6881
+ if (isCrtSessionOpen()) dashboardRenderDeferred = true;
6882
+ else updateCrtAgentPreviewCard(currentAgent);
6883
+ }
6884
+ }
6885
+
5795
6886
  function renderStructuredTranscript(transcript, force = false) {
5796
6887
  const container = document.getElementById('terminal-output');
5797
6888
  if (!container) return;
5798
6889
  const updatedAt = String(transcript && transcript.updatedAt || '');
5799
6890
  if (!force && updatedAt && updatedAt === structuredSessionRenderedAt) return;
5800
6891
  const nearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 80;
6892
+ if (!nearBottom && focusedAgentId) saveCrtStructuredReadingAnchor(focusedAgentId);
5801
6893
  const transcriptNode = document.createElement('div');
5802
6894
  transcriptNode.className = 'crt-structured-transcript';
5803
6895
  const turns = structuredTranscriptTurns(transcript);
@@ -5811,6 +6903,7 @@ function renderStructuredTranscript(transcript, force = false) {
5811
6903
  turns.forEach((turn) => {
5812
6904
  const section = document.createElement('section');
5813
6905
  section.className = 'crt-structured-turn';
6906
+ section.dataset.readingAnchorId = String(turn.id || `${turn.userMessage || ''}\n${turn.finalMessage || ''}`.slice(0, 160));
5814
6907
  if (turn.userMessage) {
5815
6908
  const user = document.createElement('p');
5816
6909
  user.className = 'crt-structured-message user';
@@ -5829,7 +6922,10 @@ function renderStructuredTranscript(transcript, force = false) {
5829
6922
 
5830
6923
  container.replaceChildren(transcriptNode);
5831
6924
  structuredSessionRenderedAt = updatedAt;
5832
- if (force || nearBottom) container.scrollTop = container.scrollHeight;
6925
+ if (!focusedAgentId || !restoreCrtStructuredReadingAnchor(focusedAgentId)) {
6926
+ if (force || nearBottom) container.scrollTop = container.scrollHeight;
6927
+ }
6928
+ container.onscroll = () => saveCrtStructuredReadingAnchor(focusedAgentId);
5833
6929
  requestAnimationFrame(updateStructuredTranscriptScrollState);
5834
6930
  }
5835
6931
 
@@ -6104,8 +7200,8 @@ async function openStructuredSession(agent, sessionToken) {
6104
7200
  function teardownSessionSurface() {
6105
7201
  stopSessionViewPolling();
6106
7202
  stopStructuredSessionPolling();
7203
+ disposeCrtTerminalReplication();
6107
7204
  disposeTerminal();
6108
- destroyTerminalInputBridge();
6109
7205
  setStructuredComposerActive(false);
6110
7206
  document.getElementById('terminal-output')?.classList.remove('crt-structured-session');
6111
7207
  if (SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.resetTerminalShell) {
@@ -6126,6 +7222,7 @@ async function openSession(agentId) {
6126
7222
 
6127
7223
  const agent = state.agents.find(a => a.id === agentId);
6128
7224
  if (!agent) return;
7225
+ crtNavigationKey = `agent:${agentId}`;
6129
7226
  void markCrtAgentReadIfNeeded(agent);
6130
7227
 
6131
7228
  const sessionModal = document.getElementById('session-modal');
@@ -6207,10 +7304,13 @@ async function openSession(agentId) {
6207
7304
  }
6208
7305
 
6209
7306
  disposeTerminal();
7307
+ disposeCrtTerminalReplication();
7308
+ crtTerminalReplication = createCrtTerminalReplication(agentId);
6210
7309
  let mountedTerminal = null;
6211
7310
  try {
6212
7311
  mountedTerminal = SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.mountTerminal
6213
7312
  ? SESSION_MODAL_BRIDGE.mountTerminal(document, terminalBundle, {
7313
+ authoritativeGeometry: true,
6214
7314
  initialOutput: shouldUseLiveSessionText(agent)
6215
7315
  ? (runtime ? runtime.prepareInitialOutput(agent.output) : agent.output)
6216
7316
  : '',
@@ -6220,11 +7320,10 @@ async function openSession(agentId) {
6220
7320
  sendTerminalInput(data);
6221
7321
  },
6222
7322
  onResize: (cols, rows) => {
7323
+ void cols;
7324
+ void rows;
6223
7325
  if (runtime && !runtime.isCurrentSession(agentId, sessionToken)) return;
6224
- if (!focusedAgentId) return;
6225
- const sessionClient = getSessionClient();
6226
- if (!sessionClient) return;
6227
- sessionClient.resizeAgent(focusedAgentId, cols, rows);
7326
+ sendSessionResize(agentId);
6228
7327
  },
6229
7328
  hasSelection: hasAnySelection,
6230
7329
  focusTerminal: focusSessionTerminal,
@@ -6265,12 +7364,13 @@ async function openSession(agentId) {
6265
7364
  updateSessionSelectionStatus();
6266
7365
  });
6267
7366
  }
7367
+ if (terminal && typeof terminal.onScroll === 'function') {
7368
+ terminal.onScroll(() => saveCrtTerminalReadingAnchor(agentId, terminal));
7369
+ }
6268
7370
  if (runtime) {
6269
7371
  runtime.setLastOutputLength(mountedTerminal ? mountedTerminal.outputLength : (runtime.prepareInitialOutput(agent.output)).length);
6270
7372
  syncSessionRuntimeState();
6271
7373
  }
6272
- setupTerminalInputBridge();
6273
-
6274
7374
  if (!mountedTerminal) {
6275
7375
  terminal.loadAddon(fitAddon);
6276
7376
  terminal.onData((data) => {
@@ -6279,11 +7379,11 @@ async function openSession(agentId) {
6279
7379
  sendTerminalInput(data);
6280
7380
  });
6281
7381
  terminal.onResize(({ cols, rows }) => {
7382
+ void cols;
7383
+ void rows;
6282
7384
  if (runtime && !runtime.isCurrentSession(agentId, sessionToken)) return;
6283
- if (!focusedAgentId) return;
6284
- const sessionClient = getSessionClient();
6285
- if (!sessionClient) return;
6286
- sessionClient.resizeAgent(focusedAgentId, cols, rows);
7385
+ if (crtTerminalReplication?.applyingLocalResize) return;
7386
+ sendSessionResize(agentId, { cols, rows });
6287
7387
  });
6288
7388
 
6289
7389
  terminalContainer.innerHTML = '';
@@ -6312,7 +7412,6 @@ async function openSession(agentId) {
6312
7412
  if (!terminal || !fitAddon) return;
6313
7413
  if (runtime && !runtime.isCurrentSession(agentId, sessionToken)) return;
6314
7414
  if (!runtime && focusedAgentId !== agentId) return;
6315
- fitAddon.fit();
6316
7415
  const initialOutput = runtime ? runtime.prepareInitialOutput(agent.output) : agent.output;
6317
7416
  if (initialOutput) {
6318
7417
  terminal.write(initialOutput);
@@ -6336,13 +7435,20 @@ async function openSession(agentId) {
6336
7435
  showCrtWebglFailure(error);
6337
7436
  }
6338
7437
 
7438
+ // Hydrate one authoritative reducer cut before reducing live transitions.
6339
7439
  await refreshSessionView(true, agentId, sessionToken);
6340
- if (shouldPollSessionView(modalState.sessionSource)) {
7440
+ restoreCrtTerminalReadingAnchor(agentId, terminal);
7441
+ sendSessionResize(agentId);
7442
+ if (!crtTerminalReplication && shouldPollSessionView(modalState.sessionSource)) {
6341
7443
  startSessionViewPolling(agentId, sessionToken);
6342
7444
  }
6343
7445
  }
6344
7446
 
6345
7447
  function closeSession() {
7448
+ if (focusedAgentId) {
7449
+ if (terminal) saveCrtTerminalReadingAnchor(focusedAgentId, terminal);
7450
+ else saveCrtStructuredReadingAnchor(focusedAgentId);
7451
+ }
6346
7452
  resetCrtRuntimeSwitchState();
6347
7453
  const runtime = getSessionRuntime();
6348
7454
  if (runtime) {
@@ -6379,14 +7485,19 @@ function closeSession() {
6379
7485
  if (crtMainView === 'history') renderCrtHistory();
6380
7486
  generateKeyMap();
6381
7487
  }
7488
+ restoreCrtNavigationSelection();
6382
7489
  }
6383
7490
 
6384
7491
  function killCurrentAgent() {
6385
7492
  if (!focusedAgentId) return;
6386
7493
 
7494
+ const killedAgentId = focusedAgentId;
7495
+ const fallbackAgentId = getCrtAgentRemovalFallback(state, killedAgentId);
7496
+ crtNavigationKey = fallbackAgentId ? `agent:${fallbackAgentId}` : '';
7497
+
6387
7498
  const sessionClient = getSessionClient();
6388
7499
  if (sessionClient) {
6389
- sessionClient.killAgent(focusedAgentId);
7500
+ sessionClient.killAgent(killedAgentId);
6390
7501
  }
6391
7502
 
6392
7503
  closeSession();
@@ -6394,97 +7505,178 @@ function killCurrentAgent() {
6394
7505
 
6395
7506
  function sendTerminalInput(input) {
6396
7507
  if (!focusedAgentId) return;
7508
+ queueCrtTerminalInput(input);
7509
+ }
6397
7510
 
6398
- const sessionClient = getSessionClient();
6399
- if (sessionClient) {
6400
- sessionClient.sendInput(focusedAgentId, input);
7511
+ function clearPendingCrtTerminalFitResize(replication) {
7512
+ if (!replication) return;
7513
+ if (replication.fitResizeTimer) clearTimeout(replication.fitResizeTimer);
7514
+ replication.fitResizeTimer = null;
7515
+ replication.pendingFitResize = null;
7516
+ }
7517
+
7518
+ function commitCrtTerminalResize(agentId, normalizedDimensions) {
7519
+ const replication = crtTerminalReplication;
7520
+ if (
7521
+ !agentId ||
7522
+ !terminal ||
7523
+ !replication ||
7524
+ replication.disposed ||
7525
+ replication.agentId !== agentId ||
7526
+ replication.replayState.recovering ||
7527
+ (
7528
+ normalizedDimensions.cols === replication.lastResizeCols &&
7529
+ normalizedDimensions.rows === replication.lastResizeRows
7530
+ )
7531
+ ) return;
7532
+ if (terminal.cols !== normalizedDimensions.cols || terminal.rows !== normalizedDimensions.rows) {
7533
+ replication.applyingLocalResize = true;
7534
+ try {
7535
+ terminal.resize(normalizedDimensions.cols, normalizedDimensions.rows);
7536
+ } finally {
7537
+ replication.applyingLocalResize = false;
7538
+ }
7539
+ }
7540
+ const delivered = getSessionClient()?.resizeAgent(
7541
+ agentId,
7542
+ normalizedDimensions.cols,
7543
+ normalizedDimensions.rows
7544
+ );
7545
+ if (delivered) {
7546
+ replication.lastResizeCols = normalizedDimensions.cols;
7547
+ replication.lastResizeRows = normalizedDimensions.rows;
6401
7548
  }
6402
7549
  }
6403
7550
 
6404
- function sendSessionResize(agentId = focusedAgentId) {
6405
- if (!agentId || !terminal) return;
6406
- if (!Number.isFinite(terminal.cols) || !Number.isFinite(terminal.rows)) return;
6407
- const sessionClient = getSessionClient();
6408
- if (!sessionClient) return;
6409
- sessionClient.resizeAgent(agentId, terminal.cols, terminal.rows);
7551
+ function scheduleCrtTerminalFitResize(replication, agentId, normalizedDimensions) {
7552
+ if (replication.fitResizeTimer) clearTimeout(replication.fitResizeTimer);
7553
+ replication.pendingFitResize = normalizedDimensions;
7554
+ replication.fitResizeTimer = setTimeout(() => {
7555
+ replication.fitResizeTimer = null;
7556
+ const next = replication.pendingFitResize;
7557
+ replication.pendingFitResize = null;
7558
+ if (
7559
+ !next ||
7560
+ replication.disposed ||
7561
+ crtTerminalReplication !== replication ||
7562
+ replication.agentId !== agentId
7563
+ ) return;
7564
+ commitCrtTerminalResize(agentId, next);
7565
+ }, CRT_TERMINAL_RESIZE_SETTLE_MS);
7566
+ }
7567
+
7568
+ function sendSessionResize(agentId = focusedAgentId, requestedDimensions = null) {
7569
+ const replication = crtTerminalReplication;
7570
+ if (
7571
+ !agentId ||
7572
+ !terminal ||
7573
+ !fitAddon ||
7574
+ !replication ||
7575
+ replication.agentId !== agentId ||
7576
+ replication.replayState.recovering
7577
+ ) return;
7578
+ const dimensions = requestedDimensions || (
7579
+ typeof fitAddon.proposeDimensions === 'function'
7580
+ ? fitAddon.proposeDimensions()
7581
+ : null
7582
+ );
7583
+ const normalizedDimensions = dimensions && {
7584
+ cols: Math.floor(Number(dimensions.cols)),
7585
+ rows: Math.floor(Number(dimensions.rows))
7586
+ };
7587
+ if (
7588
+ !normalizedDimensions ||
7589
+ !Number.isFinite(normalizedDimensions.cols) ||
7590
+ !Number.isFinite(normalizedDimensions.rows) ||
7591
+ normalizedDimensions.cols < CRT_TERMINAL_MIN_COLS ||
7592
+ normalizedDimensions.rows < CRT_TERMINAL_MIN_ROWS
7593
+ ) return;
7594
+ if (!requestedDimensions) {
7595
+ if (
7596
+ normalizedDimensions.cols === replication.lastResizeCols &&
7597
+ normalizedDimensions.rows === replication.lastResizeRows
7598
+ ) {
7599
+ clearPendingCrtTerminalFitResize(replication);
7600
+ return;
7601
+ }
7602
+ scheduleCrtTerminalFitResize(replication, agentId, normalizedDimensions);
7603
+ return;
7604
+ }
7605
+ clearPendingCrtTerminalFitResize(replication);
7606
+ commitCrtTerminalResize(agentId, normalizedDimensions);
6410
7607
  }
6411
7608
 
6412
- async function refreshSessionView(forceReplace = false, expectedAgentId = focusedAgentId, expectedSessionToken = getCurrentSessionToken()) {
7609
+ async function refreshSessionView(_forceReplace = false, expectedAgentId = focusedAgentId, expectedSessionToken = getCurrentSessionToken()) {
6413
7610
  if (!expectedAgentId || !terminal) return;
6414
7611
 
6415
7612
  const runtime = getSessionRuntime();
7613
+ const replication = crtTerminalReplication;
7614
+ if (
7615
+ !replication ||
7616
+ replication.agentId !== expectedAgentId ||
7617
+ replication.replayState.halted ||
7618
+ replication.checkpointRetryTimer ||
7619
+ replication.checkpointInFlight
7620
+ ) return;
7621
+
7622
+ const checkpointSeq = replication.checkpointSeq + 1;
7623
+ replication.checkpointSeq = checkpointSeq;
7624
+ replication.checkpointInFlight = true;
7625
+ TERMINAL_REPLAY.beginRecovery(replication.replayState);
7626
+ const controller = new globalThis.AbortController();
7627
+ replication.checkpointAbortController = controller;
7628
+ const timeout = setTimeout(
7629
+ () => controller.abort(),
7630
+ CRT_TERMINAL_CHECKPOINT_REQUEST_TIMEOUT_MS
7631
+ );
7632
+
6416
7633
  try {
6417
7634
  const sessionClient = getSessionClient();
6418
- if (!sessionClient) return;
6419
- let payload = null;
6420
- const retryDelays = forceReplace ? [0, 60, 140] : [0];
6421
- for (const delay of retryDelays) {
6422
- if (delay > 0) {
6423
- await new Promise((resolve) => setTimeout(resolve, delay));
6424
- }
6425
- payload = await sessionClient.getSessionView(expectedAgentId);
6426
- const candidate = normalizeSessionViewPayload(payload);
6427
- const dimensionsMatch = !Number.isFinite(candidate.previewCols)
6428
- || !Number.isFinite(candidate.previewRows)
6429
- || (candidate.previewCols === terminal.cols && candidate.previewRows === terminal.rows);
6430
- if (dimensionsMatch) {
6431
- break;
6432
- }
6433
- }
6434
- if (runtime && !runtime.isCurrentSession(expectedAgentId, expectedSessionToken)) {
6435
- return;
6436
- }
7635
+ if (!sessionClient) throw new Error('Terminal session client is unavailable');
7636
+ const payload = await sessionClient.getSessionView(expectedAgentId, {
7637
+ signal: controller.signal
7638
+ });
7639
+ if (
7640
+ !crtTerminalReplication ||
7641
+ crtTerminalReplication !== replication ||
7642
+ replication.checkpointSeq !== checkpointSeq
7643
+ ) return;
7644
+ if (runtime && !runtime.isCurrentSession(expectedAgentId, expectedSessionToken)) return;
7645
+
6437
7646
  const currentAgent = state && state.agents
6438
7647
  ? state.agents.find((agent) => agent.id === expectedAgentId)
6439
7648
  : null;
6440
- const sessionView = normalizeSessionViewPayload(payload, currentAgent);
6441
- const sessionText = sessionView.renderOutput || sessionView.output;
6442
- const patch = deriveSessionTextPatch(sessionText, getSessionOutputLength(), forceReplace);
6443
-
6444
- if (patch.mode === 'replace') {
6445
- replaceTerminalOutput(
6446
- terminal,
6447
- applySessionReplayCursorVisibility(patch.text, sessionView, currentAgent),
7649
+ installCrtTerminalCheckpoint(normalizeSessionViewPayload(payload, currentAgent));
7650
+ } catch (error) {
7651
+ if (
7652
+ crtTerminalReplication === replication &&
7653
+ replication.checkpointSeq === checkpointSeq
7654
+ ) {
7655
+ retryCrtTerminalReplayAfterFailure(
7656
+ replication,
7657
+ TERMINAL_REPLAY.recordTransportFailure(replication.replayState),
7658
+ error
6448
7659
  );
6449
- refreshSessionTerminalUi({ preserveSearchIndex: true });
6450
- if (runtime) {
6451
- runtime.markHydrated(patch.nextLength);
6452
- syncSessionRuntimeState();
6453
- }
6454
- return;
6455
7660
  }
6456
-
6457
- if (patch.mode === 'append') {
6458
- terminal.write(patch.text);
6459
- refreshSessionTerminalUi({ preserveSearchIndex: true });
6460
- if (runtime) {
6461
- if (isAwaitingInitialSessionSync()) {
6462
- runtime.markHydrated(patch.nextLength);
6463
- } else {
6464
- runtime.setLastOutputLength(patch.nextLength);
6465
- }
6466
- syncSessionRuntimeState();
6467
- }
6468
- }
6469
- } catch (error) {
6470
- console.error('Failed to refresh session view:', error);
6471
- if (runtime && runtime.isCurrentSession(expectedAgentId, expectedSessionToken) && runtime.isAwaitingInitialSync()) {
6472
- runtime.markHydrated(getSessionOutputLength());
6473
- syncSessionRuntimeState();
7661
+ } finally {
7662
+ clearTimeout(timeout);
7663
+ if (
7664
+ crtTerminalReplication === replication &&
7665
+ replication.checkpointSeq === checkpointSeq
7666
+ ) {
7667
+ replication.checkpointInFlight = false;
7668
+ replication.checkpointAbortController = null;
7669
+ finishCrtTerminalReplay(replication);
6474
7670
  }
6475
7671
  }
6476
7672
  }
6477
-
6478
7673
  function startSessionViewPolling(agentId = focusedAgentId, sessionToken = getCurrentSessionToken()) {
6479
7674
  const runtime = getSessionRuntime();
6480
7675
  if (runtime) {
6481
7676
  runtime.startPolling({ agentId, sessionToken });
6482
- return;
7677
+ return null;
6483
7678
  }
6484
- stopSessionViewPolling();
6485
- legacySessionPoller = setInterval(() => {
6486
- refreshSessionView(false, agentId, sessionToken);
6487
- }, 350);
7679
+ return null;
6488
7680
  }
6489
7681
 
6490
7682
  function stopSessionViewPolling() {
@@ -6493,10 +7685,6 @@ function stopSessionViewPolling() {
6493
7685
  runtime.stopPolling();
6494
7686
  return;
6495
7687
  }
6496
- if (legacySessionPoller) {
6497
- clearInterval(legacySessionPoller);
6498
- legacySessionPoller = null;
6499
- }
6500
7688
  }
6501
7689
 
6502
7690
  if (typeof document !== 'undefined') {
@@ -6516,8 +7704,6 @@ if (typeof document !== 'undefined') {
6516
7704
  }
6517
7705
  if (!terminal || !fitAddon || !focusedAgentId) return;
6518
7706
 
6519
- fitAddon.fit();
6520
- syncTerminalInputBridgePosition();
6521
7707
  sendSessionResize();
6522
7708
  });
6523
7709
 
@@ -6542,9 +7728,12 @@ if (typeof document !== 'undefined') {
6542
7728
  const searchInputFocused = crtMainView === 'search'
6543
7729
  && document.activeElement === document.getElementById('crt-search-input');
6544
7730
  const navigationArrow = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key);
7731
+ const billingHourCellFocused = crtMainView === 'billing'
7732
+ && document.activeElement?.classList?.contains('billing-day-hour-cell');
6545
7733
 
6546
7734
  if (terminalFontSizeInputFocused && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
6547
7735
  if (searchInputFocused) return;
7736
+ if (billingHourCellFocused && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
6548
7737
 
6549
7738
  if (
6550
7739
  crtMainView === 'billing'
@@ -6650,6 +7839,13 @@ if (typeof document !== 'undefined') {
6650
7839
  }
6651
7840
 
6652
7841
  if (dialogActive) {
7842
+ if (workspaceDirectoryPrompt) {
7843
+ if (e.key === 'Escape' && workspaceDirectoryPrompt.kind !== 'creating') {
7844
+ hideWorkspaceDirectoryPrompt();
7845
+ e.preventDefault();
7846
+ }
7847
+ return;
7848
+ }
6653
7849
  if (workspaceInputVisible) {
6654
7850
  if (workspaceInputFocused) {
6655
7851
  return;
@@ -6771,14 +7967,28 @@ if (typeof document !== 'undefined') {
6771
7967
  const structuredInputFocused = structuredInput && document.activeElement === structuredInput;
6772
7968
  const structuredSessionActive = document.getElementById('crt-structured-composer')?.classList.contains('active');
6773
7969
  const structuredTranscriptFocused = document.activeElement === document.getElementById('terminal-output');
6774
- if (
6775
- structuredSessionActive
6776
- && e.key === 'Escape'
6777
- && (e.ctrlKey || e.metaKey || !structuredComposerMenu)
6778
- && !(!e.ctrlKey && !e.metaKey && structuredTranscriptFocused)
6779
- ) {
6780
- closeSession();
7970
+ const structuredToolFocused = Boolean(document.activeElement?.closest?.('.crt-structured-tool'));
7971
+ const structuredMenuItemFocused = Boolean(document.activeElement?.closest?.('.crt-structured-menu-item'));
7972
+ const focusedAgent = state && state.agents.find((candidate) => candidate.id === focusedAgentId);
7973
+ const structuredInterruptFocused = Boolean(
7974
+ structuredInputFocused
7975
+ && structuredComposerAction(focusedAgent, structuredInput.value) === 'interrupt'
7976
+ );
7977
+ const sessionCommand = resolveCrtSessionKeyboardCommand(e, {
7978
+ structuredSessionActive,
7979
+ composing: e.isComposing,
7980
+ structuredInputFocused,
7981
+ structuredTranscriptFocused,
7982
+ structuredToolFocused,
7983
+ structuredMenuItemFocused,
7984
+ structuredInterruptFocused,
7985
+ structuredComposerMenuOpen: Boolean(structuredComposerMenu),
7986
+ });
7987
+ if (sessionCommand) {
6781
7988
  e.preventDefault();
7989
+ e.stopPropagation();
7990
+ if (sessionCommand === 'kill') killCurrentAgent();
7991
+ else closeSession();
6782
7992
  return;
6783
7993
  }
6784
7994
  if (structuredInputFocused) {
@@ -6806,48 +8016,6 @@ if (typeof document !== 'undefined') {
6806
8016
  if (isBrowserShortcut(e)) {
6807
8017
  return;
6808
8018
  }
6809
- if (e.isComposing || terminalInputComposing) {
6810
- if (SESSION_INPUT_SETTINGS.imeEnabled) {
6811
- e.preventDefault();
6812
- }
6813
- return;
6814
- }
6815
- if ((e.ctrlKey || e.metaKey) && e.key === 'Escape') {
6816
- closeSession();
6817
- e.preventDefault();
6818
- return;
6819
- }
6820
- if ((e.ctrlKey || e.metaKey) && (e.key === 'k' || e.key === 'K')) {
6821
- killCurrentAgent();
6822
- e.preventDefault();
6823
- return;
6824
- }
6825
- if (!SESSION_INPUT_SETTINGS.imeEnabled) {
6826
- return;
6827
- }
6828
- if (
6829
- SESSION_INPUT_SETTINGS.imeEnabled &&
6830
- !e.ctrlKey &&
6831
- !e.metaKey &&
6832
- !e.altKey &&
6833
- e.key.length === 1 &&
6834
- document.activeElement !== terminalInputBridge
6835
- ) {
6836
- e.preventDefault();
6837
- schedulePrintableInput(e.key);
6838
- focusTerminalInputBridge();
6839
- return;
6840
- }
6841
- if (routeSessionKey(e)) {
6842
- e.preventDefault();
6843
- focusTerminalInputBridge();
6844
- return;
6845
- }
6846
- if (!e.ctrlKey && !e.metaKey && e.key === 'Escape') {
6847
- sendTerminalInput('\x1b');
6848
- e.preventDefault();
6849
- return;
6850
- }
6851
8019
  return;
6852
8020
  }
6853
8021
 
@@ -6912,6 +8080,12 @@ if (typeof document !== 'undefined') {
6912
8080
  return;
6913
8081
  }
6914
8082
 
8083
+ // xterm owns paste events targeting its textarea. Calling terminal.paste()
8084
+ // here as well would submit the same clipboard text twice.
8085
+ if (isCrtNativeTerminalPasteTarget(e.target)) {
8086
+ return;
8087
+ }
8088
+
6915
8089
  const pastedText = e.clipboardData && e.clipboardData.getData
6916
8090
  ? e.clipboardData.getData('text/plain')
6917
8091
  : '';
@@ -6921,7 +8095,9 @@ if (typeof document !== 'undefined') {
6921
8095
  }
6922
8096
 
6923
8097
  e.preventDefault();
6924
- pasteTerminalText(pastedText);
8098
+ e.stopPropagation();
8099
+ e.stopImmediatePropagation();
8100
+ void pasteTerminalText(pastedText);
6925
8101
  }, true);
6926
8102
 
6927
8103
  }
@@ -6930,6 +8106,7 @@ if (typeof module !== 'undefined' && module.exports) {
6930
8106
  module.exports = {
6931
8107
  buildCrtHistoryItems,
6932
8108
  buildCrtSearchResults,
8109
+ buildCrtStructuredPreview,
6933
8110
  calculateCrtAgentPageLayout,
6934
8111
  calculateCrtHistoryPageSize,
6935
8112
  crtHistoryAgentName,
@@ -6941,8 +8118,8 @@ if (typeof module !== 'undefined' && module.exports) {
6941
8118
  findDirectionalNavigationIndex,
6942
8119
  isBrowserShortcut,
6943
8120
  isCopyShortcut,
8121
+ isCrtNativeTerminalPasteTarget,
6944
8122
  isPasteShortcut,
6945
- getTerminalSequenceForKey,
6946
8123
  shouldUseLiveSessionText,
6947
8124
  shouldPollSessionView,
6948
8125
  deriveSessionTextPatch,
@@ -6956,19 +8133,26 @@ if (typeof module !== 'undefined' && module.exports) {
6956
8133
  getCrtHistoryPage,
6957
8134
  getCrtAgentPage,
6958
8135
  getCrtAgentVerticalPageTarget,
8136
+ getCrtLiveAgents,
8137
+ getCrtRegularAgents,
8138
+ getCrtAgentRemovalFallback,
6959
8139
  getAgentDisplayText,
6960
8140
  getCrtPreviewCellStyle,
8141
+ getCrtTerminalSnapshotRows,
6961
8142
  getCrtAgentTitle,
6962
8143
  getCrtProjectName,
6963
- calculateTerminalInputBridgePosition,
6964
8144
  getCrtTerminalFontSize,
6965
8145
  isCrtAgentWorking,
8146
+ isCrtLiveAgent,
6966
8147
  getCrtAgentReadPatch,
6967
8148
  crtRuntimeView,
6968
8149
  canSwitchCrtAgentRuntime,
6969
8150
  isCrtRuntimeSwitchShortcut,
8151
+ hasCrtStructuredLocalEscapeAction,
8152
+ resolveCrtSessionKeyboardCommand,
6970
8153
  structuredComposerAction,
6971
8154
  structuredTranscriptTurns,
8155
+ crtStructuredPreviewMessageLines,
6972
8156
  getCrtBrandPaneKey,
6973
8157
  extractSessionLinks,
6974
8158
  formatSelectionStatus,
@@ -6984,6 +8168,7 @@ if (typeof module !== 'undefined' && module.exports) {
6984
8168
  getSessionModalDomState
6985
8169
  };
6986
8170
  } else {
8171
+ installCrtTerminalTestApi();
6987
8172
  setupWorkspaceHistoryControls();
6988
8173
  setupCrtSearchControls();
6989
8174
  document.addEventListener('visibilitychange', () => {