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
@@ -1,3 +1,5 @@
1
+ import { mountTmuxServerHistoryAnchorRoute } from "./tmux-server-history-anchor.ts";
2
+ import { mountTmuxServerDiagnosticsRoute } from "./tmux-server-diagnostics.ts";
1
3
  import { WindowSplitLayoutUnavailable } from "../terminal/mirror/window-split-authority.ts";
2
4
  import { streamTmuxInteractions } from "./tmux-server-interaction-events.ts";
3
5
  import { mountTmuxServerNativeBackingRoute } from "./tmux-server-native-backing.ts";
@@ -30,6 +32,7 @@ import {
30
32
  } from "../lib/tmux-server-owners.ts";
31
33
  import type { NativeTmuxServerOwner } from "../lib/tmux-server-owner.ts";
32
34
  import { WorkspaceMultiplexerError } from "../lib/workspace-multiplexer-verbs.ts";
35
+ import { WorkspacePaneCreationError } from "../lib/workspace-pane-creation.ts";
33
36
  import { ownerBearerMatches } from "./owner-authority.ts";
34
37
  import { mountPaneStreamIssueRoute } from "./pane-stream-issue.ts";
35
38
  import { projectTerminalRuntimeInventory } from "./resources/terminal-runtime-inventory.ts";
@@ -79,6 +82,8 @@ function failure(c: Context, error: unknown): Response {
79
82
  { error: { code: error.code } },
80
83
  error.code === "not-found" ? 404 : error.code === "capacity" ? 429 : 409,
81
84
  );
85
+ if (error instanceof WorkspacePaneCreationError && error.code === "harness_unavailable")
86
+ return c.json({ error: { code: "harness_unavailable" } }, 409);
82
87
  if (error instanceof WorkspaceMultiplexerError)
83
88
  return c.json({ error: { code: error.code } }, 409);
84
89
  if (error instanceof z.ZodError || error instanceof SyntaxError || error instanceof TypeError)
@@ -133,6 +138,12 @@ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOption
133
138
  }),
134
139
  );
135
140
  const scoped = `${base}/:serverId/:generation`;
141
+ mountTmuxServerHistoryAnchorRoute(app, {
142
+ scopedPath: scoped,
143
+ owners: options.owners,
144
+ readBody: (request) => boundedJson(request, 4096),
145
+ });
146
+ mountTmuxServerDiagnosticsRoute(app, { scopedPath: scoped, owners: options.owners });
136
147
  mountTmuxServerEditorContextRoute(app, {
137
148
  scopedPath: scoped,
138
149
  owners: options.owners,
@@ -100,6 +100,12 @@ function spawnOwner(entryPath: string, cwd: string): Promise<void> {
100
100
  });
101
101
  }
102
102
 
103
+ class ShutdownTransportError extends Error {
104
+ constructor(readonly original: unknown) {
105
+ super("Canonical daemon shutdown transport failed.", { cause: original });
106
+ }
107
+ }
108
+
103
109
  async function shutdownOlderOwner(info: CanonicalDaemonInfo): Promise<void> {
104
110
  const headers: Record<string, string> = { "content-type": "application/json" };
105
111
  if (info.authToken) headers.authorization = `Bearer ${info.authToken}`;
@@ -115,7 +121,9 @@ async function shutdownOlderOwner(info: CanonicalDaemonInfo): Promise<void> {
115
121
  }),
116
122
  signal: AbortSignal.timeout(2_000),
117
123
  },
118
- );
124
+ ).catch((error: unknown) => {
125
+ throw new ShutdownTransportError(error);
126
+ });
119
127
  const envelope = (await response.json().catch(() => null)) as {
120
128
  ok?: unknown;
121
129
  result?: { stopping?: unknown };
@@ -248,7 +256,16 @@ async function replaceOlderCanonicalDaemon(
248
256
  { reason: "canonical-record-invalid" },
249
257
  );
250
258
  }
251
- await deps.shutdownOlderOwner(info);
259
+ let transportFailure: ShutdownTransportError | undefined;
260
+ try {
261
+ await deps.shutdownOlderOwner(info);
262
+ } catch (error) {
263
+ if (!(error instanceof ShutdownTransportError)) throw error;
264
+ transportFailure = error;
265
+ }
266
+ // A competing upgrader may close the authenticated owner's listener before
267
+ // removing its record. Observe retirement, without repeating the mutation.
268
+ // A transport failure alone never authorizes starting a replacement.
252
269
  const deadline = deps.now() + timeoutMs;
253
270
  while (deps.now() < deadline) {
254
271
  const state = deps.inspect();
@@ -257,6 +274,7 @@ async function replaceOlderCanonicalDaemon(
257
274
  if (!(await deps.alive(info))) return;
258
275
  await deps.sleep(25);
259
276
  }
277
+ if (transportFailure) throw transportFailure.original;
260
278
  throw new DaemonBootstrapError(
261
279
  "control-timeout",
262
280
  "The older canonical daemon did not retire after accepting the version upgrade.",
@@ -1,3 +1,4 @@
1
+ import { TmuxServerVersionSchemaZ } from "@tmux-ide/contracts";
1
2
  import {
2
3
  createNativeSplitCapabilityProbe,
3
4
  createNativeSplitReadiness,
@@ -1400,6 +1401,7 @@ async function startEmbeddedDaemonGeneration(
1400
1401
  let paneStreamRuntime: PaneStreamRuntime | null = null;
1401
1402
  let serverOwners: Awaited<ReturnType<typeof createEmbeddedTmuxServerOwners>> | null = null;
1402
1403
  let defaultEditorContextRetired = false;
1404
+ const defaultDiagnosticsLifetime = new AbortController();
1403
1405
  const defaultSessionCreator = createNativeTmuxSessionCreator({
1404
1406
  generation: instanceId,
1405
1407
  registry: workspaceRegistry,
@@ -1805,6 +1807,29 @@ async function startEmbeddedDaemonGeneration(
1805
1807
  stateDirectory: resolveRuntimeNamespace().runtimeDir,
1806
1808
  webSocketBaseUrl: canonicalDaemonUrl("ws", bindHostname, port),
1807
1809
  defaultOwner: {
1810
+ readServerVersion: async (signal?: AbortSignal) => {
1811
+ const assertCurrent = () => {
1812
+ if (
1813
+ defaultEditorContextRetired ||
1814
+ serverOwners?.defaultRetired ||
1815
+ !initialNativeServerIdentity
1816
+ )
1817
+ throw new Error("Default tmux diagnostics owner is unavailable");
1818
+ };
1819
+ assertCurrent();
1820
+ const combined = AbortSignal.any([
1821
+ defaultDiagnosticsLifetime.signal,
1822
+ ...(signal ? [signal] : []),
1823
+ ]);
1824
+ combined.throwIfAborted();
1825
+ const output = await fleetFactsTmuxRunner(
1826
+ ["display-message", "-p", "#{version}"],
1827
+ combined,
1828
+ );
1829
+ combined.throwIfAborted();
1830
+ assertCurrent();
1831
+ return TmuxServerVersionSchemaZ.parse(output.trim());
1832
+ },
1808
1833
  get interactionObservation() {
1809
1834
  return interactionObservation;
1810
1835
  },
@@ -1913,6 +1938,7 @@ async function startEmbeddedDaemonGeneration(
1913
1938
  paneStreamRuntime,
1914
1939
  dispose: async () => {
1915
1940
  defaultEditorContextRetired = true;
1941
+ defaultDiagnosticsLifetime.abort();
1916
1942
  sessionMonitor?.stop();
1917
1943
  paneSourceCredentials.dispose();
1918
1944
  const transportResults = await Promise.allSettled([
@@ -30,6 +30,7 @@ import { observedAgentSchema } from "./tmux-server-agent-admission.ts";
30
30
  import { createPaneEditorContextResolver } from "./tmux-pane-editor-context.ts";
31
31
  import { createHash, randomUUID } from "node:crypto";
32
32
  import {
33
+ TmuxServerVersionSchemaZ,
33
34
  WorkspaceIdSchemaZ,
34
35
  WindowLinkTargetSchemaZ,
35
36
  type WindowLinkTarget,
@@ -594,6 +595,15 @@ export async function createNativeTmuxServerOwner(options: NativeTmuxServerOwner
594
595
  throw error;
595
596
  }
596
597
  return {
598
+ readServerVersion: async (signal?: AbortSignal) => {
599
+ assertOpen();
600
+ const combined = AbortSignal.any([credentialLifetime.signal, ...(signal ? [signal] : [])]);
601
+ combined.throwIfAborted();
602
+ const output = await generationRunAsync(["display-message", "-p", "#{version}"], combined);
603
+ combined.throwIfAborted();
604
+ assertOpen();
605
+ return TmuxServerVersionSchemaZ.parse(output.trim());
606
+ },
597
607
  serverId: options.serverId,
598
608
  generation,
599
609
  resolvePaneEditorContext,
@@ -23,7 +23,8 @@ export function createTmuxSessionMutationFence() {
23
23
  const output = run([
24
24
  "if-shell",
25
25
  "-t",
26
- `=${selected.name}`,
26
+ // if-shell resolves a pane target: the colon selects this exact session.
27
+ `=${selected.name}:`,
27
28
  "-F",
28
29
  `#{&&:#{==:#{session_id},${selected.id}},#{==:#{session_created},${selected.created}}}`,
29
30
  args.map((arg) => (arg === ";" ? ";" : shellEscape(arg))).join(" "),
@@ -918,6 +918,11 @@ function parsePaneSnapshot(
918
918
  * application-shell inventory. Names are resolved exactly to `$session_id`
919
919
  * before any pane query, and a second identity/topology snapshot closes races.
920
920
  */
921
+ const INVENTORY_FENCE_ATTEMPTS = 4;
922
+ /** 25, 50, 100 ms: enough for a mutation's identity stamps, bounded for every reader. */
923
+ const settleDelay = (attempt: number) =>
924
+ new Promise<void>((resolve) => setTimeout(resolve, 25 * 2 ** (attempt - 1)));
925
+
921
926
  export async function discoverWorkspaceRegistryTerminalInventory(
922
927
  registry: WorkspaceRegistry,
923
928
  runner: NativeTerminalInventoryReadRunner,
@@ -941,12 +946,6 @@ export async function discoverWorkspaceRegistryTerminalInventory(
941
946
  const argv = ["list-panes", "-s", "-t", identity.id, "-F", PANE_FORMAT] as const;
942
947
  const before = await requiredTmuxResult(runner, argv, signal);
943
948
  if (before === null) continue;
944
- const first = parsePaneSnapshot(before, identity);
945
- const panes = first.panes;
946
- const after = await requiredTmuxResult(runner, argv, signal);
947
- if (after === null) throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
948
- const second = parsePaneSnapshot(after, identity);
949
- const latest = second.panes;
950
949
  // Foreground commands, titles, focus and paths can change while the same
951
950
  // pane remains valid. Fence binding/topology only, then publish one complete
952
951
  // latest snapshot rather than mixing presentation values across reads.
@@ -961,14 +960,30 @@ export async function discoverWorkspaceRegistryTerminalInventory(
961
960
  "windowStamp",
962
961
  "index",
963
962
  ] as const;
964
- if (
965
- first.linksProof !== second.linksProof ||
966
- panes.length !== latest.length ||
967
- panes.some((pane, index) => proofKeys.some((key) => pane[key] !== latest[index]![key]))
968
- ) {
969
- throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
963
+ const matches = (
964
+ first: ReturnType<typeof parsePaneSnapshot>,
965
+ second: ReturnType<typeof parsePaneSnapshot>,
966
+ ) =>
967
+ first.linksProof === second.linksProof &&
968
+ first.panes.length === second.panes.length &&
969
+ first.panes.every((pane, index) =>
970
+ proofKeys.every((key) => pane[key] === second.panes[index]![key]),
971
+ );
972
+ // A just-created window or split is still being stamped, so topology can
973
+ // move between two reads. Let it settle for a bounded number of re-reads;
974
+ // only two consecutive matching reads are ever published.
975
+ let previous = parsePaneSnapshot(before, identity);
976
+ let stable: ReturnType<typeof parsePaneSnapshot> | null = null;
977
+ for (let attempt = 0; attempt < INVENTORY_FENCE_ATTEMPTS && !stable; attempt += 1) {
978
+ if (attempt > 0) await settleDelay(attempt);
979
+ const after = await requiredTmuxResult(runner, argv, signal);
980
+ if (after === null) throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
981
+ const next = parsePaneSnapshot(after, identity);
982
+ if (matches(previous, next)) stable = next;
983
+ else previous = next;
970
984
  }
971
- bySessionName.set(sessionName, latest);
985
+ if (!stable) throw new NativeTerminalAttachmentRuntimeError("discovery-failed");
986
+ bySessionName.set(sessionName, stable.panes);
972
987
  }
973
988
 
974
989
  const panes: NativeTerminalInventoryPaneSnapshot[] = [];