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.
- package/README.md +36 -23
- package/README.zh_cn.md +36 -23
- package/THIRD_PARTY_NOTICES.md +11 -2
- package/backend/acp-checkpoint-store.js +198 -0
- package/backend/acp-runtime.js +306 -83
- package/backend/acp-session-state.js +202 -6
- package/backend/acp-transcript.js +112 -0
- package/backend/agent-activity.js +6 -157
- package/backend/agent-manager.js +1482 -730
- package/backend/agent-provider-session.js +17 -242
- package/backend/agent-runtime-binding.js +219 -0
- package/backend/agent-session-history.js +66 -1
- package/backend/auth.js +79 -6
- package/backend/codex-models.js +81 -84
- package/backend/codex-session-archive.js +45 -0
- package/backend/codex-terminal-profile.js +500 -0
- package/backend/codex-transcript-sanitizer.js +12 -0
- package/backend/codex-transcript.js +230 -8
- package/backend/config-manager.js +30 -0
- package/backend/control-api.js +192 -17
- package/backend/farming-net-pass.js +285 -0
- package/backend/farming-net-registry.js +112 -0
- package/backend/farming-net-server.js +298 -0
- package/backend/farming-session-store.js +5 -13
- package/backend/git-worktree-info.js +181 -0
- package/backend/local-session-engine.js +411 -186
- package/backend/main-page-session.js +5 -2
- package/backend/native-pty-controller-generation.js +106 -0
- package/backend/native-pty-host-client.js +275 -7
- package/backend/native-pty-host-identity.js +86 -0
- package/backend/native-pty-host.js +813 -114
- package/backend/native-session-engine.js +100 -28
- package/backend/packaged-node-pty.js +22 -2
- package/backend/provider-adapters.js +253 -0
- package/backend/provider-session-service.js +241 -0
- package/backend/runtime-observation.js +81 -0
- package/backend/server.js +294 -86
- package/backend/session-engine-bridge.js +21 -2
- package/backend/session-engine-router.js +1 -1
- package/backend/session-engine.js +1 -1
- package/backend/session-stream-protocol.js +185 -0
- package/backend/storage-layout.js +55 -0
- package/backend/terminal-attach-checkpoint.js +74 -0
- package/backend/terminal-exit-quiescence.js +39 -0
- package/backend/terminal-reducer-flow-control.js +97 -0
- package/backend/terminal-screen-state.js +11 -2
- package/backend/terminal-screen-worker-pool.js +59 -6
- package/backend/terminal-screen-worker-thread.js +97 -57
- package/backend/terminal-screen-worker.js +133 -51
- package/backend/terminal-state-serialization.js +127 -0
- package/backend/terminal-status.js +23 -4
- package/backend/usage-monitor.js +81 -4
- package/backend/workspace-directory.js +152 -0
- package/backend/workspace-file-router.js +182 -76
- package/backend/workspace-file-service.js +319 -4
- package/backend/workspace-root-registry.js +164 -0
- package/dist/assets/App-CHYpgECZ.js +208 -0
- package/dist/assets/{FileEditorMarkdownPreview-elKWc8Im.js → FileEditorMarkdownPreview--Cri80nQ.js} +92 -92
- package/dist/assets/FileEditorPane-DGJlEKdf.js +2 -0
- package/dist/assets/IconGlyphs-Cc14sepw.js +1 -0
- package/dist/assets/ProjectFilesSection-Cz1wG4NT.js +12 -0
- package/dist/assets/{ReviewPage-BaXu1ZdX.js → ReviewPage-LonRc6Fz.js} +1 -1
- package/dist/assets/code-dark-B1pAtO_P.css +1 -0
- package/dist/assets/file-icons-Bw2qd5iT.js +1 -0
- package/dist/assets/{index-BrbljRqn.js → index-miO2r9IZ.js} +3 -3
- package/dist/assets/main-DNe0jPw-.css +1 -0
- package/dist/assets/workspace-editor-model-BQol4qbA.js +1 -0
- package/dist/assets/workspace-editor-monaco-5gFbeDPF.js +1 -0
- package/dist/assets/workspace-editor-monaco-xPv8JzHH.js +4 -0
- package/dist/assets/workspace-view-state-DvYG_9PH.js +7 -0
- package/dist/assets/workspace-working-copy-D8-s_Sgh.js +1 -0
- package/dist/index.html +1 -1
- package/frontend/farming-net/app.css +625 -0
- package/frontend/farming-net/app.js +268 -0
- package/frontend/farming-net/index.html +86 -0
- package/frontend/reading-anchor.js +198 -0
- package/frontend/session-bridge.js +12 -3
- package/frontend/session-modal-bridge.js +5 -12
- package/frontend/skins/crt/app.js +1976 -793
- package/frontend/skins/crt/index.html +313 -23
- package/frontend/skins/crt/styles/billing.css +294 -223
- package/frontend/skins/crt/styles/monochrome-green.css +7 -2
- package/frontend/terminal-replay.js +372 -0
- package/package.json +10 -3
- package/shared/browser-protocol.d.ts +5 -0
- package/shared/browser-protocol.js +130 -0
- package/dist/assets/App-8dYAM6ql.js +0 -124
- package/dist/assets/FileEditorPane-RWiFD2cq.js +0 -5
- package/dist/assets/IconGlyphs-DfL0EBnj.js +0 -1
- package/dist/assets/ProjectFilesSection-Q4PDsWmM.js +0 -12
- package/dist/assets/code-dark-CDkOQAtK.css +0 -1
- package/dist/assets/file-icons-EFUGSSwf.js +0 -1
- package/dist/assets/main-D073SnW4.css +0 -1
- package/dist/assets/qoder-C9LmmOSf.svg +0 -1
- package/dist/assets/qoder-Cf9gl0Y5.svg +0 -1
- package/dist/assets/qoder-gHCinseV.svg +0 -1
- package/dist/assets/workspace-view-state-CTyDzk2D.js +0 -1
- package/dist/assets/zsh-CLpveKlF.svg +0 -1
- package/dist/assets/zsh-FxSpMPbz.svg +0 -1
- /package/dist/assets/{api-D1lyBYIQ.js → api-D8nyOEbz.js} +0 -0
- /package/dist/assets/{core-ZlAPicox.js → core-D0LFJkDt.js} +0 -0
- /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
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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 =
|
|
940
|
+
title.textContent = titleText;
|
|
1292
941
|
const message = document.createElement('span');
|
|
1293
942
|
message.textContent = error && error.message
|
|
1294
943
|
? error.message
|
|
1295
|
-
:
|
|
944
|
+
: String(error || 'Terminal unavailable');
|
|
1296
945
|
const detail = document.createElement('small');
|
|
1297
|
-
detail.textContent =
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
1469
|
-
|
|
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
|
-
|
|
1622
|
-
|
|
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
|
|
2286
|
-
|
|
2287
|
-
|
|
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
|
-
|
|
2290
|
-
|
|
2291
|
-
|
|
2292
|
-
|
|
2293
|
-
|
|
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
|
-
|
|
2297
|
-
|
|
2298
|
-
|
|
2299
|
-
return
|
|
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
|
|
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
|
|
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
|
-
|
|
3105
|
-
|
|
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
|
|
3111
|
-
|
|
3112
|
-
|
|
3113
|
-
|
|
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
|
|
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'}${
|
|
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
|
-
|
|
3158
|
-
|
|
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
|
-
? '
|
|
3163
|
-
:
|
|
3164
|
-
? '
|
|
3165
|
-
:
|
|
3166
|
-
? '24 HOURLY BINS
|
|
3167
|
-
: selectedDetail
|
|
3168
|
-
? '
|
|
3169
|
-
:
|
|
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
|
-
|
|
3226
|
-
|
|
3227
|
-
|
|
3228
|
-
|
|
3229
|
-
|
|
3230
|
-
|
|
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 =
|
|
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
|
|
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-
|
|
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-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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 (
|
|
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-
|
|
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
|
-
|
|
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
|
|
3380
|
-
const
|
|
3381
|
-
|
|
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
|
-
|
|
3394
|
-
|
|
3395
|
-
|
|
3396
|
-
|
|
3397
|
-
|
|
3398
|
-
|
|
3399
|
-
const
|
|
3400
|
-
const
|
|
3401
|
-
|
|
3402
|
-
|
|
3403
|
-
|
|
3404
|
-
const
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
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
|
-
|
|
3417
|
-
|
|
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
|
|
3427
|
-
|
|
3428
|
-
|
|
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
|
|
3441
|
-
const
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3448
|
-
|
|
3449
|
-
|
|
3450
|
-
|
|
3451
|
-
|
|
3452
|
-
|
|
3453
|
-
|
|
3454
|
-
|
|
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
|
-
|
|
3474
|
-
|
|
3475
|
-
|
|
3476
|
-
|
|
3477
|
-
|
|
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, {
|
|
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
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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
|
|
4778
|
-
const
|
|
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
|
-
|
|
4794
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
|
|
4997
|
-
if (
|
|
4998
|
-
if (
|
|
4999
|
-
if (
|
|
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.
|
|
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.
|
|
5132
|
-
if (agent.
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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.
|
|
6346
|
+
if (!agent || agent.runtimeBinding?.kind !== 'acp') {
|
|
5461
6347
|
resetStructuredSessionControls();
|
|
5462
6348
|
return;
|
|
5463
6349
|
}
|
|
5464
|
-
const revision = String(agent.
|
|
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.
|
|
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.
|
|
5720
|
-
const
|
|
5721
|
-
|
|
5722
|
-
|
|
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 (
|
|
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
|
-
|
|
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 (
|
|
6284
|
-
|
|
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
|
-
|
|
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(
|
|
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
|
-
|
|
6399
|
-
if (
|
|
6400
|
-
|
|
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
|
|
6405
|
-
if (
|
|
6406
|
-
|
|
6407
|
-
|
|
6408
|
-
|
|
6409
|
-
|
|
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(
|
|
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)
|
|
6419
|
-
|
|
6420
|
-
|
|
6421
|
-
|
|
6422
|
-
|
|
6423
|
-
|
|
6424
|
-
|
|
6425
|
-
|
|
6426
|
-
|
|
6427
|
-
|
|
6428
|
-
|
|
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
|
-
|
|
6441
|
-
|
|
6442
|
-
|
|
6443
|
-
|
|
6444
|
-
|
|
6445
|
-
|
|
6446
|
-
|
|
6447
|
-
|
|
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
|
-
|
|
6458
|
-
|
|
6459
|
-
|
|
6460
|
-
|
|
6461
|
-
|
|
6462
|
-
|
|
6463
|
-
|
|
6464
|
-
|
|
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
|
-
|
|
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
|
-
|
|
6775
|
-
|
|
6776
|
-
|
|
6777
|
-
|
|
6778
|
-
|
|
6779
|
-
|
|
6780
|
-
|
|
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
|
-
|
|
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', () => {
|