tmux-ide 2.9.6 → 2.9.8

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (84) hide show
  1. package/bin/cli.js +2881 -1336
  2. package/package.json +2 -2
  3. package/packages/contracts/src/index.ts +5 -0
  4. package/packages/contracts/src/pane-stream.ts +54 -0
  5. package/packages/contracts/src/terminal-graphics.ts +118 -0
  6. package/packages/contracts/src/terminal-history-anchor.ts +61 -0
  7. package/packages/contracts/src/tmux-server-diagnostics.ts +16 -0
  8. package/packages/core/src/index.ts +1 -0
  9. package/packages/core/src/kitty-placeholders.ts +208 -0
  10. package/packages/daemon/dist/command-center/tmux-server-diagnostics.js +24 -0
  11. package/packages/daemon/dist/command-center/tmux-server-history-anchor.js +48 -0
  12. package/packages/daemon/dist/command-center/tmux-servers.js +11 -0
  13. package/packages/daemon/dist/lib/canonical-daemon-bootstrap.js +23 -1
  14. package/packages/daemon/dist/lib/daemon-embed.js +21 -0
  15. package/packages/daemon/dist/lib/tmux-server-owner.js +10 -1
  16. package/packages/daemon/dist/lib/tmux-session-mutation-fence.js +2 -1
  17. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Info.plist +2 -2
  18. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/MacOS/tmux-ide-notifier +0 -0
  19. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/Resources/Assets.car +0 -0
  20. package/packages/daemon/dist/native/TmuxIdeNotifier.app/Contents/_CodeSignature/CodeResources +2 -2
  21. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libevent_core-2.1.7.dylib +0 -0
  22. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.3.0.dylib +0 -0
  23. package/packages/daemon/dist/native/tmux/darwin-arm64/manifest.json +12 -4
  24. package/packages/daemon/dist/native/tmux/darwin-arm64/tmux +0 -0
  25. package/packages/daemon/dist/native/tmux/linux-arm64/manifest.json +9 -1
  26. package/packages/daemon/dist/native/tmux/linux-arm64/tmux +0 -0
  27. package/packages/daemon/dist/native/tmux/linux-x64/manifest.json +9 -1
  28. package/packages/daemon/dist/native/tmux/linux-x64/tmux +0 -0
  29. package/packages/daemon/dist/terminal/attachments/native-runtime.js +25 -12
  30. package/packages/daemon/dist/terminal/graphics/kitty-graphics.js +699 -0
  31. package/packages/daemon/dist/terminal/mirror/control-channel.js +17 -1
  32. package/packages/daemon/dist/terminal/mirror/mirror-service.js +7 -0
  33. package/packages/daemon/dist/terminal/mirror/native-grid-capture.js +8 -0
  34. package/packages/daemon/dist/terminal/mirror/native-grid-reader.js +6 -1
  35. package/packages/daemon/dist/terminal/mirror/session-channel.js +56 -4
  36. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +180 -1
  37. package/packages/daemon/dist/terminal/protocol/session-descriptor-discovery.js +3 -2
  38. package/packages/daemon/dist/terminal/session-runtime/history-anchor-owner.js +171 -0
  39. package/packages/daemon/dist/terminal/session-runtime/registry.js +29 -0
  40. package/packages/daemon/dist/terminal/session-runtime/terminal-delivery-hub.js +20 -1
  41. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-interpreter.js +85 -1
  42. package/packages/daemon/dist/terminal/session-runtime/terminal-replica-owner.js +30 -0
  43. package/packages/daemon/dist/terminal/session-runtime/transport-binding.js +53 -21
  44. package/packages/daemon/dist/terminal/session-runtime/xterm-terminal-interpreter-backend.js +11 -0
  45. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +7 -1
  46. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-input-ingress.js +35 -5
  47. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-paste.js +2 -2
  48. package/packages/daemon/dist/tui/mirror/runtime/terminal-input-adapter.js +2 -2
  49. package/packages/daemon/src/command-center/tmux-server-diagnostics.ts +38 -0
  50. package/packages/daemon/src/command-center/tmux-server-history-anchor.ts +69 -0
  51. package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
  52. package/packages/daemon/src/lib/canonical-daemon-bootstrap.ts +20 -2
  53. package/packages/daemon/src/lib/daemon-embed.ts +26 -0
  54. package/packages/daemon/src/lib/tmux-server-owner.ts +10 -0
  55. package/packages/daemon/src/lib/tmux-session-mutation-fence.ts +2 -1
  56. package/packages/daemon/src/terminal/attachments/native-runtime.ts +28 -13
  57. package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client-recorded.ctl +87 -0
  58. package/packages/daemon/src/terminal/graphics/__fixtures__/full-frame-client.ctl +10 -0
  59. package/packages/daemon/src/terminal/graphics/kitty-graphics.ts +756 -0
  60. package/packages/daemon/src/terminal/mirror/control-channel.ts +18 -1
  61. package/packages/daemon/src/terminal/mirror/mirror-service.ts +14 -0
  62. package/packages/daemon/src/terminal/mirror/native-grid-capture.ts +11 -0
  63. package/packages/daemon/src/terminal/mirror/native-grid-reader.ts +10 -1
  64. package/packages/daemon/src/terminal/mirror/session-channel.ts +75 -3
  65. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +199 -0
  66. package/packages/daemon/src/terminal/protocol/session-descriptor-discovery.ts +3 -2
  67. package/packages/daemon/src/terminal/session-runtime/history-anchor-owner.ts +190 -0
  68. package/packages/daemon/src/terminal/session-runtime/registry.ts +38 -0
  69. package/packages/daemon/src/terminal/session-runtime/terminal-delivery-hub.ts +38 -0
  70. package/packages/daemon/src/terminal/session-runtime/terminal-interpreter-backend.ts +4 -0
  71. package/packages/daemon/src/terminal/session-runtime/terminal-replica-interpreter.ts +99 -1
  72. package/packages/daemon/src/terminal/session-runtime/terminal-replica-owner.ts +39 -0
  73. package/packages/daemon/src/terminal/session-runtime/transport-binding.ts +58 -24
  74. package/packages/daemon/src/terminal/session-runtime/xterm-terminal-interpreter-backend.ts +13 -0
  75. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +6 -1
  76. package/packages/daemon/src/tui/mirror/runtime/application-terminal-input-ingress.ts +44 -9
  77. package/packages/daemon/src/tui/mirror/runtime/application-terminal-paste.ts +2 -1
  78. package/packages/daemon/src/tui/mirror/runtime/terminal-input-adapter.ts +5 -2
  79. package/packages/daemon-client/src/fleet-connection-status.ts +1 -1
  80. package/packages/daemon-client/src/pane-creation-refusal.ts +48 -0
  81. package/packages/daemon-client/src/pane-stream-client.ts +37 -0
  82. package/packages/daemon-client/src/tmux-server-client.ts +133 -6
  83. package/packages/daemon/dist/native/tmux/darwin-arm64/lib/libutf8proc.3.2.3.dylib +0 -0
  84. /package/packages/daemon/dist/native/tmux/darwin-arm64/licenses/{libutf8proc.3.2.3.dylib.txt → libutf8proc.3.3.0.dylib.txt} +0 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.6",
3
+ "version": "2.9.8",
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": [
@@ -49,6 +49,7 @@ export * from "./semantic-identity.ts";
49
49
  export * from "./session-runtime.ts";
50
50
  export * from "./terminal-replica.ts";
51
51
  export * from "./terminal-delivery.ts";
52
+ export * from "./terminal-graphics.ts";
52
53
  export * from "./terminal-runtime-inventory.ts";
53
54
  export * from "./experience-shell.ts";
54
55
  export * from "./application-shell.ts";
@@ -115,3 +116,7 @@ export * from "./window-split-layout.ts";
115
116
  export * from "./pane-editor-context.ts";
116
117
 
117
118
  export * from "./tmux-agent-open.ts";
119
+
120
+ export * from "./tmux-server-diagnostics.ts";
121
+
122
+ export * from "./terminal-history-anchor.ts";
@@ -18,6 +18,13 @@ import {
18
18
  TerminalAttachmentSemanticWindowIdSchemaZ,
19
19
  } from "./semantic-identity.ts";
20
20
  import { TerminalAttachmentViewerModeSchemaZ } from "./terminal-attachments.ts";
21
+ import {
22
+ TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES,
23
+ TERMINAL_GRAPHICS_MAX_REQUEST_DIGESTS,
24
+ TerminalGraphicsBlobSchemaZ,
25
+ TerminalGraphicsDigestSchemaZ,
26
+ TerminalGraphicsStateSchemaZ,
27
+ } from "./terminal-graphics.ts";
21
28
  import { WorkspaceIdSchemaZ } from "./workspace-state.ts";
22
29
  import {
23
30
  TerminalDeliveryAckSchemaZ,
@@ -341,6 +348,34 @@ export const PaneStreamClockProbeFrameSchemaZ = z
341
348
  })
342
349
  .strict();
343
350
 
351
+ /** Ask for image bytes by digest; only after the daemon sent graphics state. */
352
+ export const PaneStreamTerminalGraphicsRequestFrameSchemaZ = z
353
+ .object({
354
+ type: z.literal("terminal-graphics-request"),
355
+ pane: PaneStreamSemanticPaneIdSchemaZ,
356
+ digests: z
357
+ .array(TerminalGraphicsDigestSchemaZ)
358
+ .min(1)
359
+ .max(TERMINAL_GRAPHICS_MAX_REQUEST_DIGESTS),
360
+ })
361
+ .strict();
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
+
344
379
  export const PaneStreamClientFrameSchemaZ = z.union([
345
380
  PaneStreamInputFrameSchemaZ,
346
381
  PaneStreamConsumedFrameSchemaZ,
@@ -354,6 +389,8 @@ export const PaneStreamClientFrameSchemaZ = z.union([
354
389
  PaneStreamAuthorityRequestFrameSchemaZ,
355
390
  PaneStreamAuthorityReleaseFrameSchemaZ,
356
391
  PaneStreamClockProbeFrameSchemaZ,
392
+ PaneStreamTerminalGraphicsRequestFrameSchemaZ,
393
+ PaneStreamTerminalCellPixelsFrameSchemaZ,
357
394
  ]);
358
395
  export type PaneStreamClientFrame = z.infer<typeof PaneStreamClientFrameSchemaZ>;
359
396
 
@@ -645,6 +682,21 @@ export const PaneStreamTerminalDeliveryFaultFrameSchemaZ = z
645
682
  fault: TerminalDeliveryFaultSchemaZ,
646
683
  })
647
684
  .strict();
685
+ export const PaneStreamTerminalGraphicsFrameSchemaZ = z
686
+ .object({
687
+ type: z.literal("terminal-graphics"),
688
+ pane: PaneStreamSemanticPaneIdSchemaZ,
689
+ state: TerminalGraphicsStateSchemaZ,
690
+ })
691
+ .strict();
692
+ export const PaneStreamTerminalGraphicsBlobFrameSchemaZ = z
693
+ .object({
694
+ type: z.literal("terminal-graphics-blob"),
695
+ pane: PaneStreamSemanticPaneIdSchemaZ,
696
+ blob: TerminalGraphicsBlobSchemaZ,
697
+ data: Base64SchemaZ(Math.ceil(TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES / 3) * 4).min(4),
698
+ })
699
+ .strict();
648
700
  export const PaneStreamSemanticIntentAckFrameSchemaZ = z
649
701
  .object({
650
702
  type: z.literal("semantic-intent-ack"),
@@ -749,6 +801,8 @@ export const PaneStreamServerFrameSchemaZ = z.discriminatedUnion("type", [
749
801
  PaneStreamTerminalDeliveryEnvelopeFrameSchemaZ,
750
802
  PaneStreamTerminalDeliveryChunkFrameSchemaZ,
751
803
  PaneStreamTerminalDeliveryFaultFrameSchemaZ,
804
+ PaneStreamTerminalGraphicsFrameSchemaZ,
805
+ PaneStreamTerminalGraphicsBlobFrameSchemaZ,
752
806
  PaneStreamSemanticIntentAckFrameSchemaZ,
753
807
  PaneStreamViewportAckFrameSchemaZ,
754
808
  PaneStreamAuthoritySnapshotFrameSchemaZ,
@@ -0,0 +1,118 @@
1
+ import { z } from "zod";
2
+
3
+ /**
4
+ * Kitty graphics protocol side channel.
5
+ *
6
+ * Image bytes never enter the canonical terminal replica: the replica keeps
7
+ * the cells (including Unicode placeholder cells, which tmux retains as text)
8
+ * and this channel carries, per pane, the image registry those cells refer to
9
+ * plus best-effort cursor-anchored placements. Bytes travel separately, once
10
+ * per content digest, and only when a renderer asks for them.
11
+ *
12
+ * Capability: a renderer that understands these frames adds
13
+ * {@link TERMINAL_GRAPHICS_CAPABILITY_VERSION} to its terminal-delivery offer's
14
+ * `protocolVersions`. Daemons that predate the channel ignore the extra
15
+ * version; daemons that know it send graphics frames only to such renderers.
16
+ */
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;
28
+ /** Raw image bytes per blob frame (base64 on the wire stays below 1 MiB). */
29
+ export const TERMINAL_GRAPHICS_BLOB_CHUNK_BYTES = 512 * 1024;
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;
34
+ export const TERMINAL_GRAPHICS_MAX_IMAGES = 256;
35
+ export const TERMINAL_GRAPHICS_MAX_PLACEMENTS = 256;
36
+ export const TERMINAL_GRAPHICS_MAX_REQUEST_DIGESTS = 64;
37
+
38
+ export const TerminalGraphicsDigestSchemaZ = z.string().regex(/^[0-9a-f]{32}$/u);
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
+ ]);
51
+ export type TerminalGraphicsFormat = z.infer<typeof TerminalGraphicsFormatSchemaZ>;
52
+ const Dimension = z.number().int().min(1).max(10_000);
53
+ const Cells = z.number().int().min(1).max(1_000);
54
+ const CellIndex = z.number().int().min(0).max(1_000);
55
+
56
+ export const TerminalGraphicsImageSchemaZ = z
57
+ .object({
58
+ /** Program-chosen kitty image id (placeholder cells encode it in colour). */
59
+ imageId: z.number().int().min(0).max(0xffff_ffff),
60
+ digest: TerminalGraphicsDigestSchemaZ,
61
+ format: TerminalGraphicsFormatSchemaZ,
62
+ width: Dimension,
63
+ height: Dimension,
64
+ bytes: z.number().int().min(1).max(TERMINAL_GRAPHICS_MAX_IMAGE_BYTES),
65
+ /** The `U=1` virtual placement's cell grid, when placeholders may show it. */
66
+ placeholder: z.object({ columns: Cells, rows: Cells }).strict().nullable(),
67
+ })
68
+ .strict();
69
+ export type TerminalGraphicsImage = z.infer<typeof TerminalGraphicsImageSchemaZ>;
70
+
71
+ /** A cursor-anchored placement clipped to the pane viewport (best effort). */
72
+ export const TerminalGraphicsPlacementSchemaZ = z
73
+ .object({
74
+ digest: TerminalGraphicsDigestSchemaZ,
75
+ row: CellIndex,
76
+ column: CellIndex,
77
+ rows: Cells,
78
+ columns: Cells,
79
+ sourceRow: CellIndex,
80
+ sourceColumn: CellIndex,
81
+ totalRows: Cells,
82
+ totalColumns: Cells,
83
+ })
84
+ .strict();
85
+ export type TerminalGraphicsPlacement = z.infer<typeof TerminalGraphicsPlacementSchemaZ>;
86
+
87
+ export const TerminalGraphicsStateSchemaZ = z
88
+ .object({
89
+ revision: z.number().int().nonnegative(),
90
+ images: z.array(TerminalGraphicsImageSchemaZ).max(TERMINAL_GRAPHICS_MAX_IMAGES),
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(),
95
+ })
96
+ .strict();
97
+ export type TerminalGraphicsState = z.infer<typeof TerminalGraphicsStateSchemaZ>;
98
+
99
+ export const TerminalGraphicsBlobSchemaZ = z
100
+ .object({
101
+ digest: TerminalGraphicsDigestSchemaZ,
102
+ format: TerminalGraphicsFormatSchemaZ,
103
+ width: Dimension,
104
+ height: Dimension,
105
+ total: z.number().int().min(1).max(TERMINAL_GRAPHICS_MAX_IMAGE_BYTES),
106
+ offset: z.number().int().nonnegative().max(TERMINAL_GRAPHICS_MAX_IMAGE_BYTES),
107
+ })
108
+ .strict();
109
+ export type TerminalGraphicsBlob = z.infer<typeof TerminalGraphicsBlobSchemaZ>;
110
+
111
+ /** Hub → transport messages (in-process; the transport frames them). */
112
+ export type TerminalGraphicsServerMessage =
113
+ | { readonly type: "terminal.graphics"; readonly state: TerminalGraphicsState }
114
+ | {
115
+ readonly type: "terminal.graphics.blob";
116
+ readonly blob: TerminalGraphicsBlob;
117
+ readonly bytes: Uint8Array;
118
+ };
@@ -0,0 +1,61 @@
1
+ import { z } from "zod";
2
+ import { WorkspaceIdSchemaZ } from "./workspace-state.ts";
3
+ import { TerminalAttachmentSemanticPaneIdSchemaZ } from "./semantic-identity.ts";
4
+ import { TmuxServerScopeSchemaZ } from "./tmux-server-scope.ts";
5
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "./workspace-catalog-resource.ts";
6
+ export const HistoryAnchorProofSchemaZ = z
7
+ .object({
8
+ workspaceName: WorkspaceIdSchemaZ,
9
+ semanticPaneId: TerminalAttachmentSemanticPaneIdSchemaZ,
10
+ generation: z.uuid(),
11
+ incarnation: z.string().min(1).max(256),
12
+ revision: z.number().int().nonnegative().safe(),
13
+ stateHash: z.string().regex(/^[0-9a-f]{16}$/u),
14
+ })
15
+ .strict();
16
+ export const HistoryAnchorIdentitySchemaZ = HistoryAnchorProofSchemaZ.omit({
17
+ revision: true,
18
+ stateHash: true,
19
+ });
20
+ export const HistoryAnchorRequestSchemaZ = z.union([
21
+ z
22
+ .object({
23
+ version: z.literal(1),
24
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
25
+ proof: HistoryAnchorProofSchemaZ,
26
+ operation: z
27
+ .object({ kind: z.literal("capture"), historyIndex: z.number().int().nonnegative().safe() })
28
+ .strict(),
29
+ })
30
+ .strict(),
31
+ z
32
+ .object({
33
+ version: z.literal(1),
34
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ,
35
+ proof: HistoryAnchorIdentitySchemaZ,
36
+ operation: z.object({ kind: z.literal("resolve"), token: z.uuid() }).strict(),
37
+ })
38
+ .strict(),
39
+ ]);
40
+ export const HistoryAnchorResultSchemaZ = z.discriminatedUnion("status", [
41
+ z.object({ status: z.literal("captured"), token: z.uuid() }).strict(),
42
+ z
43
+ .object({
44
+ status: z.literal("resolved"),
45
+ proof: HistoryAnchorProofSchemaZ,
46
+ historyIndex: z.number().int().nonnegative().safe(),
47
+ })
48
+ .strict(),
49
+ z.object({ status: z.literal("unavailable") }).strict(),
50
+ z.object({ status: z.literal("stale") }).strict(),
51
+ z.object({ status: z.literal("lost") }).strict(),
52
+ ]);
53
+ export const HistoryAnchorResponseSchemaZ = z
54
+ .object({
55
+ version: z.literal(1),
56
+ server: TmuxServerScopeSchemaZ,
57
+ result: HistoryAnchorResultSchemaZ,
58
+ })
59
+ .strict();
60
+ export type HistoryAnchorRequest = z.infer<typeof HistoryAnchorRequestSchemaZ>;
61
+ export type HistoryAnchorResult = z.infer<typeof HistoryAnchorResultSchemaZ>;
@@ -0,0 +1,16 @@
1
+ import { z } from "zod";
2
+ import { TmuxServerScopeSchemaZ } from "./tmux-server-scope.ts";
3
+
4
+ /** Server format output only; never accept freeform diagnostic text. */
5
+ export const TmuxServerVersionSchemaZ = z
6
+ .string()
7
+ .max(32)
8
+ .regex(/^[0-9]+\.[0-9]+[a-z]?$/u);
9
+ export const TmuxServerDiagnosticsSchemaZ = z
10
+ .object({
11
+ version: z.literal(1),
12
+ server: TmuxServerScopeSchemaZ,
13
+ tmuxVersion: TmuxServerVersionSchemaZ,
14
+ })
15
+ .strict();
16
+ export type TmuxServerDiagnostics = z.infer<typeof TmuxServerDiagnosticsSchemaZ>;
@@ -19,3 +19,4 @@ export * from "./saved-machines.ts";
19
19
 
20
20
  export * from "./fleet-client-state.ts";
21
21
  export * from "./resize-transaction.ts";
22
+ export * from "./kitty-placeholders.ts";
@@ -0,0 +1,208 @@
1
+ /**
2
+ * Kitty graphics Unicode placeholders, decoded from replica cells.
3
+ *
4
+ * A program places an image with a `U=1` virtual placement and then prints
5
+ * U+10EEEE cells whose combining diacritics give the image row and column and
6
+ * whose foreground colour gives the image id. To tmux these are ordinary text,
7
+ * so they survive scrolling, history, capture and reattach; renderers derive
8
+ * placements from the cells they already hold and stay in exact sync.
9
+ */
10
+ import type {
11
+ TerminalGraphicsPlacement,
12
+ TerminalGraphicsState,
13
+ TerminalReplicaRow,
14
+ } from "@tmux-ide/contracts";
15
+
16
+ export const KITTY_PLACEHOLDER = 0x10eeee;
17
+
18
+ type KittyCellColor =
19
+ | { readonly kind: "default" }
20
+ | { readonly kind: "indexed"; readonly index: number }
21
+ | { readonly kind: "rgb"; readonly value: number };
22
+
23
+ // Row/column diacritics for Unicode placeholders, in protocol order (index = value).
24
+ const DIACRITICS = [
25
+ "0305 030D 030E 0310 0312 033D 033E 033F 0346 034A 034B 034C 0350 0351 0352 0357 035B 0363 0364",
26
+ "0365 0366 0367 0368 0369 036A 036B 036C 036D 036E 036F 0483 0484 0485 0486 0487 0592 0593 0594",
27
+ "0595 0597 0598 0599 059C 059D 059E 059F 05A0 05A1 05A8 05A9 05AB 05AC 05AF 05C4 0610 0611 0612",
28
+ "0613 0614 0615 0616 0617 0657 0658 0659 065A 065B 065D 065E 06D6 06D7 06D8 06D9 06DA 06DB 06DC",
29
+ "06DF 06E0 06E1 06E2 06E4 06E7 06E8 06EB 06EC 0730 0732 0733 0735 0736 073A 073D 073F 0740 0741",
30
+ "0743 0745 0747 0749 074A 07EB 07EC 07ED 07EE 07EF 07F0 07F1 07F3 0816 0817 0818 0819 081B 081C",
31
+ "081D 081E 081F 0820 0821 0822 0823 0825 0826 0827 0829 082A 082B 082C 082D 0951 0953 0954 0F82",
32
+ "0F83 0F86 0F87 135D 135E 135F 17DD 193A 1A17 1A75 1A76 1A77 1A78 1A79 1A7A 1A7B 1A7C 1B6B 1B6D",
33
+ "1B6E 1B6F 1B70 1B71 1B72 1B73 1CD0 1CD1 1CD2 1CDA 1CDB 1CE0 1DC0 1DC1 1DC3 1DC4 1DC5 1DC6 1DC7",
34
+ "1DC8 1DC9 1DCB 1DCC 1DD1 1DD2 1DD3 1DD4 1DD5 1DD6 1DD7 1DD8 1DD9 1DDA 1DDB 1DDC 1DDD 1DDE 1DDF",
35
+ "1DE0 1DE1 1DE2 1DE3 1DE4 1DE5 1DE6 1DFE 20D0 20D1 20D4 20D5 20D6 20D7 20DB 20DC 20E1 20E7 20E9",
36
+ "20F0 2CEF 2CF0 2CF1 2DE0 2DE1 2DE2 2DE3 2DE4 2DE5 2DE6 2DE7 2DE8 2DE9 2DEA 2DEB 2DEC 2DED 2DEE",
37
+ "2DEF 2DF0 2DF1 2DF2 2DF3 2DF4 2DF5 2DF6 2DF7 2DF8 2DF9 2DFA 2DFB 2DFC 2DFD 2DFE 2DFF A66F A67C",
38
+ "A67D A6F0 A6F1 A8E0 A8E1 A8E2 A8E3 A8E4 A8E5 A8E6 A8E7 A8E8 A8E9 A8EA A8EB A8EC A8ED A8EE A8EF",
39
+ "A8F0 A8F1 AAB0 AAB2 AAB3 AAB7 AAB8 AABE AABF AAC1 FE20 FE21 FE22 FE23 FE24 FE25 FE26 10A0F 10A38",
40
+ "1D185 1D186 1D187 1D188 1D189 1D1AA 1D1AB 1D1AC 1D1AD 1D242 1D243 1D244",
41
+ ]
42
+ .join(" ")
43
+ .split(" ");
44
+ const DIACRITIC_VALUE = new Map(DIACRITICS.map((hex, index) => [Number.parseInt(hex, 16), index]));
45
+
46
+ export interface PlaceholderCell {
47
+ imageId: number;
48
+ row: number;
49
+ col: number;
50
+ }
51
+
52
+ /** PURE — decode one cell; null when it is not a placeholder. */
53
+ export function decodeKittyPlaceholderCell(
54
+ grapheme: string,
55
+ foreground: KittyCellColor,
56
+ left: PlaceholderCell | null,
57
+ ): PlaceholderCell | null {
58
+ const points = [...grapheme].map((ch) => ch.codePointAt(0)!);
59
+ if (points[0] !== KITTY_PLACEHOLDER) return null;
60
+ const marks = points.slice(1).map((p) => DIACRITIC_VALUE.get(p));
61
+ let imageId =
62
+ foreground.kind === "indexed"
63
+ ? foreground.index
64
+ : foreground.kind === "rgb"
65
+ ? foreground.value & 0xffffff
66
+ : 0;
67
+ if (marks[2] !== undefined) imageId += marks[2] * 0x1000000;
68
+ const continues = left !== null && (marks[0] === undefined || marks[0] === left.row);
69
+ const row = marks[0] ?? (continues ? left!.row : 0);
70
+ const col = marks[1] ?? (continues ? left!.col + 1 : 0);
71
+ if (marks[2] === undefined && continues && imageId === (left!.imageId & 0xffffff))
72
+ imageId = left!.imageId;
73
+ return { imageId, row, col };
74
+ }
75
+
76
+ export interface PlaceholderRect {
77
+ imageId: number;
78
+ row: number;
79
+ column: number;
80
+ rows: number;
81
+ columns: number;
82
+ srcRow: number;
83
+ srcCol: number;
84
+ }
85
+
86
+ export function kittyPlaceholderRects(
87
+ grid: readonly {
88
+ readonly cells: readonly { readonly grapheme: string; readonly foreground: KittyCellColor }[];
89
+ }[],
90
+ ): PlaceholderRect[] {
91
+ // Horizontal runs: same image and source row, consecutive source columns.
92
+ const open: PlaceholderRect[] = [];
93
+ const done: PlaceholderRect[] = [];
94
+ for (let y = 0; y < grid.length; y += 1) {
95
+ const runs: PlaceholderRect[] = [];
96
+ let left: PlaceholderCell | null = null;
97
+ let run: PlaceholderRect | null = null;
98
+ const cells = grid[y]!.cells;
99
+ for (let x = 0; x < cells.length; x += 1) {
100
+ const cell = decodeKittyPlaceholderCell(cells[x]!.grapheme, cells[x]!.foreground, left);
101
+ left = cell;
102
+ if (
103
+ cell &&
104
+ run &&
105
+ cell.imageId === run.imageId &&
106
+ cell.row === run.srcRow &&
107
+ cell.col === run.srcCol + run.columns
108
+ ) {
109
+ run.columns += 1;
110
+ continue;
111
+ }
112
+ if (run) runs.push(run);
113
+ run = cell
114
+ ? {
115
+ imageId: cell.imageId,
116
+ row: y,
117
+ column: x,
118
+ rows: 1,
119
+ columns: 1,
120
+ srcRow: cell.row,
121
+ srcCol: cell.col,
122
+ }
123
+ : null;
124
+ }
125
+ if (run) runs.push(run);
126
+ // Merge with the rectangle directly above when it continues the image.
127
+ const stillOpen: PlaceholderRect[] = [];
128
+ for (const candidate of runs) {
129
+ const above = open.find(
130
+ (rect) =>
131
+ rect.imageId === candidate.imageId &&
132
+ rect.column === candidate.column &&
133
+ rect.columns === candidate.columns &&
134
+ rect.srcCol === candidate.srcCol &&
135
+ rect.row + rect.rows === y &&
136
+ rect.srcRow + rect.rows === candidate.srcRow,
137
+ );
138
+ if (above) {
139
+ above.rows += 1;
140
+ open.splice(open.indexOf(above), 1);
141
+ stillOpen.push(above);
142
+ } else stillOpen.push(candidate);
143
+ }
144
+ done.push(...open);
145
+ open.length = 0;
146
+ open.push(...stillOpen);
147
+ }
148
+ done.push(...open);
149
+ return done;
150
+ }
151
+
152
+ /**
153
+ * PURE — every visible image placement for one pane: placeholder rectangles
154
+ * resolved through the pane's image registry, then the daemon's best-effort
155
+ * cursor-anchored placements. Cells naming an unknown image are skipped.
156
+ */
157
+ export function resolveKittyPlacements(
158
+ grid: readonly TerminalReplicaRow[],
159
+ state: TerminalGraphicsState,
160
+ ): TerminalGraphicsPlacement[] {
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 }[] = [];
174
+ const byId = new Map(state.images.map((image) => [image.imageId, image]));
175
+ let scan = false;
176
+ for (const image of state.images) if (image.placeholder) scan = true;
177
+ if (scan) {
178
+ for (const rect of kittyPlaceholderRects(grid)) {
179
+ const image = byId.get(rect.imageId);
180
+ const virtualGrid = image?.placeholder;
181
+ if (!image || !virtualGrid) continue;
182
+ if (
183
+ rect.srcRow + rect.rows > virtualGrid.rows ||
184
+ rect.srcCol + rect.columns > virtualGrid.columns
185
+ )
186
+ continue;
187
+ placements.push({
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
+ },
200
+ });
201
+ }
202
+ }
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
+ }
207
+ return placements;
208
+ }
@@ -0,0 +1,24 @@
1
+ import { TmuxServerScopeSchemaZ, TmuxServerDiagnosticsSchemaZ } from "@tmux-ide/contracts";
2
+ import { TmuxServerScopeError } from "../lib/tmux-server-owners.js";
3
+ /** Mount beneath the existing owner-authenticated, no-store server router. */
4
+ export function mountTmuxServerDiagnosticsRoute(app, options) {
5
+ app.get(`${options.scopedPath}/diagnostics`, async (c) => {
6
+ const scope = TmuxServerScopeSchemaZ.safeParse({
7
+ serverId: c.req.param("serverId"),
8
+ generation: c.req.param("generation"),
9
+ });
10
+ if (!scope.success)
11
+ return c.json({ error: { code: "invalid-request" } }, 400);
12
+ try {
13
+ c.req.raw.signal.throwIfAborted();
14
+ const tmuxVersion = await options.owners.withOwner(scope.data, (owner) => owner.readServerVersion(c.req.raw.signal));
15
+ c.req.raw.signal.throwIfAborted();
16
+ return c.json(TmuxServerDiagnosticsSchemaZ.parse({ version: 1, server: scope.data, tmuxVersion }));
17
+ }
18
+ catch (error) {
19
+ if (error instanceof TmuxServerScopeError)
20
+ return c.json({ error: { code: "scope-unavailable" } }, error.code === "not-found" ? 404 : 409);
21
+ return c.json({ error: { code: "diagnostics-unavailable" } }, 503);
22
+ }
23
+ });
24
+ }
@@ -0,0 +1,48 @@
1
+ import { HistoryAnchorRequestSchemaZ, HistoryAnchorResponseSchemaZ, TmuxServerScopeSchemaZ, } from "@tmux-ide/contracts";
2
+ import { TmuxServerScopeError } from "../lib/tmux-server-owners.js";
3
+ /** Mounted below owner-authenticated no-store middleware. Never opens a runtime. */
4
+ export function mountTmuxServerHistoryAnchorRoute(app, options) {
5
+ app.post(`${options.scopedPath}/history-anchor`, async (c) => {
6
+ const scope = TmuxServerScopeSchemaZ.safeParse({
7
+ serverId: c.req.param("serverId"),
8
+ generation: c.req.param("generation"),
9
+ });
10
+ if (!scope.success)
11
+ return c.json({ error: { code: "invalid-request" } }, 400);
12
+ let body;
13
+ try {
14
+ body = HistoryAnchorRequestSchemaZ.parse(await options.readBody(c.req.raw));
15
+ }
16
+ catch {
17
+ return c.json({ error: { code: "invalid-request" } }, 400);
18
+ }
19
+ if (body.proof.generation !== scope.data.generation)
20
+ return c.json({ error: { code: "stale-generation" } }, 409);
21
+ try {
22
+ c.req.raw.signal.throwIfAborted();
23
+ const result = await options.owners.withOwner(scope.data, async (owner) => {
24
+ const sessions = await owner.catalog();
25
+ c.req.raw.signal.throwIfAborted();
26
+ const workspace = owner.workspaceRegistry.get(body.proof.workspaceName);
27
+ if (!workspace ||
28
+ !sessions.some((row) => row.sessionName === workspace.sessionName && row.liveSessionId === body.liveSessionId))
29
+ return { status: "stale" };
30
+ const result = owner.sessionRuntimeRegistry.historyAnchor(workspace.sessionName, HistoryAnchorRequestSchemaZ.parse({
31
+ ...body,
32
+ proof: { ...body.proof, workspaceName: workspace.sessionName },
33
+ }));
34
+ // Delivery aliases are transport addresses, not canonical session names.
35
+ return result.status === "resolved"
36
+ ? { ...result, proof: { ...result.proof, workspaceName: body.proof.workspaceName } }
37
+ : result;
38
+ });
39
+ c.req.raw.signal.throwIfAborted();
40
+ return c.json(HistoryAnchorResponseSchemaZ.parse({ version: 1, server: scope.data, result }));
41
+ }
42
+ catch (error) {
43
+ if (error instanceof TmuxServerScopeError)
44
+ return c.json({ error: { code: "scope-unavailable" } }, error.code === "not-found" ? 404 : 409);
45
+ return c.json({ error: { code: "history-unavailable" } }, 503);
46
+ }
47
+ });
48
+ }
@@ -1,3 +1,5 @@
1
+ import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.js";
2
+ import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.js";
1
3
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.js";
2
4
  import { streamTmuxInteractions } from "./tmux-server-interaction-events.js";
3
5
  import { mountTmuxServerNativeBackingRoute } from "./tmux-server-native-backing.js";
@@ -10,6 +12,7 @@ import { z } from "zod";
10
12
  import { TmuxAgentOpenRequestSchemaZ, TmuxAgentOpenResponseSchemaZ, WorkspaceCatalogLiveSessionIdSchemaZ, WindowLinkTargetSchemaZ, WindowSplitLayoutResourceSchemaZ, TMUX_SERVERS_API_PATH, PANE_STREAM_ISSUE_PATH, TmuxServerIdSchemaZ, TmuxServerScopeSchemaZ, TmuxServersResourceSchemaZ, TmuxServerSessionsResourceSchemaZ, WorkspaceMultiplexerMutationRequestSchemaZ, WorkspacePaneCreateMutationRequestSchemaZ, WorkspaceSessionCreateArgumentsSchemaZ, } from "@tmux-ide/contracts";
11
13
  import { TmuxServerRegistrationSchemaZ, TmuxServerScopeError, } from "../lib/tmux-server-owners.js";
12
14
  import { WorkspaceMultiplexerError } from "../lib/workspace-multiplexer-verbs.js";
15
+ import { WorkspacePaneCreationError } from "../lib/workspace-pane-creation.js";
13
16
  import { ownerBearerMatches } from "./owner-authority.js";
14
17
  import { mountPaneStreamIssueRoute } from "./pane-stream-issue.js";
15
18
  import { projectTerminalRuntimeInventory } from "./resources/terminal-runtime-inventory.js";
@@ -54,6 +57,8 @@ function failure(c, error) {
54
57
  return c.json({ error: { code: "split-layout-stale" } }, 409);
55
58
  if (error instanceof TmuxServerScopeError)
56
59
  return c.json({ error: { code: error.code } }, error.code === "not-found" ? 404 : error.code === "capacity" ? 429 : 409);
60
+ if (error instanceof WorkspacePaneCreationError && error.code === "harness_unavailable")
61
+ return c.json({ error: { code: "harness_unavailable" } }, 409);
57
62
  if (error instanceof WorkspaceMultiplexerError)
58
63
  return c.json({ error: { code: error.code } }, 409);
59
64
  if (error instanceof z.ZodError || error instanceof SyntaxError || error instanceof TypeError)
@@ -90,6 +95,12 @@ export function mountTmuxServerRoutes(app, options) {
90
95
  return c.json({ removed: true });
91
96
  }));
92
97
  const scoped = `${base}/:serverId/:generation`;
98
+ mountTmuxServerHistoryAnchorRoute(app, {
99
+ scopedPath: scoped,
100
+ owners: options.owners,
101
+ readBody: (request) => boundedJson(request, 4096),
102
+ });
103
+ mountTmuxServerDiagnosticsRoute(app, { scopedPath: scoped, owners: options.owners });
93
104
  mountTmuxServerEditorContextRoute(app, {
94
105
  scopedPath: scoped,
95
106
  owners: options.owners,