tmux-ide 2.9.6 → 2.9.7
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 +2597 -1322
- package/package.json +1 -1
- package/packages/contracts/src/index.ts +5 -0
- package/packages/contracts/src/pane-stream.ts +37 -0
- package/packages/contracts/src/terminal-graphics.ts +92 -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 +190 -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 +8 -4
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +5 -1
- package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
- package/packages/daemon/dist/terminal/graphics/kitty-graphics.js +578 -0
- package/packages/daemon/dist/terminal/mirror/control-channel.js +17 -1
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +1 -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 +11 -2
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +78 -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 +20 -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 +68 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +30 -0
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +49 -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/graphics/kitty-graphics.ts +641 -0
- package/packages/daemon/src/terminal/mirror/control-channel.ts +18 -1
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +3 -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 +14 -1
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +82 -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 +20 -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 +83 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +39 -0
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +53 -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 +29 -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
|
@@ -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({
|
|
@@ -1468,6 +1469,7 @@ export class SessionChannel {
|
|
|
1468
1469
|
return;
|
|
1469
1470
|
}
|
|
1470
1471
|
recovery.attempts += 1;
|
|
1472
|
+
lease.attemptCharged = true;
|
|
1471
1473
|
recovery.stage = "stock-pause";
|
|
1472
1474
|
recovery.stock = new StockPaneSnapshot(this.snapshotContext(pane));
|
|
1473
1475
|
// These rows were received before this pause and belong to the new seed,
|
|
@@ -1617,6 +1619,7 @@ export class SessionChannel {
|
|
|
1617
1619
|
return;
|
|
1618
1620
|
}
|
|
1619
1621
|
recovery.attempts += 1;
|
|
1622
|
+
lease.attemptCharged = true;
|
|
1620
1623
|
const ordinal = ++recovery.reseedOrdinal;
|
|
1621
1624
|
const participants = [...pane.subs]
|
|
1622
1625
|
.filter((sub) => !sub.closed && !sub.frozen)
|
|
@@ -2310,6 +2313,12 @@ export class SessionChannel {
|
|
|
2310
2313
|
continue;
|
|
2311
2314
|
const recovery = this.recoveries.get(pane.runtimeId);
|
|
2312
2315
|
if (recovery?.lease && !recovery.lease.retired) {
|
|
2316
|
+
// A newer layout cancels this capture; it is not a failed snapshot.
|
|
2317
|
+
// Refund only this lease, retaining earlier failures and the original deadline.
|
|
2318
|
+
if (recovery.lease.attemptCharged) {
|
|
2319
|
+
recovery.lease.attemptCharged = false;
|
|
2320
|
+
recovery.attempts -= 1;
|
|
2321
|
+
}
|
|
2313
2322
|
this.awaitSnapshotLayout(recovery);
|
|
2314
2323
|
this.retrySnapshot(recovery);
|
|
2315
2324
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { PaneStreamLayoutSnapshotFrameSchemaZ } from "@tmux-ide/contracts";
|
|
2
2
|
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";
|
|
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, TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES, PaneStreamClientFrameSchemaZ, PaneStreamLoopbackWebSocketUrlSchemaZ, PaneStreamRedeemFrameSchemaZ, PaneStreamServerFrameSchemaZ, sharedMonotonicMicros, } from "@tmux-ide/contracts";
|
|
4
4
|
import { canonicalOriginOrNull, digestSecret, digestsEqual, rawDataByteLength, rawDataToBuffer, safeCloseSocket, strictJsonParse, } from "../attachments/admission-util.js";
|
|
5
5
|
import { SessionRuntimeControllerLeaseError } from "../session-runtime/registry.js";
|
|
6
6
|
import { terminalDeliveryObservationOrdinal } from "../session-runtime/terminal-delivery-observation-identity.js";
|
|
@@ -592,6 +592,8 @@ class PreAuthAdmission {
|
|
|
592
592
|
socket.off("error", this.#onClose);
|
|
593
593
|
}
|
|
594
594
|
}
|
|
595
|
+
const GRAPHICS_MAX_QUEUED_DIGESTS = 256;
|
|
596
|
+
const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
595
597
|
export class PaneStreamLiveConnection {
|
|
596
598
|
#clientId;
|
|
597
599
|
#socket;
|
|
@@ -723,6 +725,9 @@ export class PaneStreamLiveConnection {
|
|
|
723
725
|
frozenByWire: false,
|
|
724
726
|
closed: false,
|
|
725
727
|
batch: null,
|
|
728
|
+
graphicsAnnounced: false,
|
|
729
|
+
graphicsQueue: [],
|
|
730
|
+
graphicsSending: false,
|
|
726
731
|
});
|
|
727
732
|
}
|
|
728
733
|
}
|
|
@@ -1097,6 +1102,7 @@ export class PaneStreamLiveConnection {
|
|
|
1097
1102
|
await this.#sendTerminalDelivery(channel.semanticPaneId, message);
|
|
1098
1103
|
pending.length = 0;
|
|
1099
1104
|
markReady();
|
|
1105
|
+
this.#startGraphics(channel);
|
|
1100
1106
|
}
|
|
1101
1107
|
this.#closeIfAllPanesGone();
|
|
1102
1108
|
}
|
|
@@ -1201,6 +1207,73 @@ export class PaneStreamLiveConnection {
|
|
|
1201
1207
|
}
|
|
1202
1208
|
await this.#awaitSemanticCredit(pane);
|
|
1203
1209
|
}
|
|
1210
|
+
/** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
|
|
1211
|
+
#startGraphics(channel) {
|
|
1212
|
+
const graphics = channel.delivery?.graphics;
|
|
1213
|
+
if (!graphics)
|
|
1214
|
+
return;
|
|
1215
|
+
graphics.subscribe((state) => {
|
|
1216
|
+
if (this.#closed || channel.closed)
|
|
1217
|
+
return;
|
|
1218
|
+
channel.graphicsAnnounced = true;
|
|
1219
|
+
this.#sendFrame(null, { type: "terminal-graphics", pane: channel.semanticPaneId, state });
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
#requestGraphics(pane, digests) {
|
|
1223
|
+
const channel = this.#panes.get(pane);
|
|
1224
|
+
// Requests are only meaningful after an announcement; ignore stray ones.
|
|
1225
|
+
if (!channel || channel.closed || !channel.graphicsAnnounced)
|
|
1226
|
+
return;
|
|
1227
|
+
for (const digest of digests)
|
|
1228
|
+
if (!channel.graphicsQueue.includes(digest) &&
|
|
1229
|
+
channel.graphicsQueue.length < GRAPHICS_MAX_QUEUED_DIGESTS)
|
|
1230
|
+
channel.graphicsQueue.push(digest);
|
|
1231
|
+
if (!channel.graphicsSending)
|
|
1232
|
+
void this.#drainGraphics(channel);
|
|
1233
|
+
}
|
|
1234
|
+
/**
|
|
1235
|
+
* Blob bytes yield to terminal frames: each chunk waits until the socket's
|
|
1236
|
+
* buffered bytes fall below a small window, so images never trip the
|
|
1237
|
+
* socket-wide backpressure ceiling or delay a pane's next seed for long.
|
|
1238
|
+
*/
|
|
1239
|
+
async #drainGraphics(channel) {
|
|
1240
|
+
channel.graphicsSending = true;
|
|
1241
|
+
try {
|
|
1242
|
+
while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
|
|
1243
|
+
const digest = channel.graphicsQueue.shift();
|
|
1244
|
+
const image = channel.delivery?.graphics?.image(digest);
|
|
1245
|
+
if (!image)
|
|
1246
|
+
continue;
|
|
1247
|
+
const total = image.data.byteLength;
|
|
1248
|
+
for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
|
|
1249
|
+
while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
|
|
1250
|
+
if (this.#closed || channel.closed)
|
|
1251
|
+
return;
|
|
1252
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1253
|
+
}
|
|
1254
|
+
if (this.#closed || channel.closed)
|
|
1255
|
+
return;
|
|
1256
|
+
const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
|
|
1257
|
+
this.#sendFrame(null, {
|
|
1258
|
+
type: "terminal-graphics-blob",
|
|
1259
|
+
pane: channel.semanticPaneId,
|
|
1260
|
+
blob: {
|
|
1261
|
+
digest,
|
|
1262
|
+
format: image.format,
|
|
1263
|
+
width: image.width,
|
|
1264
|
+
height: image.height,
|
|
1265
|
+
total,
|
|
1266
|
+
offset,
|
|
1267
|
+
},
|
|
1268
|
+
data: Buffer.from(image.data.subarray(offset, end)).toString("base64"),
|
|
1269
|
+
});
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
}
|
|
1273
|
+
finally {
|
|
1274
|
+
channel.graphicsSending = false;
|
|
1275
|
+
}
|
|
1276
|
+
}
|
|
1204
1277
|
#awaitSemanticCredit(pane) {
|
|
1205
1278
|
// Backpressure is pane-local. Aggregate pressure is guarded by the hard
|
|
1206
1279
|
// socket ceiling in #drainNow, but must not let a noisy sibling prevent a
|
|
@@ -1774,6 +1847,10 @@ export class PaneStreamLiveConnection {
|
|
|
1774
1847
|
this.#acceptSemanticIntent(frame.operationId, frame.intent);
|
|
1775
1848
|
return;
|
|
1776
1849
|
}
|
|
1850
|
+
if (frame.type === "terminal-graphics-request") {
|
|
1851
|
+
this.#requestGraphics(frame.pane, frame.digests);
|
|
1852
|
+
return;
|
|
1853
|
+
}
|
|
1777
1854
|
if (frame.type === "viewport") {
|
|
1778
1855
|
if (!this.#descriptor.terminalDelivery ||
|
|
1779
1856
|
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)
|
|
@@ -1222,6 +1228,19 @@ class SessionRuntime {
|
|
|
1222
1228
|
throw error;
|
|
1223
1229
|
}
|
|
1224
1230
|
}
|
|
1231
|
+
historyAnchor(request) {
|
|
1232
|
+
if (this.#disposed)
|
|
1233
|
+
return { status: "unavailable" };
|
|
1234
|
+
const owner = this.#terminalReplicas.get(request.proof.semanticPaneId);
|
|
1235
|
+
if (!owner)
|
|
1236
|
+
return { status: "unavailable" };
|
|
1237
|
+
if (request.operation.kind === "capture") {
|
|
1238
|
+
if (!("revision" in request.proof) || !("stateHash" in request.proof))
|
|
1239
|
+
return { status: "stale" };
|
|
1240
|
+
return owner.captureHistoryAnchor(request.proof, request.operation.historyIndex);
|
|
1241
|
+
}
|
|
1242
|
+
return owner.resolveHistoryAnchor(request.proof, request.operation.token);
|
|
1243
|
+
}
|
|
1225
1244
|
async captureNativeBacking(semanticPaneId, expected) {
|
|
1226
1245
|
const owner = this.#terminalReplicas.get(semanticPaneId);
|
|
1227
1246
|
if (!owner)
|
|
@@ -1624,6 +1643,7 @@ class SessionRuntimeConsumerImpl {
|
|
|
1624
1643
|
this.#deliveryConnections.delete(connection);
|
|
1625
1644
|
await upstream.close();
|
|
1626
1645
|
},
|
|
1646
|
+
...(upstream.graphics ? { graphics: upstream.graphics } : {}),
|
|
1627
1647
|
};
|
|
1628
1648
|
this.#deliveryConnections.add(connection);
|
|
1629
1649
|
return connection;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.js";
|
|
2
2
|
import { MAX_RETAINED_NATIVE_BACKING_BYTES, takeNativeSeedBacking, } from "./native-seed-backing.js";
|
|
3
|
-
import { WorkspaceIdSchemaZ, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
|
|
3
|
+
import { WorkspaceIdSchemaZ, TERMINAL_GRAPHICS_CAPABILITY_VERSION, TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES, TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES, TerminalDeliveryEnvelopeSchemaZ, TerminalDeliveryAckSchemaZ, TerminalDeliveryNackSchemaZ, TerminalDeliveryOfferSchemaZ, TerminalDeliveryVisibilitySchemaZ, } from "@tmux-ide/contracts";
|
|
4
4
|
import { TerminalDeliveryStateTooLargeError, terminalReplicaRowsEqual, TERMINAL_COMPACT_COOPERATIVE_SEED_CELLS, applyTerminalReplicaUpdate, applyTerminalReplicaUpdateCooperatively, terminalReplicaUpdateNeedsCooperativeReduction, encodeCompactSemanticTerminalUpdate, encodeCompactSemanticTerminalUpdateCooperatively, terminalSemanticUpdateNeedsCooperativeEncoding, encodeAnsiTerminalRepresentation, encodeSemanticTerminalUpdate, hashTerminalDeliveryRepresentation, negotiateTerminalDelivery, preaccountSemanticTerminalUpdateBytes, } from "@tmux-ide/core";
|
|
5
5
|
import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
|
|
6
6
|
import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
|
|
@@ -171,6 +171,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
171
171
|
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
172
172
|
},
|
|
173
173
|
close: async () => this.#closeClient(client),
|
|
174
|
+
...(offer.protocolVersions.includes(TERMINAL_GRAPHICS_CAPABILITY_VERSION)
|
|
175
|
+
? { graphics: this.#graphicsFor(semanticPaneId, client, closed) }
|
|
176
|
+
: {}),
|
|
174
177
|
};
|
|
175
178
|
}
|
|
176
179
|
catch (error) {
|
|
@@ -196,6 +199,22 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
196
199
|
this.#pendingClients.delete(key);
|
|
197
200
|
}
|
|
198
201
|
}
|
|
202
|
+
#graphicsFor(semanticPaneId, client, closed) {
|
|
203
|
+
const owner = this.#ownerForPane(semanticPaneId);
|
|
204
|
+
return {
|
|
205
|
+
subscribe: (listener) => {
|
|
206
|
+
if (client.closed || !owner.subscribeGraphics)
|
|
207
|
+
return () => { };
|
|
208
|
+
const unsubscribe = owner.subscribeGraphics((state) => {
|
|
209
|
+
if (!client.closed)
|
|
210
|
+
listener(state);
|
|
211
|
+
});
|
|
212
|
+
void closed.then(unsubscribe);
|
|
213
|
+
return unsubscribe;
|
|
214
|
+
},
|
|
215
|
+
image: (digest) => (client.closed ? null : (owner.graphicsImage?.(digest) ?? null)),
|
|
216
|
+
};
|
|
217
|
+
}
|
|
199
218
|
/** Retire only this pane's delivery owners before its canonical owner is disposed. */
|
|
200
219
|
failPaneCapture(semanticPaneId, error) {
|
|
201
220
|
for (const client of this.#clients.values())
|
|
@@ -6,6 +6,7 @@ import { SYSTEM_SESSION_RUNTIME_SCHEDULER, } from "./runtime-scheduler.js";
|
|
|
6
6
|
import { DISABLED_SESSION_RUNTIME_OBSERVABILITY, } from "./runtime-observability.js";
|
|
7
7
|
import { createXtermTerminalInterpreterBackend } from "./xterm-terminal-interpreter-backend.js";
|
|
8
8
|
import { CAUSAL_CELL_OSC, CausalCellLedger, cellsEqual, } from "./causal-cell-ledger.js";
|
|
9
|
+
import { KITTY_PLACEMENT_OSC, KittyApcScanner, KittyImageStore, } from "../graphics/kitty-graphics.js";
|
|
9
10
|
/** Daemon-owned, sans-I/O VT interpreter with one FIFO for bytes and geometry. */
|
|
10
11
|
export class TerminalReplicaInterpreter {
|
|
11
12
|
#nativeSeedBackingCandidate;
|
|
@@ -37,6 +38,13 @@ export class TerminalReplicaInterpreter {
|
|
|
37
38
|
#pendingWrites = [];
|
|
38
39
|
#writeFlushScheduled = false;
|
|
39
40
|
#widgetGate = false;
|
|
41
|
+
// kitty graphics: images outlive reseeds; the scanner owns split APCs.
|
|
42
|
+
#kittyScanner = new KittyApcScanner();
|
|
43
|
+
#kitty;
|
|
44
|
+
#kittyPending = new Map();
|
|
45
|
+
#graphicsListeners = new Set();
|
|
46
|
+
#graphics = null;
|
|
47
|
+
#graphicsKey = "";
|
|
40
48
|
#markerTail = "";
|
|
41
49
|
#pendingResize = null;
|
|
42
50
|
// null: no unpublished writes; undefined: a write requires full projection.
|
|
@@ -79,6 +87,8 @@ export class TerminalReplicaInterpreter {
|
|
|
79
87
|
rows: options.rows,
|
|
80
88
|
scrollback: this.#scrollback,
|
|
81
89
|
});
|
|
90
|
+
this.#kitty = new KittyImageStore(options.kittyImageBudget ? { budget: options.kittyImageBudget } : {});
|
|
91
|
+
this.#attachKitty(this.#backend);
|
|
82
92
|
this.#snapshot = blankTerminalReplicaSnapshot(options.cols, options.rows);
|
|
83
93
|
this.#seedReady = new Promise((resolve, reject) => {
|
|
84
94
|
this.#resolveSeedReady = resolve;
|
|
@@ -239,6 +249,9 @@ export class TerminalReplicaInterpreter {
|
|
|
239
249
|
}
|
|
240
250
|
const previous = this.#backend;
|
|
241
251
|
this.#backend = replacement;
|
|
252
|
+
this.#attachKitty(replacement);
|
|
253
|
+
// tmux never saw cursor-anchored placements; a capture cannot restore them.
|
|
254
|
+
this.#kitty.dropCursorPlacements();
|
|
242
255
|
this.#nativeReseedRequested = false;
|
|
243
256
|
this.#scrollback = scrollback;
|
|
244
257
|
this.#bootstrap = {
|
|
@@ -262,6 +275,7 @@ export class TerminalReplicaInterpreter {
|
|
|
262
275
|
this.#nativeSeedBackingCandidate = undefined;
|
|
263
276
|
}
|
|
264
277
|
previous.dispose();
|
|
278
|
+
this.#publishGraphics();
|
|
265
279
|
return;
|
|
266
280
|
}
|
|
267
281
|
if (operation.type === "cursor") {
|
|
@@ -287,6 +301,7 @@ export class TerminalReplicaInterpreter {
|
|
|
287
301
|
}
|
|
288
302
|
this.#backend.resize(operation.cols, operation.rows);
|
|
289
303
|
this.#commit(false);
|
|
304
|
+
this.#publishGraphics();
|
|
290
305
|
return;
|
|
291
306
|
}
|
|
292
307
|
const baseRevision = this.#revision;
|
|
@@ -304,6 +319,7 @@ export class TerminalReplicaInterpreter {
|
|
|
304
319
|
};
|
|
305
320
|
this.#revision = revision;
|
|
306
321
|
this.#closed = true;
|
|
322
|
+
this.#kitty.dispose();
|
|
307
323
|
this.#causalCell?.fail("transport-closed");
|
|
308
324
|
this.#clearSyncRecovery();
|
|
309
325
|
if (this.#needsSeed)
|
|
@@ -321,8 +337,14 @@ export class TerminalReplicaInterpreter {
|
|
|
321
337
|
this.#admitRaw(data);
|
|
322
338
|
this.#stats.parseBatches += 1;
|
|
323
339
|
this.#observeMarkerBytes(data);
|
|
340
|
+
const scanned = this.#kittyScanner.scan(data);
|
|
341
|
+
for (const command of scanned.commands)
|
|
342
|
+
this.#kittyPending.set(command.marker, command);
|
|
324
343
|
const parseStarted = this.#observability.enabled ? this.#observability.nowMicros() : 0;
|
|
325
|
-
|
|
344
|
+
// Always forward the scanner's output: tmux splits %output (~2 KiB) inside
|
|
345
|
+
// kitty APCs (~4 KiB chunks), so a chunk with no completed command can still
|
|
346
|
+
// be all image payload that must never reach the VT parser as text.
|
|
347
|
+
await this.#writeToBackend(this.#backend, scanned.forward);
|
|
326
348
|
if (this.#observability.enabled)
|
|
327
349
|
this.#observability.recordSpan("parse", "terminal-replica-write", parseStarted, this.#observability.nowMicros(), trace);
|
|
328
350
|
// The parser resets its dirty tracker per write. Synchronized output may
|
|
@@ -356,6 +378,7 @@ export class TerminalReplicaInterpreter {
|
|
|
356
378
|
else {
|
|
357
379
|
this.#commit(false, this.#backend.dirtyRange(), trace);
|
|
358
380
|
}
|
|
381
|
+
this.#publishGraphics();
|
|
359
382
|
}
|
|
360
383
|
#scheduleSyncRecovery() {
|
|
361
384
|
if (this.#syncRecovery || this.#closed)
|
|
@@ -541,6 +564,50 @@ export class TerminalReplicaInterpreter {
|
|
|
541
564
|
bootstrap: this.#bootstrap,
|
|
542
565
|
});
|
|
543
566
|
}
|
|
567
|
+
/** The pane's kitty image store (bytes for the graphics side channel). */
|
|
568
|
+
kittyImages() {
|
|
569
|
+
return this.#kitty;
|
|
570
|
+
}
|
|
571
|
+
/** Latest graphics state, or null while the pane has never held an image. */
|
|
572
|
+
currentGraphics() {
|
|
573
|
+
return this.#graphics;
|
|
574
|
+
}
|
|
575
|
+
onGraphics(listener) {
|
|
576
|
+
this.#graphicsListeners.add(listener);
|
|
577
|
+
return () => this.#graphicsListeners.delete(listener);
|
|
578
|
+
}
|
|
579
|
+
/** Publish graphics state when the store or the viewport-relative placements moved. */
|
|
580
|
+
#publishGraphics() {
|
|
581
|
+
if (this.#graphics === null && this.#kitty.revision === 0)
|
|
582
|
+
return;
|
|
583
|
+
const state = this.#kitty.state(this.#backend.graphicsViewportTop?.() ?? 0, this.#backend.rows, this.#backend.cols, this.#backend.modes().alternateScreen);
|
|
584
|
+
const key = JSON.stringify([state.revision, state.placements]);
|
|
585
|
+
if (key === this.#graphicsKey)
|
|
586
|
+
return;
|
|
587
|
+
this.#graphicsKey = key;
|
|
588
|
+
this.#graphics = state;
|
|
589
|
+
for (const listener of this.#graphicsListeners) {
|
|
590
|
+
try {
|
|
591
|
+
listener(state);
|
|
592
|
+
}
|
|
593
|
+
catch {
|
|
594
|
+
// Renderer callbacks never poison the authoritative parser FIFO.
|
|
595
|
+
}
|
|
596
|
+
}
|
|
597
|
+
}
|
|
598
|
+
#attachKitty(backend) {
|
|
599
|
+
// Minimal test backends omit the OSC seam; they simply never place images.
|
|
600
|
+
if (typeof backend.registerOscHandler !== "function")
|
|
601
|
+
return;
|
|
602
|
+
backend.registerOscHandler(KITTY_PLACEMENT_OSC, (data) => {
|
|
603
|
+
const command = this.#kittyPending.get(Number(data));
|
|
604
|
+
if (!command)
|
|
605
|
+
return true;
|
|
606
|
+
this.#kittyPending.delete(Number(data));
|
|
607
|
+
this.#kitty.apply(command, backend.graphicsCursor?.() ?? null);
|
|
608
|
+
return true;
|
|
609
|
+
});
|
|
610
|
+
}
|
|
544
611
|
#currentSnapshotHash() {
|
|
545
612
|
return (this.#snapshotHash ??= hashTerminalReplicaSnapshot(this.#snapshot));
|
|
546
613
|
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { HistoryAnchorOwner, } from "./history-anchor-owner.js";
|
|
1
2
|
import { layoutContentRows } from "../mirror/layout-content-rows.js";
|
|
2
3
|
import { terminalReplicaRowsEqual } from "@tmux-ide/core";
|
|
3
4
|
import { projectNativeGridRow } from "../mirror/native-grid-projection.js";
|
|
@@ -20,6 +21,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
20
21
|
#start;
|
|
21
22
|
#upstream = null;
|
|
22
23
|
#disposed = false;
|
|
24
|
+
#historyAnchors = new HistoryAnchorOwner();
|
|
23
25
|
#cols = 80;
|
|
24
26
|
#rows = 24;
|
|
25
27
|
#layoutEpoch = 0;
|
|
@@ -56,12 +58,14 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
56
58
|
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
57
59
|
return;
|
|
58
60
|
this.#waitingForGeometryCapture = true;
|
|
61
|
+
this.#historyAnchors.invalidate();
|
|
59
62
|
this.#supervise(this.#start.then(() => {
|
|
60
63
|
if (!this.#disposed && this.#waitingForGeometryCapture)
|
|
61
64
|
this.#upstream?.reseed();
|
|
62
65
|
}));
|
|
63
66
|
},
|
|
64
67
|
onUpdate: (update, trace) => {
|
|
68
|
+
this.#historyAnchors.observe(update);
|
|
65
69
|
this.#publishedIncarnation = update.incarnation;
|
|
66
70
|
if (update.type === "terminal.seed") {
|
|
67
71
|
this.#bootstrapped = true;
|
|
@@ -137,6 +141,16 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
137
141
|
failCausalCell(reason, traceId) {
|
|
138
142
|
this.#interpreter.failCausalCell(reason, traceId);
|
|
139
143
|
}
|
|
144
|
+
captureHistoryAnchor(proof, historyIndex) {
|
|
145
|
+
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
146
|
+
return { status: "unavailable" };
|
|
147
|
+
return this.#historyAnchors.capture(proof, historyIndex);
|
|
148
|
+
}
|
|
149
|
+
resolveHistoryAnchor(proof, token) {
|
|
150
|
+
if (this.#disposed || this.#reseed || this.#waitingForGeometryCapture)
|
|
151
|
+
return { status: "unavailable" };
|
|
152
|
+
return this.#historyAnchors.resolve(proof, token);
|
|
153
|
+
}
|
|
140
154
|
/** Scalar diagnostic only: no seed construction, hashing, or owner creation. */
|
|
141
155
|
inputDispatchIncarnation() {
|
|
142
156
|
return this.#disposed ? null : this.#publishedIncarnation;
|
|
@@ -248,6 +262,17 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
248
262
|
},
|
|
249
263
|
};
|
|
250
264
|
}
|
|
265
|
+
/** Kitty graphics side channel: current state now (if any), then every change. */
|
|
266
|
+
subscribeGraphics(listener) {
|
|
267
|
+
const current = this.#interpreter.currentGraphics();
|
|
268
|
+
if (current)
|
|
269
|
+
listener(current);
|
|
270
|
+
return this.#interpreter.onGraphics(listener);
|
|
271
|
+
}
|
|
272
|
+
/** Image bytes by content digest, while the pane's store still holds them. */
|
|
273
|
+
graphicsImage(digest) {
|
|
274
|
+
return this.#interpreter.kittyImages().imageByDigest(digest) ?? null;
|
|
275
|
+
}
|
|
251
276
|
async subscribeSource(listener, onRaw) {
|
|
252
277
|
const canonical = await this.subscribe(listener);
|
|
253
278
|
this.#rawListeners.add(onRaw);
|
|
@@ -268,6 +293,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
268
293
|
return;
|
|
269
294
|
this.#noteLifecycle(`dispose-${reason}`);
|
|
270
295
|
this.#disposed = true;
|
|
296
|
+
this.#historyAnchors.invalidate();
|
|
271
297
|
this.#historyTimer?.cancel();
|
|
272
298
|
this.#historyTimer = null;
|
|
273
299
|
this.#subscriptionEpoch += 1;
|
|
@@ -330,6 +356,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
330
356
|
// Deterministic capture point: reset opens the atomic capture. A probe
|
|
331
357
|
// armed after reset belongs to the next post-capture delta, never to this
|
|
332
358
|
// already-started reseed.
|
|
359
|
+
this.#historyAnchors.invalidate();
|
|
333
360
|
this.#reseed = {
|
|
334
361
|
nativeCols: event.cols,
|
|
335
362
|
nativeRows: event.rows,
|
|
@@ -447,6 +474,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
447
474
|
if (!this.#bootstrapped || this.#reseed || this.#waitingForGeometryCapture)
|
|
448
475
|
return;
|
|
449
476
|
this.#waitingForGeometryCapture = true;
|
|
477
|
+
this.#historyAnchors.invalidate();
|
|
450
478
|
void this.#start.then(() => {
|
|
451
479
|
if (!this.#disposed && this.#waitingForGeometryCapture)
|
|
452
480
|
this.#upstream?.reseed();
|
|
@@ -557,6 +585,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
557
585
|
this.#noteLifecycle("reseed-retry", diagnostic);
|
|
558
586
|
this.#reseedRetryCount += 1;
|
|
559
587
|
this.#waitingForGeometryCapture = true;
|
|
588
|
+
this.#historyAnchors.invalidate();
|
|
560
589
|
void this.#start.then(() => {
|
|
561
590
|
if (!this.#disposed)
|
|
562
591
|
this.#upstream?.reseed();
|
|
@@ -564,6 +593,7 @@ export class SessionRuntimeTerminalReplicaOwner {
|
|
|
564
593
|
}
|
|
565
594
|
#supervise(operation) {
|
|
566
595
|
void operation.catch((error) => {
|
|
596
|
+
this.#historyAnchors.invalidate();
|
|
567
597
|
this.#noteLifecycle("interpreter-fault");
|
|
568
598
|
this.#interpreter.abort(error);
|
|
569
599
|
this.#onFault?.(error);
|