@boxes-dev/dvb 1.0.94 → 1.0.96

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.
@@ -10,7 +10,7 @@ import { DAEMON_TIMEOUT_MS, ensureDaemonRunning, requestJson, requireDaemonFeatu
10
10
  import { ensureSpritesToken } from "../auth.js";
11
11
  import { logger } from "../logger.js";
12
12
  import { createLatencyTracker } from "../latency.js";
13
- import { BROWSER_OPEN_OSC_PREFIX, createOscMonitor } from "../terminal/osc.js";
13
+ import { createInitialAttachClearFilter, createModalTtyOutputFilter, createScrollbackEraseFilter, streamExecSession, stripModalDisconnectArtifacts, } from "../terminal/sessionProtocol.js";
14
14
  import { createStatusLine } from "../ui/statusLine.js";
15
15
  import { recordCompletionSessionsFromMap } from "../completions/index.js";
16
16
  import { listDevboxes, selectDevbox } from "./boxSelect.js";
@@ -558,911 +558,8 @@ const promptRenameSession = async (current) => {
558
558
  const trimmed = answer.trim();
559
559
  return trimmed.length ? trimmed : null;
560
560
  };
561
- const HEARTBEAT_INTERVAL_MS = 4000;
562
- const INPUT_PROBE_TIMEOUT_MS = 1000;
563
- const STREAM_STALL_PROBE_MISS_THRESHOLD = 2;
564
561
  const CONNECT_OPEN_TIMEOUT_MS = 20_000;
565
- const WS_OPEN_STATE = 1;
566
- export const createScrollbackEraseFilter = () => {
567
- // Strip only scrollback-erase control sequences:
568
- // ESC [ 3 J
569
- // CSI 3 J (C1 single-byte CSI = 0x9b)
570
- let state = 0;
571
- const flushPending = (output) => {
572
- if (state === 1) {
573
- output.push(0x1b);
574
- }
575
- else if (state === 2) {
576
- output.push(0x1b, 0x5b);
577
- }
578
- else if (state === 3) {
579
- output.push(0x1b, 0x5b, 0x33);
580
- }
581
- else if (state === 10) {
582
- output.push(0x9b);
583
- }
584
- else if (state === 11) {
585
- output.push(0x9b, 0x33);
586
- }
587
- state = 0;
588
- };
589
- const pushByte = (output, byte) => {
590
- if (state === 0) {
591
- if (byte === 0x1b) {
592
- state = 1;
593
- return;
594
- }
595
- if (byte === 0x9b) {
596
- state = 10;
597
- return;
598
- }
599
- output.push(byte);
600
- return;
601
- }
602
- if (state === 1) {
603
- if (byte === 0x5b) {
604
- state = 2;
605
- return;
606
- }
607
- output.push(0x1b);
608
- state = 0;
609
- pushByte(output, byte);
610
- return;
611
- }
612
- if (state === 2) {
613
- if (byte === 0x33) {
614
- state = 3;
615
- return;
616
- }
617
- output.push(0x1b, 0x5b);
618
- state = 0;
619
- pushByte(output, byte);
620
- return;
621
- }
622
- if (state === 3) {
623
- if (byte === 0x4a) {
624
- state = 0;
625
- return;
626
- }
627
- output.push(0x1b, 0x5b, 0x33);
628
- state = 0;
629
- pushByte(output, byte);
630
- return;
631
- }
632
- if (state === 10) {
633
- if (byte === 0x33) {
634
- state = 11;
635
- return;
636
- }
637
- output.push(0x9b);
638
- state = 0;
639
- pushByte(output, byte);
640
- return;
641
- }
642
- if (state === 11) {
643
- if (byte === 0x4a) {
644
- state = 0;
645
- return;
646
- }
647
- output.push(0x9b, 0x33);
648
- state = 0;
649
- pushByte(output, byte);
650
- }
651
- };
652
- return {
653
- reset() {
654
- state = 0;
655
- },
656
- transform(chunk) {
657
- const bytes = Buffer.from(chunk ?? Buffer.alloc(0));
658
- if (bytes.length === 0) {
659
- return bytes;
660
- }
661
- const output = [];
662
- for (const byte of bytes) {
663
- pushByte(output, byte);
664
- }
665
- return output.length === 0 ? Buffer.alloc(0) : Buffer.from(output);
666
- },
667
- flush() {
668
- const output = [];
669
- flushPending(output);
670
- return output.length === 0 ? Buffer.alloc(0) : Buffer.from(output);
671
- },
672
- };
673
- };
674
- export const createInitialAttachClearFilter = () => {
675
- const patterns = [
676
- Buffer.from("\u001b[H\u001b[J", "latin1"),
677
- Buffer.from("\u001b[H\u001b[2J", "latin1"),
678
- Buffer.from("\u001b[2J\u001b[H", "latin1"),
679
- Buffer.from("\u001b[H", "latin1"),
680
- Buffer.from("\u001b[2J", "latin1"),
681
- Buffer.from("\u001b[J", "latin1"),
682
- Buffer.from("\u001b[?1049h", "latin1"),
683
- Buffer.from("\u001b[?1048h", "latin1"),
684
- Buffer.from("\u001b[?1047h", "latin1"),
685
- Buffer.from("\u001b[?47h", "latin1"),
686
- Buffer.from("\u001b[?1h", "latin1"),
687
- Buffer.from("\u001b=", "latin1"),
688
- Buffer.from("\x9bH", "latin1"),
689
- Buffer.from("\x9b2J", "latin1"),
690
- Buffer.from("\x9bJ", "latin1"),
691
- Buffer.from("\x9b?1049h", "latin1"),
692
- Buffer.from("\x9b?1048h", "latin1"),
693
- Buffer.from("\x9b?1047h", "latin1"),
694
- Buffer.from("\x9b?47h", "latin1"),
695
- Buffer.from("\x9b?1h", "latin1"),
696
- ];
697
- const maxPatternLength = patterns.reduce((max, pattern) => Math.max(max, pattern.length), 0);
698
- const STARTUP_FILTER_CHUNKS = 6;
699
- let enabled = true;
700
- let pending = Buffer.alloc(0);
701
- let chunksRemaining = STARTUP_FILTER_CHUNKS;
702
- const startsWithPatternPrefix = (bytes) => patterns.some((pattern) => {
703
- if (bytes.length > pattern.length) {
704
- return false;
705
- }
706
- return pattern.subarray(0, bytes.length).equals(bytes);
707
- });
708
- const stripLeadingPatterns = (bytes) => {
709
- let remaining = bytes;
710
- let removed = true;
711
- while (removed && remaining.length > 0) {
712
- removed = false;
713
- for (const pattern of patterns) {
714
- if (remaining.length >= pattern.length) {
715
- const candidate = remaining.subarray(0, pattern.length);
716
- if (candidate.equals(pattern)) {
717
- remaining = remaining.subarray(pattern.length);
718
- removed = true;
719
- break;
720
- }
721
- }
722
- }
723
- }
724
- return remaining;
725
- };
726
- const stripInlineArtifacts = (bytes) => {
727
- if (bytes.length === 0)
728
- return bytes;
729
- let text = bytes.toString("latin1");
730
- // Keep prompt redraw semantics without full-screen jumps by converting
731
- // cursor-home moves into CR, then dropping destructive clear sequences.
732
- text = text.replace(/\x1b\[[0-9;]*H/g, "\r");
733
- text = text.replace(/\x9b[0-9;]*H/g, "\r");
734
- text = text.replace(/\x1b\]([^\x07\x1b]*)(\x07|\x1b\\)/g, (match, payload) => payload.startsWith(BROWSER_OPEN_OSC_PREFIX) ? match : "");
735
- const artifactPatterns = [
736
- /\x01/g,
737
- /\x1b\[\?1049h/g,
738
- /\x1b\[\?1048h/g,
739
- /\x1b\[\?1047h/g,
740
- /\x1b\[\?47h/g,
741
- /\x1b\[\?1h/g,
742
- /\x1b\[\?2004h/g,
743
- /\x1b=/g,
744
- /\x9b\?1049h/g,
745
- /\x9b\?1048h/g,
746
- /\x9b\?1047h/g,
747
- /\x9b\?47h/g,
748
- /\x9b\?1h/g,
749
- /\x9b\?2004h/g,
750
- /\x1b\[[0-9;?]*J/g,
751
- /\x9b[0-9;?]*J/g,
752
- ];
753
- for (const pattern of artifactPatterns) {
754
- text = text.replace(pattern, "");
755
- }
756
- text = text.replace(/\r{2,}/g, "\r");
757
- if (/^\r+$/.test(text)) {
758
- return Buffer.alloc(0);
759
- }
760
- return Buffer.from(text, "latin1");
761
- };
762
- return {
763
- reset() {
764
- enabled = true;
765
- pending = Buffer.alloc(0);
766
- chunksRemaining = STARTUP_FILTER_CHUNKS;
767
- },
768
- transform(chunk) {
769
- const bytes = Buffer.from(chunk ?? Buffer.alloc(0));
770
- if (bytes.length === 0) {
771
- return bytes;
772
- }
773
- if (!enabled) {
774
- return bytes;
775
- }
776
- let work = pending.length > 0
777
- ? Buffer.concat([pending, bytes])
778
- : Buffer.from(bytes);
779
- pending = Buffer.alloc(0);
780
- work = stripLeadingPatterns(work);
781
- work = stripInlineArtifacts(work);
782
- chunksRemaining = Math.max(0, chunksRemaining - 1);
783
- if (work.length === 0) {
784
- if (chunksRemaining === 0) {
785
- enabled = false;
786
- }
787
- return Buffer.alloc(0);
788
- }
789
- const holdBytes = Math.min(work.length, Math.max(1, maxPatternLength - 1));
790
- if (holdBytes > 0) {
791
- const prefix = work.subarray(0, holdBytes);
792
- if (prefix.length === work.length && startsWithPatternPrefix(prefix)) {
793
- pending = Buffer.from(prefix);
794
- return Buffer.alloc(0);
795
- }
796
- }
797
- if (chunksRemaining === 0) {
798
- enabled = false;
799
- }
800
- return work;
801
- },
802
- flush() {
803
- const out = pending;
804
- pending = Buffer.alloc(0);
805
- enabled = false;
806
- chunksRemaining = 0;
807
- return out;
808
- },
809
- };
810
- };
811
- export const createModalTtyOutputFilter = () => {
812
- const initialAttachClearFilter = createInitialAttachClearFilter();
813
- const scrollbackEraseFilter = createScrollbackEraseFilter();
814
- return {
815
- reset() {
816
- initialAttachClearFilter.reset?.();
817
- scrollbackEraseFilter.reset?.();
818
- },
819
- transform(chunk) {
820
- const initialFiltered = initialAttachClearFilter.transform(chunk);
821
- if (initialFiltered.length === 0) {
822
- return Buffer.alloc(0);
823
- }
824
- return scrollbackEraseFilter.transform(initialFiltered);
825
- },
826
- flush() {
827
- const pendingInitial = initialAttachClearFilter.flush();
828
- const pendingFiltered = pendingInitial.length > 0
829
- ? scrollbackEraseFilter.transform(pendingInitial)
830
- : Buffer.alloc(0);
831
- const pendingScrollback = scrollbackEraseFilter.flush();
832
- if (pendingFiltered.length === 0) {
833
- return pendingScrollback;
834
- }
835
- if (pendingScrollback.length === 0) {
836
- return pendingFiltered;
837
- }
838
- return Buffer.concat([pendingFiltered, pendingScrollback]);
839
- },
840
- };
841
- };
842
- const MODAL_DISCONNECT_FILTER_CHUNKS = 8;
843
- export const stripModalDisconnectArtifacts = (chunk) => {
844
- if (chunk.length === 0)
845
- return chunk;
846
- let text = chunk.toString("latin1");
847
- const artifactPatterns = [
848
- /\x01/g,
849
- /\x03\x00/g,
850
- /\x1b\[\?1000l/g,
851
- /\x1b\[\?1002l/g,
852
- /\x1b\[\?1003l/g,
853
- /\x1b\[\?1004l/g,
854
- /\x1b\[\?1005l/g,
855
- /\x1b\[\?1006l/g,
856
- /\x1b\[\?2004l/g,
857
- /\x1b\[\?1049l/g,
858
- /\x1b\[\?1048l/g,
859
- /\x1b\[\?1047l/g,
860
- /\x1b\[\?47l/g,
861
- /\x1b\[\?25h/g,
862
- /\x1b\[<u/g,
863
- /\x1b\[>4;0m/g,
864
- /\x1b\[\?1l/g,
865
- /\x1b>/g,
866
- ];
867
- for (const pattern of artifactPatterns) {
868
- text = text.replace(pattern, "");
869
- }
870
- return Buffer.from(text, "latin1");
871
- };
872
- const streamExecSession = async (ws, options) => new Promise((resolve, reject) => {
873
- let exitCode = null;
874
- let resolved = false;
875
- let opened = false;
876
- let closeCode;
877
- let pendingResize = null;
878
- let lastResize = null;
879
- const pendingInput = [];
880
- let lastSeenAt = Date.now();
881
- let lastStreamActivityAt = Date.now();
882
- let heartbeatTimer = null;
883
- let inputProbeTimer = null;
884
- let openTimer = null;
885
- let inputProbeStamp = 0;
886
- let inputProbeStreamStamp = 0;
887
- let streamProbeMisses = 0;
888
- let disconnectNoted = false;
889
- let errorMessage;
890
- const stdin = process.stdin;
891
- const stdout = process.stdout;
892
- const stderr = process.stderr;
893
- const latency = options.latency;
894
- const oscMonitor = createOscMonitor();
895
- const ttyOutputFilter = options.ttyOutputFilter;
896
- let disconnectFilterChunksRemaining = 0;
897
- const armDisconnectFilter = () => {
898
- if (!options.stripDisconnectArtifacts)
899
- return;
900
- disconnectFilterChunksRemaining = Math.max(disconnectFilterChunksRemaining, MODAL_DISCONNECT_FILTER_CHUNKS);
901
- };
902
- const applyDisconnectFilter = (chunk) => {
903
- if (!options.stripDisconnectArtifacts)
904
- return chunk;
905
- if (disconnectFilterChunksRemaining <= 0)
906
- return chunk;
907
- disconnectFilterChunksRemaining -= 1;
908
- return stripModalDisconnectArtifacts(chunk);
909
- };
910
- const initialInput = typeof options.initialInput === "string"
911
- ? Buffer.from(options.initialInput, "utf8")
912
- : options.initialInput;
913
- const supportsResizeControlMessages = options.supportsResizeControlMessages ?? true;
914
- if (options.stdin && initialInput && initialInput.length > 0) {
915
- pendingInput.push(initialInput);
916
- }
917
- const markAlive = () => {
918
- lastSeenAt = Date.now();
919
- };
920
- const markStreamAlive = () => {
921
- const now = Date.now();
922
- lastSeenAt = now;
923
- lastStreamActivityAt = now;
924
- streamProbeMisses = 0;
925
- };
926
- const noteDisconnect = () => {
927
- options.onDisconnectNotice?.();
928
- };
929
- const recordDisconnect = (kind) => {
930
- if (disconnectNoted)
931
- return;
932
- disconnectNoted = true;
933
- const details = { kind, opened };
934
- if (typeof closeCode === "number") {
935
- details.closeCode = closeCode;
936
- }
937
- latency?.noteDisconnect(details);
938
- };
939
- const finish = (code) => {
940
- if (resolved)
941
- return;
942
- resolved = true;
943
- cleanup();
944
- resolve({ kind: "exit", code });
945
- };
946
- const finishDetach = () => {
947
- if (resolved)
948
- return;
949
- resolved = true;
950
- cleanup();
951
- try {
952
- ws.close();
953
- }
954
- catch {
955
- // ignore close failures
956
- }
957
- resolve({ kind: "detach" });
958
- };
959
- const writeTtyOutput = (chunk) => {
960
- if (chunk.length === 0)
961
- return;
962
- const { output, browserUrls } = oscMonitor.write(chunk);
963
- if (output.length > 0) {
964
- stdout.write(output);
965
- }
966
- for (const url of browserUrls) {
967
- if (!openBrowser(url)) {
968
- stderr.write(`Failed to open browser. Visit: ${url}\r\n`);
969
- }
970
- }
971
- };
972
- const handleBinary = (buffer) => {
973
- if (buffer.length === 0)
974
- return;
975
- if (options.tty) {
976
- const rawBuffer = Buffer.from(buffer);
977
- options.onOutputChunk?.(rawBuffer);
978
- options.onOutput?.();
979
- const filteredBuffer = ttyOutputFilter
980
- ? ttyOutputFilter.transform(rawBuffer)
981
- : rawBuffer;
982
- writeTtyOutput(applyDisconnectFilter(filteredBuffer));
983
- return;
984
- }
985
- const streamId = buffer[0];
986
- const payload = buffer.subarray(1);
987
- if (streamId === 1) {
988
- if (payload.length > 0) {
989
- options.onOutputChunk?.(payload);
990
- }
991
- if (payload.length > 0)
992
- options.onOutput?.();
993
- stdout.write(Buffer.from(payload));
994
- }
995
- if (streamId === 2) {
996
- if (payload.length > 0) {
997
- options.onOutputChunk?.(payload);
998
- }
999
- if (payload.length > 0)
1000
- options.onOutput?.();
1001
- stderr.write(Buffer.from(payload));
1002
- }
1003
- if (streamId === 3 && payload.length > 0) {
1004
- armDisconnectFilter();
1005
- exitCode = payload[0] ?? 0;
1006
- ws.close();
1007
- }
1008
- };
1009
- const handleText = (data) => {
1010
- try {
1011
- const payload = JSON.parse(data);
1012
- if (payload.type === "session_info") {
1013
- const rawId = payload.session_id ?? payload.id ?? payload.sessionId;
1014
- if (rawId !== undefined) {
1015
- options.onSessionInfo?.(String(rawId));
1016
- }
1017
- return;
1018
- }
1019
- if (payload.type === "exit" && typeof payload.exit_code === "number") {
1020
- exitCode = payload.exit_code;
1021
- ws.close();
1022
- return;
1023
- }
1024
- if (payload.type === "port_opened" || payload.type === "port_closed") {
1025
- const port = Number(payload.port);
1026
- if (Number.isFinite(port)) {
1027
- const event = { type: payload.type, port };
1028
- if (typeof payload.pid === "number")
1029
- event.pid = payload.pid;
1030
- if (typeof payload.address === "string") {
1031
- event.address = payload.address;
1032
- }
1033
- options.onPortEvent?.(event);
1034
- }
1035
- return;
1036
- }
1037
- if (payload.type === "tty_replay_start") {
1038
- options.onReplayStart?.();
1039
- return;
1040
- }
1041
- if (payload.type === "tty_replay_done") {
1042
- if (options.tty && ttyOutputFilter) {
1043
- writeTtyOutput(applyDisconnectFilter(ttyOutputFilter.flush()));
1044
- }
1045
- options.onReplayDone?.();
1046
- return;
1047
- }
1048
- }
1049
- catch {
1050
- // fall through
1051
- }
1052
- if (data.length > 0) {
1053
- options.onOutputChunk?.(Buffer.from(data, "utf8"));
1054
- }
1055
- options.onOutput?.();
1056
- if (options.tty) {
1057
- if (data.includes("logout")) {
1058
- armDisconnectFilter();
1059
- }
1060
- const dataBuffer = Buffer.from(data, "utf8");
1061
- const filtered = ttyOutputFilter
1062
- ? ttyOutputFilter.transform(dataBuffer)
1063
- : dataBuffer;
1064
- writeTtyOutput(applyDisconnectFilter(filtered));
1065
- }
1066
- else {
1067
- stdout.write(data);
1068
- }
1069
- };
1070
- const onMessage = (event) => {
1071
- markStreamAlive();
1072
- const data = event.data;
1073
- if (typeof data === "string") {
1074
- handleText(data);
1075
- return;
1076
- }
1077
- if (data instanceof ArrayBuffer) {
1078
- handleBinary(new Uint8Array(data));
1079
- return;
1080
- }
1081
- if (data instanceof Uint8Array) {
1082
- handleBinary(data);
1083
- return;
1084
- }
1085
- if (typeof Blob !== "undefined" && data instanceof Blob) {
1086
- data
1087
- .arrayBuffer()
1088
- .then((buffer) => handleBinary(new Uint8Array(buffer)))
1089
- .catch(() => {
1090
- // ignore blob decode failure
1091
- });
1092
- }
1093
- };
1094
- const sendResize = () => {
1095
- if (!options.tty)
1096
- return;
1097
- if (!supportsResizeControlMessages)
1098
- return;
1099
- const { cols, rows } = resolveTtySize();
1100
- if (lastResize && lastResize.cols === cols && lastResize.rows === rows) {
1101
- return;
1102
- }
1103
- if (!opened) {
1104
- pendingResize = { cols, rows };
1105
- return;
1106
- }
1107
- ws.send(JSON.stringify({ type: "resize", cols, rows }));
1108
- lastResize = { cols, rows };
1109
- };
1110
- const sendInput = (chunk) => {
1111
- if (chunk.length > 0) {
1112
- options.onInputChunk?.(chunk);
1113
- }
1114
- if (!latency) {
1115
- ws.send(chunk);
1116
- return;
1117
- }
1118
- const bufferedAmount = typeof ws.bufferedAmount === "number" ? ws.bufferedAmount : undefined;
1119
- const onSent = latency.noteSend(chunk.byteLength, bufferedAmount);
1120
- try {
1121
- ws.send(chunk, onSent);
1122
- }
1123
- catch (error) {
1124
- onSent(error instanceof Error ? error : new Error(String(error)));
1125
- throw error;
1126
- }
1127
- };
1128
- const summarizeInputChunk = (chunk) => ({
1129
- length: chunk.length,
1130
- hasCtrlC: chunk.includes(0x03),
1131
- hasCtrlD: chunk.includes(0x04),
1132
- hasCr: chunk.includes(0x0d),
1133
- hasLf: chunk.includes(0x0a),
1134
- });
1135
- const onStdin = (chunk) => {
1136
- if (options.tty && isRawCtrlCChunk(chunk)) {
1137
- const handled = options.onRawCtrlC?.() ?? false;
1138
- if (handled) {
1139
- return;
1140
- }
1141
- }
1142
- if (!opened) {
1143
- pendingInput.push(chunk);
1144
- logger.debug("connect_stdin_buffered_preopen", {
1145
- ...summarizeInputChunk(chunk),
1146
- pendingInputCount: pendingInput.length,
1147
- });
1148
- return;
1149
- }
1150
- if (options.tty && chunk.length === 1 && chunk[0] === 0x1c) {
1151
- stderr.write("\r\nDetached.\r\n");
1152
- logger.info("connect_stdin_detach_key", {
1153
- opened,
1154
- readyState: ws.readyState,
1155
- });
1156
- finishDetach();
1157
- return;
1158
- }
1159
- if (options.tty && chunk.length === 1 && chunk[0] === 0x04) {
1160
- armDisconnectFilter();
1161
- }
1162
- if (typeof ws.readyState === "number" &&
1163
- ws.readyState !== WS_OPEN_STATE) {
1164
- logger.warn("connect_stdin_socket_not_open", {
1165
- readyState: ws.readyState,
1166
- opened,
1167
- ...summarizeInputChunk(chunk),
1168
- });
1169
- noteDisconnect();
1170
- forceClose();
1171
- onError();
1172
- return;
1173
- }
1174
- const ageMs = Date.now() - lastSeenAt;
1175
- const hasRecoveryKey = chunk.includes(0x03) || // Ctrl+C
1176
- chunk.includes(0x04) || // Ctrl+D
1177
- chunk.includes(0x0d) || // Enter (CR)
1178
- chunk.includes(0x0a); // Enter (LF)
1179
- if (ageMs > HEARTBEAT_INTERVAL_MS ||
1180
- (streamProbeMisses > 0 && hasRecoveryKey)) {
1181
- startInputProbe();
1182
- }
1183
- if (hasRecoveryKey) {
1184
- logger.info("connect_stdin_recovery_key", {
1185
- ...summarizeInputChunk(chunk),
1186
- ageMs,
1187
- streamProbeMisses,
1188
- readyState: ws.readyState,
1189
- });
1190
- }
1191
- try {
1192
- sendInput(chunk);
1193
- }
1194
- catch {
1195
- logger.warn("connect_stdin_send_failed", {
1196
- ...summarizeInputChunk(chunk),
1197
- ageMs,
1198
- readyState: ws.readyState,
1199
- });
1200
- noteDisconnect();
1201
- forceClose();
1202
- onError();
1203
- }
1204
- };
1205
- const startInputProbe = () => {
1206
- if (inputProbeTimer || typeof ws.ping !== "function")
1207
- return;
1208
- inputProbeStamp = lastSeenAt;
1209
- inputProbeStreamStamp = lastStreamActivityAt;
1210
- try {
1211
- ws.ping?.();
1212
- latency?.notePingSent("input_probe");
1213
- }
1214
- catch {
1215
- // ignore ping failures
1216
- }
1217
- inputProbeTimer = setTimeout(() => {
1218
- inputProbeTimer = null;
1219
- if (resolved)
1220
- return;
1221
- if (lastSeenAt === inputProbeStamp) {
1222
- streamProbeMisses = 0;
1223
- latency?.noteInputProbeTimeout();
1224
- noteDisconnect();
1225
- forceClose();
1226
- onError();
1227
- return;
1228
- }
1229
- if (lastStreamActivityAt === inputProbeStreamStamp) {
1230
- streamProbeMisses += 1;
1231
- if (streamProbeMisses >= STREAM_STALL_PROBE_MISS_THRESHOLD) {
1232
- latency?.noteInputProbeTimeout();
1233
- noteDisconnect();
1234
- forceClose();
1235
- onError();
1236
- }
1237
- return;
1238
- }
1239
- }, INPUT_PROBE_TIMEOUT_MS);
1240
- };
1241
- const stopInputProbe = () => {
1242
- if (inputProbeTimer) {
1243
- clearTimeout(inputProbeTimer);
1244
- inputProbeTimer = null;
1245
- }
1246
- };
1247
- const stopOpenTimer = () => {
1248
- if (openTimer) {
1249
- clearTimeout(openTimer);
1250
- openTimer = null;
1251
- }
1252
- };
1253
- const onPong = () => {
1254
- markAlive();
1255
- latency?.notePong();
1256
- };
1257
- const forceClose = () => {
1258
- if (typeof ws.terminate === "function") {
1259
- ws.terminate();
1260
- return;
1261
- }
1262
- ws.close();
1263
- };
1264
- const startHeartbeat = () => {
1265
- if (typeof ws.ping !== "function")
1266
- return;
1267
- markAlive();
1268
- if (typeof ws.on === "function") {
1269
- ws.on("pong", onPong);
1270
- }
1271
- heartbeatTimer = setInterval(() => {
1272
- if (resolved)
1273
- return;
1274
- try {
1275
- ws.ping?.();
1276
- latency?.notePingSent("heartbeat");
1277
- }
1278
- catch {
1279
- // ignore ping failures
1280
- }
1281
- }, HEARTBEAT_INTERVAL_MS);
1282
- };
1283
- const stopHeartbeat = () => {
1284
- if (heartbeatTimer) {
1285
- clearInterval(heartbeatTimer);
1286
- heartbeatTimer = null;
1287
- }
1288
- if (typeof ws.off === "function") {
1289
- ws.off("pong", onPong);
1290
- }
1291
- else if (typeof ws.removeListener === "function") {
1292
- ws.removeListener("pong", onPong);
1293
- }
1294
- };
1295
- const startInput = () => {
1296
- if (!options.stdin)
1297
- return;
1298
- let rawModeEnabled = false;
1299
- if (options.tty && stdin.isTTY) {
1300
- try {
1301
- stdin.setRawMode(true);
1302
- rawModeEnabled = true;
1303
- }
1304
- catch {
1305
- logger.warn("connect_stdin_rawmode_enable_failed");
1306
- // ignore raw mode failure
1307
- }
1308
- }
1309
- logger.info("connect_stdin_start", {
1310
- tty: options.tty,
1311
- stdinIsTty: stdin.isTTY,
1312
- rawModeEnabled,
1313
- });
1314
- stdin.resume();
1315
- stdin.on("data", onStdin);
1316
- process.on("SIGWINCH", sendResize);
1317
- };
1318
- const stopInput = () => {
1319
- if (!options.stdin)
1320
- return;
1321
- stdin.removeListener("data", onStdin);
1322
- process.removeListener("SIGWINCH", sendResize);
1323
- stdin.pause();
1324
- let rawModeRestored = false;
1325
- if (options.tty && stdin.isTTY) {
1326
- try {
1327
- stdin.setRawMode(false);
1328
- rawModeRestored = true;
1329
- }
1330
- catch {
1331
- logger.warn("connect_stdin_rawmode_restore_failed");
1332
- // ignore restore errors
1333
- }
1334
- }
1335
- logger.info("connect_stdin_stop", {
1336
- tty: options.tty,
1337
- stdinIsTty: stdin.isTTY,
1338
- rawModeRestored,
1339
- });
1340
- };
1341
- const flushPending = () => {
1342
- if (!opened)
1343
- return;
1344
- if (pendingResize) {
1345
- ws.send(JSON.stringify({
1346
- type: "resize",
1347
- cols: pendingResize.cols,
1348
- rows: pendingResize.rows,
1349
- }));
1350
- lastResize = pendingResize;
1351
- pendingResize = null;
1352
- }
1353
- if (pendingInput.length > 0) {
1354
- for (const chunk of pendingInput) {
1355
- sendInput(chunk);
1356
- }
1357
- pendingInput.length = 0;
1358
- }
1359
- };
1360
- const cleanup = () => {
1361
- if (options.tty && ttyOutputFilter) {
1362
- writeTtyOutput(ttyOutputFilter.flush());
1363
- }
1364
- ws.removeEventListener("message", onMessage);
1365
- ws.removeEventListener("close", onClose);
1366
- ws.removeEventListener("error", onError);
1367
- ws.removeEventListener("open", onOpen);
1368
- stopInputProbe();
1369
- stopOpenTimer();
1370
- stopHeartbeat();
1371
- stopInput();
1372
- };
1373
- const onClose = (event) => {
1374
- closeCode = typeof event?.code === "number" ? event.code : closeCode;
1375
- logger.info("connect_ws_close", {
1376
- opened,
1377
- closeCode,
1378
- exitCode,
1379
- });
1380
- recordDisconnect("close");
1381
- if (exitCode !== null) {
1382
- finish(exitCode);
1383
- return;
1384
- }
1385
- if (closeCode === 1000) {
1386
- finish(0);
1387
- return;
1388
- }
1389
- const result = {
1390
- kind: "disconnect",
1391
- opened,
1392
- };
1393
- if (closeCode !== undefined)
1394
- result.closeCode = closeCode;
1395
- cleanup();
1396
- resolve(result);
1397
- };
1398
- const onError = (event) => {
1399
- if (resolved)
1400
- return;
1401
- const raw = event?.data;
1402
- if (raw instanceof Error && raw.message.trim()) {
1403
- errorMessage = raw.message.trim();
1404
- }
1405
- else if (typeof raw === "string" && raw.trim()) {
1406
- errorMessage = raw.trim();
1407
- }
1408
- else if (raw &&
1409
- typeof raw === "object" &&
1410
- "message" in raw &&
1411
- typeof raw.message === "string" &&
1412
- raw.message.trim()) {
1413
- errorMessage = raw.message.trim();
1414
- }
1415
- recordDisconnect("error");
1416
- const result = {
1417
- kind: "disconnect",
1418
- opened,
1419
- };
1420
- if (closeCode !== undefined)
1421
- result.closeCode = closeCode;
1422
- if (errorMessage)
1423
- result.errorMessage = errorMessage;
1424
- logger.warn("connect_ws_error", {
1425
- opened,
1426
- closeCode,
1427
- errorMessage,
1428
- });
1429
- cleanup();
1430
- resolve(result);
1431
- };
1432
- const onOpen = () => {
1433
- stopOpenTimer();
1434
- opened = true;
1435
- markAlive();
1436
- latency?.noteConnectOpen();
1437
- logger.info("connect_ws_open", {
1438
- tty: options.tty,
1439
- stdin: options.stdin,
1440
- });
1441
- flushPending();
1442
- startInput();
1443
- startHeartbeat();
1444
- options.onOpen?.();
1445
- };
1446
- ws.binaryType = "arraybuffer";
1447
- ws.addEventListener("message", onMessage);
1448
- ws.addEventListener("close", onClose);
1449
- ws.addEventListener("error", onError);
1450
- ws.addEventListener("open", onOpen);
1451
- sendResize();
1452
- const openTimeoutMs = options.openTimeoutMs ?? CONNECT_OPEN_TIMEOUT_MS;
1453
- if (openTimeoutMs > 0) {
1454
- openTimer = setTimeout(() => {
1455
- if (resolved || opened)
1456
- return;
1457
- noteDisconnect();
1458
- forceClose();
1459
- onError({
1460
- data: new Error(`Timed out waiting for session connection after ${openTimeoutMs}ms.`),
1461
- });
1462
- }, openTimeoutMs);
1463
- openTimer.unref();
1464
- }
1465
- });
562
+ export { createInitialAttachClearFilter, createModalTtyOutputFilter, createScrollbackEraseFilter, stripModalDisconnectArtifacts, };
1466
563
  export const runConnect = async (args, overrides) => {
1467
564
  const parsed = parseConnectArgs(args);
1468
565
  const latency = createLatencyTracker();
@@ -1978,6 +1075,9 @@ export const runConnect = async (args, overrides) => {
1978
1075
  tty: isTty,
1979
1076
  stdin: false,
1980
1077
  supportsResizeControlMessages,
1078
+ resolveTtySize,
1079
+ openBrowser,
1080
+ isRawCtrlCChunk,
1981
1081
  ...(computeProvider === "modal" && isTty
1982
1082
  ? (() => {
1983
1083
  const ttyOutputFilter = createModalTtyOutputFilter();
@@ -2169,6 +1269,9 @@ export const runConnect = async (args, overrides) => {
2169
1269
  tty: isTty,
2170
1270
  stdin: localStdin,
2171
1271
  supportsResizeControlMessages,
1272
+ resolveTtySize,
1273
+ openBrowser,
1274
+ isRawCtrlCChunk,
2172
1275
  ...(computeProvider === "modal" && isTty
2173
1276
  ? (() => {
2174
1277
  const ttyOutputFilter = createModalTtyOutputFilter();