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,3 +1,14 @@
|
|
|
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: readonly string[]): boolean {
|
|
6
|
+
return (
|
|
7
|
+
/^G [1-9][0-9]*x[1-9][0-9]* \([0-9]+\/[0-9]+\)$/.test(lines[0] ?? "") &&
|
|
8
|
+
/^\tL 0 \((?:-|0)\) flags=/.test(lines[1] ?? "")
|
|
9
|
+
);
|
|
10
|
+
}
|
|
11
|
+
|
|
1
12
|
/** Private native capture experiment; this is not the canonical renderer schema. */
|
|
2
13
|
export interface NativeGridCaptureCell {
|
|
3
14
|
readonly flags: number;
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
import type { MirrorChannelIo } from "./control-channel.ts";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
decodeNativeGridCapture,
|
|
4
|
+
isStockRawGridCapture,
|
|
5
|
+
type NativeGridCapture,
|
|
6
|
+
} from "./native-grid-capture.ts";
|
|
3
7
|
|
|
4
8
|
export type NativeGridReadResult =
|
|
5
9
|
| {
|
|
@@ -83,6 +87,11 @@ export class NativeGridCaptureReader {
|
|
|
83
87
|
finish({ status: "changed" });
|
|
84
88
|
return;
|
|
85
89
|
}
|
|
90
|
+
if (isStockRawGridCapture(reply.lines)) {
|
|
91
|
+
this.unsupported = true;
|
|
92
|
+
finish({ status: "unsupported" });
|
|
93
|
+
return;
|
|
94
|
+
}
|
|
86
95
|
const snapshot = decodeNativeGridCapture(reply.lines.join("\n"));
|
|
87
96
|
finish(
|
|
88
97
|
snapshot
|
|
@@ -10,6 +10,7 @@ import {
|
|
|
10
10
|
import { boundedTmuxInteractionAppendCommand } from "../../lib/tmux-interaction-retention.ts";
|
|
11
11
|
import {
|
|
12
12
|
decodeNativeGridCapture,
|
|
13
|
+
isStockRawGridCapture,
|
|
13
14
|
isNativeBootstrapCapture,
|
|
14
15
|
type NativeGridCapture,
|
|
15
16
|
} from "./native-grid-capture.ts";
|
|
@@ -212,7 +213,10 @@ function nativeBootstrapUnsupported(
|
|
|
212
213
|
native: NativeGridCapture | null,
|
|
213
214
|
): boolean {
|
|
214
215
|
if (ok)
|
|
215
|
-
return
|
|
216
|
+
return (
|
|
217
|
+
isStockRawGridCapture(lines) ||
|
|
218
|
+
(native !== null && (native.version !== 2 || native.currentAttributes === undefined))
|
|
219
|
+
);
|
|
216
220
|
return lines.some((line) =>
|
|
217
221
|
/^(?:parse error: )?(?:command capture-pane: )?unknown flag -R$/.test(line.trim()),
|
|
218
222
|
);
|
|
@@ -345,6 +349,7 @@ interface SnapshotLease {
|
|
|
345
349
|
cancelFence: (() => void) | null;
|
|
346
350
|
cleanup?: () => void;
|
|
347
351
|
ownsPause?: boolean;
|
|
352
|
+
attemptCharged?: boolean;
|
|
348
353
|
}
|
|
349
354
|
|
|
350
355
|
interface RecoveryRecord {
|
|
@@ -1959,6 +1964,7 @@ export class SessionChannel {
|
|
|
1959
1964
|
return;
|
|
1960
1965
|
}
|
|
1961
1966
|
recovery.attempts += 1;
|
|
1967
|
+
lease.attemptCharged = true;
|
|
1962
1968
|
recovery.stage = "stock-pause";
|
|
1963
1969
|
recovery.stock = new StockPaneSnapshot(this.snapshotContext(pane));
|
|
1964
1970
|
// These rows were received before this pause and belong to the new seed,
|
|
@@ -2135,6 +2141,7 @@ export class SessionChannel {
|
|
|
2135
2141
|
return;
|
|
2136
2142
|
}
|
|
2137
2143
|
recovery.attempts += 1;
|
|
2144
|
+
lease.attemptCharged = true;
|
|
2138
2145
|
const ordinal = ++recovery.reseedOrdinal;
|
|
2139
2146
|
const participants = [...pane.subs]
|
|
2140
2147
|
.filter((sub) => !sub.closed && !sub.frozen)
|
|
@@ -2924,6 +2931,12 @@ export class SessionChannel {
|
|
|
2924
2931
|
if (pane.windowRuntimeId !== change.windowId) continue;
|
|
2925
2932
|
const recovery = this.recoveries.get(pane.runtimeId);
|
|
2926
2933
|
if (recovery?.lease && !recovery.lease.retired) {
|
|
2934
|
+
// A newer layout cancels this capture; it is not a failed snapshot.
|
|
2935
|
+
// Refund only this lease, retaining earlier failures and the original deadline.
|
|
2936
|
+
if (recovery.lease.attemptCharged) {
|
|
2937
|
+
recovery.lease.attemptCharged = false;
|
|
2938
|
+
recovery.attempts -= 1;
|
|
2939
|
+
}
|
|
2927
2940
|
this.awaitSnapshotLayout(recovery);
|
|
2928
2941
|
this.retrySnapshot(recovery);
|
|
2929
2942
|
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { PaneStreamLayoutSnapshotFrameSchemaZ, type WindowLinkTopology } from "@tmux-ide/contracts";
|
|
2
|
+
import type { TerminalDeliveryGraphics } from "../session-runtime/terminal-delivery-hub.ts";
|
|
2
3
|
import { z } from "zod";
|
|
3
4
|
import {
|
|
4
5
|
PANE_STREAM_MAX_HELD_DELTAS,
|
|
@@ -8,6 +9,7 @@ import {
|
|
|
8
9
|
PANE_STREAM_PROTOCOL_VERSION,
|
|
9
10
|
PANE_STREAM_REDEEM_PATH,
|
|
10
11
|
PANE_STREAM_WEBSOCKET_SUBPROTOCOL,
|
|
12
|
+
TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
|
|
11
13
|
PaneStreamClientFrameSchemaZ,
|
|
12
14
|
PaneStreamLoopbackWebSocketUrlSchemaZ,
|
|
13
15
|
PaneStreamRedeemFrameSchemaZ,
|
|
@@ -314,6 +316,7 @@ export interface SessionRuntimeTerminalDeliveryConnection {
|
|
|
314
316
|
nack(nack: TerminalDeliveryNack): void;
|
|
315
317
|
setVisibility(visibility: TerminalDeliveryVisibility): void;
|
|
316
318
|
close(): Promise<void>;
|
|
319
|
+
readonly graphics?: TerminalDeliveryGraphics;
|
|
317
320
|
}
|
|
318
321
|
|
|
319
322
|
interface PendingTicket {
|
|
@@ -1039,6 +1042,10 @@ interface PaneChannel {
|
|
|
1039
1042
|
frozenByWire: boolean;
|
|
1040
1043
|
closed: boolean;
|
|
1041
1044
|
batch: SeedBatch | null;
|
|
1045
|
+
/** Kitty graphics: state announced to this client, and blob digests queued. */
|
|
1046
|
+
graphicsAnnounced: boolean;
|
|
1047
|
+
graphicsQueue: string[];
|
|
1048
|
+
graphicsSending: boolean;
|
|
1042
1049
|
}
|
|
1043
1050
|
|
|
1044
1051
|
interface QueuedSend {
|
|
@@ -1046,6 +1053,9 @@ interface QueuedSend {
|
|
|
1046
1053
|
remaining: number;
|
|
1047
1054
|
}
|
|
1048
1055
|
|
|
1056
|
+
const GRAPHICS_MAX_QUEUED_DIGESTS = 256;
|
|
1057
|
+
const GRAPHICS_SEND_WINDOW_BYTES = 2 * 1024 * 1024;
|
|
1058
|
+
|
|
1049
1059
|
export class PaneStreamLiveConnection {
|
|
1050
1060
|
readonly #clientId: string;
|
|
1051
1061
|
readonly #socket: DirectTerminalSocket;
|
|
@@ -1214,6 +1224,9 @@ export class PaneStreamLiveConnection {
|
|
|
1214
1224
|
frozenByWire: false,
|
|
1215
1225
|
closed: false,
|
|
1216
1226
|
batch: null,
|
|
1227
|
+
graphicsAnnounced: false,
|
|
1228
|
+
graphicsQueue: [],
|
|
1229
|
+
graphicsSending: false,
|
|
1217
1230
|
});
|
|
1218
1231
|
}
|
|
1219
1232
|
}
|
|
@@ -1619,6 +1632,7 @@ export class PaneStreamLiveConnection {
|
|
|
1619
1632
|
await this.#sendTerminalDelivery(channel.semanticPaneId, message);
|
|
1620
1633
|
pending.length = 0;
|
|
1621
1634
|
markReady();
|
|
1635
|
+
this.#startGraphics(channel);
|
|
1622
1636
|
}
|
|
1623
1637
|
this.#closeIfAllPanesGone();
|
|
1624
1638
|
}
|
|
@@ -1730,6 +1744,70 @@ export class PaneStreamLiveConnection {
|
|
|
1730
1744
|
await this.#awaitSemanticCredit(pane);
|
|
1731
1745
|
}
|
|
1732
1746
|
|
|
1747
|
+
/** Announce the pane's kitty graphics state; the client pulls bytes it lacks. */
|
|
1748
|
+
#startGraphics(channel: PaneChannel): void {
|
|
1749
|
+
const graphics = channel.delivery?.graphics;
|
|
1750
|
+
if (!graphics) return;
|
|
1751
|
+
graphics.subscribe((state) => {
|
|
1752
|
+
if (this.#closed || channel.closed) return;
|
|
1753
|
+
channel.graphicsAnnounced = true;
|
|
1754
|
+
this.#sendFrame(null, { type: "terminal-graphics", pane: channel.semanticPaneId, state });
|
|
1755
|
+
});
|
|
1756
|
+
}
|
|
1757
|
+
|
|
1758
|
+
#requestGraphics(pane: string, digests: readonly string[]): void {
|
|
1759
|
+
const channel = this.#panes.get(pane);
|
|
1760
|
+
// Requests are only meaningful after an announcement; ignore stray ones.
|
|
1761
|
+
if (!channel || channel.closed || !channel.graphicsAnnounced) return;
|
|
1762
|
+
for (const digest of digests)
|
|
1763
|
+
if (
|
|
1764
|
+
!channel.graphicsQueue.includes(digest) &&
|
|
1765
|
+
channel.graphicsQueue.length < GRAPHICS_MAX_QUEUED_DIGESTS
|
|
1766
|
+
)
|
|
1767
|
+
channel.graphicsQueue.push(digest);
|
|
1768
|
+
if (!channel.graphicsSending) void this.#drainGraphics(channel);
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
/**
|
|
1772
|
+
* Blob bytes yield to terminal frames: each chunk waits until the socket's
|
|
1773
|
+
* buffered bytes fall below a small window, so images never trip the
|
|
1774
|
+
* socket-wide backpressure ceiling or delay a pane's next seed for long.
|
|
1775
|
+
*/
|
|
1776
|
+
async #drainGraphics(channel: PaneChannel): Promise<void> {
|
|
1777
|
+
channel.graphicsSending = true;
|
|
1778
|
+
try {
|
|
1779
|
+
while (!this.#closed && !channel.closed && channel.graphicsQueue.length > 0) {
|
|
1780
|
+
const digest = channel.graphicsQueue.shift()!;
|
|
1781
|
+
const image = channel.delivery?.graphics?.image(digest);
|
|
1782
|
+
if (!image) continue;
|
|
1783
|
+
const total = image.data.byteLength;
|
|
1784
|
+
for (let offset = 0; offset < total; offset += TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES) {
|
|
1785
|
+
while ((this.#socket.bufferedAmount ?? 0) > GRAPHICS_SEND_WINDOW_BYTES) {
|
|
1786
|
+
if (this.#closed || channel.closed) return;
|
|
1787
|
+
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
1788
|
+
}
|
|
1789
|
+
if (this.#closed || channel.closed) return;
|
|
1790
|
+
const end = Math.min(total, offset + TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES);
|
|
1791
|
+
this.#sendFrame(null, {
|
|
1792
|
+
type: "terminal-graphics-blob",
|
|
1793
|
+
pane: channel.semanticPaneId,
|
|
1794
|
+
blob: {
|
|
1795
|
+
digest,
|
|
1796
|
+
format: image.format,
|
|
1797
|
+
width: image.width,
|
|
1798
|
+
height: image.height,
|
|
1799
|
+
total,
|
|
1800
|
+
offset,
|
|
1801
|
+
},
|
|
1802
|
+
data: Buffer.from(image.data.subarray(offset, end)).toString("base64"),
|
|
1803
|
+
});
|
|
1804
|
+
}
|
|
1805
|
+
}
|
|
1806
|
+
} finally {
|
|
1807
|
+
channel.graphicsSending = false;
|
|
1808
|
+
}
|
|
1809
|
+
}
|
|
1810
|
+
|
|
1733
1811
|
#awaitSemanticCredit(pane: string): Promise<void> {
|
|
1734
1812
|
// Backpressure is pane-local. Aggregate pressure is guarded by the hard
|
|
1735
1813
|
// socket ceiling in #drainNow, but must not let a noisy sibling prevent a
|
|
@@ -2301,6 +2379,10 @@ export class PaneStreamLiveConnection {
|
|
|
2301
2379
|
this.#acceptSemanticIntent(frame.operationId, frame.intent);
|
|
2302
2380
|
return;
|
|
2303
2381
|
}
|
|
2382
|
+
if (frame.type === "terminal-graphics-request") {
|
|
2383
|
+
this.#requestGraphics(frame.pane, frame.digests);
|
|
2384
|
+
return;
|
|
2385
|
+
}
|
|
2304
2386
|
if (frame.type === "viewport") {
|
|
2305
2387
|
if (
|
|
2306
2388
|
!this.#descriptor.terminalDelivery ||
|
|
@@ -183,7 +183,6 @@ export function parseSessionPaneDescriptorReply(
|
|
|
183
183
|
!bounded(type, 256) ||
|
|
184
184
|
!bounded(currentCommand, 512) ||
|
|
185
185
|
!bounded(cwd, 4_096) ||
|
|
186
|
-
!bounded(title, 1_024) ||
|
|
187
186
|
!bounded(windowName, 1_024) ||
|
|
188
187
|
!bounded(name, 256) ||
|
|
189
188
|
!bounded(missionStamp, 256) ||
|
|
@@ -203,7 +202,9 @@ export function parseSessionPaneDescriptorReply(
|
|
|
203
202
|
type: nonempty(type),
|
|
204
203
|
currentCommand: nonempty(currentCommand),
|
|
205
204
|
cwd: nonempty(cwd),
|
|
206
|
-
|
|
205
|
+
// Stock tmux turns any APC, including kitty graphics payloads, into the
|
|
206
|
+
// pane title; an overlong title names nothing and must not drop the pane.
|
|
207
|
+
title: bounded(title, 1_024) ? nonempty(title) : null,
|
|
207
208
|
windowIndex,
|
|
208
209
|
windowName: nonempty(windowName),
|
|
209
210
|
windowId: /^@(?:0|[1-9][0-9]*)$/u.test(windowId) ? windowId : null,
|
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import { randomUUID } from "node:crypto";
|
|
2
|
+
import type { CanonicalTerminalReplicaUpdate } from "@tmux-ide/contracts";
|
|
3
|
+
|
|
4
|
+
export interface HistoryAnchorProof {
|
|
5
|
+
readonly workspaceName: string;
|
|
6
|
+
readonly semanticPaneId: string;
|
|
7
|
+
readonly generation: string;
|
|
8
|
+
readonly incarnation: string;
|
|
9
|
+
readonly revision: number;
|
|
10
|
+
readonly stateHash: string;
|
|
11
|
+
}
|
|
12
|
+
export type HistoryAnchorIdentity = Pick<
|
|
13
|
+
HistoryAnchorProof,
|
|
14
|
+
"workspaceName" | "semanticPaneId" | "generation" | "incarnation"
|
|
15
|
+
>;
|
|
16
|
+
type Failure = { readonly status: "unavailable" | "stale" | "lost" };
|
|
17
|
+
const MAX_TOKENS = 32;
|
|
18
|
+
const PROOF_TTL_MS = 5 * 60_000;
|
|
19
|
+
|
|
20
|
+
/** Owner-private row ordinals. Never changes terminal v1 payloads or hashes. */
|
|
21
|
+
export class HistoryAnchorOwner {
|
|
22
|
+
#current: (HistoryAnchorProof & { cols: number; rows: number; alternate: boolean }) | null = null;
|
|
23
|
+
#first = 0;
|
|
24
|
+
#length = 0;
|
|
25
|
+
#proofs = new Map<string, { first: number; length: number; expires: number }>();
|
|
26
|
+
#tokens = new Map<string, { ordinal: number }>();
|
|
27
|
+
constructor(private readonly now: () => number = () => performance.now()) {}
|
|
28
|
+
|
|
29
|
+
invalidate(): void {
|
|
30
|
+
this.#current = null;
|
|
31
|
+
this.#first = 0;
|
|
32
|
+
this.#length = 0;
|
|
33
|
+
this.#tokens.clear();
|
|
34
|
+
this.#proofs.clear();
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
observe(update: CanonicalTerminalReplicaUpdate): void {
|
|
38
|
+
if (update.type === "terminal.tombstone") {
|
|
39
|
+
this.invalidate();
|
|
40
|
+
return;
|
|
41
|
+
}
|
|
42
|
+
if (update.type === "terminal.seed") {
|
|
43
|
+
this.invalidate();
|
|
44
|
+
this.#length = update.snapshot.history.length;
|
|
45
|
+
this.#current = {
|
|
46
|
+
workspaceName: update.workspaceName,
|
|
47
|
+
semanticPaneId: update.semanticPaneId,
|
|
48
|
+
generation: update.generation,
|
|
49
|
+
incarnation: update.incarnation,
|
|
50
|
+
revision: update.revision,
|
|
51
|
+
stateHash: update.stateHash,
|
|
52
|
+
cols: update.cols,
|
|
53
|
+
rows: update.rows,
|
|
54
|
+
alternate: update.snapshot.modes.alternateScreen,
|
|
55
|
+
};
|
|
56
|
+
this.#remember();
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
const old = this.#current;
|
|
60
|
+
if (
|
|
61
|
+
!old ||
|
|
62
|
+
old.workspaceName !== update.workspaceName ||
|
|
63
|
+
old.semanticPaneId !== update.semanticPaneId ||
|
|
64
|
+
old.generation !== update.generation ||
|
|
65
|
+
old.incarnation !== update.incarnation ||
|
|
66
|
+
old.revision !== update.baseRevision
|
|
67
|
+
) {
|
|
68
|
+
this.invalidate();
|
|
69
|
+
return;
|
|
70
|
+
}
|
|
71
|
+
const delta = update.patch.historyDelta;
|
|
72
|
+
const alternate = update.patch.modes?.alternateScreen ?? old.alternate;
|
|
73
|
+
if (
|
|
74
|
+
delta &&
|
|
75
|
+
(!Number.isSafeInteger(delta.trim) || delta.trim < 0 || delta.trim > this.#length)
|
|
76
|
+
) {
|
|
77
|
+
this.invalidate();
|
|
78
|
+
return;
|
|
79
|
+
}
|
|
80
|
+
const length =
|
|
81
|
+
update.patch.history?.length ??
|
|
82
|
+
this.#length - (delta?.trim ?? 0) + (delta?.append.length ?? 0);
|
|
83
|
+
const reset =
|
|
84
|
+
update.patch.history !== undefined ||
|
|
85
|
+
update.cols !== old.cols ||
|
|
86
|
+
update.rows !== old.rows ||
|
|
87
|
+
alternate !== old.alternate;
|
|
88
|
+
if (reset) {
|
|
89
|
+
this.#tokens.clear();
|
|
90
|
+
this.#proofs.clear();
|
|
91
|
+
this.#first = 0;
|
|
92
|
+
} else this.#first += delta?.trim ?? 0;
|
|
93
|
+
if (!Number.isSafeInteger(this.#first + length)) {
|
|
94
|
+
this.invalidate();
|
|
95
|
+
return;
|
|
96
|
+
}
|
|
97
|
+
this.#length = length;
|
|
98
|
+
this.#current = {
|
|
99
|
+
workspaceName: update.workspaceName,
|
|
100
|
+
semanticPaneId: update.semanticPaneId,
|
|
101
|
+
generation: update.generation,
|
|
102
|
+
incarnation: update.incarnation,
|
|
103
|
+
revision: update.revision,
|
|
104
|
+
stateHash: update.stateHash,
|
|
105
|
+
cols: update.cols,
|
|
106
|
+
rows: update.rows,
|
|
107
|
+
alternate,
|
|
108
|
+
};
|
|
109
|
+
this.#prune();
|
|
110
|
+
this.#remember();
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
capture(
|
|
114
|
+
proof: HistoryAnchorProof,
|
|
115
|
+
historyIndex: number,
|
|
116
|
+
): Failure | { status: "captured"; token: string } {
|
|
117
|
+
const failure = this.#admitIdentity(proof);
|
|
118
|
+
if (failure) return failure;
|
|
119
|
+
this.#prune();
|
|
120
|
+
const known =
|
|
121
|
+
this.#current && this.#key(proof) === this.#key(this.#current)
|
|
122
|
+
? { first: this.#first, length: this.#length }
|
|
123
|
+
: this.#proofs.get(this.#key(proof));
|
|
124
|
+
if (!known) return { status: "stale" };
|
|
125
|
+
if (!Number.isSafeInteger(historyIndex) || historyIndex < 0 || historyIndex >= known.length)
|
|
126
|
+
return { status: "lost" };
|
|
127
|
+
const ordinal = known.first + historyIndex;
|
|
128
|
+
if (ordinal < this.#first || ordinal >= this.#first + this.#length) return { status: "lost" };
|
|
129
|
+
while (this.#tokens.size >= MAX_TOKENS) this.#tokens.delete(this.#tokens.keys().next().value!);
|
|
130
|
+
const token = randomUUID();
|
|
131
|
+
// Saved tabs retain identity until capacity eviction or actual history loss.
|
|
132
|
+
this.#tokens.set(token, { ordinal });
|
|
133
|
+
return { status: "captured", token };
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
resolve(
|
|
137
|
+
proof: HistoryAnchorIdentity,
|
|
138
|
+
token: string,
|
|
139
|
+
): Failure | { status: "resolved"; proof: HistoryAnchorProof; historyIndex: number } {
|
|
140
|
+
const failure = this.#admitIdentity(proof);
|
|
141
|
+
if (failure) return failure;
|
|
142
|
+
this.#prune();
|
|
143
|
+
const entry = this.#tokens.get(token);
|
|
144
|
+
if (!entry) return { status: "lost" };
|
|
145
|
+
const { workspaceName, semanticPaneId, generation, incarnation, revision, stateHash } =
|
|
146
|
+
this.#current!;
|
|
147
|
+
return {
|
|
148
|
+
status: "resolved",
|
|
149
|
+
proof: { workspaceName, semanticPaneId, generation, incarnation, revision, stateHash },
|
|
150
|
+
historyIndex: entry.ordinal - this.#first,
|
|
151
|
+
};
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
#admitIdentity(proof: HistoryAnchorIdentity): Failure | null {
|
|
155
|
+
const current = this.#current;
|
|
156
|
+
if (!current || current.alternate) return { status: "unavailable" };
|
|
157
|
+
return current.workspaceName === proof.workspaceName &&
|
|
158
|
+
current.semanticPaneId === proof.semanticPaneId &&
|
|
159
|
+
current.generation === proof.generation &&
|
|
160
|
+
current.incarnation === proof.incarnation
|
|
161
|
+
? null
|
|
162
|
+
: { status: "stale" };
|
|
163
|
+
}
|
|
164
|
+
#key(proof: HistoryAnchorProof): string {
|
|
165
|
+
return JSON.stringify([
|
|
166
|
+
proof.workspaceName,
|
|
167
|
+
proof.semanticPaneId,
|
|
168
|
+
proof.generation,
|
|
169
|
+
proof.incarnation,
|
|
170
|
+
proof.revision,
|
|
171
|
+
proof.stateHash,
|
|
172
|
+
]);
|
|
173
|
+
}
|
|
174
|
+
#remember(): void {
|
|
175
|
+
if (!this.#current || this.#current.alternate) return;
|
|
176
|
+
while (this.#proofs.size >= 64) this.#proofs.delete(this.#proofs.keys().next().value!);
|
|
177
|
+
this.#proofs.set(this.#key(this.#current), {
|
|
178
|
+
first: this.#first,
|
|
179
|
+
length: this.#length,
|
|
180
|
+
expires: this.now() + PROOF_TTL_MS,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
#prune(): void {
|
|
184
|
+
const now = this.now();
|
|
185
|
+
for (const [key, value] of this.#proofs) if (value.expires <= now) this.#proofs.delete(key);
|
|
186
|
+
for (const [token, entry] of this.#tokens)
|
|
187
|
+
if (entry.ordinal < this.#first || entry.ordinal >= this.#first + this.#length)
|
|
188
|
+
this.#tokens.delete(token);
|
|
189
|
+
}
|
|
190
|
+
}
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { HistoryAnchorRequest, HistoryAnchorResult } from "@tmux-ide/contracts";
|
|
1
2
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
2
3
|
import type { OwnedNativePlanCompletion } from "../../lib/owned-native-interaction-bindings.ts";
|
|
3
4
|
import type {
|
|
@@ -875,6 +876,12 @@ export class SessionRuntimeRegistry implements PaneStreamMirror {
|
|
|
875
876
|
return await this.#runtime(session).describe();
|
|
876
877
|
}
|
|
877
878
|
|
|
879
|
+
historyAnchor(session: string, request: HistoryAnchorRequest): HistoryAnchorResult {
|
|
880
|
+
const runtime = this.#sessions.get(session);
|
|
881
|
+
if (this.#disposed || !runtime) return { status: "unavailable" };
|
|
882
|
+
return runtime.historyAnchor(request);
|
|
883
|
+
}
|
|
884
|
+
|
|
878
885
|
async captureNativeBacking(
|
|
879
886
|
session: string,
|
|
880
887
|
semanticPaneId: string,
|
|
@@ -1880,6 +1887,18 @@ class SessionRuntime {
|
|
|
1880
1887
|
}
|
|
1881
1888
|
}
|
|
1882
1889
|
|
|
1890
|
+
historyAnchor(request: HistoryAnchorRequest): HistoryAnchorResult {
|
|
1891
|
+
if (this.#disposed) return { status: "unavailable" };
|
|
1892
|
+
const owner = this.#terminalReplicas.get(request.proof.semanticPaneId);
|
|
1893
|
+
if (!owner) return { status: "unavailable" };
|
|
1894
|
+
if (request.operation.kind === "capture") {
|
|
1895
|
+
if (!("revision" in request.proof) || !("stateHash" in request.proof))
|
|
1896
|
+
return { status: "stale" };
|
|
1897
|
+
return owner.captureHistoryAnchor(request.proof, request.operation.historyIndex);
|
|
1898
|
+
}
|
|
1899
|
+
return owner.resolveHistoryAnchor(request.proof, request.operation.token);
|
|
1900
|
+
}
|
|
1901
|
+
|
|
1883
1902
|
async captureNativeBacking(
|
|
1884
1903
|
semanticPaneId: string,
|
|
1885
1904
|
expected?: NativeBackingIdentity,
|
|
@@ -2424,6 +2443,7 @@ class SessionRuntimeConsumerImpl implements SessionRuntimeConsumer {
|
|
|
2424
2443
|
this.#deliveryConnections.delete(connection);
|
|
2425
2444
|
await upstream.close();
|
|
2426
2445
|
},
|
|
2446
|
+
...(upstream.graphics ? { graphics: upstream.graphics } : {}),
|
|
2427
2447
|
};
|
|
2428
2448
|
this.#deliveryConnections.add(connection);
|
|
2429
2449
|
return connection;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { KittyImage } from "../graphics/kitty-graphics.ts";
|
|
1
2
|
import { StockCaptureTabUnavailableError } from "./stock-capture-fidelity.ts";
|
|
2
3
|
import {
|
|
3
4
|
MAX_RETAINED_NATIVE_BACKING_BYTES,
|
|
@@ -8,6 +9,8 @@ import {
|
|
|
8
9
|
} from "./native-seed-backing.ts";
|
|
9
10
|
import {
|
|
10
11
|
WorkspaceIdSchemaZ,
|
|
12
|
+
TERMINAL_GRAPHICS_CAPABILITY_VERSION,
|
|
13
|
+
type TerminalGraphicsState,
|
|
11
14
|
TERMINAL_DELIVERY_PATCH_TO_SEED_BYTES,
|
|
12
15
|
TERMINAL_DELIVERY_MAX_REPRESENTATION_BYTES,
|
|
13
16
|
TerminalDeliveryEnvelopeSchemaZ,
|
|
@@ -124,6 +127,17 @@ export interface TerminalDeliveryConnection {
|
|
|
124
127
|
nack(nack: TerminalDeliveryNack): void;
|
|
125
128
|
setVisibility(visibility: TerminalDeliveryVisibility): void;
|
|
126
129
|
close(): Promise<void>;
|
|
130
|
+
/**
|
|
131
|
+
* Kitty graphics side channel, present only when the client offered
|
|
132
|
+
* TERMINAL_GRAPHICS_CAPABILITY_VERSION. Delivery is independent of the
|
|
133
|
+
* canonical FIFO: renderers resolve state against cells they already hold.
|
|
134
|
+
*/
|
|
135
|
+
readonly graphics?: TerminalDeliveryGraphics;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface TerminalDeliveryGraphics {
|
|
139
|
+
subscribe(listener: (state: TerminalGraphicsState) => void): () => void;
|
|
140
|
+
image(digest: string): KittyImage | null;
|
|
127
141
|
}
|
|
128
142
|
|
|
129
143
|
interface NativeBackingLease {
|
|
@@ -173,6 +187,8 @@ export interface TerminalDeliverySourceOwner {
|
|
|
173
187
|
) => void,
|
|
174
188
|
onRaw: (record: TerminalReplicaCommittedRaw) => void,
|
|
175
189
|
): Promise<TerminalReplicaSourceSubscription>;
|
|
190
|
+
subscribeGraphics?(listener: (state: TerminalGraphicsState) => void): () => void;
|
|
191
|
+
graphicsImage?(digest: string): KittyImage | null;
|
|
176
192
|
}
|
|
177
193
|
|
|
178
194
|
interface ClientState {
|
|
@@ -451,6 +467,9 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
451
467
|
this.#recordDeliveryStatus(client, this.#panes.get(client.paneId));
|
|
452
468
|
},
|
|
453
469
|
close: async () => this.#closeClient(client),
|
|
470
|
+
...(offer.protocolVersions.includes(TERMINAL_GRAPHICS_CAPABILITY_VERSION)
|
|
471
|
+
? { graphics: this.#graphicsFor(semanticPaneId, client, closed) }
|
|
472
|
+
: {}),
|
|
454
473
|
};
|
|
455
474
|
} catch (error) {
|
|
456
475
|
if (!(error instanceof StockCaptureTabUnavailableError)) throw error;
|
|
@@ -474,6 +493,25 @@ export class SessionRuntimeTerminalDeliveryHub {
|
|
|
474
493
|
}
|
|
475
494
|
}
|
|
476
495
|
|
|
496
|
+
#graphicsFor(
|
|
497
|
+
semanticPaneId: string,
|
|
498
|
+
client: ClientState,
|
|
499
|
+
closed: Promise<unknown>,
|
|
500
|
+
): TerminalDeliveryGraphics {
|
|
501
|
+
const owner = this.#ownerForPane(semanticPaneId);
|
|
502
|
+
return {
|
|
503
|
+
subscribe: (listener) => {
|
|
504
|
+
if (client.closed || !owner.subscribeGraphics) return () => {};
|
|
505
|
+
const unsubscribe = owner.subscribeGraphics((state) => {
|
|
506
|
+
if (!client.closed) listener(state);
|
|
507
|
+
});
|
|
508
|
+
void closed.then(unsubscribe);
|
|
509
|
+
return unsubscribe;
|
|
510
|
+
},
|
|
511
|
+
image: (digest) => (client.closed ? null : (owner.graphicsImage?.(digest) ?? null)),
|
|
512
|
+
};
|
|
513
|
+
}
|
|
514
|
+
|
|
477
515
|
/** Retire only this pane's delivery owners before its canonical owner is disposed. */
|
|
478
516
|
failPaneCapture(semanticPaneId: string, error: StockCaptureTabUnavailableError): void {
|
|
479
517
|
for (const client of this.#clients.values())
|
|
@@ -25,6 +25,10 @@ export interface TerminalInterpreterBackend {
|
|
|
25
25
|
/** Public-parser diagnostic OSC seam; installed only for an active probe. */
|
|
26
26
|
registerOscHandler(identifier: number, handler: (data: string) => boolean): () => void;
|
|
27
27
|
write(data: Uint8Array | string): Promise<void>;
|
|
28
|
+
/** SPIKE (kitty graphics): cursor as an absolute buffer line, read mid-parse. */
|
|
29
|
+
graphicsCursor?(): { readonly x: number; readonly line: number; readonly alternate: boolean };
|
|
30
|
+
/** SPIKE (kitty graphics): absolute buffer line of viewport row 0. */
|
|
31
|
+
graphicsViewportTop?(): number;
|
|
28
32
|
/** Exact native painted state; false means this parser cannot import it. */
|
|
29
33
|
canImportNativeGrid?(): boolean;
|
|
30
34
|
importNativeGrid?(snapshot: NativeGridCapture): boolean;
|