tmux-ide 2.9.6 → 2.9.8
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/bin/cli.js +2881 -1336
- package/package.json +2 -2
- package/packages/contracts/src/index.ts +5 -0
- package/packages/contracts/src/pane-stream.ts +54 -0
- package/packages/contracts/src/terminal-graphics.ts +118 -0
- package/packages/contracts/src/terminal-history-anchor.ts +61 -0
- package/packages/contracts/src/tmux-server-diagnostics.ts +16 -0
- package/packages/core/src/index.ts +1 -0
- package/packages/core/src/kitty-placeholders.ts +208 -0
- package/packages/daemon/dist/command-center/tmux-server-diagnostics.js +24 -0
- package/packages/daemon/dist/command-center/tmux-server-history-anchor.js +48 -0
- package/packages/daemon/dist/command-center/tmux-servers.js +11 -0
- package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +23 -1
- package/packages/daemon/dist/lib/daemon-embed.js +21 -0
- package/packages/daemon/dist/lib/tmux-server-owner.js +10 -1
- package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +2 -1
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
- package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libevent_core-2.1.7.dylib +0 -0
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.3.0.dylib +0 -0
- package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +12 -4
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +9 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +9 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/attachments/native-runtime.js +25 -12
- package/packages/daemon/dist/terminal/graphics/kitty-graphics.js +699 -0
- package/packages/daemon/dist/terminal/mirror/control-channel.js +17 -1
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +7 -0
- package/packages/daemon/dist/terminal/mirror/native-grid-capture.js +8 -0
- package/packages/daemon/dist/terminal/mirror/native-grid-reader.js +6 -1
- package/packages/daemon/dist/terminal/mirror/session-channel.js +56 -4
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +180 -1
- package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +3 -2
- package/packages/daemon/dist/terminal/session-runtime/history-anchor-owner.js +171 -0
- package/packages/daemon/dist/terminal/session-runtime/registry.js +29 -0
- package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +20 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +85 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +30 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +53 -21
- package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +11 -0
- package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +7 -1
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +35 -5
- package/packages/daemon/dist/tui/mirror/runtime/application-terminal-paste.js +2 -2
- package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +2 -2
- package/packages/daemon/src/command-center/tmux-server-diagnostics.ts +38 -0
- package/packages/daemon/src/command-center/tmux-server-history-anchor.ts +69 -0
- package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
- package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +20 -2
- package/packages/daemon/src/lib/daemon-embed.ts +26 -0
- package/packages/daemon/src/lib/tmux-server-owner.ts +10 -0
- package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +2 -1
- package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
- package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client-recorded.ctl +87 -0
- package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client.ctl +10 -0
- package/packages/daemon/src/terminal/graphics/kitty-graphics.ts +756 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +18 -1
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +14 -0
- package/packages/daemon/src/terminal/mirror/native-grid-capture.ts +11 -0
- package/packages/daemon/src/terminal/mirror/native-grid-reader.ts +10 -1
- package/packages/daemon/src/terminal/mirror/session-channel.ts +75 -3
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +199 -0
- package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +3 -2
- package/packages/daemon/src/terminal/session-runtime/history-anchor-owner.ts +190 -0
- package/packages/daemon/src/terminal/session-runtime/registry.ts +38 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +38 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-interpreter-backend.ts +4 -0
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +99 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +39 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +58 -24
- package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +13 -0
- package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +6 -1
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +44 -9
- package/packages/daemon/src/tui/mirror/runtime/application-terminal-paste.ts +2 -1
- package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +5 -2
- package/packages/daemon-client/src/fleet-connection-status.ts +1 -1
- package/packages/daemon-client/src/pane-creation-refusal.ts +48 -0
- package/packages/daemon-client/src/pane-stream-client.ts +37 -0
- package/packages/daemon-client/src/tmux-server-client.ts +133 -6
- package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.2.3.dylib +0 -0
- /package/packages/daemon/dist/native/tmux/darwin-arm64/licenses/{libutf8proc.3.2.3.dylib.txt → libutf8proc.3.3.0.dylib.txt} +0 -0
|
@@ -868,6 +868,16 @@ export class ControlChannelCore {
|
|
|
868
868
|
}));
|
|
869
869
|
}
|
|
870
870
|
}
|
|
871
|
+
/**
|
|
872
|
+
* Server-wide, idempotent: `-q` keeps a stock tmux (no such option) quiet.
|
|
873
|
+
* Images reach this control client either way; the option only lets programs
|
|
874
|
+
* detect support, and the environment tells new panes' programs that this
|
|
875
|
+
* server's renderer shows placeholder images.
|
|
876
|
+
*/
|
|
877
|
+
export const KITTY_GRAPHICS_SETUP = [
|
|
878
|
+
"set-option -gq kitty-graphics on",
|
|
879
|
+
"set-environment -g TMUX_IDE_GRAPHICS kitty",
|
|
880
|
+
];
|
|
871
881
|
export function mirrorControlAttachArgs(options, pauseAfterSeconds = DEFAULT_PAUSE_AFTER_SECONDS) {
|
|
872
882
|
const attach = [
|
|
873
883
|
"attach",
|
|
@@ -1009,7 +1019,13 @@ export class MirrorControlChannel {
|
|
|
1009
1019
|
this.core.fail(String(err));
|
|
1010
1020
|
reject(err);
|
|
1011
1021
|
});
|
|
1012
|
-
}).then(() =>
|
|
1022
|
+
}).then(() => {
|
|
1023
|
+
// One command per send: tmux answers a `;` list with one block per command.
|
|
1024
|
+
if (this.opts.kittyGraphics)
|
|
1025
|
+
for (const command of KITTY_GRAPHICS_SETUP)
|
|
1026
|
+
this.send(command);
|
|
1027
|
+
return this.initializeNativeViewer();
|
|
1028
|
+
});
|
|
1013
1029
|
}
|
|
1014
1030
|
get nativeViewerIdentity() {
|
|
1015
1031
|
return this.viewerIdentity ? { ...this.viewerIdentity } : null;
|
|
@@ -369,6 +369,12 @@ export class MirrorService {
|
|
|
369
369
|
this.assertWindowOwnership(session);
|
|
370
370
|
entry.channel.fitWindowViewport(semanticWindowId, cols, rows);
|
|
371
371
|
}
|
|
372
|
+
setCellPixels(session, width, height, precise) {
|
|
373
|
+
const entry = this.channels.get(session);
|
|
374
|
+
if (!entry || entry.retired)
|
|
375
|
+
return;
|
|
376
|
+
entry.channel.setCellPixels(width, height, precise);
|
|
377
|
+
}
|
|
372
378
|
clearWindowViewports(session) {
|
|
373
379
|
const entry = this.channels.get(session);
|
|
374
380
|
if (entry && !entry.retired)
|
|
@@ -532,6 +538,7 @@ export class MirrorService {
|
|
|
532
538
|
nativeServerIdentity: this.opts.nativeServerIdentity,
|
|
533
539
|
configFile: this.opts.configFile,
|
|
534
540
|
pauseAfterSeconds: this.opts.pauseAfterSeconds,
|
|
541
|
+
kittyGraphics: this.opts.kittyGraphics ?? process.env.TMUX_IDE_KITTY_GRAPHICS !== "0",
|
|
535
542
|
nowMicros: this.opts.nowMicros,
|
|
536
543
|
nativeViewer: ownedViewer?.controlOptions(),
|
|
537
544
|
nativeViewerReady: ownedViewer
|
|
@@ -1,3 +1,11 @@
|
|
|
1
|
+
/** Upstream tmux 3.8 reuses -R for a textual grid dump, not our JSON extension.
|
|
2
|
+
* Require the generated grid and first-row headers; arbitrary malformed native
|
|
3
|
+
* replies must remain errors rather than silently downgrading capture fidelity.
|
|
4
|
+
*/
|
|
5
|
+
export function isStockRawGridCapture(lines) {
|
|
6
|
+
return (/^G [1-9][0-9]*x[1-9][0-9]* \([0-9]+\/[0-9]+\)$/.test(lines[0] ?? "") &&
|
|
7
|
+
/^\tL 0 \((?:-|0)\) flags=/.test(lines[1] ?? ""));
|
|
8
|
+
}
|
|
1
9
|
const MAX_BYTES = 16 * 1024 * 1024;
|
|
2
10
|
const MAX_ROWS = MAX_BYTES / 64;
|
|
3
11
|
const MAX_CELLS = 1_000_000;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { decodeNativeGridCapture } from "./native-grid-capture.js";
|
|
1
|
+
import { decodeNativeGridCapture, isStockRawGridCapture, } from "./native-grid-capture.js";
|
|
2
2
|
/** One experimental capability owner per control connection. */
|
|
3
3
|
export class NativeGridCaptureReader {
|
|
4
4
|
io;
|
|
@@ -57,6 +57,11 @@ export class NativeGridCaptureReader {
|
|
|
57
57
|
finish({ status: "changed" });
|
|
58
58
|
return;
|
|
59
59
|
}
|
|
60
|
+
if (isStockRawGridCapture(reply.lines)) {
|
|
61
|
+
this.unsupported = true;
|
|
62
|
+
finish({ status: "unsupported" });
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
60
65
|
const snapshot = decodeNativeGridCapture(reply.lines.join("\n"));
|
|
61
66
|
finish(snapshot
|
|
62
67
|
? {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { layoutContentRows } from "./layout-content-rows.js";
|
|
2
2
|
import { decodeNativeAtomicSnapshot, decodeNativeAtomicDualSnapshot, nativeAtomicDualSnapshotPlan, nativeAtomicSnapshotPlan, } from "./native-atomic-snapshot.js";
|
|
3
3
|
import { boundedTmuxInteractionAppendCommand } from "../../lib/tmux-interaction-retention.js";
|
|
4
|
-
import { decodeNativeGridCapture, isNativeBootstrapCapture, } from "./native-grid-capture.js";
|
|
4
|
+
import { decodeNativeGridCapture, isStockRawGridCapture, isNativeBootstrapCapture, } from "./native-grid-capture.js";
|
|
5
5
|
/**
|
|
6
6
|
* SessionChannel — one control-mode channel serving every pane subscription
|
|
7
7
|
* of one tmux session (m43 card 1).
|
|
@@ -114,7 +114,8 @@ const RECOVERY_CURSOR_PROBE_FORMAT = [
|
|
|
114
114
|
/** Cache only proven server capability; malformed or failed reads can recover. */
|
|
115
115
|
function nativeBootstrapUnsupported(ok, lines, native) {
|
|
116
116
|
if (ok)
|
|
117
|
-
return
|
|
117
|
+
return (isStockRawGridCapture(lines) ||
|
|
118
|
+
(native !== null && (native.version !== 2 || native.currentAttributes === undefined)));
|
|
118
119
|
return lines.some((line) => /^(?:parse error: )?(?:command capture-pane: )?unknown flag -R$/.test(line.trim()));
|
|
119
120
|
}
|
|
120
121
|
const FAILED_RESEED_RESULT = Object.freeze({
|
|
@@ -852,7 +853,50 @@ export class SessionChannel {
|
|
|
852
853
|
this.input.flush();
|
|
853
854
|
this.clearWindowViewports();
|
|
854
855
|
this.restoreWindowSizing(this.attachedIdentity?.runtimeSessionId ?? `=${this.opts.session}`);
|
|
855
|
-
this.io.send(`refresh-client -C ${cols}x${rows}`);
|
|
856
|
+
this.io.send(`refresh-client -C ${cols}x${rows}${this.cellPixelSuffix()}`);
|
|
857
|
+
this.clientViewport = { cols, rows };
|
|
858
|
+
}
|
|
859
|
+
clientViewport = null;
|
|
860
|
+
cellPixels = null;
|
|
861
|
+
/** "/WxH" for tmux's refresh-client -C (patched tmux; stock ignores it). */
|
|
862
|
+
cellPixelSuffix() {
|
|
863
|
+
if (!this.cellPixels)
|
|
864
|
+
return "";
|
|
865
|
+
const { width, height, precise } = this.cellPixels;
|
|
866
|
+
return `/${width}x${height}` + (precise ? `/${precise.width}x${precise.height}` : "");
|
|
867
|
+
}
|
|
868
|
+
/**
|
|
869
|
+
* The renderer's cell size in device pixels. tmux answers CSI 14t/16t and
|
|
870
|
+
* sets TIOCGWINSZ pixels from it, so image programs render 1:1 for the
|
|
871
|
+
* window. Re-applies the current viewport when the size changes.
|
|
872
|
+
*/
|
|
873
|
+
setCellPixels(width, height, precise) {
|
|
874
|
+
if (!Number.isSafeInteger(width) ||
|
|
875
|
+
!Number.isSafeInteger(height) ||
|
|
876
|
+
width < 1 ||
|
|
877
|
+
height < 1 ||
|
|
878
|
+
width > 1024 ||
|
|
879
|
+
height > 1024)
|
|
880
|
+
throw new RangeError("cell pixel size out of range");
|
|
881
|
+
if (precise &&
|
|
882
|
+
[precise.width, precise.height].some((value) => !Number.isSafeInteger(value) || value < 1000 || value > 1024000))
|
|
883
|
+
throw new RangeError("precise cell pixel size out of range");
|
|
884
|
+
if (this.cellPixels?.width === width &&
|
|
885
|
+
this.cellPixels.height === height &&
|
|
886
|
+
this.cellPixels.precise?.width === precise?.width &&
|
|
887
|
+
this.cellPixels.precise?.height === precise?.height)
|
|
888
|
+
return;
|
|
889
|
+
this.cellPixels = { width, height, ...(precise ? { precise } : {}) };
|
|
890
|
+
if (!this.geometryParticipating)
|
|
891
|
+
return;
|
|
892
|
+
this.input.flush();
|
|
893
|
+
if (this.fittedWindows.size > 0) {
|
|
894
|
+
for (const [runtimeId, size] of this.fittedWindows)
|
|
895
|
+
if (this.windowsByRuntime.has(runtimeId))
|
|
896
|
+
this.io.send(`refresh-client -C ${runtimeId}:${size.cols}x${size.rows}${this.cellPixelSuffix()}`);
|
|
897
|
+
}
|
|
898
|
+
else if (this.clientViewport)
|
|
899
|
+
this.io.send(`refresh-client -C ${this.clientViewport.cols}x${this.clientViewport.rows}${this.cellPixelSuffix()}`);
|
|
856
900
|
}
|
|
857
901
|
restoreWindowSizing(target) {
|
|
858
902
|
if (!this.geometryParticipating)
|
|
@@ -893,7 +937,7 @@ export class SessionChannel {
|
|
|
893
937
|
return;
|
|
894
938
|
this.input.flush();
|
|
895
939
|
this.restoreWindowSizing(window.runtimeId);
|
|
896
|
-
this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}`);
|
|
940
|
+
this.io.send(`refresh-client -C ${window.runtimeId}:${cols}x${rows}${this.cellPixelSuffix()}`);
|
|
897
941
|
this.fittedWindows.set(window.runtimeId, { cols, rows });
|
|
898
942
|
}
|
|
899
943
|
/** Must also run on geometry-owner handoff, before the next owner fits. */
|
|
@@ -1468,6 +1512,7 @@ export class SessionChannel {
|
|
|
1468
1512
|
return;
|
|
1469
1513
|
}
|
|
1470
1514
|
recovery.attempts += 1;
|
|
1515
|
+
lease.attemptCharged = true;
|
|
1471
1516
|
recovery.stage = "stock-pause";
|
|
1472
1517
|
recovery.stock = new StockPaneSnapshot(this.snapshotContext(pane));
|
|
1473
1518
|
// These rows were received before this pause and belong to the new seed,
|
|
@@ -1617,6 +1662,7 @@ export class SessionChannel {
|
|
|
1617
1662
|
return;
|
|
1618
1663
|
}
|
|
1619
1664
|
recovery.attempts += 1;
|
|
1665
|
+
lease.attemptCharged = true;
|
|
1620
1666
|
const ordinal = ++recovery.reseedOrdinal;
|
|
1621
1667
|
const participants = [...pane.subs]
|
|
1622
1668
|
.filter((sub) => !sub.closed && !sub.frozen)
|
|
@@ -2310,6 +2356,12 @@ export class SessionChannel {
|
|
|
2310
2356
|
continue;
|
|
2311
2357
|
const recovery = this.recoveries.get(pane.runtimeId);
|
|
2312
2358
|
if (recovery?.lease && !recovery.lease.retired) {
|
|
2359
|
+
// A newer layout cancels this capture; it is not a failed snapshot.
|
|
2360
|
+
// Refund only this lease, retaining earlier failures and the original deadline.
|
|
2361
|
+
if (recovery.lease.attemptCharged) {
|
|
2362
|
+
recovery.lease.attemptCharged = false;
|
|
2363
|
+
recovery.attempts -= 1;
|
|
2364
|
+
}
|
|
2313
2365
|
this.awaitSnapshotLayout(recovery);
|
|
2314
2366
|
this.retrySnapshot(recovery);
|
|
2315
2367
|
}
|
|
@@ -1,6 +1,7 @@
|
|
|
1
|
+
import { inflateSync } from "node:zlib";
|
|
1
2
|
import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
|
|
2
3
|
import { z } from "zod";
|
|
3
|
-
import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
4
|
+
import { PANE_STREAM_MAX_HELD_DELTAS, PANE_STREAM_MAX_OUTPUT_BYTES, PANE_STREAM_MAX_PANES, PANE_STREAM_MAX_SEED_BYTES, PANE_STREAM_PROTOCOL_VERSION, PANE_STREAM_REDEEM_PATH, PANE_STREAM_WEBSOCKET_SUBPROTOCOL, TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES, TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION, TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES, TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
4
5
|
import { canonicalOriginOrNull, digestSecret, digestsEqual, rawDataByteLength, rawDataToBuffer, safeCloseSocket, strictJsonParse, } from "../attachments/admission-util.js";
|
|
5
6
|
import { SessionRuntimeControllerLeaseError } from "../session-runtime/registry.js";
|
|
6
7
|
import { terminalDeliveryObservationOrdinal } from "../session-runtime/terminal-delivery-observation-identity.js";
|
|
@@ -592,6 +593,25 @@ class PreAuthAdmission {
|
|
|
592
593
|
socket.off("error", this.#onClose);
|
|
593
594
|
}
|
|
594
595
|
}
|
|
596
|
+
const GRAPHICS_MAX_QUEUED_DIGESTS = 256;
|
|
597
|
+
/** Graphics state as renderers without zlib support must see it. */
|
|
598
|
+
export function rawGraphicsState(state) {
|
|
599
|
+
const legacy = state.images.map((image) => image.format === "rgba-zlib" || image.format === "rgb-zlib"
|
|
600
|
+
? {
|
|
601
|
+
...image,
|
|
602
|
+
format: image.format === "rgba-zlib" ? "rgba" : "rgb",
|
|
603
|
+
bytes: image.width * image.height * (image.format === "rgba-zlib" ? 4 : 3),
|
|
604
|
+
}
|
|
605
|
+
: image);
|
|
606
|
+
const images = legacy.filter((image) => image.bytes <= TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES);
|
|
607
|
+
const kept = new Set(images.map((image) => image.digest));
|
|
608
|
+
return {
|
|
609
|
+
...state,
|
|
610
|
+
images,
|
|
611
|
+
placements: state.placements.filter((placement) => kept.has(placement.digest)),
|
|
612
|
+
};
|
|
613
|
+
}
|
|
614
|
+
const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
595
615
|
export class PaneStreamLiveConnection {
|
|
596
616
|
#clientId;
|
|
597
617
|
#socket;
|
|
@@ -723,6 +743,11 @@ export class PaneStreamLiveConnection {
|
|
|
723
743
|
frozenByWire: false,
|
|
724
744
|
closed: false,
|
|
725
745
|
batch: null,
|
|
746
|
+
graphicsAnnounced: false,
|
|
747
|
+
graphicsQueue: [],
|
|
748
|
+
graphicsSending: false,
|
|
749
|
+
graphicsServed: new Map(),
|
|
750
|
+
graphicsState: null,
|
|
726
751
|
});
|
|
727
752
|
}
|
|
728
753
|
}
|
|
@@ -1097,6 +1122,7 @@ export class PaneStreamLiveConnection {
|
|
|
1097
1122
|
await this.#sendTerminalDelivery(channel.semanticPaneId, message);
|
|
1098
1123
|
pending.length = 0;
|
|
1099
1124
|
markReady();
|
|
1125
|
+
this.#startGraphics(channel);
|
|
1100
1126
|
}
|
|
1101
1127
|
this.#closeIfAllPanesGone();
|
|
1102
1128
|
}
|
|
@@ -1201,6 +1227,146 @@ export class PaneStreamLiveConnection {
|
|
|
1201
1227
|
}
|
|
1202
1228
|
await this.#awaitSemanticCredit(pane);
|
|
1203
1229
|
}
|
|
1230
|
+
/** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
|
|
1231
|
+
#startGraphics(channel) {
|
|
1232
|
+
const graphics = channel.delivery?.graphics;
|
|
1233
|
+
if (!graphics)
|
|
1234
|
+
return;
|
|
1235
|
+
// Renderers that cannot inflate see raw formats; their bytes are inflated
|
|
1236
|
+
// when served.
|
|
1237
|
+
const zlib = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION) === true;
|
|
1238
|
+
channel.graphicsZlib = zlib;
|
|
1239
|
+
// Renderers that report cell pixels learn up front that this daemon
|
|
1240
|
+
// accepts them, before any image exists.
|
|
1241
|
+
const cellPixels = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION) === true;
|
|
1242
|
+
const preciseCellPixels = this.#descriptor.terminalDelivery?.protocolVersions.includes(TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION) === true;
|
|
1243
|
+
let announced = false;
|
|
1244
|
+
graphics.subscribe((state) => {
|
|
1245
|
+
announced = true;
|
|
1246
|
+
if (this.#closed || channel.closed)
|
|
1247
|
+
return;
|
|
1248
|
+
channel.graphicsAnnounced = true;
|
|
1249
|
+
channel.graphicsState = state;
|
|
1250
|
+
// A program redrawing one image id (every frame of an animation or a
|
|
1251
|
+
// page) needs the new bytes at once: push them instead of waiting for
|
|
1252
|
+
// the renderer's round trip.
|
|
1253
|
+
const pushed = [];
|
|
1254
|
+
for (const image of state.images) {
|
|
1255
|
+
const served = channel.graphicsServed.get(image.imageId);
|
|
1256
|
+
if (served !== undefined && served !== image.digest)
|
|
1257
|
+
pushed.push(image.digest);
|
|
1258
|
+
}
|
|
1259
|
+
this.#sendFrame(null, {
|
|
1260
|
+
type: "terminal-graphics",
|
|
1261
|
+
pane: channel.semanticPaneId,
|
|
1262
|
+
state: {
|
|
1263
|
+
...(zlib ? state : rawGraphicsState(state)),
|
|
1264
|
+
...(cellPixels ? { cellPixels: true } : {}),
|
|
1265
|
+
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1266
|
+
},
|
|
1267
|
+
});
|
|
1268
|
+
if (pushed.length > 0)
|
|
1269
|
+
this.#queueGraphics(channel, pushed);
|
|
1270
|
+
});
|
|
1271
|
+
if (cellPixels && !announced && !this.#closed && !channel.closed) {
|
|
1272
|
+
channel.graphicsAnnounced = true;
|
|
1273
|
+
this.#sendFrame(null, {
|
|
1274
|
+
type: "terminal-graphics",
|
|
1275
|
+
pane: channel.semanticPaneId,
|
|
1276
|
+
state: {
|
|
1277
|
+
revision: 0,
|
|
1278
|
+
images: [],
|
|
1279
|
+
placements: [],
|
|
1280
|
+
cellPixels: true,
|
|
1281
|
+
...(preciseCellPixels ? { preciseCellPixels: true } : {}),
|
|
1282
|
+
},
|
|
1283
|
+
});
|
|
1284
|
+
}
|
|
1285
|
+
}
|
|
1286
|
+
#requestGraphics(pane, digests) {
|
|
1287
|
+
const channel = this.#panes.get(pane);
|
|
1288
|
+
// Requests are only meaningful after an announcement; ignore stray ones.
|
|
1289
|
+
if (!channel || channel.closed || !channel.graphicsAnnounced)
|
|
1290
|
+
return;
|
|
1291
|
+
this.#queueGraphics(channel, digests);
|
|
1292
|
+
}
|
|
1293
|
+
#queueGraphics(channel, digests) {
|
|
1294
|
+
for (const digest of digests)
|
|
1295
|
+
if (!channel.graphicsQueue.includes(digest) &&
|
|
1296
|
+
channel.graphicsQueue.length < GRAPHICS_MAX_QUEUED_DIGESTS)
|
|
1297
|
+
channel.graphicsQueue.push(digest);
|
|
1298
|
+
if (!channel.graphicsSending)
|
|
1299
|
+
void this.#drainGraphics(channel);
|
|
1300
|
+
}
|
|
1301
|
+
/**
|
|
1302
|
+
* Blob bytes yield to terminal frames: each chunk waits until the socket's
|
|
1303
|
+
* buffered bytes fall below a small window, so images never trip the
|
|
1304
|
+
* socket-wide backpressure ceiling or delay a pane's next seed for long.
|
|
1305
|
+
*/
|
|
1306
|
+
async #drainGraphics(channel) {
|
|
1307
|
+
channel.graphicsSending = true;
|
|
1308
|
+
try {
|
|
1309
|
+
while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
|
|
1310
|
+
const digest = channel.graphicsQueue.shift();
|
|
1311
|
+
const graphics = channel.delivery?.graphics;
|
|
1312
|
+
const image = graphics?.image(digest);
|
|
1313
|
+
if (!graphics || !image)
|
|
1314
|
+
continue;
|
|
1315
|
+
for (const entry of channel.graphicsState?.images ?? [])
|
|
1316
|
+
if (entry.digest === digest)
|
|
1317
|
+
channel.graphicsServed.set(entry.imageId, digest);
|
|
1318
|
+
const raw = !channel.graphicsZlib && image.format.endsWith("-zlib");
|
|
1319
|
+
let data = image.data;
|
|
1320
|
+
if (raw) {
|
|
1321
|
+
const expected = image.width * image.height * (image.format === "rgba-zlib" ? 4 : 3);
|
|
1322
|
+
if (expected < 1 || expected > TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES)
|
|
1323
|
+
continue;
|
|
1324
|
+
try {
|
|
1325
|
+
data = inflateSync(image.data, { maxOutputLength: expected });
|
|
1326
|
+
}
|
|
1327
|
+
catch {
|
|
1328
|
+
continue;
|
|
1329
|
+
}
|
|
1330
|
+
if (data.byteLength !== expected)
|
|
1331
|
+
continue;
|
|
1332
|
+
}
|
|
1333
|
+
if (!channel.graphicsZlib && data.byteLength > TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES)
|
|
1334
|
+
continue;
|
|
1335
|
+
const format = raw ? (image.format === "rgba-zlib" ? "rgba" : "rgb") : image.format;
|
|
1336
|
+
const total = data.byteLength;
|
|
1337
|
+
for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
|
|
1338
|
+
while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
|
|
1339
|
+
if (this.#closed || channel.closed)
|
|
1340
|
+
return;
|
|
1341
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1342
|
+
}
|
|
1343
|
+
if (this.#closed || channel.closed)
|
|
1344
|
+
return;
|
|
1345
|
+
// A superseded frame (the id was redrawn) is abandoned mid-transfer;
|
|
1346
|
+
// the renderer drops the partial image and shows the newer one.
|
|
1347
|
+
if (offset > 0 && graphics.image(digest) !== image)
|
|
1348
|
+
break;
|
|
1349
|
+
const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
|
|
1350
|
+
this.#sendFrame(null, {
|
|
1351
|
+
type: "terminal-graphics-blob",
|
|
1352
|
+
pane: channel.semanticPaneId,
|
|
1353
|
+
blob: {
|
|
1354
|
+
digest,
|
|
1355
|
+
format,
|
|
1356
|
+
width: image.width,
|
|
1357
|
+
height: image.height,
|
|
1358
|
+
total,
|
|
1359
|
+
offset,
|
|
1360
|
+
},
|
|
1361
|
+
data: Buffer.from(data.subarray(offset, end)).toString("base64"),
|
|
1362
|
+
});
|
|
1363
|
+
}
|
|
1364
|
+
}
|
|
1365
|
+
}
|
|
1366
|
+
finally {
|
|
1367
|
+
channel.graphicsSending = false;
|
|
1368
|
+
}
|
|
1369
|
+
}
|
|
1204
1370
|
#awaitSemanticCredit(pane) {
|
|
1205
1371
|
// Backpressure is pane-local. Aggregate pressure is guarded by the hard
|
|
1206
1372
|
// socket ceiling in #drainNow, but must not let a noisy sibling prevent a
|
|
@@ -1774,6 +1940,19 @@ export class PaneStreamLiveConnection {
|
|
|
1774
1940
|
this.#acceptSemanticIntent(frame.operationId, frame.intent);
|
|
1775
1941
|
return;
|
|
1776
1942
|
}
|
|
1943
|
+
if (frame.type === "terminal-graphics-request") {
|
|
1944
|
+
this.#requestGraphics(frame.pane, frame.digests);
|
|
1945
|
+
return;
|
|
1946
|
+
}
|
|
1947
|
+
if (frame.type === "terminal-cell-pixels") {
|
|
1948
|
+
try {
|
|
1949
|
+
this.#sessionRuntimeBinding?.setCellPixels?.(frame.width, frame.height, frame.precise);
|
|
1950
|
+
}
|
|
1951
|
+
catch {
|
|
1952
|
+
// A retired runtime simply keeps tmux's default cell size.
|
|
1953
|
+
}
|
|
1954
|
+
return;
|
|
1955
|
+
}
|
|
1777
1956
|
if (frame.type === "viewport") {
|
|
1778
1957
|
if (!this.#descriptor.terminalDelivery ||
|
|
1779
1958
|
this.#descriptor.viewerMode !== "interactive" ||
|
|
@@ -129,7 +129,6 @@ export function parseSessionPaneDescriptorReply(lines) {
|
|
|
129
129
|
!bounded(type, 256) ||
|
|
130
130
|
!bounded(currentCommand, 512) ||
|
|
131
131
|
!bounded(cwd, 4_096) ||
|
|
132
|
-
!bounded(title, 1_024) ||
|
|
133
132
|
!bounded(windowName, 1_024) ||
|
|
134
133
|
!bounded(name, 256) ||
|
|
135
134
|
!bounded(missionStamp, 256) ||
|
|
@@ -148,7 +147,9 @@ export function parseSessionPaneDescriptorReply(lines) {
|
|
|
148
147
|
type: nonempty(type),
|
|
149
148
|
currentCommand: nonempty(currentCommand),
|
|
150
149
|
cwd: nonempty(cwd),
|
|
151
|
-
|
|
150
|
+
// Stock tmux turns any APC, including kitty graphics payloads, into the
|
|
151
|
+
// pane title; an overlong title names nothing and must not drop the pane.
|
|
152
|
+
title: bounded(title, 1_024) ? nonempty(title) : null,
|
|
152
153
|
windowIndex,
|
|
153
154
|
windowName: nonempty(windowName),
|
|
154
155
|
windowId: /^@(?:0|[1-9][0-9]*)$/u.test(windowId) ? windowId : null,
|
|
@@ -0,0 +1,171 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
const MAX_TOKENS = 32;
|
|
3
|
+
const PROOF_TTL_MS = 5 * 60_000;
|
|
4
|
+
/** Owner-private row ordinals. Never changes terminal v1 payloads or hashes. */
|
|
5
|
+
export class HistoryAnchorOwner {
|
|
6
|
+
now;
|
|
7
|
+
#current = null;
|
|
8
|
+
#first = 0;
|
|
9
|
+
#length = 0;
|
|
10
|
+
#proofs = new Map();
|
|
11
|
+
#tokens = new Map();
|
|
12
|
+
constructor(now = () => performance.now()) {
|
|
13
|
+
this.now = now;
|
|
14
|
+
}
|
|
15
|
+
invalidate() {
|
|
16
|
+
this.#current = null;
|
|
17
|
+
this.#first = 0;
|
|
18
|
+
this.#length = 0;
|
|
19
|
+
this.#tokens.clear();
|
|
20
|
+
this.#proofs.clear();
|
|
21
|
+
}
|
|
22
|
+
observe(update) {
|
|
23
|
+
if (update.type === "terminal.tombstone") {
|
|
24
|
+
this.invalidate();
|
|
25
|
+
return;
|
|
26
|
+
}
|
|
27
|
+
if (update.type === "terminal.seed") {
|
|
28
|
+
this.invalidate();
|
|
29
|
+
this.#length = update.snapshot.history.length;
|
|
30
|
+
this.#current = {
|
|
31
|
+
workspaceName: update.workspaceName,
|
|
32
|
+
semanticPaneId: update.semanticPaneId,
|
|
33
|
+
generation: update.generation,
|
|
34
|
+
incarnation: update.incarnation,
|
|
35
|
+
revision: update.revision,
|
|
36
|
+
stateHash: update.stateHash,
|
|
37
|
+
cols: update.cols,
|
|
38
|
+
rows: update.rows,
|
|
39
|
+
alternate: update.snapshot.modes.alternateScreen,
|
|
40
|
+
};
|
|
41
|
+
this.#remember();
|
|
42
|
+
return;
|
|
43
|
+
}
|
|
44
|
+
const old = this.#current;
|
|
45
|
+
if (!old ||
|
|
46
|
+
old.workspaceName !== update.workspaceName ||
|
|
47
|
+
old.semanticPaneId !== update.semanticPaneId ||
|
|
48
|
+
old.generation !== update.generation ||
|
|
49
|
+
old.incarnation !== update.incarnation ||
|
|
50
|
+
old.revision !== update.baseRevision) {
|
|
51
|
+
this.invalidate();
|
|
52
|
+
return;
|
|
53
|
+
}
|
|
54
|
+
const delta = update.patch.historyDelta;
|
|
55
|
+
const alternate = update.patch.modes?.alternateScreen ?? old.alternate;
|
|
56
|
+
if (delta &&
|
|
57
|
+
(!Number.isSafeInteger(delta.trim) || delta.trim < 0 || delta.trim > this.#length)) {
|
|
58
|
+
this.invalidate();
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
const length = update.patch.history?.length ??
|
|
62
|
+
this.#length - (delta?.trim ?? 0) + (delta?.append.length ?? 0);
|
|
63
|
+
const reset = update.patch.history !== undefined ||
|
|
64
|
+
update.cols !== old.cols ||
|
|
65
|
+
update.rows !== old.rows ||
|
|
66
|
+
alternate !== old.alternate;
|
|
67
|
+
if (reset) {
|
|
68
|
+
this.#tokens.clear();
|
|
69
|
+
this.#proofs.clear();
|
|
70
|
+
this.#first = 0;
|
|
71
|
+
}
|
|
72
|
+
else
|
|
73
|
+
this.#first += delta?.trim ?? 0;
|
|
74
|
+
if (!Number.isSafeInteger(this.#first + length)) {
|
|
75
|
+
this.invalidate();
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.#length = length;
|
|
79
|
+
this.#current = {
|
|
80
|
+
workspaceName: update.workspaceName,
|
|
81
|
+
semanticPaneId: update.semanticPaneId,
|
|
82
|
+
generation: update.generation,
|
|
83
|
+
incarnation: update.incarnation,
|
|
84
|
+
revision: update.revision,
|
|
85
|
+
stateHash: update.stateHash,
|
|
86
|
+
cols: update.cols,
|
|
87
|
+
rows: update.rows,
|
|
88
|
+
alternate,
|
|
89
|
+
};
|
|
90
|
+
this.#prune();
|
|
91
|
+
this.#remember();
|
|
92
|
+
}
|
|
93
|
+
capture(proof, historyIndex) {
|
|
94
|
+
const failure = this.#admitIdentity(proof);
|
|
95
|
+
if (failure)
|
|
96
|
+
return failure;
|
|
97
|
+
this.#prune();
|
|
98
|
+
const known = this.#current && this.#key(proof) === this.#key(this.#current)
|
|
99
|
+
? { first: this.#first, length: this.#length }
|
|
100
|
+
: this.#proofs.get(this.#key(proof));
|
|
101
|
+
if (!known)
|
|
102
|
+
return { status: "stale" };
|
|
103
|
+
if (!Number.isSafeInteger(historyIndex) || historyIndex < 0 || historyIndex >= known.length)
|
|
104
|
+
return { status: "lost" };
|
|
105
|
+
const ordinal = known.first + historyIndex;
|
|
106
|
+
if (ordinal < this.#first || ordinal >= this.#first + this.#length)
|
|
107
|
+
return { status: "lost" };
|
|
108
|
+
while (this.#tokens.size >= MAX_TOKENS)
|
|
109
|
+
this.#tokens.delete(this.#tokens.keys().next().value);
|
|
110
|
+
const token = randomUUID();
|
|
111
|
+
// Saved tabs retain identity until capacity eviction or actual history loss.
|
|
112
|
+
this.#tokens.set(token, { ordinal });
|
|
113
|
+
return { status: "captured", token };
|
|
114
|
+
}
|
|
115
|
+
resolve(proof, token) {
|
|
116
|
+
const failure = this.#admitIdentity(proof);
|
|
117
|
+
if (failure)
|
|
118
|
+
return failure;
|
|
119
|
+
this.#prune();
|
|
120
|
+
const entry = this.#tokens.get(token);
|
|
121
|
+
if (!entry)
|
|
122
|
+
return { status: "lost" };
|
|
123
|
+
const { workspaceName, semanticPaneId, generation, incarnation, revision, stateHash } = this.#current;
|
|
124
|
+
return {
|
|
125
|
+
status: "resolved",
|
|
126
|
+
proof: { workspaceName, semanticPaneId, generation, incarnation, revision, stateHash },
|
|
127
|
+
historyIndex: entry.ordinal - this.#first,
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
#admitIdentity(proof) {
|
|
131
|
+
const current = this.#current;
|
|
132
|
+
if (!current || current.alternate)
|
|
133
|
+
return { status: "unavailable" };
|
|
134
|
+
return current.workspaceName === proof.workspaceName &&
|
|
135
|
+
current.semanticPaneId === proof.semanticPaneId &&
|
|
136
|
+
current.generation === proof.generation &&
|
|
137
|
+
current.incarnation === proof.incarnation
|
|
138
|
+
? null
|
|
139
|
+
: { status: "stale" };
|
|
140
|
+
}
|
|
141
|
+
#key(proof) {
|
|
142
|
+
return JSON.stringify([
|
|
143
|
+
proof.workspaceName,
|
|
144
|
+
proof.semanticPaneId,
|
|
145
|
+
proof.generation,
|
|
146
|
+
proof.incarnation,
|
|
147
|
+
proof.revision,
|
|
148
|
+
proof.stateHash,
|
|
149
|
+
]);
|
|
150
|
+
}
|
|
151
|
+
#remember() {
|
|
152
|
+
if (!this.#current || this.#current.alternate)
|
|
153
|
+
return;
|
|
154
|
+
while (this.#proofs.size >= 64)
|
|
155
|
+
this.#proofs.delete(this.#proofs.keys().next().value);
|
|
156
|
+
this.#proofs.set(this.#key(this.#current), {
|
|
157
|
+
first: this.#first,
|
|
158
|
+
length: this.#length,
|
|
159
|
+
expires: this.now() + PROOF_TTL_MS,
|
|
160
|
+
});
|
|
161
|
+
}
|
|
162
|
+
#prune() {
|
|
163
|
+
const now = this.now();
|
|
164
|
+
for (const [key, value] of this.#proofs)
|
|
165
|
+
if (value.expires <= now)
|
|
166
|
+
this.#proofs.delete(key);
|
|
167
|
+
for (const [token, entry] of this.#tokens)
|
|
168
|
+
if (entry.ordinal < this.#first || entry.ordinal >= this.#first + this.#length)
|
|
169
|
+
this.#tokens.delete(token);
|
|
170
|
+
}
|
|
171
|
+
}
|
|
@@ -457,6 +457,12 @@ export class SessionRuntimeRegistry {
|
|
|
457
457
|
async describeSession(session) {
|
|
458
458
|
return await this.#runtime(session).describe();
|
|
459
459
|
}
|
|
460
|
+
historyAnchor(session, request) {
|
|
461
|
+
const runtime = this.#sessions.get(session);
|
|
462
|
+
if (this.#disposed || !runtime)
|
|
463
|
+
return { status: "unavailable" };
|
|
464
|
+
return runtime.historyAnchor(request);
|
|
465
|
+
}
|
|
460
466
|
async captureNativeBacking(session, semanticPaneId, expected) {
|
|
461
467
|
const runtime = this.#sessions.get(session);
|
|
462
468
|
if (this.#disposed || !runtime)
|
|
@@ -1128,6 +1134,11 @@ class SessionRuntime {
|
|
|
1128
1134
|
throw new SessionRuntimeControllerLeaseError("invalid-client-capability", "The client does not own this geometry authority lease.");
|
|
1129
1135
|
}
|
|
1130
1136
|
}
|
|
1137
|
+
/** Renderer cell size in device pixels; benign, so any connected client may set it. */
|
|
1138
|
+
setCellPixels(clientId, width, height, precise) {
|
|
1139
|
+
this.#assertConnected(clientId);
|
|
1140
|
+
this.#mirror.setCellPixels?.(this.session, width, height, precise);
|
|
1141
|
+
}
|
|
1131
1142
|
fitViewportWithAuthority(clientId, lease, cols, rows, semanticWindowId) {
|
|
1132
1143
|
this.assertGeometryAuthority(clientId, lease);
|
|
1133
1144
|
this.#mirror.setGeometryParticipation(this.session, true);
|
|
@@ -1222,6 +1233,19 @@ class SessionRuntime {
|
|
|
1222
1233
|
throw error;
|
|
1223
1234
|
}
|
|
1224
1235
|
}
|
|
1236
|
+
historyAnchor(request) {
|
|
1237
|
+
if (this.#disposed)
|
|
1238
|
+
return { status: "unavailable" };
|
|
1239
|
+
const owner = this.#terminalReplicas.get(request.proof.semanticPaneId);
|
|
1240
|
+
if (!owner)
|
|
1241
|
+
return { status: "unavailable" };
|
|
1242
|
+
if (request.operation.kind === "capture") {
|
|
1243
|
+
if (!("revision" in request.proof) || !("stateHash" in request.proof))
|
|
1244
|
+
return { status: "stale" };
|
|
1245
|
+
return owner.captureHistoryAnchor(request.proof, request.operation.historyIndex);
|
|
1246
|
+
}
|
|
1247
|
+
return owner.resolveHistoryAnchor(request.proof, request.operation.token);
|
|
1248
|
+
}
|
|
1225
1249
|
async captureNativeBacking(semanticPaneId, expected) {
|
|
1226
1250
|
const owner = this.#terminalReplicas.get(semanticPaneId);
|
|
1227
1251
|
if (!owner)
|
|
@@ -1552,6 +1576,10 @@ class SessionRuntimeConsumerImpl {
|
|
|
1552
1576
|
this.#assertOpen();
|
|
1553
1577
|
this.#runtime.fitViewport(this.clientId, lease, cols, rows, semanticWindowId);
|
|
1554
1578
|
}
|
|
1579
|
+
setCellPixels(width, height, precise) {
|
|
1580
|
+
this.#assertOpen();
|
|
1581
|
+
this.#runtime.setCellPixels(this.clientId, width, height, precise);
|
|
1582
|
+
}
|
|
1555
1583
|
fitViewportWithAuthority(lease, cols, rows, semanticWindowId) {
|
|
1556
1584
|
this.#assertOpen();
|
|
1557
1585
|
this.#runtime.fitViewportWithAuthority(this.clientId, lease, cols, rows, semanticWindowId);
|
|
@@ -1624,6 +1652,7 @@ class SessionRuntimeConsumerImpl {
|
|
|
1624
1652
|
this.#deliveryConnections.delete(connection);
|
|
1625
1653
|
await upstream.close();
|
|
1626
1654
|
},
|
|
1655
|
+
...(upstream.graphics ? { graphics: upstream.graphics } : {}),
|
|
1627
1656
|
};
|
|
1628
1657
|
this.#deliveryConnections.add(connection);
|
|
1629
1658
|
return connection;
|