farming-code 2.2.8 → 2.2.11

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 (102) hide show
  1. package/README.md +36 -23
  2. package/README.zh_cn.md +36 -23
  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 +1482 -730
  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 +45 -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 +30 -0
  20. package/backend/control-api.js +192 -17
  21. package/backend/farming-net-pass.js +285 -0
  22. package/backend/farming-net-registry.js +112 -0
  23. package/backend/farming-net-server.js +298 -0
  24. package/backend/farming-session-store.js +5 -13
  25. package/backend/git-worktree-info.js +181 -0
  26. package/backend/local-session-engine.js +411 -186
  27. package/backend/main-page-session.js +5 -2
  28. package/backend/native-pty-controller-generation.js +106 -0
  29. package/backend/native-pty-host-client.js +275 -7
  30. package/backend/native-pty-host-identity.js +86 -0
  31. package/backend/native-pty-host.js +813 -114
  32. package/backend/native-session-engine.js +100 -28
  33. package/backend/packaged-node-pty.js +22 -2
  34. package/backend/provider-adapters.js +253 -0
  35. package/backend/provider-session-service.js +241 -0
  36. package/backend/runtime-observation.js +81 -0
  37. package/backend/server.js +294 -86
  38. package/backend/session-engine-bridge.js +21 -2
  39. package/backend/session-engine-router.js +1 -1
  40. package/backend/session-engine.js +1 -1
  41. package/backend/session-stream-protocol.js +185 -0
  42. package/backend/storage-layout.js +55 -0
  43. package/backend/terminal-attach-checkpoint.js +74 -0
  44. package/backend/terminal-exit-quiescence.js +39 -0
  45. package/backend/terminal-reducer-flow-control.js +97 -0
  46. package/backend/terminal-screen-state.js +11 -2
  47. package/backend/terminal-screen-worker-pool.js +59 -6
  48. package/backend/terminal-screen-worker-thread.js +97 -57
  49. package/backend/terminal-screen-worker.js +133 -51
  50. package/backend/terminal-state-serialization.js +127 -0
  51. package/backend/terminal-status.js +23 -4
  52. package/backend/usage-monitor.js +81 -4
  53. package/backend/workspace-directory.js +152 -0
  54. package/backend/workspace-file-router.js +182 -76
  55. package/backend/workspace-file-service.js +319 -4
  56. package/backend/workspace-root-registry.js +164 -0
  57. package/dist/assets/App-CHYpgECZ.js +208 -0
  58. package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
  59. package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
  60. package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
  61. package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
  62. package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
  63. package/dist/assets/code-dark-B1pAtO_P.css +1 -0
  64. package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
  65. package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
  66. package/dist/assets/main-DNe0jPw-.css +1 -0
  67. package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
  68. package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
  69. package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
  70. package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
  71. package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
  72. package/dist/index.html +1 -1
  73. package/frontend/farming-net/app.css +625 -0
  74. package/frontend/farming-net/app.js +268 -0
  75. package/frontend/farming-net/index.html +86 -0
  76. package/frontend/reading-anchor.js +198 -0
  77. package/frontend/session-bridge.js +12 -3
  78. package/frontend/session-modal-bridge.js +5 -12
  79. package/frontend/skins/crt/app.js +1976 -793
  80. package/frontend/skins/crt/index.html +313 -23
  81. package/frontend/skins/crt/styles/billing.css +294 -223
  82. package/frontend/skins/crt/styles/monochrome-green.css +7 -2
  83. package/frontend/terminal-replay.js +372 -0
  84. package/package.json +10 -3
  85. package/shared/browser-protocol.d.ts +5 -0
  86. package/shared/browser-protocol.js +130 -0
  87. package/dist/assets/App-8dYAM6ql.js +0 -124
  88. package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
  89. package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
  90. package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
  91. package/dist/assets/code-dark-CDkOQAtK.css +0 -1
  92. package/dist/assets/file-icons-EFUGSSwf.js +0 -1
  93. package/dist/assets/main-D073SnW4.css +0 -1
  94. package/dist/assets/qoder-C9LmmOSf.svg +0 -1
  95. package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
  96. package/dist/assets/qoder-gHCinseV.svg +0 -1
  97. package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
  98. package/dist/assets/zsh-CLpveKlF.svg +0 -1
  99. package/dist/assets/zsh-FxSpMPbz.svg +0 -1
  100. /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
  101. /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
  102. /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,438 @@ 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
+ }
2618
+ return;
2619
+ }
2620
+
2621
+ const chunks = Array.isArray(stream.chunks) ? stream.chunks : [stream];
2622
+ chunks.forEach((chunk) => {
2623
+ const event = {
2624
+ kind: chunk.kind || 'output',
2625
+ data: typeof chunk.data === 'string' ? chunk.data : '',
2626
+ runtimeEpoch: chunk.runtimeEpoch || stream.runtimeEpoch,
2627
+ outputSeq: chunk.outputSeq,
2628
+ stateRevision: chunk.stateRevision,
2629
+ cols: chunk.cols,
2630
+ rows: chunk.rows
2631
+ };
2632
+ if (
2633
+ replication.replayState.recovering ||
2634
+ replication.checkpointInFlight ||
2635
+ replication.installInProgress ||
2636
+ replication.writeInProgress
2637
+ ) {
2638
+ queueCrtTerminalTransition(event);
2639
+ } else {
2640
+ applyCrtTerminalTransition(event);
2641
+ }
2642
+ });
2643
+ }
2304
2644
  function deriveSessionStreamPatch(stream, currentFocusedAgentId, currentSessionSource) {
2305
2645
  if (!stream) return null;
2306
2646
  if (stream.agentId !== currentFocusedAgentId) return null;
@@ -2335,7 +2675,7 @@ function shouldPollSessionView(sessionSource) {
2335
2675
  if (SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.shouldPollSessionView) {
2336
2676
  return SESSION_MODAL_BRIDGE.shouldPollSessionView(sessionSource);
2337
2677
  }
2338
- return sessionSource === 'live-text';
2678
+ return false;
2339
2679
  }
2340
2680
 
2341
2681
  function getSessionModalDomState(documentRef) {
@@ -2354,8 +2694,6 @@ function getSessionRuntime() {
2354
2694
  sessionRuntime = SESSION_MODAL_BRIDGE.createRuntime({
2355
2695
  deriveSessionStreamPatch,
2356
2696
  refreshSessionView,
2357
- schedulePoll: (handler) => setInterval(handler, 350),
2358
- clearPoll: (poller) => clearInterval(poller)
2359
2697
  });
2360
2698
  }
2361
2699
 
@@ -2384,11 +2722,6 @@ function getCurrentSessionToken() {
2384
2722
  return runtime ? runtime.getSessionToken() : 0;
2385
2723
  }
2386
2724
 
2387
- function isAwaitingInitialSessionSync() {
2388
- const runtime = getSessionRuntime();
2389
- return runtime ? runtime.isAwaitingInitialSync() : false;
2390
- }
2391
-
2392
2725
  function setSessionOutputLength(length) {
2393
2726
  const runtime = getSessionRuntime();
2394
2727
  if (runtime) {
@@ -2550,12 +2883,9 @@ function applyCrtTerminalFontSize(value) {
2550
2883
  if (input) input.value = String(fontSize);
2551
2884
  if (output) output.textContent = `${fontSize} px`;
2552
2885
  if (terminal && terminal.options) terminal.options.fontSize = fontSize;
2553
- if (terminalInputBridge) terminalInputBridge.style.fontSize = `${fontSize}px`;
2554
2886
  if (terminal && fitAddon && focusedAgentId && typeof requestAnimationFrame === 'function') {
2555
2887
  requestAnimationFrame(() => {
2556
2888
  if (!terminal || !fitAddon || !focusedAgentId) return;
2557
- fitAddon.fit();
2558
- syncTerminalInputBridgePosition();
2559
2889
  sendSessionResize();
2560
2890
  });
2561
2891
  }
@@ -3064,6 +3394,7 @@ function formatCrtExactUsageValue(value) {
3064
3394
  }
3065
3395
 
3066
3396
  function formatCrtCompactTotalValue(value) {
3397
+ if (value === null || value === undefined || value === '') return '--';
3067
3398
  const numberValue = Number(value);
3068
3399
  if (!Number.isFinite(numberValue) || numberValue < 0) return '--';
3069
3400
  const units = [
@@ -3098,26 +3429,199 @@ function crtBillingDayPoint(dateValue = billingSelectedDate) {
3098
3429
  return points.find(point => point && point.date === dateValue) || null;
3099
3430
  }
3100
3431
 
3101
- function crtBillingLogPosition(value, minimum, maximum) {
3432
+ function crtBillingSelectedDayIsCurrent(point = crtBillingDayPoint()) {
3433
+ const daily = billingSummary && billingSummary.daily;
3434
+ return Boolean(daily && point && point.date === daily.endDate);
3435
+ }
3436
+
3437
+ function cancelCrtBillingTotalAnimation() {
3438
+ if (billingTotalAnimationFrame !== null) {
3439
+ window.cancelAnimationFrame(billingTotalAnimationFrame);
3440
+ billingTotalAnimationFrame = null;
3441
+ }
3442
+ billingTotalAnimationTarget = null;
3443
+ }
3444
+
3445
+ function cancelCrtBillingMetricAnimations() {
3446
+ billingAnimatedMetrics.forEach((metric) => {
3447
+ if (metric.frame !== null) window.cancelAnimationFrame(metric.frame);
3448
+ });
3449
+ billingAnimatedMetrics.clear();
3450
+ }
3451
+
3452
+ function updateCrtBillingAnimatedMetric(key, value, {
3453
+ date = '',
3454
+ live = false,
3455
+ write,
3456
+ } = {}) {
3457
+ const target = Number(value);
3458
+ const existing = billingAnimatedMetrics.get(key);
3459
+ if (!Number.isFinite(target) || target < 0 || typeof write !== 'function') {
3460
+ if (existing && existing.frame !== null) window.cancelAnimationFrame(existing.frame);
3461
+ billingAnimatedMetrics.delete(key);
3462
+ write?.(null, null);
3463
+ return;
3464
+ }
3465
+
3466
+ const roundedTarget = Math.round(target);
3467
+ if (existing && existing.frame !== null && existing.target === roundedTarget && existing.date === date) {
3468
+ existing.write = write;
3469
+ return;
3470
+ }
3471
+ if (existing && existing.frame !== null) window.cancelAnimationFrame(existing.frame);
3472
+ const shouldSnap = !live
3473
+ || !existing
3474
+ || existing.date !== date
3475
+ || !Number.isFinite(existing.value)
3476
+ || roundedTarget <= existing.value;
3477
+ if (shouldSnap) {
3478
+ const metric = { date, value: roundedTarget, target: roundedTarget, frame: null, write };
3479
+ billingAnimatedMetrics.set(key, metric);
3480
+ write(roundedTarget, roundedTarget);
3481
+ return;
3482
+ }
3483
+
3484
+ const metric = {
3485
+ date,
3486
+ value: existing.value,
3487
+ target: roundedTarget,
3488
+ frame: null,
3489
+ write,
3490
+ };
3491
+ billingAnimatedMetrics.set(key, metric);
3492
+ const startValue = existing.value;
3493
+ const startedAt = window.performance.now();
3494
+ const step = (now) => {
3495
+ const current = billingAnimatedMetrics.get(key);
3496
+ if (current !== metric) return;
3497
+ const progress = Math.min(1, Math.max(0, (now - startedAt) / CRT_BILLING_TOTAL_ANIMATION_MS));
3498
+ const steppedProgress = Math.min(1, Math.floor(progress * 18) / 18);
3499
+ const easedProgress = 1 - ((1 - steppedProgress) ** 3);
3500
+ metric.value = Math.round(startValue + (roundedTarget - startValue) * easedProgress);
3501
+ metric.write(metric.value, roundedTarget);
3502
+ if (progress < 1) {
3503
+ metric.frame = window.requestAnimationFrame(step);
3504
+ return;
3505
+ }
3506
+ metric.value = roundedTarget;
3507
+ metric.frame = null;
3508
+ metric.write(roundedTarget, roundedTarget);
3509
+ };
3510
+ metric.frame = window.requestAnimationFrame(step);
3511
+ }
3512
+
3513
+ function updateCrtBillingExactMetric(id, value, { date = '', live = false } = {}) {
3514
+ const element = document.getElementById(id);
3515
+ if (!element) return;
3516
+ updateCrtBillingAnimatedMetric(id, value, {
3517
+ date,
3518
+ live,
3519
+ write: (displayed, target) => {
3520
+ element.textContent = formatCrtExactUsageValue(displayed);
3521
+ element.dataset.displayedValue = displayed === null ? '' : String(displayed);
3522
+ element.dataset.targetValue = target === null ? '' : String(target);
3523
+ },
3524
+ });
3525
+ }
3526
+
3527
+ function writeCrtBillingTotalDisplay(value, target, { live = false } = {}) {
3528
+ const total = document.getElementById('billing-day-total');
3529
+ const compact = document.getElementById('billing-day-total-compact');
3530
+ const meter = document.getElementById('billing-day-total-meter');
3531
+ const numericValue = Number.isFinite(Number(value)) ? Math.max(0, Math.round(Number(value))) : null;
3532
+ const numericTarget = Number.isFinite(Number(target)) ? Math.max(0, Math.round(Number(target))) : null;
3533
+ if (total) total.textContent = formatCrtExactUsageValue(numericValue);
3534
+ if (compact) compact.textContent = formatCrtCompactTotalValue(numericValue);
3535
+ if (meter) {
3536
+ meter.classList.toggle('is-live', live);
3537
+ meter.dataset.displayedTotal = numericValue === null ? '' : String(numericValue);
3538
+ meter.dataset.targetTotal = numericTarget === null ? '' : String(numericTarget);
3539
+ meter.setAttribute('aria-label', numericTarget === null
3540
+ ? 'Total tokens unavailable'
3541
+ : `${formatCrtExactUsageValue(numericTarget)} total tokens${live ? ', live refresh every 5 seconds' : ''}`);
3542
+ }
3543
+ }
3544
+
3545
+ function updateCrtBillingTotalDisplay(value, { date = '', live = false } = {}) {
3546
+ const target = Number(value);
3547
+ if (!Number.isFinite(target) || target < 0) {
3548
+ cancelCrtBillingTotalAnimation();
3549
+ billingDisplayedTotalDate = date;
3550
+ billingDisplayedTotalValue = null;
3551
+ writeCrtBillingTotalDisplay(null, null, { live });
3552
+ return;
3553
+ }
3554
+
3555
+ const roundedTarget = Math.round(target);
3556
+ const shouldSnap = !live
3557
+ || billingDisplayedTotalDate !== date
3558
+ || !Number.isFinite(billingDisplayedTotalValue)
3559
+ || roundedTarget <= billingDisplayedTotalValue;
3560
+ if (shouldSnap) {
3561
+ cancelCrtBillingTotalAnimation();
3562
+ billingDisplayedTotalDate = date;
3563
+ billingDisplayedTotalValue = roundedTarget;
3564
+ writeCrtBillingTotalDisplay(roundedTarget, roundedTarget, { live });
3565
+ return;
3566
+ }
3567
+ if (billingTotalAnimationFrame !== null && billingTotalAnimationTarget === roundedTarget) return;
3568
+
3569
+ cancelCrtBillingTotalAnimation();
3570
+ const startValue = billingDisplayedTotalValue;
3571
+ const startedAt = window.performance.now();
3572
+ billingTotalAnimationTarget = roundedTarget;
3573
+ const step = (now) => {
3574
+ const progress = Math.min(1, Math.max(0, (now - startedAt) / CRT_BILLING_TOTAL_ANIMATION_MS));
3575
+ const steppedProgress = Math.min(1, Math.floor(progress * 18) / 18);
3576
+ const easedProgress = 1 - ((1 - steppedProgress) ** 3);
3577
+ billingDisplayedTotalValue = Math.round(startValue + (roundedTarget - startValue) * easedProgress);
3578
+ writeCrtBillingTotalDisplay(billingDisplayedTotalValue, roundedTarget, { live: true });
3579
+ if (progress < 1) {
3580
+ billingTotalAnimationFrame = window.requestAnimationFrame(step);
3581
+ return;
3582
+ }
3583
+ billingDisplayedTotalValue = roundedTarget;
3584
+ billingTotalAnimationFrame = null;
3585
+ billingTotalAnimationTarget = null;
3586
+ writeCrtBillingTotalDisplay(roundedTarget, roundedTarget, { live: true });
3587
+ };
3588
+ billingTotalAnimationFrame = window.requestAnimationFrame(step);
3589
+ }
3590
+
3591
+ const CRT_BILLING_OVERRANGE_BASE = 1_000_000_000;
3592
+
3593
+ function crtBillingOverrangeTier(value) {
3594
+ const total = Math.max(0, Number(value) || 0);
3595
+ if (total < CRT_BILLING_OVERRANGE_BASE) return 0;
3596
+ return Math.min(4, Math.floor(Math.log2(total / CRT_BILLING_OVERRANGE_BASE)) + 1);
3597
+ }
3598
+
3599
+ function crtBillingOverrangeLabel(tier) {
3600
+ return tier > 0 ? `${2 ** (tier - 1)}B+ OVERRANGE` : '';
3601
+ }
3602
+
3603
+ function crtBillingHeatThresholds(values) {
3604
+ const activeValues = (Array.isArray(values) ? values : [])
3605
+ .map(value => Math.max(0, Number(value) || 0))
3606
+ .filter(value => value > 0 && value < CRT_BILLING_OVERRANGE_BASE)
3607
+ .sort((left, right) => left - right);
3608
+ if (activeValues.length === 0) return [];
3609
+ return [0.2, 0.4, 0.6, 0.8].map(quantile => (
3610
+ activeValues[Math.min(activeValues.length - 1, Math.ceil(activeValues.length * quantile) - 1)]
3611
+ ));
3612
+ }
3613
+
3614
+ function crtBillingHeatLevel(value, thresholds) {
3102
3615
  const total = Math.max(0, Number(value) || 0);
3103
3616
  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));
3617
+ const bands = Array.isArray(thresholds) ? thresholds : [];
3618
+ return Math.max(1, Math.min(5, 1 + bands.filter(threshold => total > threshold).length));
3108
3619
  }
3109
3620
 
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);
3621
+ function crtBillingDayDetailHasHourlyActivity(detail) {
3622
+ return Boolean(detail && Array.isArray(detail.hours) && detail.hours.some(hour => (
3623
+ Math.max(0, Number(hour && hour.totalTokens) || 0) > 0
3624
+ )));
3121
3625
  }
3122
3626
 
3123
3627
  function crtBillingHourlyPath(hours, valueForHour, maximum) {
@@ -3126,10 +3630,11 @@ function crtBillingHourlyPath(hours, valueForHour, maximum) {
3126
3630
  const points = Array.isArray(hours) ? hours : [];
3127
3631
  if (points.length === 0 || maximum <= 0) return '';
3128
3632
  return points.map((hour, index) => {
3129
- const x = points.length === 1 ? 0 : index / (points.length - 1) * width;
3633
+ const startX = index / points.length * width;
3634
+ const endX = (index + 1) / points.length * width;
3130
3635
  const value = Math.max(0, Number(valueForHour(hour)) || 0);
3131
3636
  const y = height - Math.min(1, value / maximum) * height;
3132
- return `${index === 0 ? 'M' : 'L'}${x.toFixed(1)} ${y.toFixed(1)}`;
3637
+ return `${index === 0 ? 'M' : 'L'}${startX.toFixed(1)} ${y.toFixed(1)} L${endX.toFixed(1)} ${y.toFixed(1)}`;
3133
3638
  }).join(' ');
3134
3639
  }
3135
3640
 
@@ -3145,6 +3650,8 @@ function renderCrtBillingDayInsight() {
3145
3650
  const cachePath = document.getElementById('billing-day-cache-path');
3146
3651
  const scale = document.getElementById('billing-day-curve-scale');
3147
3652
  const maximumLabel = document.getElementById('billing-day-curve-max');
3653
+ const hourStrip = document.getElementById('billing-day-hour-strip');
3654
+ const hourReadout = document.getElementById('billing-day-hour-readout');
3148
3655
  const shares = document.getElementById('billing-day-provider-shares');
3149
3656
  const maximum = Math.max(0, ...hours.map(hour => Math.max(0, Number(hour && hour.totalTokens) || 0)));
3150
3657
 
@@ -3154,19 +3661,94 @@ function renderCrtBillingDayInsight() {
3154
3661
  hour => (Number(hour.cacheReadTokens) || 0) + (Number(hour.cacheWriteTokens) || 0),
3155
3662
  maximum,
3156
3663
  ));
3157
- if (scale) scale.textContent = maximum > 0 ? `${formatCrtUsageValue(maximum)} TOK/H PEAK` : '-- TOK/H PEAK';
3158
- if (maximumLabel) maximumLabel.textContent = maximum > 0 ? formatCrtUsageValue(maximum) : '--';
3664
+ const isToday = crtBillingSelectedDayIsCurrent(point);
3665
+ updateCrtBillingAnimatedMetric('billing-day-curve-scale', maximum > 0 ? maximum : null, {
3666
+ date: point && point.date || '',
3667
+ live: isToday,
3668
+ write: displayed => {
3669
+ if (scale) scale.textContent = displayed === null ? '-- TOK/H PEAK' : `${formatCrtUsageValue(displayed)} TOK/H PEAK`;
3670
+ },
3671
+ });
3672
+ updateCrtBillingAnimatedMetric('billing-day-curve-max', maximum > 0 ? maximum : null, {
3673
+ date: point && point.date || '',
3674
+ live: isToday,
3675
+ write: displayed => {
3676
+ if (maximumLabel) maximumLabel.textContent = displayed === null ? '--' : formatCrtUsageValue(displayed);
3677
+ },
3678
+ });
3679
+ if (hourStrip) {
3680
+ hourStrip.replaceChildren();
3681
+ if (hours.length > 0) {
3682
+ const heatThresholds = crtBillingHeatThresholds(hours.map(hour => hour && hour.totalTokens));
3683
+ if (!Number.isInteger(billingSelectedHour) || billingSelectedHour < 0 || billingSelectedHour >= hours.length) {
3684
+ billingSelectedHour = hours.reduce((peakIndex, hour, index) => (
3685
+ Number(hour && hour.totalTokens) > Number(hours[peakIndex] && hours[peakIndex].totalTokens) ? index : peakIndex
3686
+ ), 0);
3687
+ }
3688
+ const selectHour = (index, { focus = false } = {}) => {
3689
+ billingSelectedHour = index;
3690
+ hourStrip.querySelectorAll('.billing-day-hour-cell').forEach((cell, cellIndex) => {
3691
+ const selected = cellIndex === index;
3692
+ cell.classList.toggle('selected', selected);
3693
+ cell.setAttribute('aria-selected', selected ? 'true' : 'false');
3694
+ cell.tabIndex = selected ? 0 : -1;
3695
+ });
3696
+ const hour = hours[index] || {};
3697
+ const hourValue = Number.isFinite(Number(hour.hour)) ? Number(hour.hour) : index;
3698
+ const total = Math.max(0, Number(hour.totalTokens) || 0);
3699
+ const cache = Math.max(0, (Number(hour.cacheReadTokens) || 0) + (Number(hour.cacheWriteTokens) || 0));
3700
+ if (hourReadout) {
3701
+ const endHour = hourValue + 1;
3702
+ const cacheShare = total > 0 ? `${(cache / total * 100).toFixed(1)}% CACHE` : 'NO ACTIVITY';
3703
+ hourReadout.textContent = `[${String(hourValue).padStart(2, '0')}:00—${String(endHour).padStart(2, '0')}:00] TOTAL ${formatCrtUsageValue(total)} // CACHE ${formatCrtUsageValue(cache)} // ${cacheShare}`;
3704
+ hourReadout.title = `${String(hourValue).padStart(2, '0')}:00—${String(endHour).padStart(2, '0')}:00 · ${formatCrtExactUsageValue(total)} total tokens · ${formatCrtExactUsageValue(cache)} cache tokens`;
3705
+ }
3706
+ if (focus) hourStrip.children[index]?.focus();
3707
+ };
3708
+ hours.forEach((hour, index) => {
3709
+ const total = Math.max(0, Number(hour && hour.totalTokens) || 0);
3710
+ const cache = Math.max(0, (Number(hour && hour.cacheReadTokens) || 0) + (Number(hour && hour.cacheWriteTokens) || 0));
3711
+ const hourValue = Number.isFinite(Number(hour && hour.hour)) ? Number(hour.hour) : index;
3712
+ const overrangeTier = crtBillingOverrangeTier(total);
3713
+ const cell = document.createElement('button');
3714
+ cell.type = 'button';
3715
+ cell.className = 'billing-day-hour-cell';
3716
+ cell.dataset.level = overrangeTier ? 'overrange' : String(crtBillingHeatLevel(total, heatThresholds));
3717
+ if (overrangeTier) cell.dataset.overrange = String(overrangeTier);
3718
+ cell.dataset.hour = String(hourValue);
3719
+ cell.setAttribute('role', 'gridcell');
3720
+ 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`);
3721
+ cell.tabIndex = index === billingSelectedHour ? 0 : -1;
3722
+ cell.addEventListener('click', () => selectHour(index));
3723
+ cell.addEventListener('mouseenter', () => selectHour(index));
3724
+ cell.addEventListener('focus', () => selectHour(index));
3725
+ cell.addEventListener('keydown', (event) => {
3726
+ if (!['ArrowLeft', 'ArrowRight'].includes(event.key)) return;
3727
+ event.preventDefault();
3728
+ event.stopPropagation();
3729
+ selectHour(Math.max(0, Math.min(hours.length - 1, index + (event.key === 'ArrowLeft' ? -1 : 1))), { focus: true });
3730
+ });
3731
+ hourStrip.appendChild(cell);
3732
+ });
3733
+ selectHour(billingSelectedHour);
3734
+ } else if (hourReadout) {
3735
+ hourReadout.textContent = billingDayDetailLoading ? 'READING HOURLY COORDINATES' : 'NO HOURLY ACTIVITY';
3736
+ hourReadout.removeAttribute('title');
3737
+ }
3738
+ }
3159
3739
  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';
3740
+ state.classList.toggle('is-error', Boolean(billingDayDetailError && !selectedDetail));
3741
+ state.textContent = billingDayDetailError && selectedDetail
3742
+ ? '24 HOURLY BINS READY · STALE'
3743
+ : billingDayDetailError
3744
+ ? 'DAY SIGNAL LOST'
3745
+ : billingDayDetailLoading && !selectedDetail
3746
+ ? 'READING 24 HOURLY BINS'
3747
+ : selectedDetail && maximum > 0
3748
+ ? '24 HOURLY BINS READY'
3749
+ : selectedDetail
3750
+ ? 'NO HOURLY ACTIVITY'
3751
+ : 'SELECTED DAY DETAIL';
3170
3752
  }
3171
3753
 
3172
3754
  if (!shares) return;
@@ -3209,42 +3791,46 @@ function renderCrtBillingDayInsight() {
3209
3791
  });
3210
3792
  }
3211
3793
 
3212
- function renderCrtBillingSelectedDay() {
3794
+ function renderCrtBillingSelectedDay({ preferSummary = false } = {}) {
3213
3795
  const daily = billingSummary && billingSummary.daily;
3214
3796
  const point = crtBillingDayPoint();
3797
+ const cachedEntry = billingDayDetailCache.get(billingSelectedDate);
3798
+ const selectedDetail = billingDayDetail && billingDayDetail.date === billingSelectedDate
3799
+ ? billingDayDetail
3800
+ : cachedEntry && cachedEntry.detail || null;
3801
+ const displayDetail = preferSummary ? null : selectedDetail;
3802
+ const displayPoint = displayDetail && displayDetail.total || point;
3803
+ const displayProviders = displayDetail && displayDetail.providers || point && point.providers;
3804
+ const isToday = crtBillingSelectedDayIsCurrent(point);
3215
3805
  const date = document.getElementById('billing-day-date');
3216
3806
  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
3807
  const providers = document.getElementById('billing-day-providers');
3224
3808
  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);
3809
+ updateCrtBillingTotalDisplay(displayPoint && displayPoint.totalTokens, {
3810
+ date: point && point.date || '',
3811
+ live: isToday,
3812
+ });
3813
+ const selectedDate = point && point.date || '';
3814
+ updateCrtBillingExactMetric('billing-day-input', displayPoint && displayPoint.inputTokens, { date: selectedDate, live: isToday });
3815
+ updateCrtBillingExactMetric('billing-day-output', displayPoint && displayPoint.outputTokens, { date: selectedDate, live: isToday });
3816
+ updateCrtBillingExactMetric('billing-day-cache-read', displayPoint && displayPoint.cacheReadTokens, { date: selectedDate, live: isToday });
3817
+ updateCrtBillingExactMetric('billing-day-cache-write', displayPoint && displayPoint.cacheWriteTokens, { date: selectedDate, live: isToday });
3231
3818
  if (providers) {
3232
- const providerTotals = point && point.providers ? Object.entries(point.providers) : [];
3819
+ const providerTotals = displayProviders ? Object.entries(displayProviders) : [];
3233
3820
  providers.textContent = providerTotals
3234
3821
  .map(([provider, usage]) => `${provider.toUpperCase()} ${formatCrtExactUsageValue(usage && usage.totalTokens)}`)
3235
3822
  .join(' · ') || '--';
3236
3823
  providers.title = providers.textContent;
3237
3824
  }
3238
3825
  if (stateLabel) {
3239
- const isToday = Boolean(daily && point && point.date === daily.endDate);
3240
- const notes = [isToday ? 'PARTIAL DAY' : 'COMPLETE DAY', 'INCL CACHE'];
3826
+ const notes = isToday ? ['LIVE 5S', 'PARTIAL DAY', 'INCL CACHE'] : ['COMPLETE DAY', 'INCL CACHE'];
3241
3827
  if (daily && daily.partial) notes.push('PARTIAL SOURCE');
3242
3828
  if (point && Number(point.unattributedTokens) > 0) {
3243
3829
  notes.push(`${formatCrtUsageValue(point.unattributedTokens)} UNCLASSIFIED`);
3244
3830
  }
3245
3831
  stateLabel.textContent = point ? notes.join(' · ') : 'LOCAL HISTORY';
3246
3832
  }
3247
- document.querySelectorAll('#billing-daily-bars .billing-daily-column').forEach((cell) => {
3833
+ document.querySelectorAll('#billing-calendar-grid .billing-calendar-day').forEach((cell) => {
3248
3834
  const selected = cell.dataset.date === billingSelectedDate;
3249
3835
  cell.classList.toggle('selected', selected);
3250
3836
  cell.setAttribute('aria-selected', selected ? 'true' : 'false');
@@ -3254,13 +3840,16 @@ function renderCrtBillingSelectedDay() {
3254
3840
 
3255
3841
  function selectCrtBillingDay(dateValue, { focus = false } = {}) {
3256
3842
  if (!crtBillingDayPoint(dateValue)) return false;
3843
+ cancelCrtBillingDayDetailRetry();
3257
3844
  billingSelectedDate = dateValue;
3845
+ billingSelectedHour = null;
3846
+ const isToday = crtBillingSelectedDayIsCurrent(crtBillingDayPoint(dateValue));
3258
3847
  billingDayDetail = billingDayDetailCache.get(dateValue)?.detail || null;
3259
3848
  billingDayDetailError = '';
3260
- renderCrtBillingSelectedDay();
3261
- void loadCrtBillingDayDetail(dateValue);
3849
+ renderCrtBillingSelectedDay({ preferSummary: isToday });
3850
+ void loadCrtBillingDayDetail(dateValue, { force: isToday, live: isToday });
3262
3851
  if (focus) {
3263
- const cell = document.querySelector(`#billing-daily-bars .billing-daily-column[data-date="${dateValue}"]`);
3852
+ const cell = document.querySelector(`#billing-calendar-grid .billing-calendar-day[data-date="${dateValue}"]`);
3264
3853
  if (cell) {
3265
3854
  cell.focus({ preventScroll: true });
3266
3855
  scrollCrtBillingSelectedDayIntoView();
@@ -3269,18 +3858,27 @@ function selectCrtBillingDay(dateValue, { focus = false } = {}) {
3269
3858
  return true;
3270
3859
  }
3271
3860
 
3272
- async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3861
+ function cancelCrtBillingDayDetailRetry() {
3862
+ if (billingDayDetailRetryTimer !== null) {
3863
+ clearTimeout(billingDayDetailRetryTimer);
3864
+ billingDayDetailRetryTimer = null;
3865
+ }
3866
+ }
3867
+
3868
+ async function loadCrtBillingDayDetail(dateValue, { force = false, live = false, retryCount = 0 } = {}) {
3273
3869
  const date = String(dateValue || '').trim();
3274
3870
  if (!crtBillingDayPoint(date)) return;
3871
+ if (retryCount === 0) cancelCrtBillingDayDetailRetry();
3275
3872
  const cachedEntry = billingDayDetailCache.get(date);
3276
3873
  const cached = cachedEntry && cachedEntry.detail;
3277
- const cacheFresh = cachedEntry && Date.now() - cachedEntry.fetchedAt <= CRT_BILLING_DAY_DETAIL_CACHE_MS;
3874
+ const cacheMaxAge = live ? CRT_BILLING_LIVE_DAY_REFRESH_MS : CRT_BILLING_DAY_DETAIL_CACHE_MS;
3875
+ const cacheFresh = cachedEntry && Date.now() - cachedEntry.fetchedAt <= cacheMaxAge;
3278
3876
  if (cached && cacheFresh && !force) {
3279
3877
  if (billingSelectedDate === date) {
3280
3878
  billingDayDetail = cached;
3281
3879
  billingDayDetailLoading = false;
3282
3880
  billingDayDetailError = '';
3283
- renderCrtBillingDayInsight();
3881
+ renderCrtBillingSelectedDay();
3284
3882
  }
3285
3883
  return;
3286
3884
  }
@@ -3293,11 +3891,12 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3293
3891
  billingDayDetailLoading = true;
3294
3892
  billingDayDetailError = '';
3295
3893
  if (billingSelectedDate === date) {
3296
- billingDayDetail = null;
3894
+ if (!cached) billingDayDetail = null;
3297
3895
  renderCrtBillingDayInsight();
3298
3896
  }
3897
+ let shouldRetry = false;
3299
3898
  try {
3300
- const response = await fetch(farmingApiPath(`/usage/day?date=${encodeURIComponent(date)}`), {
3899
+ const response = await fetch(farmingApiPath(`/usage/day?date=${encodeURIComponent(date)}${live ? '&live=1' : ''}`), {
3301
3900
  signal: controller.signal,
3302
3901
  cache: 'no-store',
3303
3902
  });
@@ -3306,18 +3905,44 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3306
3905
  throw new Error(data && data.error ? data.error : `Usage day request failed (${response.status})`);
3307
3906
  }
3308
3907
  if (requestSequence !== billingDayDetailRequestSequence) return;
3908
+ const previousDetail = cached || (billingDayDetail && billingDayDetail.date === date ? billingDayDetail : null);
3909
+ const nextTotal = Math.max(0, Number(data.detail.total && data.detail.total.totalTokens) || 0);
3910
+ if (
3911
+ nextTotal > 0
3912
+ && crtBillingDayDetailHasHourlyActivity(previousDetail)
3913
+ && !crtBillingDayDetailHasHourlyActivity(data.detail)
3914
+ ) {
3915
+ throw new Error('Usage day response omitted previously available hourly bins');
3916
+ }
3309
3917
  billingDayDetailCache.set(date, { detail: data.detail, fetchedAt: Date.now() });
3310
3918
  if (billingSelectedDate === date) billingDayDetail = data.detail;
3919
+ if (live) renderCrtBillingDaily();
3311
3920
  } catch (error) {
3312
3921
  if (controller.signal.aborted || requestSequence !== billingDayDetailRequestSequence) return;
3313
- if (billingSelectedDate === date) {
3922
+ if (billingSelectedDate === date && retryCount < CRT_BILLING_DAY_DETAIL_MAX_RETRIES) {
3923
+ shouldRetry = true;
3924
+ } else if (billingSelectedDate === date) {
3314
3925
  billingDayDetailError = error instanceof Error ? error.message : 'Failed to load selected day';
3315
3926
  }
3316
3927
  } finally {
3317
3928
  if (requestSequence === billingDayDetailRequestSequence) {
3318
- billingDayDetailLoading = false;
3319
3929
  billingDayDetailAbortController = null;
3320
- if (billingSelectedDate === date) renderCrtBillingDayInsight();
3930
+ if (shouldRetry) {
3931
+ billingDayDetailLoading = true;
3932
+ cancelCrtBillingDayDetailRetry();
3933
+ const retryDelay = CRT_BILLING_DAY_DETAIL_RETRY_MS * (2 ** retryCount);
3934
+ billingDayDetailRetryTimer = setTimeout(() => {
3935
+ billingDayDetailRetryTimer = null;
3936
+ if (billingSelectedDate !== date || crtMainView !== 'billing') {
3937
+ billingDayDetailLoading = false;
3938
+ return;
3939
+ }
3940
+ void loadCrtBillingDayDetail(date, { force: true, live, retryCount: retryCount + 1 });
3941
+ }, retryDelay);
3942
+ } else {
3943
+ billingDayDetailLoading = false;
3944
+ }
3945
+ if (billingSelectedDate === date) renderCrtBillingSelectedDay();
3321
3946
  }
3322
3947
  }
3323
3948
  }
@@ -3325,7 +3950,7 @@ async function loadCrtBillingDayDetail(dateValue, { force = false } = {}) {
3325
3950
  function scrollCrtBillingSelectedDayIntoView() {
3326
3951
  const scroll = document.getElementById('billing-daily-scroll');
3327
3952
  const cell = billingSelectedDate
3328
- ? document.querySelector(`#billing-daily-bars .billing-daily-column[data-date="${billingSelectedDate}"]`)
3953
+ ? document.querySelector(`#billing-calendar-grid .billing-calendar-day[data-date="${billingSelectedDate}"]`)
3329
3954
  : null;
3330
3955
  if (!scroll || !cell) return;
3331
3956
  const left = cell.offsetLeft;
@@ -3355,7 +3980,19 @@ function renderCrtBillingDaily(summary = billingSummary) {
3355
3980
  const element = document.getElementById(id);
3356
3981
  if (element) element.textContent = formatCrtUsageValue(value);
3357
3982
  };
3358
- setValue('billing-today-total', totals.todayTokens);
3983
+ const todayDetail = daily && billingDayDetailCache.get(daily.endDate)?.detail;
3984
+ const todayTokens = todayDetail && todayDetail.total ? todayDetail.total.totalTokens : totals.todayTokens;
3985
+ updateCrtBillingAnimatedMetric('billing-today-total', todayTokens, {
3986
+ date: daily && daily.endDate || '',
3987
+ live: true,
3988
+ write: (displayed, target) => {
3989
+ const element = document.getElementById('billing-today-total');
3990
+ if (!element) return;
3991
+ element.textContent = formatCrtUsageValue(displayed);
3992
+ element.dataset.displayedValue = displayed === null ? '' : String(displayed);
3993
+ element.dataset.targetValue = target === null ? '' : String(target);
3994
+ },
3995
+ });
3359
3996
  setValue('billing-7d-total', totals.sevenDayTokens);
3360
3997
  setValue('billing-30d-total', totals.thirtyDayTokens);
3361
3998
  setValue('billing-period-total', totals.periodTokens);
@@ -3376,12 +4013,9 @@ function renderCrtBillingDaily(summary = billingSummary) {
3376
4013
  : 'LOCAL TIME';
3377
4014
  }
3378
4015
 
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;
4016
+ const calendar = document.getElementById('billing-calendar-grid');
4017
+ const months = document.getElementById('billing-calendar-months');
4018
+ if (!calendar || !months) return;
3385
4019
  const signature = points.map(point => [
3386
4020
  point.date,
3387
4021
  point.totalTokens,
@@ -3390,95 +4024,62 @@ function renderCrtBillingDaily(summary = billingSummary) {
3390
4024
  ].join(':')).join('|');
3391
4025
  if (signature !== billingDailyRenderSignature) {
3392
4026
  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);
4027
+ calendar.replaceChildren();
4028
+ months.replaceChildren();
4029
+
4030
+ const chartPoints = points.slice(-(52 * 7));
4031
+ const heatThresholds = crtBillingHeatThresholds(chartPoints.map(point => point && point.totalTokens));
4032
+ const firstDate = parseCrtBillingDate(chartPoints[0] && chartPoints[0].date);
4033
+ const leadingDays = firstDate ? (firstDate.getDay() + 6) % 7 : 0;
4034
+ const weekCount = Math.max(1, Math.ceil((leadingDays + chartPoints.length) / 7));
4035
+ calendar.style.setProperty('--billing-calendar-weeks', String(weekCount));
4036
+ months.style.setProperty('--billing-calendar-weeks', String(weekCount));
4037
+ Array.from({ length: leadingDays }).forEach(() => {
4038
+ const spacer = document.createElement('span');
4039
+ spacer.className = 'billing-calendar-spacer';
4040
+ spacer.setAttribute('aria-hidden', 'true');
4041
+ calendar.appendChild(spacer);
3415
4042
  });
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)));
4043
+
4044
+ const monthLabels = Array.from({ length: weekCount }, () => '');
3424
4045
  chartPoints.forEach((point, index) => {
3425
4046
  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' : ''}`;
4047
+ const weekIndex = Math.floor((leadingDays + index) / 7);
4048
+ if (pointDate && (index === 0 || pointDate.getDate() === 1) && !monthLabels[weekIndex]) {
4049
+ monthLabels[weekIndex] = pointDate.toLocaleDateString('en-US', { month: 'short' }).toUpperCase();
3434
4050
  }
3435
- xAxis.appendChild(axisTick);
3436
-
3437
4051
  const total = Math.max(0, Number(point.totalTokens) || 0);
3438
4052
  const cache = Math.min(total, Math.max(0, Number(point.cacheReadTokens) || 0)
3439
4053
  + 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);
4054
+ const overrangeTier = crtBillingOverrangeTier(total);
4055
+ const overrangeLabel = crtBillingOverrangeLabel(overrangeTier);
4056
+ const day = document.createElement('button');
4057
+ day.type = 'button';
4058
+ day.className = 'billing-calendar-day';
4059
+ day.dataset.date = point.date;
4060
+ day.dataset.level = overrangeTier ? 'overrange' : String(crtBillingHeatLevel(total, heatThresholds));
4061
+ if (overrangeTier) day.dataset.overrange = String(overrangeTier);
4062
+ day.setAttribute('role', 'gridcell');
4063
+ day.setAttribute('aria-label', `${point.date}: ${formatCrtExactUsageValue(total)} tokens, ${formatCrtExactUsageValue(cache)} cache tokens${overrangeLabel ? `, ${overrangeLabel}` : ''}`);
4064
+ day.setAttribute('aria-selected', 'false');
4065
+ day.tabIndex = -1;
4066
+ day.title = `${point.date} · ${formatCrtExactUsageValue(total)} total · ${formatCrtExactUsageValue(cache)} cache${overrangeLabel ? ` · ${overrangeLabel}` : ''}`;
4067
+ day.addEventListener('click', () => selectCrtBillingDay(point.date));
4068
+ calendar.appendChild(day);
3471
4069
  });
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);
4070
+ const trailingDays = weekCount * 7 - leadingDays - chartPoints.length;
4071
+ Array.from({ length: trailingDays }).forEach(() => {
4072
+ const spacer = document.createElement('span');
4073
+ spacer.className = 'billing-calendar-spacer';
4074
+ spacer.setAttribute('aria-hidden', 'true');
4075
+ calendar.appendChild(spacer);
3481
4076
  });
4077
+ monthLabels.forEach((label) => {
4078
+ const month = document.createElement('span');
4079
+ month.textContent = label;
4080
+ months.appendChild(month);
4081
+ });
4082
+ calendar.setAttribute('aria-label', `${chartPoints.length}-day token activity: ${activeDays} active days, ${billionDays} days at or above one billion tokens`);
3482
4083
 
3483
4084
  if (!billingSelectedDate || !points.some(point => point.date === billingSelectedDate)) {
3484
4085
  billingSelectedDate = daily && daily.endDate || points.at(-1)?.date || '';
@@ -3765,10 +4366,6 @@ async function loadCrtBilling({ fresh = false } = {}) {
3765
4366
  billingAbortController = controller;
3766
4367
  billingLoading = true;
3767
4368
  billingError = '';
3768
- if (fresh) {
3769
- billingDayDetailCache.clear();
3770
- billingDayDetail = null;
3771
- }
3772
4369
  renderCrtBilling();
3773
4370
  try {
3774
4371
  const response = await fetch(farmingApiPath(`/usage${fresh ? '?fresh=1' : ''}`), {
@@ -3788,7 +4385,10 @@ async function loadCrtBilling({ fresh = false } = {}) {
3788
4385
  billingAbortController = null;
3789
4386
  renderCrtBilling();
3790
4387
  if (!billingError && billingSelectedDate) {
3791
- void loadCrtBillingDayDetail(billingSelectedDate, { force: fresh });
4388
+ void loadCrtBillingDayDetail(billingSelectedDate, {
4389
+ force: fresh,
4390
+ live: crtBillingSelectedDayIsCurrent(),
4391
+ });
3792
4392
  }
3793
4393
  }
3794
4394
  }
@@ -3799,6 +4399,10 @@ function stopCrtBillingRefresh({ abort = false } = {}) {
3799
4399
  clearInterval(billingRefreshTimer);
3800
4400
  billingRefreshTimer = null;
3801
4401
  }
4402
+ if (billingLiveDayRefreshTimer !== null) {
4403
+ clearInterval(billingLiveDayRefreshTimer);
4404
+ billingLiveDayRefreshTimer = null;
4405
+ }
3802
4406
  if (abort && billingAbortController) {
3803
4407
  billingRequestSequence += 1;
3804
4408
  billingAbortController.abort();
@@ -3811,6 +4415,11 @@ function stopCrtBillingRefresh({ abort = false } = {}) {
3811
4415
  billingDayDetailAbortController = null;
3812
4416
  billingDayDetailLoading = false;
3813
4417
  }
4418
+ if (abort) {
4419
+ cancelCrtBillingDayDetailRetry();
4420
+ cancelCrtBillingTotalAnimation();
4421
+ cancelCrtBillingMetricAnimations();
4422
+ }
3814
4423
  }
3815
4424
 
3816
4425
  function startCrtBillingRefresh() {
@@ -3818,6 +4427,16 @@ function startCrtBillingRefresh() {
3818
4427
  billingRefreshTimer = setInterval(() => {
3819
4428
  if (crtMainView === 'billing' && document.visibilityState !== 'hidden') void loadCrtBilling();
3820
4429
  }, CRT_BILLING_REFRESH_MS);
4430
+ billingLiveDayRefreshTimer = setInterval(() => {
4431
+ if (
4432
+ crtMainView !== 'billing'
4433
+ || billingMode !== 'days'
4434
+ || document.visibilityState === 'hidden'
4435
+ || billingDayDetailLoading
4436
+ || !crtBillingSelectedDayIsCurrent()
4437
+ ) return;
4438
+ void loadCrtBillingDayDetail(billingSelectedDate, { force: true, live: true });
4439
+ }, CRT_BILLING_LIVE_DAY_REFRESH_MS);
3821
4440
  }
3822
4441
 
3823
4442
  function refreshCrtBilling() {
@@ -4140,7 +4759,7 @@ function needsMainAgent(currentState = state) {
4140
4759
  const mainAgent = currentState && currentState.mainAgentId
4141
4760
  ? currentState.agents.find((agent) => agent.id === currentState.mainAgentId)
4142
4761
  : null;
4143
- return !currentState || !currentState.mainAgentId || (mainAgent && mainAgent.status === 'dead');
4762
+ return !currentState || !currentState.mainAgentId || !isCrtLiveAgent(mainAgent);
4144
4763
  }
4145
4764
 
4146
4765
  function getDefaultWorkspaceForDialog(asMainAgent) {
@@ -4337,6 +4956,7 @@ function refreshWorkspaceMemoryUI() {
4337
4956
  function seedWorkspaceInput() {
4338
4957
  const workspaceInput = document.getElementById('workspace-input');
4339
4958
  if (!workspaceInput) return;
4959
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4340
4960
  workspaceInput.value = '';
4341
4961
  workspaceInput.placeholder = pendingMainAgentLaunch
4342
4962
  ? formatWorkspaceForDisplay(getDefaultWorkspaceForDialog(true))
@@ -4400,13 +5020,129 @@ function setupWorkspaceHistoryControls() {
4400
5020
  }
4401
5021
 
4402
5022
  async function confirmStartAgent() {
4403
- if (waitingForAgent || selectedAgentIndex === null || selectedAgentIndex < 0 || selectedAgentIndex >= agents.length) return;
5023
+ if (waitingForAgent || workspaceDirectoryPrompt || selectedAgentIndex === null || selectedAgentIndex < 0 || selectedAgentIndex >= agents.length) return;
4404
5024
 
4405
5025
  const agent = agents[selectedAgentIndex];
4406
5026
  const workspaceInput = normalizeWorkspaceValue(document.getElementById('workspace-input').value);
4407
5027
  const asMainAgent = pendingMainAgentLaunch;
4408
5028
  const workspaceToUse = resolveWorkspaceToStart(workspaceInput, asMainAgent);
4409
5029
 
5030
+ if (asMainAgent || !workspaceToUse) {
5031
+ await startCrtAgent(agent, workspaceToUse, asMainAgent);
5032
+ return;
5033
+ }
5034
+
5035
+ try {
5036
+ const result = await prepareCrtWorkspaceDirectory(workspaceToUse, false);
5037
+ if (result.status === 'ready') {
5038
+ await startCrtAgent(agent, result.workspace, false);
5039
+ return;
5040
+ }
5041
+ showWorkspaceDirectoryPrompt(result.status === 'missing' ? 'confirm' : 'error', {
5042
+ workspace: result.workspace || workspaceToUse,
5043
+ code: result.code || ''
5044
+ });
5045
+ } catch {
5046
+ showWorkspaceDirectoryPrompt('error', { workspace: workspaceToUse, code: '' });
5047
+ }
5048
+ }
5049
+
5050
+ async function prepareCrtWorkspaceDirectory(workspace, create) {
5051
+ const response = await fetch(farmingApiPath('/workspaces/prepare'), {
5052
+ method: 'POST',
5053
+ headers: { 'Content-Type': 'application/json' },
5054
+ body: JSON.stringify({ workspace, create: create === true })
5055
+ });
5056
+ const result = await response.json().catch(() => null);
5057
+ if (result && typeof result.status === 'string' && typeof result.workspace === 'string') {
5058
+ return result;
5059
+ }
5060
+ throw new Error(result && result.message ? result.message : `Failed to prepare workspace (${response.status})`);
5061
+ }
5062
+
5063
+ function showWorkspaceDirectoryPrompt(kind, { workspace, code = '' }) {
5064
+ const prompt = document.getElementById('workspace-directory-prompt');
5065
+ const input = document.getElementById('workspace-input');
5066
+ const history = document.getElementById('workspace-history');
5067
+ const startActions = document.getElementById('workspace-start-actions');
5068
+ const createButton = document.getElementById('workspace-directory-create');
5069
+ const cancelButton = document.getElementById('workspace-directory-cancel');
5070
+ const errorBackButton = document.getElementById('workspace-directory-error-back');
5071
+ if (!prompt || !input || !createButton || !cancelButton || !errorBackButton) return;
5072
+
5073
+ workspaceDirectoryPrompt = { kind, workspace, code };
5074
+ const isError = kind === 'error';
5075
+ const isCreating = kind === 'creating';
5076
+ prompt.hidden = false;
5077
+ prompt.classList.toggle('error', isError);
5078
+ input.disabled = true;
5079
+ if (history) history.style.display = 'none';
5080
+ if (startActions) startActions.style.display = 'none';
5081
+
5082
+ document.getElementById('workspace-directory-prompt-icon').textContent = isError ? '[!]' : '[+]';
5083
+ document.getElementById('workspace-directory-prompt-title').textContent = isError
5084
+ ? 'Could not create workspace'
5085
+ : isCreating
5086
+ ? 'Creating workspace...'
5087
+ : 'Create this workspace?';
5088
+ document.getElementById('workspace-directory-prompt-description').textContent = isError
5089
+ ? code === 'workspace-create-forbidden'
5090
+ ? 'Farming does not have permission to create this directory. Choose another location or update the parent directory permissions.'
5091
+ : 'Farming could not create this directory. Check the path and try again.'
5092
+ : 'This directory does not exist yet. Farming can create it and start the Agent there.';
5093
+ document.getElementById('workspace-directory-prompt-path').textContent = formatWorkspaceForDisplay(workspace);
5094
+
5095
+ createButton.hidden = isError;
5096
+ cancelButton.hidden = isError;
5097
+ errorBackButton.hidden = !isError;
5098
+ createButton.disabled = isCreating;
5099
+ cancelButton.disabled = isCreating;
5100
+ createButton.textContent = isCreating ? 'Creating...' : 'Create & Start [Enter]';
5101
+ clearCrtNavigationSelection();
5102
+ const primary = isError ? errorBackButton : createButton;
5103
+ if (!isCreating) {
5104
+ setCrtNavigationSelection(primary);
5105
+ primary.focus();
5106
+ }
5107
+ }
5108
+
5109
+ function hideWorkspaceDirectoryPrompt({ focusInput = true } = {}) {
5110
+ const prompt = document.getElementById('workspace-directory-prompt');
5111
+ const input = document.getElementById('workspace-input');
5112
+ const startActions = document.getElementById('workspace-start-actions');
5113
+ workspaceDirectoryPrompt = null;
5114
+ if (prompt) prompt.hidden = true;
5115
+ if (input) input.disabled = false;
5116
+ if (startActions) startActions.style.display = 'block';
5117
+ renderWorkspaceHistoryUI();
5118
+ if (focusInput && input) {
5119
+ input.focus();
5120
+ input.setSelectionRange(input.value.length, input.value.length);
5121
+ }
5122
+ }
5123
+
5124
+ async function createCrtWorkspaceAndStart() {
5125
+ if (!workspaceDirectoryPrompt || workspaceDirectoryPrompt.kind !== 'confirm') return;
5126
+ const target = workspaceDirectoryPrompt.workspace;
5127
+ const agent = selectedAgentIndex === null ? null : agents[selectedAgentIndex];
5128
+ if (!agent) return;
5129
+ showWorkspaceDirectoryPrompt('creating', { workspace: target });
5130
+ try {
5131
+ const result = await prepareCrtWorkspaceDirectory(target, true);
5132
+ if (result.status === 'created' || result.status === 'ready') {
5133
+ await startCrtAgent(agent, result.workspace, false);
5134
+ return;
5135
+ }
5136
+ showWorkspaceDirectoryPrompt('error', {
5137
+ workspace: result.workspace || target,
5138
+ code: result.code || ''
5139
+ });
5140
+ } catch {
5141
+ showWorkspaceDirectoryPrompt('error', { workspace: target, code: '' });
5142
+ }
5143
+ }
5144
+
5145
+ async function startCrtAgent(agent, workspaceToUse, asMainAgent) {
4410
5146
  console.log('Starting agent:', agent.name, 'workspace:', workspaceToUse || 'default');
4411
5147
 
4412
5148
  waitingForAgent = true;
@@ -4436,7 +5172,10 @@ async function confirmStartAgent() {
4436
5172
  }));
4437
5173
  }
4438
5174
 
5175
+ globalThis.createCrtWorkspaceAndStart = createCrtWorkspaceAndStart;
5176
+
4439
5177
  function backToAgentList() {
5178
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4440
5179
  clearCrtNavigationSelection();
4441
5180
  selectedAgentIndex = null;
4442
5181
  document.getElementById('agent-list').style.display = 'block';
@@ -4449,6 +5188,7 @@ function selectAgent(index) {
4449
5188
  if (index < 0 || index >= agents.length) return;
4450
5189
 
4451
5190
  const agent = agents[index];
5191
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4452
5192
 
4453
5193
  console.log('Selected agent:', agent.name);
4454
5194
  clearCrtNavigationSelection();
@@ -4478,10 +5218,11 @@ function requestedCrtAgentId(search = typeof window !== 'undefined' ? window.loc
4478
5218
  function openCrtAgentDeeplinkIfReady() {
4479
5219
  if (didApplyAgentDeeplink || !state) return false;
4480
5220
  didApplyAgentDeeplink = true;
5221
+ crtReadingAnchorApi()?.importFromSearch(window.location.search);
4481
5222
 
4482
5223
  const agentId = requestedCrtAgentId();
4483
5224
  const agent = agentId
4484
- ? state.agents.find((candidate) => candidate.id === agentId && candidate.archived !== true)
5225
+ ? state.agents.find((candidate) => candidate.id === agentId && isCrtLiveAgent(candidate))
4485
5226
  : null;
4486
5227
  if (!agent) return false;
4487
5228
 
@@ -4505,13 +5246,25 @@ function connect() {
4505
5246
  socket.onopen = () => {
4506
5247
  if (ws !== socket) return;
4507
5248
  console.log('Connected to server');
5249
+ socket.send(JSON.stringify({ type: 'protocol-hello', protocolVersion: CRT_PROTOCOL_VERSION }));
4508
5250
  const activeAgentId = isCrtSessionOpen() ? focusedAgentId : null;
4509
5251
  getSessionClient()?.focusAgent(activeAgentId, {
4510
5252
  streamScope: 'focused',
4511
5253
  previewScope: activeAgentId ? 'none' : 'all',
4512
5254
  });
4513
5255
  if (activeAgentId && terminal) {
4514
- void refreshSessionView(true, activeAgentId, getCurrentSessionToken());
5256
+ if (crtTerminalReplication) {
5257
+ clearPendingCrtTerminalFitResize(crtTerminalReplication);
5258
+ crtTerminalReplication.lastResizeCols = null;
5259
+ crtTerminalReplication.lastResizeRows = null;
5260
+ if (crtTerminalReplication.checkpointRetryTimer) {
5261
+ clearTimeout(crtTerminalReplication.checkpointRetryTimer);
5262
+ crtTerminalReplication.checkpointRetryTimer = null;
5263
+ }
5264
+ TERMINAL_REPLAY.resetRecovery(crtTerminalReplication.replayState);
5265
+ TERMINAL_REPLAY.beginRecovery(crtTerminalReplication.replayState);
5266
+ requestCrtTerminalReplay();
5267
+ }
4515
5268
  }
4516
5269
  loadAgents();
4517
5270
  };
@@ -4519,9 +5272,20 @@ function connect() {
4519
5272
  socket.onmessage = (event) => {
4520
5273
  if (ws !== socket) return;
4521
5274
  const data = JSON.parse(event.data);
5275
+ if (data.type === 'protocol-hello') {
5276
+ if (data.protocolVersion !== CRT_PROTOCOL_VERSION) {
5277
+ socket.close(4002, `Unsupported Farming protocol version ${data.protocolVersion}`);
5278
+ }
5279
+ return;
5280
+ }
5281
+ if (data.type === 'protocol-error') {
5282
+ console.error(data.message || 'Farming protocol error');
5283
+ return;
5284
+ }
4522
5285
  if (data.type === 'state') {
4523
5286
  const prevAgentCount = state ? state.agents.length : 0;
4524
5287
  state = data.state;
5288
+ pruneCrtStructuredPreviews(state);
4525
5289
  const activeAgentIds = new Set(state.agents.map((agent) => agent.id));
4526
5290
  terminalPreviewSnapshots.forEach((_snapshot, agentId) => {
4527
5291
  if (!activeAgentIds.has(agentId)) terminalPreviewSnapshots.delete(agentId);
@@ -4532,6 +5296,7 @@ function connect() {
4532
5296
  }
4533
5297
  });
4534
5298
  const dashboardRendered = renderCrtDashboardIfNeeded();
5299
+ scheduleCrtRenderedStructuredPreviews();
4535
5300
  if (dashboardRendered && crtMainView === 'history') renderCrtHistory();
4536
5301
  if (crtMainView === 'search') renderCrtSearch();
4537
5302
  generateKeyMap();
@@ -4558,6 +5323,14 @@ function connect() {
4558
5323
  openCrtAgentDeeplinkIfReady();
4559
5324
  } else if (data.type === 'agent-started') {
4560
5325
  selectCrtStartedAgent(data.agentId);
5326
+ } else if (data.type === 'agent-update') {
5327
+ const update = data.update;
5328
+ const agent = update && state && state.agents.find(candidate => candidate.id === update.agentId);
5329
+ if (agent && update.patch && typeof update.patch === 'object') {
5330
+ Object.assign(agent, update.patch);
5331
+ renderCrtDashboardIfNeeded();
5332
+ if (agent.id === focusedAgentId) updateCrtRuntimeSwitchControl(agent);
5333
+ }
4561
5334
  } else if (data.type === 'session-preview') {
4562
5335
  const preview = data.preview;
4563
5336
  if (preview && preview.agentId) {
@@ -4573,11 +5346,16 @@ function connect() {
4573
5346
  agent.previewRows = preview.rows || agent.previewRows;
4574
5347
  agent.previewSnapshot = preview.previewSnapshot || null;
4575
5348
  if (preview.terminalStatus) agent.terminalStatus = preview.terminalStatus;
5349
+ if (preview.runtimeObservation) agent.runtimeObservation = preview.runtimeObservation;
4576
5350
  if (isCrtSessionOpen()) dashboardRenderDeferred = true;
4577
5351
  else scheduleCrtPreviewCardRender(agent, previousSnapshot, previousText, previewChanged);
4578
5352
  }
4579
5353
  }
4580
5354
  } else if (data.type === 'session-output') {
5355
+ if (crtTerminalReplication) {
5356
+ handleCrtTerminalStream(data.stream);
5357
+ return;
5358
+ }
4581
5359
  const runtime = getSessionRuntime();
4582
5360
  const sessionToken = runtime ? runtime.getSessionToken() : 0;
4583
5361
  const runtimeResult = runtime ? runtime.handleStreamMessage(data.stream) : null;
@@ -4596,10 +5374,20 @@ function connect() {
4596
5374
  setSessionOutputLength(getSessionOutputLength() + patch.nextLengthDelta);
4597
5375
  }
4598
5376
  }
5377
+ } else if (data.type === 'agent-activity') {
5378
+ const activity = data.activity;
5379
+ const agent = activity && state && state.agents.find((candidate) => candidate.id === activity.agentId);
5380
+ if (agent) {
5381
+ Object.assign(agent, activity);
5382
+ renderCrtDashboardIfNeeded();
5383
+ }
4599
5384
  } else if (data.type === 'system-stats') {
4600
5385
  updateSystemStats(data.stats, data.uptime, data.usageRate);
4601
5386
  } else if (data.type === 'error') {
4602
5387
  waitingForAgent = false;
5388
+ if (workspaceDirectoryPrompt?.kind === 'creating') {
5389
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
5390
+ }
4603
5391
  alert('Error: ' + data.message);
4604
5392
  }
4605
5393
  };
@@ -4608,6 +5396,21 @@ function connect() {
4608
5396
  if (ws !== socket) return;
4609
5397
  ws = null;
4610
5398
  console.log('Disconnected from server');
5399
+ if (crtTerminalReplication) {
5400
+ clearPendingCrtTerminalFitResize(crtTerminalReplication);
5401
+ crtTerminalReplication.lastResizeCols = null;
5402
+ crtTerminalReplication.lastResizeRows = null;
5403
+ crtTerminalReplication.checkpointSeq += 1;
5404
+ crtTerminalReplication.checkpointAbortController?.abort();
5405
+ crtTerminalReplication.checkpointAbortController = null;
5406
+ crtTerminalReplication.checkpointInFlight = false;
5407
+ if (crtTerminalReplication.checkpointRetryTimer) {
5408
+ clearTimeout(crtTerminalReplication.checkpointRetryTimer);
5409
+ crtTerminalReplication.checkpointRetryTimer = null;
5410
+ }
5411
+ TERMINAL_REPLAY.resetRecovery(crtTerminalReplication.replayState);
5412
+ TERMINAL_REPLAY.beginRecovery(crtTerminalReplication.replayState);
5413
+ }
4611
5414
  if (typeof document === 'undefined' || document.visibilityState !== 'hidden') {
4612
5415
  wsReconnectTimer = setTimeout(() => {
4613
5416
  wsReconnectTimer = null;
@@ -4651,7 +5454,7 @@ function checkMainAgentStatus() {
4651
5454
  ? state.agents.find(a => a.id === state.mainAgentId)
4652
5455
  : null;
4653
5456
 
4654
- if (!state.mainAgentId || (mainAgent && mainAgent.status === 'dead')) {
5457
+ if (!state.mainAgentId || !isCrtLiveAgent(mainAgent)) {
4655
5458
  showInputDialog();
4656
5459
  }
4657
5460
  }
@@ -4729,9 +5532,34 @@ function updateSystemStats(stats, uptime, usageRate) {
4729
5532
  }
4730
5533
  }
4731
5534
 
5535
+ function isCrtLiveAgent(agent) {
5536
+ return Boolean(
5537
+ agent
5538
+ && agent.archived !== true
5539
+ && agent.status !== 'dead'
5540
+ && agent.status !== 'stopped'
5541
+ );
5542
+ }
5543
+
5544
+ function getCrtLiveAgents(currentState = state) {
5545
+ if (!currentState || !Array.isArray(currentState.agents)) return [];
5546
+ return currentState.agents.filter(isCrtLiveAgent);
5547
+ }
5548
+
4732
5549
  function getCrtRegularAgents(currentState = state) {
4733
5550
  if (!currentState || !Array.isArray(currentState.agents)) return [];
4734
- return currentState.agents.filter((agent) => agent.id !== currentState.mainAgentId);
5551
+ return getCrtLiveAgents(currentState).filter((agent) => (
5552
+ agent.id !== currentState.mainAgentId && agent.isMain !== true
5553
+ ));
5554
+ }
5555
+
5556
+ function getCrtAgentRemovalFallback(currentState, removedAgentId) {
5557
+ const liveAgents = getCrtLiveAgents(currentState);
5558
+ const removedIndex = liveAgents.findIndex((agent) => agent.id === removedAgentId);
5559
+ const remaining = liveAgents.filter((agent) => agent.id !== removedAgentId);
5560
+ if (!remaining.length) return '';
5561
+ if (removedIndex < 0) return remaining[0].id;
5562
+ return remaining[Math.min(removedIndex, remaining.length - 1)].id;
4735
5563
  }
4736
5564
 
4737
5565
  function updateCrtAgentPageStatus(pageState) {
@@ -4774,8 +5602,9 @@ function renderState() {
4774
5602
  lastCrtDashboardSignature = crtDashboardStateSignature(state);
4775
5603
 
4776
5604
  // 更新吊顶的 Agent 数量
4777
- const activeAgents = state.agents.filter(a => a.status === 'running').length;
4778
- const totalAgents = state.agents.length;
5605
+ const visibleAgents = getCrtLiveAgents(state);
5606
+ const activeAgents = visibleAgents.filter(a => a.status === 'running').length;
5607
+ const totalAgents = visibleAgents.length;
4779
5608
  document.getElementById('active-agents').textContent = activeAgents;
4780
5609
  document.getElementById('total-agents').textContent = totalAgents;
4781
5610
  updateCrtBrandState(state);
@@ -4789,18 +5618,21 @@ function renderState() {
4789
5618
  agentBlocks.forEach(block => block.remove());
4790
5619
 
4791
5620
  mainAgentBlock.innerHTML = '';
5621
+ mainAgentBlock.removeAttribute('data-agent-id');
5622
+ mainAgentBlock.removeAttribute('data-crt-nav-key');
4792
5623
 
4793
- if (state.agents.length === 0) {
4794
- emptyState.style.display = 'flex';
5624
+ const regularAgents = getCrtRegularAgents(state);
5625
+ const hasRegularAgents = regularAgents.length > 0;
5626
+ mapArea.classList.toggle('empty', !hasRegularAgents);
5627
+ emptyState.style.display = hasRegularAgents ? 'none' : 'flex';
5628
+
5629
+ if (visibleAgents.length === 0) {
4795
5630
  mainAgentPanel.style.display = 'none';
4796
5631
  updateCrtAgentPageStatus(getCrtAgentPage([], 0, 1));
4797
5632
  showInputDialog();
4798
5633
  return;
4799
5634
  }
4800
5635
 
4801
- emptyState.style.display = 'none';
4802
-
4803
- const regularAgents = getCrtRegularAgents(state);
4804
5636
  const pageLayout = calculateCrtAgentPageLayout(
4805
5637
  mapArea.clientWidth,
4806
5638
  mapArea.clientHeight,
@@ -4823,7 +5655,7 @@ function renderState() {
4823
5655
  // 渲染 Main Agent 到右下角
4824
5656
  if (state.mainAgentId) {
4825
5657
  const mainAgent = state.agents.find(a => a.id === state.mainAgentId);
4826
- if (mainAgent) {
5658
+ if (isCrtLiveAgent(mainAgent)) {
4827
5659
  mainAgentPanel.style.display = 'block';
4828
5660
  mainAgentBlock.dataset.crtNavKey = `agent:${mainAgent.id}`;
4829
5661
  mainAgentBlock.dataset.agentId = mainAgent.id;
@@ -4843,13 +5675,7 @@ function renderState() {
4843
5675
  const output = document.createElement('div');
4844
5676
  output.className = 'agent-output';
4845
5677
  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);
5678
+ renderCrtAgentOutput(output, mainAgent, { main: true });
4853
5679
  mainAgentBlock.appendChild(output);
4854
5680
 
4855
5681
  mainAgentBlock.onclick = () => openSession(mainAgent.id);
@@ -4893,13 +5719,7 @@ function renderState() {
4893
5719
 
4894
5720
  const output = document.createElement('div');
4895
5721
  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);
5722
+ renderCrtAgentOutput(output, agent);
4903
5723
  block.appendChild(output);
4904
5724
 
4905
5725
  block.onclick = () => openSession(agent.id);
@@ -4914,14 +5734,14 @@ function generateKeyMap() {
4914
5734
 
4915
5735
  keyMap = {};
4916
5736
  let keyIndex = 1;
4917
- state.agents.forEach((agent) => {
4918
- if (agent.id === state.mainAgentId) return; // 跳过 Main Agent
5737
+ getCrtRegularAgents(state).forEach((agent) => {
4919
5738
  keyMap[keyIndex] = agent.id;
4920
5739
  keyIndex++;
4921
5740
  });
4922
5741
  }
4923
5742
 
4924
5743
  function showInputDialog(prefill = null) {
5744
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4925
5745
  clearCrtNavigationSelection();
4926
5746
  void loadAgents();
4927
5747
  const title = document.getElementById('dialog-title');
@@ -4972,6 +5792,7 @@ function hideInputDialog() {
4972
5792
  pendingMainAgentLaunch = false;
4973
5793
  pendingAgentLaunchPrefill = null;
4974
5794
  waitingForAgent = false;
5795
+ hideWorkspaceDirectoryPrompt({ focusInput: false });
4975
5796
  document.getElementById('agent-list').style.display = 'block';
4976
5797
  document.getElementById('workspace-input-container').style.display = 'none';
4977
5798
  document.getElementById('input-dialog').classList.remove('active');
@@ -4993,10 +5814,10 @@ function isCrtAgentInteractive(agent) {
4993
5814
  }
4994
5815
 
4995
5816
  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';
5817
+ const kind = agent && agent.runtimeBinding && agent.runtimeBinding.kind;
5818
+ if (kind === 'acp') return 'ACP';
5819
+ if (kind === 'json') return 'JSON';
5820
+ if (kind === 'app-server') return 'APP SERVER';
5000
5821
  return '';
5001
5822
  }
5002
5823
 
@@ -5011,7 +5832,7 @@ function crtRuntimeView(agent) {
5011
5832
  function canSwitchCrtAgentRuntime(agent) {
5012
5833
  const freshCodexTerminal = agent
5013
5834
  && agent.providerSessionProvider === 'codex'
5014
- && agent.agentRuntimeMode === 'terminal'
5835
+ && agent.runtimeBinding?.kind === 'terminal'
5015
5836
  && agent.providerSessionTemporary === true
5016
5837
  && agent.terminalInputReceived !== true;
5017
5838
  return Boolean(
@@ -5033,6 +5854,43 @@ function isCrtRuntimeSwitchShortcut(event) {
5033
5854
  );
5034
5855
  }
5035
5856
 
5857
+ function hasCrtStructuredLocalEscapeAction(context = {}) {
5858
+ return Boolean(
5859
+ context.structuredTranscriptFocused
5860
+ || context.structuredToolFocused
5861
+ || context.structuredMenuItemFocused
5862
+ || context.structuredInterruptFocused
5863
+ || (context.structuredInputFocused && context.structuredComposerMenuOpen)
5864
+ );
5865
+ }
5866
+
5867
+ function resolveCrtSessionKeyboardCommand(event, context = {}) {
5868
+ if (!event) return '';
5869
+ const key = String(event.key || '').toLowerCase();
5870
+ const primaryModifier = Boolean(event.ctrlKey || event.metaKey);
5871
+
5872
+ // Session-wide commands must remain reachable from every focus owner,
5873
+ // including the hidden Terminal IME bridge and every structured Chat control.
5874
+ if (primaryModifier && key === 'k') return 'kill';
5875
+ if (primaryModifier && key === 'escape') return 'close';
5876
+
5877
+ // Plain Escape closes an idle Chat only when a more local transition does
5878
+ // not own it. Terminal keeps plain Escape for the running TUI.
5879
+ if (
5880
+ context.structuredSessionActive === true
5881
+ && key === 'escape'
5882
+ && !event.altKey
5883
+ && !event.shiftKey
5884
+ && event.isComposing !== true
5885
+ && context.composing !== true
5886
+ && !hasCrtStructuredLocalEscapeAction(context)
5887
+ ) {
5888
+ return 'close';
5889
+ }
5890
+
5891
+ return '';
5892
+ }
5893
+
5036
5894
  function setCrtRuntimeSwitchStatus(message = '', error = false) {
5037
5895
  const status = document.getElementById('crt-runtime-switch-status');
5038
5896
  const messageNode = document.getElementById('crt-runtime-switch-message');
@@ -5128,23 +5986,19 @@ function toggleCrtSessionRuntimeMode() {
5128
5986
  }
5129
5987
 
5130
5988
  function structuredTranscriptEndpoint(agent) {
5131
- if (agent.agentRuntimeMode === 'acp') return 'acp-transcript';
5132
- if (agent.agentRuntimeMode === 'json') return 'json-cli-transcript';
5989
+ if (agent.runtimeBinding.kind === 'acp') return 'acp-transcript';
5990
+ if (agent.runtimeBinding.kind === 'json') return 'json-cli-transcript';
5133
5991
  return 'codex-app-server-transcript';
5134
5992
  }
5135
5993
 
5136
5994
  function structuredRuntimeStatus(agent) {
5137
5995
  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';
5996
+ return agent.runtimeBinding?.state || 'idle';
5141
5997
  }
5142
5998
 
5143
5999
  function structuredRuntimeError(agent) {
5144
6000
  if (!agent) return '';
5145
- if (agent.agentRuntimeMode === 'acp') return agent.acpError || '';
5146
- if (agent.agentRuntimeMode === 'json') return agent.jsonCliError || '';
5147
- return agent.codexAppServerError || '';
6001
+ return agent.runtimeBinding?.error || '';
5148
6002
  }
5149
6003
 
5150
6004
  function structuredComposerAction(agent, draft = '') {
@@ -5153,12 +6007,8 @@ function structuredComposerAction(agent, draft = '') {
5153
6007
  const status = String(structuredRuntimeStatus(agent) || 'idle');
5154
6008
  const working = ['working', 'waiting-for-permission'].includes(status);
5155
6009
  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';
6010
+ if (agent.runtimeBinding?.kind === 'acp' && String(draft || '').trim()) return 'send';
6011
+ if (agent.runtimeBinding?.kind === 'app-server' && String(draft || '').trim()) return 'steer';
5162
6012
  return 'interrupt';
5163
6013
  }
5164
6014
  if (['starting', 'interrupting'].includes(status)) return 'disabled';
@@ -5295,6 +6145,37 @@ function focusStructuredComposerMenuButton(current, offset = 0) {
5295
6145
  return true;
5296
6146
  }
5297
6147
 
6148
+ function structuredComposerMenuPath() {
6149
+ return `${structuredComposerMenu}:${structuredComposerConfigId}`;
6150
+ }
6151
+
6152
+ function captureStructuredComposerMenuFocus(menu) {
6153
+ const focused = document.activeElement && document.activeElement.closest
6154
+ ? document.activeElement.closest('.crt-structured-menu-item')
6155
+ : null;
6156
+ if (!focused || !menu.contains(focused)) return null;
6157
+ const buttons = structuredComposerMenuButtons();
6158
+ return {
6159
+ path: structuredComposerMenuPath(),
6160
+ key: focused.dataset.menuKey || '',
6161
+ index: buttons.indexOf(focused)
6162
+ };
6163
+ }
6164
+
6165
+ function restoreStructuredComposerMenuFocus(snapshot) {
6166
+ if (!snapshot || snapshot.path !== structuredComposerMenuPath()) return false;
6167
+ const buttons = structuredComposerMenuButtons();
6168
+ if (!buttons.length) return false;
6169
+ const matching = snapshot.key
6170
+ ? buttons.find((button) => button.dataset.menuKey === snapshot.key)
6171
+ : null;
6172
+ const fallbackIndex = Math.min(Math.max(snapshot.index, 0), buttons.length - 1);
6173
+ const target = matching || buttons[fallbackIndex];
6174
+ target.focus();
6175
+ target.scrollIntoView({ block: 'nearest' });
6176
+ return true;
6177
+ }
6178
+
5298
6179
  function closeStructuredComposerMenu({ restoreFocus = true } = {}) {
5299
6180
  structuredComposerMenu = '';
5300
6181
  structuredComposerConfigId = '';
@@ -5334,10 +6215,11 @@ async function patchStructuredAcpSession(patch) {
5334
6215
  if (opener && !opener.hidden && !opener.disabled) opener.focus();
5335
6216
  }
5336
6217
 
5337
- function structuredMenuButton(label, description, active, onClick) {
6218
+ function structuredMenuButton(label, description, active, onClick, menuKey = '') {
5338
6219
  const button = document.createElement('button');
5339
6220
  button.type = 'button';
5340
6221
  button.className = `crt-structured-menu-item${active ? ' active' : ''}`;
6222
+ button.dataset.menuKey = menuKey;
5341
6223
  const title = document.createElement('span');
5342
6224
  title.textContent = label;
5343
6225
  button.appendChild(title);
@@ -5353,6 +6235,7 @@ function structuredMenuButton(label, description, active, onClick) {
5353
6235
  function renderStructuredComposerMenu() {
5354
6236
  const menu = document.getElementById('crt-structured-composer-menu');
5355
6237
  if (!menu) return;
6238
+ const retainedFocus = captureStructuredComposerMenuFocus(menu);
5356
6239
  menu.replaceChildren();
5357
6240
  const session = structuredSessionSnapshot;
5358
6241
  if (!structuredComposerMenu || !session) {
@@ -5389,7 +6272,7 @@ function renderStructuredComposerMenu() {
5389
6272
  renderStructuredSessionControls();
5390
6273
  updateStructuredComposerState(state && state.agents.find((agent) => agent.id === focusedAgentId));
5391
6274
  input.focus();
5392
- }));
6275
+ }, `command:${command.name}`));
5393
6276
  });
5394
6277
  } else if (structuredComposerMenu === 'mode') {
5395
6278
  const modes = session.modes && Array.isArray(session.modes.availableModes) ? session.modes.availableModes : [];
@@ -5397,7 +6280,7 @@ function renderStructuredComposerMenu() {
5397
6280
  modes.forEach((mode) => {
5398
6281
  items.appendChild(structuredMenuButton(mode.name || mode.id, mode.description || '', mode.id === current, () => {
5399
6282
  void patchStructuredAcpSession({ modeId: mode.id }).catch(showStructuredComposerError);
5400
- }));
6283
+ }, `mode:${mode.id}`));
5401
6284
  });
5402
6285
  } else if (!selectedConfig) {
5403
6286
  structuredVisibleConfigOptions(session).forEach((option) => {
@@ -5409,20 +6292,21 @@ function renderStructuredComposerMenu() {
5409
6292
  structuredComposerConfigId = option.id;
5410
6293
  structuredComposerMenuFocusPending = true;
5411
6294
  renderStructuredSessionControls();
5412
- }
6295
+ },
6296
+ `config:${option.id}`
5413
6297
  ));
5414
6298
  });
5415
6299
  } else if (selectedConfig.type === 'boolean') {
5416
6300
  [false, true].forEach((value) => {
5417
6301
  items.appendChild(structuredMenuButton(value ? 'ON' : 'OFF', selectedConfig.description || '', selectedConfig.currentValue === value, () => {
5418
6302
  void patchStructuredAcpSession({ configId: selectedConfig.id, value }).catch(showStructuredComposerError);
5419
- }));
6303
+ }, `boolean:${value}`));
5420
6304
  });
5421
6305
  } else {
5422
6306
  structuredSelectOptions(selectedConfig).forEach((candidate) => {
5423
6307
  items.appendChild(structuredMenuButton(candidate.name || candidate.value, candidate.description || '', candidate.value === selectedConfig.currentValue, () => {
5424
6308
  void patchStructuredAcpSession({ configId: selectedConfig.id, value: candidate.value }).catch(showStructuredComposerError);
5425
- }));
6309
+ }, `option:${candidate.value}`));
5426
6310
  });
5427
6311
  }
5428
6312
 
@@ -5431,6 +6315,8 @@ function renderStructuredComposerMenu() {
5431
6315
  if (structuredComposerMenuFocusPending) {
5432
6316
  structuredComposerMenuFocusPending = false;
5433
6317
  window.requestAnimationFrame(() => focusStructuredComposerMenuButton(null, 0));
6318
+ } else {
6319
+ restoreStructuredComposerMenuFocus(retainedFocus);
5434
6320
  }
5435
6321
  }
5436
6322
 
@@ -5457,11 +6343,11 @@ function renderStructuredSessionControls() {
5457
6343
  async function refreshStructuredSessionControls(agentId = focusedAgentId, force = false) {
5458
6344
  if (!agentId || structuredSessionControlsLoading) return;
5459
6345
  const agent = state && state.agents.find((candidate) => candidate.id === agentId);
5460
- if (!agent || agent.agentRuntimeMode !== 'acp') {
6346
+ if (!agent || agent.runtimeBinding?.kind !== 'acp') {
5461
6347
  resetStructuredSessionControls();
5462
6348
  return;
5463
6349
  }
5464
- const revision = String(agent.acpSessionUpdatedAt || '');
6350
+ const revision = String(agent.runtimeBinding.sessionUpdatedAt || '');
5465
6351
  if (!force && (
5466
6352
  (revision && revision === structuredSessionControlsRevision)
5467
6353
  || (!revision && structuredSessionSnapshot)
@@ -5579,7 +6465,7 @@ function updateStructuredComposerState(agent) {
5579
6465
  statusNode.textContent = error || `${kind} ${String(runtimeStatus || 'idle').toUpperCase()}`;
5580
6466
  statusNode.classList.toggle('error', Boolean(error));
5581
6467
  renderStructuredPermissions(agent);
5582
- if (agent && agent.agentRuntimeMode === 'acp') void refreshStructuredSessionControls(agent.id);
6468
+ if (agent && agent.runtimeBinding?.kind === 'acp') void refreshStructuredSessionControls(agent.id);
5583
6469
  }
5584
6470
 
5585
6471
  function structuredAttachmentId(file) {
@@ -5716,10 +6602,11 @@ function renderStructuredPermissions(agent) {
5716
6602
  const container = document.getElementById('crt-structured-composer-notices');
5717
6603
  if (!container) return;
5718
6604
  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] : []);
6605
+ if (!agent || agent.runtimeBinding?.kind !== 'acp') return;
6606
+ const runtime = agent.runtimeBinding;
6607
+ const requests = Array.isArray(runtime.pendingPermissions) && runtime.pendingPermissions.length
6608
+ ? runtime.pendingPermissions
6609
+ : (runtime.pendingPermission ? [runtime.pendingPermission] : []);
5723
6610
  requests.forEach((request) => {
5724
6611
  const panel = document.createElement('section');
5725
6612
  panel.className = 'crt-structured-permission';
@@ -5778,13 +6665,13 @@ function structuredTranscriptTurns(transcript) {
5778
6665
  const text = structuredTranscriptContentText(entry.content);
5779
6666
  if (!text) return;
5780
6667
  if (entry.role === 'user') {
5781
- current = { userMessage: text, finalMessage: '' };
6668
+ current = { id: entry.id || `user-${turns.length}`, userMessage: text, finalMessage: '' };
5782
6669
  turns.push(current);
5783
6670
  return;
5784
6671
  }
5785
6672
  if (entry.role !== 'assistant') return;
5786
6673
  if (!current) {
5787
- current = { userMessage: '', finalMessage: '' };
6674
+ current = { id: entry.id || `assistant-${turns.length}`, userMessage: '', finalMessage: '' };
5788
6675
  turns.push(current);
5789
6676
  }
5790
6677
  current.finalMessage = text;
@@ -5792,12 +6679,215 @@ function structuredTranscriptTurns(transcript) {
5792
6679
  return turns;
5793
6680
  }
5794
6681
 
6682
+ function normalizeCrtStructuredPreviewText(value, limit = 240) {
6683
+ const text = String(value || '').replace(/\s+/g, ' ').trim();
6684
+ if (text.length <= limit) return text;
6685
+ return `${text.slice(0, Math.max(1, limit - 1)).trimEnd()}…`;
6686
+ }
6687
+
6688
+ function structuredTranscriptAttachmentLabel(content) {
6689
+ const labels = (Array.isArray(content) ? content : []).flatMap((block) => {
6690
+ const type = String(block && block.type || '').toLowerCase();
6691
+ if (type.includes('image')) return ['Image attachment'];
6692
+ if (type.includes('audio')) return ['Audio attachment'];
6693
+ if (type.includes('file') || type.includes('resource')) return ['File attachment'];
6694
+ return [];
6695
+ });
6696
+ return Array.from(new Set(labels)).join(' + ');
6697
+ }
6698
+
6699
+ function crtStructuredPreviewMessageLines(transcript, limit = 8) {
6700
+ const maxLines = Math.max(1, Number(limit) || 8);
6701
+ const turnLines = structuredTranscriptTurns(transcript).flatMap((turn) => [
6702
+ turn && turn.userMessage
6703
+ ? { role: 'user', label: 'YOU', text: normalizeCrtStructuredPreviewText(turn.userMessage) }
6704
+ : null,
6705
+ turn && turn.finalMessage
6706
+ ? { role: 'assistant', label: 'AGENT', text: normalizeCrtStructuredPreviewText(turn.finalMessage) }
6707
+ : null,
6708
+ ]).filter(Boolean);
6709
+ if (turnLines.length > 0) return turnLines.slice(-maxLines);
6710
+
6711
+ const entries = transcript && Array.isArray(transcript.entries)
6712
+ ? transcript.entries
6713
+ .filter((entry) => (
6714
+ entry
6715
+ && entry.internal !== true
6716
+ && entry.type === 'message'
6717
+ && (entry.role === 'user' || entry.role === 'assistant')
6718
+ ))
6719
+ .map((entry) => ({
6720
+ role: entry.role,
6721
+ label: entry.role === 'user' ? 'YOU' : 'AGENT',
6722
+ text: normalizeCrtStructuredPreviewText(structuredTranscriptContentText(entry.content))
6723
+ || structuredTranscriptAttachmentLabel(entry.content),
6724
+ }))
6725
+ .filter((line) => line.text)
6726
+ : [];
6727
+ return entries.slice(-maxLines);
6728
+ }
6729
+
6730
+ function structuredEntryActivityText(entry) {
6731
+ if (!entry || entry.internal === true) return '';
6732
+ if (entry.type === 'tool') {
6733
+ const title = normalizeCrtStructuredPreviewText(entry.title || entry.kind || 'Tool activity', 160);
6734
+ const status = normalizeCrtStructuredPreviewText(entry.status, 40).replaceAll('_', ' ');
6735
+ return [title, status].filter(Boolean).join(' · ');
6736
+ }
6737
+ if (entry.type === 'thought') {
6738
+ return normalizeCrtStructuredPreviewText(structuredTranscriptContentText(entry.content), 180);
6739
+ }
6740
+ if (entry.type === 'plan') {
6741
+ const steps = Array.isArray(entry.entries) ? entry.entries : [];
6742
+ const current = steps.find((step) => step && step.status === 'in_progress')
6743
+ || [...steps].reverse().find((step) => step && step.status !== 'completed')
6744
+ || steps.at(-1);
6745
+ return normalizeCrtStructuredPreviewText(current && (current.content || current.title), 180);
6746
+ }
6747
+ return '';
6748
+ }
6749
+
6750
+ function buildCrtStructuredPreview(transcript, agent = null) {
6751
+ const entries = transcript && Array.isArray(transcript.entries)
6752
+ ? transcript.entries.filter((entry) => entry && entry.internal !== true)
6753
+ : [];
6754
+ const turns = structuredTranscriptTurns(transcript);
6755
+ const latestTurn = turns.at(-1) || null;
6756
+ const messageLines = crtStructuredPreviewMessageLines(transcript);
6757
+ let userText = normalizeCrtStructuredPreviewText(latestTurn && latestTurn.userMessage);
6758
+ let assistantText = normalizeCrtStructuredPreviewText(latestTurn && latestTurn.finalMessage);
6759
+ let activityText = '';
6760
+
6761
+ if (entries.length > 0) {
6762
+ const latestUserIndex = entries.findLastIndex((entry) => entry.type === 'message' && entry.role === 'user');
6763
+ const latestUser = latestUserIndex >= 0 ? entries[latestUserIndex] : null;
6764
+ if (latestUser) {
6765
+ userText = normalizeCrtStructuredPreviewText(structuredTranscriptContentText(latestUser.content))
6766
+ || structuredTranscriptAttachmentLabel(latestUser.content);
6767
+ }
6768
+ const turnEntries = latestUserIndex >= 0 ? entries.slice(latestUserIndex + 1) : entries;
6769
+ const latestAssistant = [...turnEntries].reverse().find((entry) => (
6770
+ entry.type === 'message' && entry.role === 'assistant'
6771
+ ));
6772
+ if (latestAssistant) {
6773
+ assistantText = normalizeCrtStructuredPreviewText(structuredTranscriptContentText(latestAssistant.content))
6774
+ || structuredTranscriptAttachmentLabel(latestAssistant.content);
6775
+ }
6776
+ activityText = [...turnEntries].reverse().map(structuredEntryActivityText).find(Boolean) || '';
6777
+ } else if (latestTurn && Array.isArray(latestTurn.processItems)) {
6778
+ const processItem = [...latestTurn.processItems].reverse().find(Boolean);
6779
+ activityText = normalizeCrtStructuredPreviewText(
6780
+ processItem && (processItem.detail || processItem.title),
6781
+ 180,
6782
+ );
6783
+ }
6784
+
6785
+ return {
6786
+ messageLines,
6787
+ userText,
6788
+ assistantText,
6789
+ activityText,
6790
+ state: String(structuredRuntimeStatus(agent) || (transcript && transcript.state) || ''),
6791
+ };
6792
+ }
6793
+
6794
+ function crtStructuredPreviewRevision(agent) {
6795
+ if (!agent || !isStructuredRuntimeAgent(agent)) return '';
6796
+ const runtime = agent.runtimeBinding;
6797
+ return JSON.stringify([
6798
+ structuredRuntimeKind(agent),
6799
+ structuredRuntimeStatus(agent),
6800
+ runtime.sessionRevision || 0,
6801
+ runtime.sessionUpdatedAt || '',
6802
+ runtime.transcriptUpdatedAt || '',
6803
+ runtime.turnId || '',
6804
+ agent.lastActivity || 0,
6805
+ ]);
6806
+ }
6807
+
6808
+ function pruneCrtStructuredPreviews(currentState = state) {
6809
+ const visibleIds = new Set(getCrtLiveAgents(currentState).map((agent) => agent.id));
6810
+ crtStructuredPreviewCache.forEach((_value, agentId) => {
6811
+ if (!visibleIds.has(agentId)) crtStructuredPreviewCache.delete(agentId);
6812
+ });
6813
+ crtStructuredPreviewTimers.forEach((timer, agentId) => {
6814
+ if (visibleIds.has(agentId)) return;
6815
+ clearTimeout(timer);
6816
+ crtStructuredPreviewTimers.delete(agentId);
6817
+ });
6818
+ }
6819
+
6820
+ function scheduleCrtStructuredPreviewRefresh(agent) {
6821
+ if (!isCrtLiveAgent(agent) || !isStructuredRuntimeAgent(agent)) return;
6822
+ const revision = crtStructuredPreviewRevision(agent);
6823
+ const cached = crtStructuredPreviewCache.get(agent.id);
6824
+ if (cached && cached.revision === revision && (cached.loading || cached.ready)) return;
6825
+ if (crtStructuredPreviewTimers.has(agent.id)) return;
6826
+ const timer = setTimeout(() => {
6827
+ crtStructuredPreviewTimers.delete(agent.id);
6828
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6829
+ if (isCrtLiveAgent(currentAgent) && isStructuredRuntimeAgent(currentAgent)) {
6830
+ void refreshCrtStructuredPreview(currentAgent);
6831
+ }
6832
+ }, CRT_STRUCTURED_PREVIEW_REFRESH_MS);
6833
+ crtStructuredPreviewTimers.set(agent.id, timer);
6834
+ }
6835
+
6836
+ async function refreshCrtStructuredPreview(agent) {
6837
+ const revision = crtStructuredPreviewRevision(agent);
6838
+ const cached = crtStructuredPreviewCache.get(agent.id);
6839
+ if (cached && cached.revision === revision && (cached.loading || cached.ready)) return;
6840
+ crtStructuredPreviewCache.set(agent.id, {
6841
+ revision,
6842
+ loading: true,
6843
+ ready: false,
6844
+ preview: cached && cached.preview || null,
6845
+ error: '',
6846
+ });
6847
+
6848
+ try {
6849
+ const endpoint = structuredTranscriptEndpoint(agent);
6850
+ const response = await fetch(farmingApiPath(`/agents/${encodeURIComponent(agent.id)}/${endpoint}?maxTurns=20`));
6851
+ const body = await response.json().catch(() => null);
6852
+ if (!response.ok || !body || !body.transcript) {
6853
+ throw new Error(body && body.error ? body.error : `Conversation preview failed (${response.status})`);
6854
+ }
6855
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6856
+ if (!isCrtLiveAgent(currentAgent) || crtStructuredPreviewRevision(currentAgent) !== revision) {
6857
+ if (isCrtLiveAgent(currentAgent)) scheduleCrtStructuredPreviewRefresh(currentAgent);
6858
+ return;
6859
+ }
6860
+ crtStructuredPreviewCache.set(agent.id, {
6861
+ revision,
6862
+ loading: false,
6863
+ ready: true,
6864
+ preview: buildCrtStructuredPreview(body.transcript, currentAgent),
6865
+ error: '',
6866
+ });
6867
+ if (isCrtSessionOpen()) dashboardRenderDeferred = true;
6868
+ else updateCrtAgentPreviewCard(currentAgent);
6869
+ } catch (error) {
6870
+ const currentAgent = state && state.agents.find((candidate) => candidate.id === agent.id);
6871
+ if (!isCrtLiveAgent(currentAgent) || crtStructuredPreviewRevision(currentAgent) !== revision) return;
6872
+ crtStructuredPreviewCache.set(agent.id, {
6873
+ revision,
6874
+ loading: false,
6875
+ ready: true,
6876
+ preview: cached && cached.preview || null,
6877
+ error: error && error.message ? error.message : 'Conversation preview unavailable',
6878
+ });
6879
+ if (isCrtSessionOpen()) dashboardRenderDeferred = true;
6880
+ else updateCrtAgentPreviewCard(currentAgent);
6881
+ }
6882
+ }
6883
+
5795
6884
  function renderStructuredTranscript(transcript, force = false) {
5796
6885
  const container = document.getElementById('terminal-output');
5797
6886
  if (!container) return;
5798
6887
  const updatedAt = String(transcript && transcript.updatedAt || '');
5799
6888
  if (!force && updatedAt && updatedAt === structuredSessionRenderedAt) return;
5800
6889
  const nearBottom = container.scrollHeight - container.scrollTop - container.clientHeight < 80;
6890
+ if (!nearBottom && focusedAgentId) saveCrtStructuredReadingAnchor(focusedAgentId);
5801
6891
  const transcriptNode = document.createElement('div');
5802
6892
  transcriptNode.className = 'crt-structured-transcript';
5803
6893
  const turns = structuredTranscriptTurns(transcript);
@@ -5811,6 +6901,7 @@ function renderStructuredTranscript(transcript, force = false) {
5811
6901
  turns.forEach((turn) => {
5812
6902
  const section = document.createElement('section');
5813
6903
  section.className = 'crt-structured-turn';
6904
+ section.dataset.readingAnchorId = String(turn.id || `${turn.userMessage || ''}\n${turn.finalMessage || ''}`.slice(0, 160));
5814
6905
  if (turn.userMessage) {
5815
6906
  const user = document.createElement('p');
5816
6907
  user.className = 'crt-structured-message user';
@@ -5829,7 +6920,10 @@ function renderStructuredTranscript(transcript, force = false) {
5829
6920
 
5830
6921
  container.replaceChildren(transcriptNode);
5831
6922
  structuredSessionRenderedAt = updatedAt;
5832
- if (force || nearBottom) container.scrollTop = container.scrollHeight;
6923
+ if (!focusedAgentId || !restoreCrtStructuredReadingAnchor(focusedAgentId)) {
6924
+ if (force || nearBottom) container.scrollTop = container.scrollHeight;
6925
+ }
6926
+ container.onscroll = () => saveCrtStructuredReadingAnchor(focusedAgentId);
5833
6927
  requestAnimationFrame(updateStructuredTranscriptScrollState);
5834
6928
  }
5835
6929
 
@@ -6104,8 +7198,8 @@ async function openStructuredSession(agent, sessionToken) {
6104
7198
  function teardownSessionSurface() {
6105
7199
  stopSessionViewPolling();
6106
7200
  stopStructuredSessionPolling();
7201
+ disposeCrtTerminalReplication();
6107
7202
  disposeTerminal();
6108
- destroyTerminalInputBridge();
6109
7203
  setStructuredComposerActive(false);
6110
7204
  document.getElementById('terminal-output')?.classList.remove('crt-structured-session');
6111
7205
  if (SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.resetTerminalShell) {
@@ -6126,6 +7220,7 @@ async function openSession(agentId) {
6126
7220
 
6127
7221
  const agent = state.agents.find(a => a.id === agentId);
6128
7222
  if (!agent) return;
7223
+ crtNavigationKey = `agent:${agentId}`;
6129
7224
  void markCrtAgentReadIfNeeded(agent);
6130
7225
 
6131
7226
  const sessionModal = document.getElementById('session-modal');
@@ -6207,10 +7302,13 @@ async function openSession(agentId) {
6207
7302
  }
6208
7303
 
6209
7304
  disposeTerminal();
7305
+ disposeCrtTerminalReplication();
7306
+ crtTerminalReplication = createCrtTerminalReplication(agentId);
6210
7307
  let mountedTerminal = null;
6211
7308
  try {
6212
7309
  mountedTerminal = SESSION_MODAL_BRIDGE && SESSION_MODAL_BRIDGE.mountTerminal
6213
7310
  ? SESSION_MODAL_BRIDGE.mountTerminal(document, terminalBundle, {
7311
+ authoritativeGeometry: true,
6214
7312
  initialOutput: shouldUseLiveSessionText(agent)
6215
7313
  ? (runtime ? runtime.prepareInitialOutput(agent.output) : agent.output)
6216
7314
  : '',
@@ -6220,11 +7318,10 @@ async function openSession(agentId) {
6220
7318
  sendTerminalInput(data);
6221
7319
  },
6222
7320
  onResize: (cols, rows) => {
7321
+ void cols;
7322
+ void rows;
6223
7323
  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);
7324
+ sendSessionResize(agentId);
6228
7325
  },
6229
7326
  hasSelection: hasAnySelection,
6230
7327
  focusTerminal: focusSessionTerminal,
@@ -6265,12 +7362,13 @@ async function openSession(agentId) {
6265
7362
  updateSessionSelectionStatus();
6266
7363
  });
6267
7364
  }
7365
+ if (terminal && typeof terminal.onScroll === 'function') {
7366
+ terminal.onScroll(() => saveCrtTerminalReadingAnchor(agentId, terminal));
7367
+ }
6268
7368
  if (runtime) {
6269
7369
  runtime.setLastOutputLength(mountedTerminal ? mountedTerminal.outputLength : (runtime.prepareInitialOutput(agent.output)).length);
6270
7370
  syncSessionRuntimeState();
6271
7371
  }
6272
- setupTerminalInputBridge();
6273
-
6274
7372
  if (!mountedTerminal) {
6275
7373
  terminal.loadAddon(fitAddon);
6276
7374
  terminal.onData((data) => {
@@ -6279,11 +7377,11 @@ async function openSession(agentId) {
6279
7377
  sendTerminalInput(data);
6280
7378
  });
6281
7379
  terminal.onResize(({ cols, rows }) => {
7380
+ void cols;
7381
+ void rows;
6282
7382
  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);
7383
+ if (crtTerminalReplication?.applyingLocalResize) return;
7384
+ sendSessionResize(agentId, { cols, rows });
6287
7385
  });
6288
7386
 
6289
7387
  terminalContainer.innerHTML = '';
@@ -6312,7 +7410,6 @@ async function openSession(agentId) {
6312
7410
  if (!terminal || !fitAddon) return;
6313
7411
  if (runtime && !runtime.isCurrentSession(agentId, sessionToken)) return;
6314
7412
  if (!runtime && focusedAgentId !== agentId) return;
6315
- fitAddon.fit();
6316
7413
  const initialOutput = runtime ? runtime.prepareInitialOutput(agent.output) : agent.output;
6317
7414
  if (initialOutput) {
6318
7415
  terminal.write(initialOutput);
@@ -6336,13 +7433,20 @@ async function openSession(agentId) {
6336
7433
  showCrtWebglFailure(error);
6337
7434
  }
6338
7435
 
7436
+ // Hydrate one authoritative reducer cut before reducing live transitions.
6339
7437
  await refreshSessionView(true, agentId, sessionToken);
6340
- if (shouldPollSessionView(modalState.sessionSource)) {
7438
+ restoreCrtTerminalReadingAnchor(agentId, terminal);
7439
+ sendSessionResize(agentId);
7440
+ if (!crtTerminalReplication && shouldPollSessionView(modalState.sessionSource)) {
6341
7441
  startSessionViewPolling(agentId, sessionToken);
6342
7442
  }
6343
7443
  }
6344
7444
 
6345
7445
  function closeSession() {
7446
+ if (focusedAgentId) {
7447
+ if (terminal) saveCrtTerminalReadingAnchor(focusedAgentId, terminal);
7448
+ else saveCrtStructuredReadingAnchor(focusedAgentId);
7449
+ }
6346
7450
  resetCrtRuntimeSwitchState();
6347
7451
  const runtime = getSessionRuntime();
6348
7452
  if (runtime) {
@@ -6379,14 +7483,19 @@ function closeSession() {
6379
7483
  if (crtMainView === 'history') renderCrtHistory();
6380
7484
  generateKeyMap();
6381
7485
  }
7486
+ restoreCrtNavigationSelection();
6382
7487
  }
6383
7488
 
6384
7489
  function killCurrentAgent() {
6385
7490
  if (!focusedAgentId) return;
6386
7491
 
7492
+ const killedAgentId = focusedAgentId;
7493
+ const fallbackAgentId = getCrtAgentRemovalFallback(state, killedAgentId);
7494
+ crtNavigationKey = fallbackAgentId ? `agent:${fallbackAgentId}` : '';
7495
+
6387
7496
  const sessionClient = getSessionClient();
6388
7497
  if (sessionClient) {
6389
- sessionClient.killAgent(focusedAgentId);
7498
+ sessionClient.killAgent(killedAgentId);
6390
7499
  }
6391
7500
 
6392
7501
  closeSession();
@@ -6394,97 +7503,178 @@ function killCurrentAgent() {
6394
7503
 
6395
7504
  function sendTerminalInput(input) {
6396
7505
  if (!focusedAgentId) return;
7506
+ queueCrtTerminalInput(input);
7507
+ }
6397
7508
 
6398
- const sessionClient = getSessionClient();
6399
- if (sessionClient) {
6400
- sessionClient.sendInput(focusedAgentId, input);
7509
+ function clearPendingCrtTerminalFitResize(replication) {
7510
+ if (!replication) return;
7511
+ if (replication.fitResizeTimer) clearTimeout(replication.fitResizeTimer);
7512
+ replication.fitResizeTimer = null;
7513
+ replication.pendingFitResize = null;
7514
+ }
7515
+
7516
+ function commitCrtTerminalResize(agentId, normalizedDimensions) {
7517
+ const replication = crtTerminalReplication;
7518
+ if (
7519
+ !agentId ||
7520
+ !terminal ||
7521
+ !replication ||
7522
+ replication.disposed ||
7523
+ replication.agentId !== agentId ||
7524
+ replication.replayState.recovering ||
7525
+ (
7526
+ normalizedDimensions.cols === replication.lastResizeCols &&
7527
+ normalizedDimensions.rows === replication.lastResizeRows
7528
+ )
7529
+ ) return;
7530
+ if (terminal.cols !== normalizedDimensions.cols || terminal.rows !== normalizedDimensions.rows) {
7531
+ replication.applyingLocalResize = true;
7532
+ try {
7533
+ terminal.resize(normalizedDimensions.cols, normalizedDimensions.rows);
7534
+ } finally {
7535
+ replication.applyingLocalResize = false;
7536
+ }
7537
+ }
7538
+ const delivered = getSessionClient()?.resizeAgent(
7539
+ agentId,
7540
+ normalizedDimensions.cols,
7541
+ normalizedDimensions.rows
7542
+ );
7543
+ if (delivered) {
7544
+ replication.lastResizeCols = normalizedDimensions.cols;
7545
+ replication.lastResizeRows = normalizedDimensions.rows;
6401
7546
  }
6402
7547
  }
6403
7548
 
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);
7549
+ function scheduleCrtTerminalFitResize(replication, agentId, normalizedDimensions) {
7550
+ if (replication.fitResizeTimer) clearTimeout(replication.fitResizeTimer);
7551
+ replication.pendingFitResize = normalizedDimensions;
7552
+ replication.fitResizeTimer = setTimeout(() => {
7553
+ replication.fitResizeTimer = null;
7554
+ const next = replication.pendingFitResize;
7555
+ replication.pendingFitResize = null;
7556
+ if (
7557
+ !next ||
7558
+ replication.disposed ||
7559
+ crtTerminalReplication !== replication ||
7560
+ replication.agentId !== agentId
7561
+ ) return;
7562
+ commitCrtTerminalResize(agentId, next);
7563
+ }, CRT_TERMINAL_RESIZE_SETTLE_MS);
7564
+ }
7565
+
7566
+ function sendSessionResize(agentId = focusedAgentId, requestedDimensions = null) {
7567
+ const replication = crtTerminalReplication;
7568
+ if (
7569
+ !agentId ||
7570
+ !terminal ||
7571
+ !fitAddon ||
7572
+ !replication ||
7573
+ replication.agentId !== agentId ||
7574
+ replication.replayState.recovering
7575
+ ) return;
7576
+ const dimensions = requestedDimensions || (
7577
+ typeof fitAddon.proposeDimensions === 'function'
7578
+ ? fitAddon.proposeDimensions()
7579
+ : null
7580
+ );
7581
+ const normalizedDimensions = dimensions && {
7582
+ cols: Math.floor(Number(dimensions.cols)),
7583
+ rows: Math.floor(Number(dimensions.rows))
7584
+ };
7585
+ if (
7586
+ !normalizedDimensions ||
7587
+ !Number.isFinite(normalizedDimensions.cols) ||
7588
+ !Number.isFinite(normalizedDimensions.rows) ||
7589
+ normalizedDimensions.cols < CRT_TERMINAL_MIN_COLS ||
7590
+ normalizedDimensions.rows < CRT_TERMINAL_MIN_ROWS
7591
+ ) return;
7592
+ if (!requestedDimensions) {
7593
+ if (
7594
+ normalizedDimensions.cols === replication.lastResizeCols &&
7595
+ normalizedDimensions.rows === replication.lastResizeRows
7596
+ ) {
7597
+ clearPendingCrtTerminalFitResize(replication);
7598
+ return;
7599
+ }
7600
+ scheduleCrtTerminalFitResize(replication, agentId, normalizedDimensions);
7601
+ return;
7602
+ }
7603
+ clearPendingCrtTerminalFitResize(replication);
7604
+ commitCrtTerminalResize(agentId, normalizedDimensions);
6410
7605
  }
6411
7606
 
6412
- async function refreshSessionView(forceReplace = false, expectedAgentId = focusedAgentId, expectedSessionToken = getCurrentSessionToken()) {
7607
+ async function refreshSessionView(_forceReplace = false, expectedAgentId = focusedAgentId, expectedSessionToken = getCurrentSessionToken()) {
6413
7608
  if (!expectedAgentId || !terminal) return;
6414
7609
 
6415
7610
  const runtime = getSessionRuntime();
7611
+ const replication = crtTerminalReplication;
7612
+ if (
7613
+ !replication ||
7614
+ replication.agentId !== expectedAgentId ||
7615
+ replication.replayState.halted ||
7616
+ replication.checkpointRetryTimer ||
7617
+ replication.checkpointInFlight
7618
+ ) return;
7619
+
7620
+ const checkpointSeq = replication.checkpointSeq + 1;
7621
+ replication.checkpointSeq = checkpointSeq;
7622
+ replication.checkpointInFlight = true;
7623
+ TERMINAL_REPLAY.beginRecovery(replication.replayState);
7624
+ const controller = new globalThis.AbortController();
7625
+ replication.checkpointAbortController = controller;
7626
+ const timeout = setTimeout(
7627
+ () => controller.abort(),
7628
+ CRT_TERMINAL_CHECKPOINT_REQUEST_TIMEOUT_MS
7629
+ );
7630
+
6416
7631
  try {
6417
7632
  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
- }
7633
+ if (!sessionClient) throw new Error('Terminal session client is unavailable');
7634
+ const payload = await sessionClient.getSessionView(expectedAgentId, {
7635
+ signal: controller.signal
7636
+ });
7637
+ if (
7638
+ !crtTerminalReplication ||
7639
+ crtTerminalReplication !== replication ||
7640
+ replication.checkpointSeq !== checkpointSeq
7641
+ ) return;
7642
+ if (runtime && !runtime.isCurrentSession(expectedAgentId, expectedSessionToken)) return;
7643
+
6437
7644
  const currentAgent = state && state.agents
6438
7645
  ? state.agents.find((agent) => agent.id === expectedAgentId)
6439
7646
  : 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),
7647
+ installCrtTerminalCheckpoint(normalizeSessionViewPayload(payload, currentAgent));
7648
+ } catch (error) {
7649
+ if (
7650
+ crtTerminalReplication === replication &&
7651
+ replication.checkpointSeq === checkpointSeq
7652
+ ) {
7653
+ retryCrtTerminalReplayAfterFailure(
7654
+ replication,
7655
+ TERMINAL_REPLAY.recordTransportFailure(replication.replayState),
7656
+ error
6448
7657
  );
6449
- refreshSessionTerminalUi({ preserveSearchIndex: true });
6450
- if (runtime) {
6451
- runtime.markHydrated(patch.nextLength);
6452
- syncSessionRuntimeState();
6453
- }
6454
- return;
6455
7658
  }
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();
7659
+ } finally {
7660
+ clearTimeout(timeout);
7661
+ if (
7662
+ crtTerminalReplication === replication &&
7663
+ replication.checkpointSeq === checkpointSeq
7664
+ ) {
7665
+ replication.checkpointInFlight = false;
7666
+ replication.checkpointAbortController = null;
7667
+ finishCrtTerminalReplay(replication);
6474
7668
  }
6475
7669
  }
6476
7670
  }
6477
-
6478
7671
  function startSessionViewPolling(agentId = focusedAgentId, sessionToken = getCurrentSessionToken()) {
6479
7672
  const runtime = getSessionRuntime();
6480
7673
  if (runtime) {
6481
7674
  runtime.startPolling({ agentId, sessionToken });
6482
- return;
7675
+ return null;
6483
7676
  }
6484
- stopSessionViewPolling();
6485
- legacySessionPoller = setInterval(() => {
6486
- refreshSessionView(false, agentId, sessionToken);
6487
- }, 350);
7677
+ return null;
6488
7678
  }
6489
7679
 
6490
7680
  function stopSessionViewPolling() {
@@ -6493,10 +7683,6 @@ function stopSessionViewPolling() {
6493
7683
  runtime.stopPolling();
6494
7684
  return;
6495
7685
  }
6496
- if (legacySessionPoller) {
6497
- clearInterval(legacySessionPoller);
6498
- legacySessionPoller = null;
6499
- }
6500
7686
  }
6501
7687
 
6502
7688
  if (typeof document !== 'undefined') {
@@ -6516,8 +7702,6 @@ if (typeof document !== 'undefined') {
6516
7702
  }
6517
7703
  if (!terminal || !fitAddon || !focusedAgentId) return;
6518
7704
 
6519
- fitAddon.fit();
6520
- syncTerminalInputBridgePosition();
6521
7705
  sendSessionResize();
6522
7706
  });
6523
7707
 
@@ -6542,9 +7726,12 @@ if (typeof document !== 'undefined') {
6542
7726
  const searchInputFocused = crtMainView === 'search'
6543
7727
  && document.activeElement === document.getElementById('crt-search-input');
6544
7728
  const navigationArrow = ['ArrowUp', 'ArrowDown', 'ArrowLeft', 'ArrowRight'].includes(e.key);
7729
+ const billingHourCellFocused = crtMainView === 'billing'
7730
+ && document.activeElement?.classList?.contains('billing-day-hour-cell');
6545
7731
 
6546
7732
  if (terminalFontSizeInputFocused && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
6547
7733
  if (searchInputFocused) return;
7734
+ if (billingHourCellFocused && (e.key === 'ArrowLeft' || e.key === 'ArrowRight')) return;
6548
7735
 
6549
7736
  if (
6550
7737
  crtMainView === 'billing'
@@ -6650,6 +7837,13 @@ if (typeof document !== 'undefined') {
6650
7837
  }
6651
7838
 
6652
7839
  if (dialogActive) {
7840
+ if (workspaceDirectoryPrompt) {
7841
+ if (e.key === 'Escape' && workspaceDirectoryPrompt.kind !== 'creating') {
7842
+ hideWorkspaceDirectoryPrompt();
7843
+ e.preventDefault();
7844
+ }
7845
+ return;
7846
+ }
6653
7847
  if (workspaceInputVisible) {
6654
7848
  if (workspaceInputFocused) {
6655
7849
  return;
@@ -6771,14 +7965,28 @@ if (typeof document !== 'undefined') {
6771
7965
  const structuredInputFocused = structuredInput && document.activeElement === structuredInput;
6772
7966
  const structuredSessionActive = document.getElementById('crt-structured-composer')?.classList.contains('active');
6773
7967
  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();
7968
+ const structuredToolFocused = Boolean(document.activeElement?.closest?.('.crt-structured-tool'));
7969
+ const structuredMenuItemFocused = Boolean(document.activeElement?.closest?.('.crt-structured-menu-item'));
7970
+ const focusedAgent = state && state.agents.find((candidate) => candidate.id === focusedAgentId);
7971
+ const structuredInterruptFocused = Boolean(
7972
+ structuredInputFocused
7973
+ && structuredComposerAction(focusedAgent, structuredInput.value) === 'interrupt'
7974
+ );
7975
+ const sessionCommand = resolveCrtSessionKeyboardCommand(e, {
7976
+ structuredSessionActive,
7977
+ composing: e.isComposing,
7978
+ structuredInputFocused,
7979
+ structuredTranscriptFocused,
7980
+ structuredToolFocused,
7981
+ structuredMenuItemFocused,
7982
+ structuredInterruptFocused,
7983
+ structuredComposerMenuOpen: Boolean(structuredComposerMenu),
7984
+ });
7985
+ if (sessionCommand) {
6781
7986
  e.preventDefault();
7987
+ e.stopPropagation();
7988
+ if (sessionCommand === 'kill') killCurrentAgent();
7989
+ else closeSession();
6782
7990
  return;
6783
7991
  }
6784
7992
  if (structuredInputFocused) {
@@ -6806,48 +8014,6 @@ if (typeof document !== 'undefined') {
6806
8014
  if (isBrowserShortcut(e)) {
6807
8015
  return;
6808
8016
  }
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
8017
  return;
6852
8018
  }
6853
8019
 
@@ -6912,6 +8078,12 @@ if (typeof document !== 'undefined') {
6912
8078
  return;
6913
8079
  }
6914
8080
 
8081
+ // xterm owns paste events targeting its textarea. Calling terminal.paste()
8082
+ // here as well would submit the same clipboard text twice.
8083
+ if (isCrtNativeTerminalPasteTarget(e.target)) {
8084
+ return;
8085
+ }
8086
+
6915
8087
  const pastedText = e.clipboardData && e.clipboardData.getData
6916
8088
  ? e.clipboardData.getData('text/plain')
6917
8089
  : '';
@@ -6921,7 +8093,9 @@ if (typeof document !== 'undefined') {
6921
8093
  }
6922
8094
 
6923
8095
  e.preventDefault();
6924
- pasteTerminalText(pastedText);
8096
+ e.stopPropagation();
8097
+ e.stopImmediatePropagation();
8098
+ void pasteTerminalText(pastedText);
6925
8099
  }, true);
6926
8100
 
6927
8101
  }
@@ -6930,6 +8104,7 @@ if (typeof module !== 'undefined' && module.exports) {
6930
8104
  module.exports = {
6931
8105
  buildCrtHistoryItems,
6932
8106
  buildCrtSearchResults,
8107
+ buildCrtStructuredPreview,
6933
8108
  calculateCrtAgentPageLayout,
6934
8109
  calculateCrtHistoryPageSize,
6935
8110
  crtHistoryAgentName,
@@ -6941,8 +8116,8 @@ if (typeof module !== 'undefined' && module.exports) {
6941
8116
  findDirectionalNavigationIndex,
6942
8117
  isBrowserShortcut,
6943
8118
  isCopyShortcut,
8119
+ isCrtNativeTerminalPasteTarget,
6944
8120
  isPasteShortcut,
6945
- getTerminalSequenceForKey,
6946
8121
  shouldUseLiveSessionText,
6947
8122
  shouldPollSessionView,
6948
8123
  deriveSessionTextPatch,
@@ -6956,19 +8131,26 @@ if (typeof module !== 'undefined' && module.exports) {
6956
8131
  getCrtHistoryPage,
6957
8132
  getCrtAgentPage,
6958
8133
  getCrtAgentVerticalPageTarget,
8134
+ getCrtLiveAgents,
8135
+ getCrtRegularAgents,
8136
+ getCrtAgentRemovalFallback,
6959
8137
  getAgentDisplayText,
6960
8138
  getCrtPreviewCellStyle,
8139
+ getCrtTerminalSnapshotRows,
6961
8140
  getCrtAgentTitle,
6962
8141
  getCrtProjectName,
6963
- calculateTerminalInputBridgePosition,
6964
8142
  getCrtTerminalFontSize,
6965
8143
  isCrtAgentWorking,
8144
+ isCrtLiveAgent,
6966
8145
  getCrtAgentReadPatch,
6967
8146
  crtRuntimeView,
6968
8147
  canSwitchCrtAgentRuntime,
6969
8148
  isCrtRuntimeSwitchShortcut,
8149
+ hasCrtStructuredLocalEscapeAction,
8150
+ resolveCrtSessionKeyboardCommand,
6970
8151
  structuredComposerAction,
6971
8152
  structuredTranscriptTurns,
8153
+ crtStructuredPreviewMessageLines,
6972
8154
  getCrtBrandPaneKey,
6973
8155
  extractSessionLinks,
6974
8156
  formatSelectionStatus,
@@ -6984,6 +8166,7 @@ if (typeof module !== 'undefined' && module.exports) {
6984
8166
  getSessionModalDomState
6985
8167
  };
6986
8168
  } else {
8169
+ installCrtTerminalTestApi();
6987
8170
  setupWorkspaceHistoryControls();
6988
8171
  setupCrtSearchControls();
6989
8172
  document.addEventListener('visibilitychange', () => {