tmux-ide 2.9.7 → 2.9.9
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 +323 -55
- package/package.json +2 -2
- package/packages/contracts/src/pane-stream.ts +17 -0
- package/packages/contracts/src/terminal-graphics.ts +29 -3
- package/packages/core/src/kitty-placeholders.ts +29 -11
- 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/manifest.json +6 -2
- package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +6 -2
- package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
- package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +6 -2
- 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 +146 -25
- package/packages/daemon/dist/terminal/mirror/mirror-service.js +6 -0
- package/packages/daemon/dist/terminal/mirror/session-channel.js +49 -5
- package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +109 -7
- package/packages/daemon/dist/terminal/session-runtime/registry.js +9 -1
- package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +19 -2
- package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +4 -0
- 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 +143 -28
- package/packages/daemon/src/terminal/mirror/mirror-service.ts +11 -0
- package/packages/daemon/src/terminal/mirror/session-channel.ts +65 -4
- package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +123 -6
- package/packages/daemon/src/terminal/session-runtime/registry.ts +18 -1
- package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +18 -2
- package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +5 -0
- package/packages/daemon-client/src/pane-stream-client.ts +8 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tmux-ide",
|
|
3
|
-
"version": "2.9.
|
|
3
|
+
"version": "2.9.9",
|
|
4
4
|
"description": "The open-source workspace for coding agents: run Claude Code, Codex and other agents in tmux with live status",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"bin": {
|
|
@@ -105,7 +105,7 @@
|
|
|
105
105
|
"test:tui-gallery": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./scripts/tui-gallery/gallery.test.tsx",
|
|
106
106
|
"typecheck:tui-gallery": "tsc --noEmit -p scripts/tsconfig.tui-gallery.json",
|
|
107
107
|
"test:spark-qualification": "node --import tsx --test scripts/lib/spark-*.test.ts scripts/lib/spark-*.test.mjs",
|
|
108
|
-
"test:installer": "node --test scripts/install.test.mjs",
|
|
108
|
+
"test:installer": "node --test scripts/install.test.mjs scripts/lib/installer-service-qualification.test.mjs",
|
|
109
109
|
"qualify:styled-ssh": "node scripts/qualify-styled-ssh.mjs"
|
|
110
110
|
},
|
|
111
111
|
"keywords": [
|
|
@@ -360,6 +360,22 @@ export const PaneStreamTerminalGraphicsRequestFrameSchemaZ = z
|
|
|
360
360
|
})
|
|
361
361
|
.strict();
|
|
362
362
|
|
|
363
|
+
/** The renderer's terminal cell size in device pixels (see capability 103). */
|
|
364
|
+
export const PaneStreamTerminalCellPixelsFrameSchemaZ = z
|
|
365
|
+
.object({
|
|
366
|
+
type: z.literal("terminal-cell-pixels"),
|
|
367
|
+
width: z.number().int().min(1).max(1024),
|
|
368
|
+
height: z.number().int().min(1).max(1024),
|
|
369
|
+
precise: z
|
|
370
|
+
.object({
|
|
371
|
+
width: z.number().int().min(1000).max(1024000),
|
|
372
|
+
height: z.number().int().min(1000).max(1024000),
|
|
373
|
+
})
|
|
374
|
+
.strict()
|
|
375
|
+
.optional(),
|
|
376
|
+
})
|
|
377
|
+
.strict();
|
|
378
|
+
|
|
363
379
|
export const PaneStreamClientFrameSchemaZ = z.union([
|
|
364
380
|
PaneStreamInputFrameSchemaZ,
|
|
365
381
|
PaneStreamConsumedFrameSchemaZ,
|
|
@@ -374,6 +390,7 @@ export const PaneStreamClientFrameSchemaZ = z.union([
|
|
|
374
390
|
PaneStreamAuthorityReleaseFrameSchemaZ,
|
|
375
391
|
PaneStreamClockProbeFrameSchemaZ,
|
|
376
392
|
PaneStreamTerminalGraphicsRequestFrameSchemaZ,
|
|
393
|
+
PaneStreamTerminalCellPixelsFrameSchemaZ,
|
|
377
394
|
]);
|
|
378
395
|
export type PaneStreamClientFrame = z.infer<typeof PaneStreamClientFrameSchemaZ>;
|
|
379
396
|
|
|
@@ -15,16 +15,39 @@ import { z } from "zod";
|
|
|
15
15
|
* version; daemons that know it send graphics frames only to such renderers.
|
|
16
16
|
*/
|
|
17
17
|
export const TERMINAL_GRAPHICS_CAPABILITY_VERSION = 101;
|
|
18
|
+
/** Renderers that inflate `rgba-zlib`/`rgb-zlib` themselves offer this too. */
|
|
19
|
+
export const TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION = 102;
|
|
20
|
+
/**
|
|
21
|
+
* Renderers that report their cell size in device pixels offer this; the
|
|
22
|
+
* daemon then marks graphics state with `cellPixels: true` and accepts
|
|
23
|
+
* `terminal-cell-pixels`, so programs size images 1:1 for the window.
|
|
24
|
+
*/
|
|
25
|
+
export const TERMINAL_GRAPHICS_CELL_PIXELS_CAPABILITY_VERSION = 103;
|
|
26
|
+
/** Thousandths of a device pixel, avoiding accumulated per-cell rounding. */
|
|
27
|
+
export const TERMINAL_GRAPHICS_PRECISE_PIXELS_CAPABILITY_VERSION = 104;
|
|
18
28
|
/** Raw image bytes per blob frame (base64 on the wire stays below 1 MiB). */
|
|
19
29
|
export const TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES = 512 * 1024;
|
|
20
|
-
/** Largest single image a daemon publishes. */
|
|
21
|
-
export const TERMINAL_GRAPHICS_MAX_IMAGE_BYTES =
|
|
30
|
+
/** Largest single image a daemon publishes (a 4K RGBA frame is ~33 MB). */
|
|
31
|
+
export const TERMINAL_GRAPHICS_MAX_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
32
|
+
/** What renderers without the zlib capability accept. */
|
|
33
|
+
export const TERMINAL_GRAPHICS_LEGACY_MAX_IMAGE_BYTES = 32 * 1024 * 1024;
|
|
22
34
|
export const TERMINAL_GRAPHICS_MAX_IMAGES = 256;
|
|
23
35
|
export const TERMINAL_GRAPHICS_MAX_PLACEMENTS = 256;
|
|
24
36
|
export const TERMINAL_GRAPHICS_MAX_REQUEST_DIGESTS = 64;
|
|
25
37
|
|
|
26
38
|
export const TerminalGraphicsDigestSchemaZ = z.string().regex(/^[0-9a-f]{32}$/u);
|
|
27
|
-
|
|
39
|
+
/**
|
|
40
|
+
* `*-zlib` carry raw pixels still deflated as the program sent them (`o=z`);
|
|
41
|
+
* only renderers that offered {@link TERMINAL_GRAPHICS_ZLIB_CAPABILITY_VERSION}
|
|
42
|
+
* receive them.
|
|
43
|
+
*/
|
|
44
|
+
export const TerminalGraphicsFormatSchemaZ = z.enum([
|
|
45
|
+
"png",
|
|
46
|
+
"rgba",
|
|
47
|
+
"rgb",
|
|
48
|
+
"rgba-zlib",
|
|
49
|
+
"rgb-zlib",
|
|
50
|
+
]);
|
|
28
51
|
export type TerminalGraphicsFormat = z.infer<typeof TerminalGraphicsFormatSchemaZ>;
|
|
29
52
|
const Dimension = z.number().int().min(1).max(10_000);
|
|
30
53
|
const Cells = z.number().int().min(1).max(1_000);
|
|
@@ -66,6 +89,9 @@ export const TerminalGraphicsStateSchemaZ = z
|
|
|
66
89
|
revision: z.number().int().nonnegative(),
|
|
67
90
|
images: z.array(TerminalGraphicsImageSchemaZ).max(TERMINAL_GRAPHICS_MAX_IMAGES),
|
|
68
91
|
placements: z.array(TerminalGraphicsPlacementSchemaZ).max(TERMINAL_GRAPHICS_MAX_PLACEMENTS),
|
|
92
|
+
/** The daemon accepts `terminal-cell-pixels` (only sent to renderers offering 103). */
|
|
93
|
+
cellPixels: z.literal(true).optional(),
|
|
94
|
+
preciseCellPixels: z.literal(true).optional(),
|
|
69
95
|
})
|
|
70
96
|
.strict();
|
|
71
97
|
export type TerminalGraphicsState = z.infer<typeof TerminalGraphicsStateSchemaZ>;
|
|
@@ -158,7 +158,19 @@ export function resolveKittyPlacements(
|
|
|
158
158
|
grid: readonly TerminalReplicaRow[],
|
|
159
159
|
state: TerminalGraphicsState,
|
|
160
160
|
): TerminalGraphicsPlacement[] {
|
|
161
|
-
|
|
161
|
+
return resolveKittyImagePlacements(grid, state).map(({ placement }) => placement);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* As {@link resolveKittyPlacements}, with the kitty image id each placement
|
|
166
|
+
* shows: exact for placeholder cells (they encode it); for cursor placements
|
|
167
|
+
* the first image with that content.
|
|
168
|
+
*/
|
|
169
|
+
export function resolveKittyImagePlacements(
|
|
170
|
+
grid: readonly TerminalReplicaRow[],
|
|
171
|
+
state: TerminalGraphicsState,
|
|
172
|
+
): { placement: TerminalGraphicsPlacement; imageId: number }[] {
|
|
173
|
+
const placements: { placement: TerminalGraphicsPlacement; imageId: number }[] = [];
|
|
162
174
|
const byId = new Map(state.images.map((image) => [image.imageId, image]));
|
|
163
175
|
let scan = false;
|
|
164
176
|
for (const image of state.images) if (image.placeholder) scan = true;
|
|
@@ -173,18 +185,24 @@ export function resolveKittyPlacements(
|
|
|
173
185
|
)
|
|
174
186
|
continue;
|
|
175
187
|
placements.push({
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
188
|
+
imageId: image.imageId,
|
|
189
|
+
placement: {
|
|
190
|
+
digest: image.digest,
|
|
191
|
+
row: rect.row,
|
|
192
|
+
column: rect.column,
|
|
193
|
+
rows: rect.rows,
|
|
194
|
+
columns: rect.columns,
|
|
195
|
+
sourceRow: rect.srcRow,
|
|
196
|
+
sourceColumn: rect.srcCol,
|
|
197
|
+
totalRows: virtualGrid.rows,
|
|
198
|
+
totalColumns: virtualGrid.columns,
|
|
199
|
+
},
|
|
185
200
|
});
|
|
186
201
|
}
|
|
187
202
|
}
|
|
188
|
-
|
|
203
|
+
for (const placement of state.placements) {
|
|
204
|
+
const image = state.images.find((candidate) => candidate.digest === placement.digest);
|
|
205
|
+
if (image) placements.push({ placement, imageId: image.imageId });
|
|
206
|
+
}
|
|
189
207
|
return placements;
|
|
190
208
|
}
|
|
@@ -21,9 +21,9 @@
|
|
|
21
21
|
<key>CFBundlePackageType</key>
|
|
22
22
|
<string>APPL</string>
|
|
23
23
|
<key>CFBundleShortVersionString</key>
|
|
24
|
-
<string>2.9.
|
|
24
|
+
<string>2.9.9</string>
|
|
25
25
|
<key>CFBundleVersion</key>
|
|
26
|
-
<string>2.9.
|
|
26
|
+
<string>2.9.9</string>
|
|
27
27
|
<key>LSMinimumSystemVersion</key>
|
|
28
28
|
<string>11.0</string>
|
|
29
29
|
<key>LSUIElement</key>
|
|
Binary file
|
|
Binary file
|
package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</data>
|
|
11
11
|
<key>Resources/Assets.car</key>
|
|
12
12
|
<data>
|
|
13
|
-
|
|
13
|
+
SmelhC8h5DucT+rzFXGF+vwuM4M=
|
|
14
14
|
</data>
|
|
15
15
|
</dict>
|
|
16
16
|
<key>files2</key>
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
<dict>
|
|
27
27
|
<key>hash2</key>
|
|
28
28
|
<data>
|
|
29
|
-
|
|
29
|
+
jOmbXxbTijoy+G+1Ruyi6wQKiSnUSw3V8mkHnJwyMlg=
|
|
30
30
|
</data>
|
|
31
31
|
</dict>
|
|
32
32
|
</dict>
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"patch": "kitty-graphics.patch",
|
|
28
|
-
"patchSha256": "
|
|
28
|
+
"patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"patch": "precise-cell-pixels.patch",
|
|
32
|
+
"patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
|
|
29
33
|
}
|
|
30
34
|
],
|
|
31
35
|
"experimentalExtensions": [
|
|
@@ -44,7 +48,7 @@
|
|
|
44
48
|
"platform": "darwin",
|
|
45
49
|
"arch": "arm64",
|
|
46
50
|
"files": {
|
|
47
|
-
"tmux": "
|
|
51
|
+
"tmux": "78b5efc7cb7308e892487a0ea046c531a271680c812c8b8851796dbdf57b0fe8",
|
|
48
52
|
"lib/libutf8proc.3.3.0.dylib": "da17133f84767a9873ff61402ac1a9d6a19ddfc1efb90cffc7c894df68532ce2",
|
|
49
53
|
"lib/libncursesw.6.dylib": "c9b3c77b5874b0c694590774f7b7b1568b2bb13e82707bdf29cb7296446fe596",
|
|
50
54
|
"lib/libevent_core-2.1.7.dylib": "65a205eee103b07356c1d6b1102b6b6ca08b0abaad44d960a0dad809722d0a9d",
|
|
Binary file
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"patch": "kitty-graphics.patch",
|
|
28
|
-
"patchSha256": "
|
|
28
|
+
"patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"patch": "precise-cell-pixels.patch",
|
|
32
|
+
"patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
|
|
29
33
|
}
|
|
30
34
|
],
|
|
31
35
|
"experimentalExtensions": [
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
"platform": "linux",
|
|
73
77
|
"arch": "arm64",
|
|
74
78
|
"files": {
|
|
75
|
-
"tmux": "
|
|
79
|
+
"tmux": "23ce24278d4c778f6ce974c492e46fcc09e20de577125a841f1d5f91b21a79ce",
|
|
76
80
|
"lib/libutf8proc.so.3": "83cd7bce65037a46494b154b0f7bfd2479bc1659f56321b9ec896f4b64fb490c",
|
|
77
81
|
"lib/libtinfo.so.6": "22952a4f434fd0f356fad23b92de42084e6666d8a594eadd11511b059cc5bb66",
|
|
78
82
|
"lib/libevent_core-2.1.so.7": "80ad9f14cdea14f65c7a778fef4d831d51d23edc55efd263d7f265587eab73d4",
|
|
Binary file
|
|
@@ -25,7 +25,11 @@
|
|
|
25
25
|
},
|
|
26
26
|
{
|
|
27
27
|
"patch": "kitty-graphics.patch",
|
|
28
|
-
"patchSha256": "
|
|
28
|
+
"patchSha256": "1ff7b3022aa2a3d7014b7339c0f2ae868292d5d6df3c668cefa403509ecbc1f6"
|
|
29
|
+
},
|
|
30
|
+
{
|
|
31
|
+
"patch": "precise-cell-pixels.patch",
|
|
32
|
+
"patchSha256": "11b2f09f347d0369a744be5aef14e7be3407f4bfa6cae2047b9477234a01ba24"
|
|
29
33
|
}
|
|
30
34
|
],
|
|
31
35
|
"experimentalExtensions": [
|
|
@@ -72,7 +76,7 @@
|
|
|
72
76
|
"platform": "linux",
|
|
73
77
|
"arch": "x64",
|
|
74
78
|
"files": {
|
|
75
|
-
"tmux": "
|
|
79
|
+
"tmux": "ef3549656f724cb37e5e94f718d9997570fb3cf55bdafa2585fa1f827e66ba3c",
|
|
76
80
|
"lib/libutf8proc.so.3": "a5aa10c99ac2e9a30c4e01fa4baa85a03dadf71eb73119c76fb3f9f54798962d",
|
|
77
81
|
"lib/libtinfo.so.6": "170db4dd0c324ee73d8d5440a38708726066e1288aba942d57191c6f002240d9",
|
|
78
82
|
"lib/libevent_core-2.1.so.7": "adf343350b739d3dba2b8a6cccbe0729cd9e9ee32b38ba69c08bd14f24a8431e",
|
|
Binary file
|
|
@@ -583,6 +583,9 @@ function parsePaneSnapshot(stdout, expected) {
|
|
|
583
583
|
* application-shell inventory. Names are resolved exactly to `$session_id`
|
|
584
584
|
* before any pane query, and a second identity/topology snapshot closes races.
|
|
585
585
|
*/
|
|
586
|
+
const INVENTORY_FENCE_ATTEMPTS = 4;
|
|
587
|
+
/** 25, 50, 100 ms: enough for a mutation's identity stamps, bounded for every reader. */
|
|
588
|
+
const settleDelay = (attempt) => new Promise((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
|
|
586
589
|
export async function discoverWorkspaceRegistryTerminalInventory(registry, runner, signal) {
|
|
587
590
|
const workspaces = registry.list();
|
|
588
591
|
if (workspaces.length > MAX_DISCOVERED_WORKSPACES) {
|
|
@@ -604,13 +607,6 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
|
|
|
604
607
|
const before = await requiredTmuxResult(runner, argv, signal);
|
|
605
608
|
if (before === null)
|
|
606
609
|
continue;
|
|
607
|
-
const first = parsePaneSnapshot(before, identity);
|
|
608
|
-
const panes = first.panes;
|
|
609
|
-
const after = await requiredTmuxResult(runner, argv, signal);
|
|
610
|
-
if (after === null)
|
|
611
|
-
throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
612
|
-
const second = parsePaneSnapshot(after, identity);
|
|
613
|
-
const latest = second.panes;
|
|
614
610
|
// Foreground commands, titles, focus and paths can change while the same
|
|
615
611
|
// pane remains valid. Fence binding/topology only, then publish one complete
|
|
616
612
|
// latest snapshot rather than mixing presentation values across reads.
|
|
@@ -625,12 +621,29 @@ export async function discoverWorkspaceRegistryTerminalInventory(registry, runne
|
|
|
625
621
|
"windowStamp",
|
|
626
622
|
"index",
|
|
627
623
|
];
|
|
628
|
-
|
|
629
|
-
panes.length
|
|
630
|
-
panes.
|
|
631
|
-
|
|
624
|
+
const matches = (first, second) => first.linksProof === second.linksProof &&
|
|
625
|
+
first.panes.length === second.panes.length &&
|
|
626
|
+
first.panes.every((pane, index) => proofKeys.every((key) => pane[key] === second.panes[index][key]));
|
|
627
|
+
// A just-created window or split is still being stamped, so topology can
|
|
628
|
+
// move between two reads. Let it settle for a bounded number of re-reads;
|
|
629
|
+
// only two consecutive matching reads are ever published.
|
|
630
|
+
let previous = parsePaneSnapshot(before, identity);
|
|
631
|
+
let stable = null;
|
|
632
|
+
for (let attempt = 0; attempt < INVENTORY_FENCE_ATTEMPTS && !stable; attempt += 1) {
|
|
633
|
+
if (attempt > 0)
|
|
634
|
+
await settleDelay(attempt);
|
|
635
|
+
const after = await requiredTmuxResult(runner, argv, signal);
|
|
636
|
+
if (after === null)
|
|
637
|
+
throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
638
|
+
const next = parsePaneSnapshot(after, identity);
|
|
639
|
+
if (matches(previous, next))
|
|
640
|
+
stable = next;
|
|
641
|
+
else
|
|
642
|
+
previous = next;
|
|
632
643
|
}
|
|
633
|
-
|
|
644
|
+
if (!stable)
|
|
645
|
+
throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
|
|
646
|
+
bySessionName.set(sessionName, stable.panes);
|
|
634
647
|
}
|
|
635
648
|
const panes = [];
|
|
636
649
|
for (const workspace of workspaces) {
|
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
* marker OSC the scanner substitutes in place of the APC, so the VT parser
|
|
19
19
|
* reports the cursor at the exact byte position. tmux never sees these
|
|
20
20
|
* placements: they do not move the cursor, do not follow reflow, and are
|
|
21
|
-
*
|
|
21
|
+
* kept across capture reseeds only when the coordinate basis is unchanged.
|
|
22
22
|
*
|
|
23
23
|
* Limits: direct transmission (`t=d`) only — file and shared-memory media
|
|
24
24
|
* cannot cross machines; `a=d` handles all and by-id; animation, relative
|
|
@@ -26,7 +26,7 @@
|
|
|
26
26
|
* from the patched tmux, in stream order (native/tmux/kitty-graphics.patch).
|
|
27
27
|
*/
|
|
28
28
|
import { createHash } from "node:crypto";
|
|
29
|
-
import { inflateSync } from "node:zlib";
|
|
29
|
+
import { deflateSync, inflateSync } from "node:zlib";
|
|
30
30
|
import { TERMINAL_GRAPHICS_MAX_IMAGE_BYTES, TERMINAL_GRAPHICS_MAX_IMAGES, TERMINAL_GRAPHICS_MAX_PLACEMENTS, } from "@tmux-ide/contracts";
|
|
31
31
|
/** Private OSC used to mark an APC's byte position for the VT parser. */
|
|
32
32
|
export const KITTY_PLACEMENT_OSC = 6974;
|
|
@@ -296,11 +296,12 @@ export const DEFAULT_KITTY_IMAGE_BUDGET = new KittyImageBudget({
|
|
|
296
296
|
perPaneBytes: 64 * 1024 * 1024,
|
|
297
297
|
globalBytes: 256 * 1024 * 1024,
|
|
298
298
|
});
|
|
299
|
-
/** Per-pane image store. Survives reseeds; cursor
|
|
299
|
+
/** Per-pane image store. Survives reseeds; the interpreter validates cursor placement coordinates. */
|
|
300
300
|
export class KittyImageStore {
|
|
301
301
|
#images = new Map();
|
|
302
302
|
#used = new Map();
|
|
303
303
|
#cursorPlacements = [];
|
|
304
|
+
/** `U=1` virtual placements per image, by placement id (0 when unnamed). */
|
|
304
305
|
#virtual = new Map();
|
|
305
306
|
#pending = null;
|
|
306
307
|
#nextAnonymous = 0x7f000000;
|
|
@@ -340,7 +341,7 @@ export class KittyImageStore {
|
|
|
340
341
|
this.#used.clear();
|
|
341
342
|
this.#bytes = 0;
|
|
342
343
|
}
|
|
343
|
-
/**
|
|
344
|
+
/** The coordinate basis changed: tmux holds no record to restore cursor anchors. */
|
|
344
345
|
dropCursorPlacements() {
|
|
345
346
|
if (this.#cursorPlacements.length === 0)
|
|
346
347
|
return;
|
|
@@ -366,12 +367,17 @@ export class KittyImageStore {
|
|
|
366
367
|
return 0;
|
|
367
368
|
return this.#remove(victim);
|
|
368
369
|
}
|
|
369
|
-
#remove(imageId) {
|
|
370
|
+
#remove(imageId, keepPlacements = false) {
|
|
370
371
|
const image = this.#images.get(imageId);
|
|
371
372
|
if (!image)
|
|
372
373
|
return 0;
|
|
373
374
|
this.#images.delete(imageId);
|
|
374
375
|
this.#used.delete(imageId);
|
|
376
|
+
if (keepPlacements) {
|
|
377
|
+
this.#bytes -= image.data.byteLength;
|
|
378
|
+
this.#revision += 1;
|
|
379
|
+
return image.data.byteLength;
|
|
380
|
+
}
|
|
375
381
|
this.#virtual.delete(imageId);
|
|
376
382
|
this.#bytes -= image.data.byteLength;
|
|
377
383
|
const kept = this.#cursorPlacements.filter((p) => p.imageId !== imageId);
|
|
@@ -404,7 +410,7 @@ export class KittyImageStore {
|
|
|
404
410
|
return;
|
|
405
411
|
}
|
|
406
412
|
const action = control.get("a") ?? "t";
|
|
407
|
-
if ((action === "t" || action === "T") && more) {
|
|
413
|
+
if ((action === "t" || action === "T" || action === "f") && more) {
|
|
408
414
|
this.#pending = { control, payload: [command.payload], bytes: command.payload.length };
|
|
409
415
|
return;
|
|
410
416
|
}
|
|
@@ -425,18 +431,18 @@ export class KittyImageStore {
|
|
|
425
431
|
const image = this.#decode(imageId, control, payload);
|
|
426
432
|
if (!image)
|
|
427
433
|
return;
|
|
428
|
-
|
|
434
|
+
// Re-transmitting an id replaces its pixels; its placements keep showing it.
|
|
435
|
+
this.#remove(imageId, true);
|
|
429
436
|
this.#images.set(imageId, image);
|
|
430
437
|
this.#bytes += image.data.byteLength;
|
|
431
438
|
this.#used.set(imageId, this.#budget.nextTick());
|
|
432
439
|
this.#revision += 1;
|
|
433
|
-
|
|
434
|
-
this.#images.size > 1)
|
|
435
|
-
this.evictOldest();
|
|
436
|
-
this.#budget.enforce();
|
|
440
|
+
this.#enforceBudget();
|
|
437
441
|
if (action === "t" || !this.#images.has(imageId))
|
|
438
442
|
return;
|
|
439
443
|
}
|
|
444
|
+
if (action === "f")
|
|
445
|
+
return this.#editFrame(imageId, control, payload);
|
|
440
446
|
if (action !== "T" && action !== "p")
|
|
441
447
|
return;
|
|
442
448
|
const image = this.#images.get(imageId);
|
|
@@ -447,7 +453,24 @@ export class KittyImageStore {
|
|
|
447
453
|
const rows = clampCells(int(control, "r") || Math.ceil(image.height / this.#cellHeight));
|
|
448
454
|
const placementId = int(control, "p");
|
|
449
455
|
if (control.get("U") === "1") {
|
|
450
|
-
this.#virtual.
|
|
456
|
+
const placements = this.#virtual.get(imageId) ?? new Map();
|
|
457
|
+
placements.set(placementId, { columns, rows });
|
|
458
|
+
this.#virtual.set(imageId, placements);
|
|
459
|
+
let count = 0;
|
|
460
|
+
for (const entries of this.#virtual.values())
|
|
461
|
+
count += entries.size;
|
|
462
|
+
for (const [id, entries] of this.#virtual) {
|
|
463
|
+
for (const placement of entries.keys()) {
|
|
464
|
+
if (count <= TERMINAL_GRAPHICS_MAX_PLACEMENTS)
|
|
465
|
+
break;
|
|
466
|
+
entries.delete(placement);
|
|
467
|
+
count -= 1;
|
|
468
|
+
}
|
|
469
|
+
if (entries.size === 0)
|
|
470
|
+
this.#virtual.delete(id);
|
|
471
|
+
if (count <= TERMINAL_GRAPHICS_MAX_PLACEMENTS)
|
|
472
|
+
break;
|
|
473
|
+
}
|
|
451
474
|
}
|
|
452
475
|
else if (cursor) {
|
|
453
476
|
const existing = this.#cursorPlacements.findIndex((p) => p.imageId === imageId && placementId !== 0 && p.placementId === placementId);
|
|
@@ -469,7 +492,13 @@ export class KittyImageStore {
|
|
|
469
492
|
}
|
|
470
493
|
#decode(imageId, control, payload) {
|
|
471
494
|
let data = Buffer.from(payload, "base64");
|
|
472
|
-
|
|
495
|
+
const f = int(control, "f", 32);
|
|
496
|
+
const zlib = control.get("o") === "z";
|
|
497
|
+
// Raw pixels stay compressed end to end: renderers inflate off their UI
|
|
498
|
+
// thread, and the stream carries a fraction of the bytes. PNG and other
|
|
499
|
+
// compressed payloads are inflated here.
|
|
500
|
+
const keepCompressed = zlib && (f === 32 || f === 24);
|
|
501
|
+
if (zlib && !keepCompressed) {
|
|
473
502
|
try {
|
|
474
503
|
data = inflateSync(data, { maxOutputLength: TERMINAL_GRAPHICS_MAX_IMAGE_BYTES });
|
|
475
504
|
}
|
|
@@ -479,7 +508,6 @@ export class KittyImageStore {
|
|
|
479
508
|
}
|
|
480
509
|
if (data.byteLength === 0 || data.byteLength > TERMINAL_GRAPHICS_MAX_IMAGE_BYTES)
|
|
481
510
|
return null;
|
|
482
|
-
const f = int(control, "f", 32);
|
|
483
511
|
let format;
|
|
484
512
|
let width;
|
|
485
513
|
let height;
|
|
@@ -493,9 +521,16 @@ export class KittyImageStore {
|
|
|
493
521
|
else if (f === 32 || f === 24) {
|
|
494
522
|
width = int(control, "s");
|
|
495
523
|
height = int(control, "v");
|
|
496
|
-
|
|
497
|
-
if (!width || !height ||
|
|
524
|
+
const bytes = width * height * (f === 32 ? 4 : 3);
|
|
525
|
+
if (!width || !height || bytes > MAX_RAW_IMAGE_BYTES)
|
|
498
526
|
return null;
|
|
527
|
+
if (keepCompressed)
|
|
528
|
+
format = f === 32 ? "rgba-zlib" : "rgb-zlib";
|
|
529
|
+
else {
|
|
530
|
+
format = f === 32 ? "rgba" : "rgb";
|
|
531
|
+
if (data.length !== bytes)
|
|
532
|
+
return null;
|
|
533
|
+
}
|
|
499
534
|
}
|
|
500
535
|
else
|
|
501
536
|
return null;
|
|
@@ -504,6 +539,75 @@ export class KittyImageStore {
|
|
|
504
539
|
const digest = createHash("sha256").update(data).digest("hex").slice(0, 32);
|
|
505
540
|
return { imageId, format, width, height, data, digest };
|
|
506
541
|
}
|
|
542
|
+
/**
|
|
543
|
+
* `a=f` edits of the root frame (`r=1`): RGBA/RGB rectangles written at
|
|
544
|
+
* `x,y`, replacing (`X=1`) or alpha-blending the existing pixels. New
|
|
545
|
+
* animation frames, `c` base frames and PNG data are not modelled.
|
|
546
|
+
*/
|
|
547
|
+
#editFrame(imageId, control, payload) {
|
|
548
|
+
const image = this.#images.get(imageId);
|
|
549
|
+
if (!image || int(control, "r") !== 1 || (control.get("t") ?? "d") !== "d")
|
|
550
|
+
return;
|
|
551
|
+
const source = image.format === "rgba" || image.format === "rgba-zlib" ? 4 : 3;
|
|
552
|
+
if (image.format === "png")
|
|
553
|
+
return;
|
|
554
|
+
let pixels;
|
|
555
|
+
let patch = Buffer.from(payload, "base64");
|
|
556
|
+
try {
|
|
557
|
+
pixels =
|
|
558
|
+
image.format === "rgba-zlib" || image.format === "rgb-zlib"
|
|
559
|
+
? inflateSync(image.data, { maxOutputLength: MAX_RAW_IMAGE_BYTES })
|
|
560
|
+
: Uint8Array.from(image.data);
|
|
561
|
+
if (control.get("o") === "z")
|
|
562
|
+
patch = inflateSync(patch, { maxOutputLength: MAX_RAW_IMAGE_BYTES });
|
|
563
|
+
}
|
|
564
|
+
catch {
|
|
565
|
+
return;
|
|
566
|
+
}
|
|
567
|
+
const f = int(control, "f", 32);
|
|
568
|
+
const depth = f === 24 ? 3 : 4;
|
|
569
|
+
const width = int(control, "s") || image.width;
|
|
570
|
+
const height = int(control, "v") || image.height;
|
|
571
|
+
const x0 = int(control, "x");
|
|
572
|
+
const y0 = int(control, "y");
|
|
573
|
+
if ((f !== 32 && f !== 24) || patch.length !== width * height * depth)
|
|
574
|
+
return;
|
|
575
|
+
if (pixels.length !== image.width * image.height * source)
|
|
576
|
+
return;
|
|
577
|
+
const replace = control.get("X") === "1";
|
|
578
|
+
for (let row = 0; row < height && y0 + row < image.height; row += 1)
|
|
579
|
+
for (let col = 0; col < width && x0 + col < image.width; col += 1) {
|
|
580
|
+
const from = (row * width + col) * depth;
|
|
581
|
+
const to = ((y0 + row) * image.width + x0 + col) * source;
|
|
582
|
+
const alpha = depth === 4 && !replace ? patch[from + 3] / 255 : 1;
|
|
583
|
+
for (let channel = 0; channel < 3; channel += 1)
|
|
584
|
+
pixels[to + channel] = Math.round(patch[from + channel] * alpha + pixels[to + channel] * (1 - alpha));
|
|
585
|
+
if (source === 4)
|
|
586
|
+
pixels[to + 3] =
|
|
587
|
+
depth === 4 && replace
|
|
588
|
+
? patch[from + 3]
|
|
589
|
+
: Math.round(255 * alpha + pixels[to + 3] * (1 - alpha));
|
|
590
|
+
}
|
|
591
|
+
const data = deflateSync(pixels, { level: 1 });
|
|
592
|
+
const format = source === 4 ? "rgba-zlib" : "rgb-zlib";
|
|
593
|
+
this.#remove(imageId, true);
|
|
594
|
+
this.#images.set(imageId, {
|
|
595
|
+
...image,
|
|
596
|
+
format,
|
|
597
|
+
data,
|
|
598
|
+
digest: createHash("sha256").update(data).digest("hex").slice(0, 32),
|
|
599
|
+
});
|
|
600
|
+
this.#bytes += data.byteLength;
|
|
601
|
+
this.#used.set(imageId, this.#budget.nextTick());
|
|
602
|
+
this.#revision += 1;
|
|
603
|
+
this.#enforceBudget();
|
|
604
|
+
}
|
|
605
|
+
#enforceBudget() {
|
|
606
|
+
while ((this.#bytes > this.#budget.perPaneBytes || this.#images.size > MAX_IMAGES_PER_PANE) &&
|
|
607
|
+
this.#images.size > 0)
|
|
608
|
+
this.evictOldest();
|
|
609
|
+
this.#budget.enforce();
|
|
610
|
+
}
|
|
507
611
|
#delete(control) {
|
|
508
612
|
const what = control.get("d") ?? "a";
|
|
509
613
|
const id = int(control, "i");
|
|
@@ -511,23 +615,28 @@ export class KittyImageStore {
|
|
|
511
615
|
const byId = (what === "i" || what === "I") && id !== 0;
|
|
512
616
|
if (!all && !byId)
|
|
513
617
|
return;
|
|
514
|
-
|
|
515
|
-
const
|
|
618
|
+
// With `p`, only that placement of the image goes (spec: d=i/I with p).
|
|
619
|
+
const placementId = control.has("p") ? int(control, "p") : null;
|
|
620
|
+
const doomed = (imageId, placement) => all || (imageId === id && (placementId === null || placement === placementId));
|
|
621
|
+
const kept = this.#cursorPlacements.filter((p) => !doomed(p.imageId, p.placementId));
|
|
516
622
|
if (kept.length !== this.#cursorPlacements.length) {
|
|
517
623
|
this.#cursorPlacements.length = 0;
|
|
518
624
|
this.#cursorPlacements.push(...kept);
|
|
519
625
|
this.#revision += 1;
|
|
520
626
|
}
|
|
521
|
-
for (const
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
627
|
+
for (const [imageId, placements] of [...this.#virtual])
|
|
628
|
+
for (const placement of [...placements.keys()])
|
|
629
|
+
if (doomed(imageId, placement)) {
|
|
630
|
+
placements.delete(placement);
|
|
631
|
+
if (placements.size === 0)
|
|
632
|
+
this.#virtual.delete(imageId);
|
|
633
|
+
this.#revision += 1;
|
|
634
|
+
}
|
|
526
635
|
// Upper-case variants also free the image data.
|
|
527
636
|
if (what === "A")
|
|
528
637
|
for (const key of [...this.#images.keys()])
|
|
529
638
|
this.#remove(key);
|
|
530
|
-
if (what === "I")
|
|
639
|
+
if (what === "I" && placementId === null)
|
|
531
640
|
this.#remove(id);
|
|
532
641
|
}
|
|
533
642
|
/**
|
|
@@ -543,7 +652,7 @@ export class KittyImageStore {
|
|
|
543
652
|
width: image.width,
|
|
544
653
|
height: image.height,
|
|
545
654
|
bytes: image.data.byteLength,
|
|
546
|
-
placeholder: this.#virtual.get(image.imageId)
|
|
655
|
+
placeholder: virtualGrid(this.#virtual.get(image.imageId)),
|
|
547
656
|
}));
|
|
548
657
|
const placements = [];
|
|
549
658
|
for (const placement of this.#cursorPlacements) {
|
|
@@ -574,5 +683,17 @@ export class KittyImageStore {
|
|
|
574
683
|
}
|
|
575
684
|
}
|
|
576
685
|
const MAX_IMAGES_PER_PANE = TERMINAL_GRAPHICS_MAX_IMAGES;
|
|
686
|
+
/** Largest decoded raw image (a 4K RGBA frame is ~33 MB). */
|
|
687
|
+
const MAX_RAW_IMAGE_BYTES = 64 * 1024 * 1024;
|
|
688
|
+
/**
|
|
689
|
+
* Placeholder cells carry the image id; with several virtual placements the
|
|
690
|
+
* protocol picks one by the placement id in the underline colour. Renderers
|
|
691
|
+
* resolve cells by image id, so publish the most recently placed grid.
|
|
692
|
+
*/
|
|
693
|
+
function virtualGrid(placements) {
|
|
694
|
+
if (!placements || placements.size === 0)
|
|
695
|
+
return null;
|
|
696
|
+
return [...placements.values()].at(-1);
|
|
697
|
+
}
|
|
577
698
|
const MAX_IMAGE_SIDE = 10_000;
|
|
578
699
|
const clampCells = (cells) => Math.max(1, Math.min(1_000, cells));
|
|
@@ -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)
|