tmux-ide 2.9.0-beta.16 → 2.9.0-beta.17

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 (58) hide show
  1. package/bin/cli.js +165 -24
  2. package/package.json +2 -2
  3. package/packages/contracts/src/fleet-lifecycle.ts +5 -1
  4. package/packages/contracts/src/workspace-multiplexer.ts +11 -1
  5. package/packages/daemon/dist/command-center/actions/handlers/fleet-lifecycle.js +5 -0
  6. package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +97 -16
  7. package/packages/daemon/dist/lib/fleet-preview-model.js +1 -0
  8. package/packages/daemon/dist/lib/workspace-multiplexer-verbs.js +35 -4
  9. package/packages/daemon/dist/terminal/pane-stream/pane-stream-websocket.js +20 -2
  10. package/packages/daemon/dist/terminal/session-runtime/multiplexer-backend.js +12 -1
  11. package/packages/daemon/dist/terminal/session-runtime/registry.js +5 -1
  12. package/packages/daemon/dist/terminal/session-runtime/semantic-mutation-executor.js +3 -1
  13. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +80 -13
  14. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +163 -8
  15. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +9 -1
  16. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +2 -0
  17. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +1 -0
  18. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +80 -0
  19. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +66 -8
  20. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +10 -0
  21. package/packages/daemon/dist/tui/mirror/runtime/application-palette-search-owner.js +57 -0
  22. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +47 -1
  23. package/packages/daemon/dist/tui/mirror/runtime/fleet-presentation.js +20 -0
  24. package/packages/daemon/dist/tui/mirror/theme.js +9 -0
  25. package/packages/daemon/dist/tui/mirror/ui/badge.jsx +3 -1
  26. package/packages/daemon/dist/tui/mirror/ui/navigation-row.jsx +3 -1
  27. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +23 -6
  28. package/packages/daemon/src/command-center/actions/handlers/fleet-lifecycle.ts +5 -0
  29. package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +136 -32
  30. package/packages/daemon/src/lib/fleet-preview-model.ts +13 -0
  31. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +41 -3
  32. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +33 -2
  33. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +14 -1
  34. package/packages/daemon/src/terminal/session-runtime/registry.ts +6 -1
  35. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +4 -1
  36. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +84 -14
  37. package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +185 -9
  38. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +244 -0
  39. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +17 -1
  40. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +8 -0
  41. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +3 -0
  42. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +89 -0
  43. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +84 -8
  44. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +16 -0
  45. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +208 -0
  46. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +61 -0
  47. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +25 -0
  48. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +17 -1
  49. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +57 -5
  50. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +21 -1
  51. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +66 -1
  52. package/packages/daemon/src/tui/mirror/runtime/fleet-presentation.ts +33 -0
  53. package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +2 -0
  54. package/packages/daemon/src/tui/mirror/theme.ts +10 -0
  55. package/packages/daemon/src/tui/mirror/ui/badge.tsx +10 -1
  56. package/packages/daemon/src/tui/mirror/ui/navigation-row.tsx +10 -1
  57. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +42 -6
  58. package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +1 -0
package/bin/cli.js CHANGED
@@ -3730,7 +3730,14 @@ var init_workspace_multiplexer = __esm({
3730
3730
  WorkspacePaneKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.extend({
3731
3731
  semanticPaneId: TerminalAttachmentSemanticPaneIdSchemaZ
3732
3732
  }).strict())();
3733
- WorkspaceSessionKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.strict())();
3733
+ WorkspaceSessionKillArgumentsSchemaZ = /* @__PURE__ */ (() => WorkspaceScopedSchemaZ.extend({
3734
+ /** Passive fleet actions fence the daemon and exact live session incarnation. */
3735
+ fleetTarget: z31.object({
3736
+ daemonInstanceId: z31.uuid(),
3737
+ liveSessionId: z31.string().regex(/^live-session\.[a-f0-9]{20}$/),
3738
+ sessionName: WorkspaceMultiplexerNameSchemaZ
3739
+ }).strict().optional()
3740
+ }).strict())();
3734
3741
  WorkspaceRenameArgumentsSchemaZ = /* @__PURE__ */ (() => z31.discriminatedUnion("scope", [
3735
3742
  WorkspaceScopedSchemaZ.extend({
3736
3743
  scope: z31.literal("session"),
@@ -3938,7 +3945,11 @@ var init_fleet_lifecycle = __esm({
3938
3945
  "display name contains control bytes"
3939
3946
  ).refine((value) => !value.startsWith("-"), "display name cannot be parsed as an option"))();
3940
3947
  OwnerPathSchemaZ = /* @__PURE__ */ (() => z32.string().min(1).max(4096).refine((value) => !value.includes("\0")))();
3941
- WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z32.object({ displayName: SafeDisplayNameSchemaZ, cwd: OwnerPathSchemaZ.optional() }).strict())();
3948
+ WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z32.object({
3949
+ displayName: SafeDisplayNameSchemaZ,
3950
+ cwd: OwnerPathSchemaZ.optional(),
3951
+ expectedDaemonInstanceId: z32.uuid().optional()
3952
+ }).strict())();
3942
3953
  WorkspaceSessionCreateResultSchemaZ = /* @__PURE__ */ (() => z32.object({
3943
3954
  operationId: z32.uuid(),
3944
3955
  daemonInstanceId: z32.uuid(),
@@ -10261,7 +10272,7 @@ var require_package = __commonJS({
10261
10272
  "package.json"(exports, module) {
10262
10273
  module.exports = {
10263
10274
  name: "tmux-ide",
10264
- version: "2.9.0-beta.16",
10275
+ version: "2.9.0-beta.17",
10265
10276
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
10266
10277
  type: "module",
10267
10278
  bin: {
@@ -10323,7 +10334,7 @@ var require_package = __commonJS({
10323
10334
  postinstall: "node scripts/postinstall.js",
10324
10335
  docs: "turbo run dev --filter=@tmux-ide/docs",
10325
10336
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
10326
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx",
10337
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
10327
10338
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
10328
10339
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
10329
10340
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
@@ -18414,7 +18425,7 @@ import { z as z71 } from "zod";
18414
18425
  import { bodyLimit } from "hono/body-limit";
18415
18426
  import { stripVTControlCharacters } from "node:util";
18416
18427
  function createFleetPreviewCapture(run) {
18417
- return async (liveSessionId2, signal) => {
18428
+ const snapshot = async (liveSessionId2, signal, windowId) => {
18418
18429
  const readSessions = async () => {
18419
18430
  const raw = await run(
18420
18431
  ["list-panes", "-a", "-F", "#{pid} #{session_id} #{session_created} #{session_name}"],
@@ -18424,24 +18435,80 @@ function createFleetPreviewCapture(run) {
18424
18435
  };
18425
18436
  const session = (await readSessions()).find((s) => s.liveSessionId === liveSessionId2);
18426
18437
  if (!session) return null;
18427
- const panes = await run(
18438
+ const readPanes = async () => (await run(
18428
18439
  [
18429
18440
  "list-panes",
18430
18441
  "-s",
18431
18442
  "-t",
18432
18443
  `=${session.sessionName}`,
18433
18444
  "-F",
18434
- "#{pane_id} #{window_active} #{pane_active}"
18445
+ "#{pane_id} #{window_active} #{pane_active} #{window_id} #{window_index}"
18435
18446
  ],
18436
18447
  signal
18448
+ )).trim().split("\n").map((line) => line.split(" "));
18449
+ const rows = await readPanes();
18450
+ const names = /* @__PURE__ */ new Map();
18451
+ const windowNames = await run(
18452
+ ["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id} #{window_name}"],
18453
+ signal
18437
18454
  );
18438
- const rows = panes.trim().split("\n").map((line) => line.split(" "));
18439
- const pane = rows.find((row) => row[1] === "1" && row[2] === "1")?.[0] ?? rows[0]?.[0];
18455
+ for (const line of windowNames.split("\n")) {
18456
+ const tab = line.indexOf(" ");
18457
+ if (tab > 0)
18458
+ names.set(
18459
+ line.slice(0, tab),
18460
+ cleanText(line.slice(tab + 1)).replace(/[\n\t]/gu, " ").slice(0, 80)
18461
+ );
18462
+ }
18463
+ const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
18464
+ const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
18465
+ if (windowId && !windowRows.some((row) => row[3] === windowId)) return null;
18466
+ const toWindow = (row) => ({
18467
+ id: row[3],
18468
+ index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
18469
+ name: names.get(row[3]) ?? `Window ${row[4]}`,
18470
+ active: row[1] === "1"
18471
+ });
18472
+ const windows = [];
18473
+ for (const row of windowRows) {
18474
+ if (windows.some((w) => w.id === row[3])) continue;
18475
+ if (windows.length >= 64) break;
18476
+ windows.push(toWindow(row));
18477
+ }
18478
+ if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
18479
+ const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
18480
+ if (selectedRow) windows.splice(63, 1, toWindow(selectedRow));
18481
+ }
18482
+ let paneBudget = 256;
18483
+ for (const window2 of [...windows].sort(
18484
+ (a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId)
18485
+ )) {
18486
+ const ids = [
18487
+ ...new Set(
18488
+ windowRows.filter((row) => row[3] === window2.id && /^%\d+$/u.test(row[0] ?? "")).map((row) => row[0])
18489
+ )
18490
+ ];
18491
+ if (ids.length <= paneBudget) {
18492
+ window2.paneIds = ids;
18493
+ paneBudget -= ids.length;
18494
+ }
18495
+ }
18496
+ windows.sort((a, b) => a.index - b.index);
18497
+ const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
18498
+ const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
18440
18499
  if (!pane || !/^%\d+$/u.test(pane)) return null;
18441
18500
  const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
18442
18501
  if (!(await readSessions()).some((s) => s.liveSessionId === liveSessionId2)) return null;
18443
- return stripVTControlCharacters(captured).replace(/[^\P{Cc}\n\t]/gu, "").split("\n").slice(-24).map((line) => line.slice(0, 180)).join("\n").slice(0, 8192);
18502
+ if (selectedWindowId && !(await readPanes()).some((r) => r[3] === selectedWindowId && r[0] === pane))
18503
+ return null;
18504
+ return {
18505
+ windows,
18506
+ selectedWindowId,
18507
+ text: cleanText(captured).split("\n").slice(-24).map((line) => line.slice(0, 180)).join("\n").slice(0, 8192)
18508
+ };
18444
18509
  };
18510
+ const capture = async (liveSessionId2, signal) => (await snapshot(liveSessionId2, signal))?.text ?? null;
18511
+ return Object.assign(capture, { snapshot });
18445
18512
  }
18446
18513
  function mountFleetPreviewRoute(app, options) {
18447
18514
  const authorize = ownerAuthorityGate(options.ownerToken, {
@@ -18476,11 +18543,18 @@ function mountFleetPreviewRoute(app, options) {
18476
18543
  next = now + 250;
18477
18544
  pending = true;
18478
18545
  try {
18479
- const text = await options.capture(
18480
- input.data.liveSessionId,
18481
- AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)])
18482
- );
18483
- return text === null ? c.json({ error: "Session changed" }, 409) : c.json({ daemon: options.daemon, liveSessionId: input.data.liveSessionId, text });
18546
+ const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
18547
+ if (input.data.windowId && !options.capture.snapshot)
18548
+ return c.json({ error: "Window preview unavailable" }, 503);
18549
+ const snapshot = options.capture.snapshot ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId) : null;
18550
+ const legacy = !options.capture.snapshot ? await options.capture(input.data.liveSessionId, signal) : null;
18551
+ const text = snapshot?.text ?? legacy;
18552
+ return text === null ? c.json({ error: "Session changed" }, 409) : c.json({
18553
+ daemon: options.daemon,
18554
+ liveSessionId: input.data.liveSessionId,
18555
+ text,
18556
+ ...snapshot ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId } : {}
18557
+ });
18484
18558
  } catch {
18485
18559
  return c.json({ error: "Preview unavailable" }, 503);
18486
18560
  } finally {
@@ -18488,14 +18562,16 @@ function mountFleetPreviewRoute(app, options) {
18488
18562
  }
18489
18563
  });
18490
18564
  }
18491
- var requestSchema;
18565
+ var cleanText, requestSchema;
18492
18566
  var init_fleet_preview_route = __esm({
18493
18567
  "packages/daemon/src/command-center/resources/fleet-preview-route.ts"() {
18494
18568
  "use strict";
18495
18569
  init_owner_authority();
18496
18570
  init_discovery();
18571
+ cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
18497
18572
  requestSchema = z71.strictObject({
18498
18573
  expectedInstanceId: z71.uuid(),
18574
+ windowId: z71.string().regex(/^@\d+$/u).optional(),
18499
18575
  liveSessionId: z71.string().regex(/^live-session\.[a-f0-9]{20}$/u)
18500
18576
  });
18501
18577
  }
@@ -33670,6 +33746,7 @@ var CREATION_OPTION2, SEMANTIC_PANE_OPTION4, SEMANTIC_WINDOW_OPTION3, DISPLAY_TI
33670
33746
  var init_workspace_multiplexer_verbs = __esm({
33671
33747
  "packages/daemon/src/lib/workspace-multiplexer-verbs.ts"() {
33672
33748
  "use strict";
33749
+ init_discovery();
33673
33750
  init_src();
33674
33751
  init_src2();
33675
33752
  init_workspace_pane_creation2();
@@ -33888,6 +33965,37 @@ var init_workspace_multiplexer_verbs = __esm({
33888
33965
  operationId: request.operationId
33889
33966
  });
33890
33967
  }
33968
+ if (request.intent.verb === "workspace.session.kill" && request.intent.fleetTarget) {
33969
+ const target = request.intent.fleetTarget;
33970
+ if (target.daemonInstanceId !== this.#daemonInstanceId)
33971
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
33972
+ operationId: request.operationId
33973
+ });
33974
+ const raw2 = this.#io.runTmux([
33975
+ "list-panes",
33976
+ "-a",
33977
+ "-F",
33978
+ "#{pid} #{session_id} #{session_created} #{session_name}"
33979
+ ]);
33980
+ const match = raw2.split("\n").find((line) => {
33981
+ const session = discoverLiveSessionSummaries(() => line)[0];
33982
+ return session?.liveSessionId === target.liveSessionId && session.sessionName === target.sessionName;
33983
+ });
33984
+ if (!match)
33985
+ throw new WorkspaceMultiplexerError("workspace_not_found", {
33986
+ reason: "session_incarnation_changed"
33987
+ });
33988
+ const runtimeId = match.split(" ")[1];
33989
+ return this.#killSession(
33990
+ target.sessionName,
33991
+ {
33992
+ operationId: request.operationId,
33993
+ daemonInstanceId: this.#daemonInstanceId,
33994
+ workspaceName: request.intent.workspaceName
33995
+ },
33996
+ runtimeId
33997
+ );
33998
+ }
33891
33999
  const workspace = this.#registry.get(request.intent.workspaceName);
33892
34000
  if (!workspace) {
33893
34001
  throw new WorkspaceMultiplexerError("workspace_not_found", {
@@ -34128,19 +34236,19 @@ var init_workspace_multiplexer_verbs = __esm({
34128
34236
  remainingWindowCount: windowIdsOf(after).length
34129
34237
  };
34130
34238
  }
34131
- #killSession(sessionName, envelope) {
34239
+ #killSession(sessionName, envelope, exactTarget = `=${sessionName}`) {
34132
34240
  let existed = true;
34133
34241
  try {
34134
- this.#io.runTmux(["has-session", "-t", `=${sessionName}`]);
34242
+ this.#io.runTmux(["has-session", "-t", exactTarget]);
34135
34243
  } catch (error) {
34136
34244
  if (!this.#io.isMissingTmuxTarget(error)) throw error;
34137
34245
  existed = false;
34138
34246
  }
34139
34247
  if (existed) {
34140
- this.#io.runTmux(["kill-session", "-t", `=${sessionName}`]);
34248
+ this.#io.runTmux(["kill-session", "-t", exactTarget]);
34141
34249
  let stillPresent = true;
34142
34250
  try {
34143
- this.#io.runTmux(["has-session", "-t", `=${sessionName}`]);
34251
+ this.#io.runTmux(["has-session", "-t", exactTarget]);
34144
34252
  } catch {
34145
34253
  stillPresent = false;
34146
34254
  }
@@ -39533,7 +39641,7 @@ var init_semantic_mutation_executor = __esm({
39533
39641
  intent = { ...intent, origin: authority.origin };
39534
39642
  }
39535
39643
  const authenticatedSourceSemanticPaneId = authority.authenticatedSourceSemanticPaneId ?? null;
39536
- const session = this.#options.resolveSession(intent.workspaceName);
39644
+ const session = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.sessionName : this.#options.resolveSession(intent.workspaceName);
39537
39645
  const ledger2 = this.#ledger(session ?? MISSING_SESSION_LEDGER);
39538
39646
  const origin = authority.origin;
39539
39647
  const fingerprint2 = JSON.stringify([intent, authenticatedSourceSemanticPaneId, origin]);
@@ -56807,7 +56915,7 @@ var init_registry2 = __esm({
56807
56915
  if (this.#disposed) return Promise.reject(new Error("SessionRuntimeRegistry is disposed"));
56808
56916
  runtime.assertController(lease);
56809
56917
  let intent = SessionRuntimeSemanticIntentSchemaZ.parse(rawIntent);
56810
- const resolvedSession = this.#resolveSession?.(intent.workspaceName) ?? null;
56918
+ const resolvedSession = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.daemonInstanceId === this.generation ? intent.fleetTarget.sessionName : null : this.#resolveSession?.(intent.workspaceName) ?? null;
56811
56919
  if (resolvedSession !== runtime.session) {
56812
56920
  return Promise.reject(
56813
56921
  new SessionRuntimeControllerLeaseError(
@@ -64732,6 +64840,14 @@ var init_pane_stream_websocket = __esm({
64732
64840
  this.close(1011, "stream-unavailable");
64733
64841
  return;
64734
64842
  }
64843
+ const visibleAtOpen = new Set(
64844
+ stagedAuthority.layouts.flatMap(
64845
+ (layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
64846
+ )
64847
+ );
64848
+ channels.sort(
64849
+ (left, right) => Number(visibleAtOpen.has(right.semanticPaneId)) - Number(visibleAtOpen.has(left.semanticPaneId))
64850
+ );
64735
64851
  const openings = channels.map(async (channel) => {
64736
64852
  const pending = [];
64737
64853
  let ready = false;
@@ -64802,7 +64918,15 @@ var init_pane_stream_websocket = __esm({
64802
64918
  });
64803
64919
  layoutActivated = true;
64804
64920
  this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
64805
- for (const { channel, delivery, pending, markReady } of opened) {
64921
+ const visibleAtDelivery = new Set(
64922
+ authority.layouts.flatMap(
64923
+ (layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
64924
+ )
64925
+ );
64926
+ opened.sort(
64927
+ (left, right) => Number(visibleAtDelivery.has(right.channel.semanticPaneId)) - Number(visibleAtDelivery.has(left.channel.semanticPaneId))
64928
+ );
64929
+ for (const { channel, delivery } of opened) {
64806
64930
  if (this.#closed || channel.closed) {
64807
64931
  await delivery.close();
64808
64932
  continue;
@@ -64824,6 +64948,9 @@ var init_pane_stream_websocket = __esm({
64824
64948
  pane: channel.semanticPaneId,
64825
64949
  negotiation: delivery.negotiation
64826
64950
  });
64951
+ }
64952
+ for (const { channel, pending, markReady } of opened) {
64953
+ if (this.#closed || channel.closed) continue;
64827
64954
  markReady();
64828
64955
  for (const message of pending)
64829
64956
  await this.#sendTerminalDelivery(channel.semanticPaneId, message);
@@ -65895,7 +66022,16 @@ function createSessionRuntimeMultiplexerBackend(options) {
65895
66022
  };
65896
66023
  return {
65897
66024
  mutate: async (request, authenticatedHostClientId, sourcePaneCredential, ownerAuthorized = false) => {
65898
- const session = options.resolveSession(request.intent.workspaceName);
66025
+ const fleetTarget = request.intent.verb === "workspace.session.kill" ? request.intent.fleetTarget : void 0;
66026
+ if (fleetTarget && (!ownerAuthorized || authenticatedHostClientId || sourcePaneCredential))
66027
+ throw new WorkspaceMultiplexerError("operation_conflict", {
66028
+ reason: "fleet_close_requires_explicit_owner"
66029
+ });
66030
+ if (fleetTarget && fleetTarget.daemonInstanceId !== options.registry.generation)
66031
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
66032
+ operationId: request.operationId
66033
+ });
66034
+ const session = fleetTarget && ownerAuthorized && !authenticatedHostClientId && !sourcePaneCredential ? fleetTarget.sessionName : options.resolveSession(request.intent.workspaceName);
65899
66035
  if (!session) {
65900
66036
  throw new Error(`Workspace ${request.intent.workspaceName} has no live tmux session`);
65901
66037
  }
@@ -68774,6 +68910,11 @@ async function workspaceSessionCreateHandler(input, context) {
68774
68910
  message: "Fleet lifecycle is unavailable."
68775
68911
  });
68776
68912
  const [operation, generation] = authorityContext(context);
68913
+ if (input.expectedDaemonInstanceId && input.expectedDaemonInstanceId !== generation)
68914
+ throw new ActionError({
68915
+ code: "bad_request",
68916
+ message: "The selected daemon has been replaced."
68917
+ });
68777
68918
  return await mapAuthority(
68778
68919
  () => context.fleetLifecycleBackend.createSession(operation, generation, input)
68779
68920
  );
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.16",
3
+ "version": "2.9.0-beta.17",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -62,7 +62,7 @@
62
62
  "postinstall": "node scripts/postinstall.js",
63
63
  "docs": "turbo run dev --filter=@tmux-ide/docs",
64
64
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
65
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx",
65
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/pane-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/native-grid-projection-renderer.test.tsx ./packages/daemon/src/tui/mirror/widget-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/missions-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/recipes-gallery-renderer.test.tsx ./packages/daemon/src/tui/mirror/shell-chrome-renderer.test.tsx ./packages/daemon/src/tui/mirror/sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/home-files-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/changes-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/activity-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/files/session-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-terminal-workspace-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-view-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-root-error-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-backpressure-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-demand-cadence-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/files-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/changes-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/missions-activity-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/dialogs-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/optional-feature-registry-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/palette-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/pane-scoped-terminal-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/features/rich-preview/feature.test.ts ./packages/daemon/src/tui/mirror/runtime/rich-preview-optional-feature-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/application-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/pane-frame-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-chrome-view.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-window-strip-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-shell-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/workbench-dock-dual-host-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/agent-terminal-canvas-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/command-palette-surface-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/opentui-insertion-stability-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-home-renderer.test.tsx ./packages/daemon/src/tui/mirror/workspace/terminal-pane-header-polish-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-roster-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-home-agent-flow-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-shell-sidebar-catalog-renderer.test.tsx ./packages/daemon/src/tui/mirror/ui/ui-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-machine-sidebar-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-switcher-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-palette-preview-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-palette-renderer.test.tsx ./packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions-renderer.test.tsx",
66
66
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
67
67
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
68
68
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
@@ -27,7 +27,11 @@ const OwnerPathSchemaZ = z
27
27
  .max(4_096)
28
28
  .refine((value) => !value.includes("\0"));
29
29
  export const WorkspaceSessionCreateArgumentsSchemaZ = z
30
- .object({ displayName: SafeDisplayNameSchemaZ, cwd: OwnerPathSchemaZ.optional() })
30
+ .object({
31
+ displayName: SafeDisplayNameSchemaZ,
32
+ cwd: OwnerPathSchemaZ.optional(),
33
+ expectedDaemonInstanceId: z.uuid().optional(),
34
+ })
31
35
  .strict();
32
36
  export type WorkspaceSessionCreateArguments = z.infer<
33
37
  typeof WorkspaceSessionCreateArgumentsSchemaZ
@@ -84,7 +84,17 @@ export const WorkspacePaneKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
84
84
  }).strict();
85
85
  export type WorkspacePaneKillArguments = z.infer<typeof WorkspacePaneKillArgumentsSchemaZ>;
86
86
 
87
- export const WorkspaceSessionKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.strict();
87
+ export const WorkspaceSessionKillArgumentsSchemaZ = WorkspaceScopedSchemaZ.extend({
88
+ /** Passive fleet actions fence the daemon and exact live session incarnation. */
89
+ fleetTarget: z
90
+ .object({
91
+ daemonInstanceId: z.uuid(),
92
+ liveSessionId: z.string().regex(/^live-session\.[a-f0-9]{20}$/),
93
+ sessionName: WorkspaceMultiplexerNameSchemaZ,
94
+ })
95
+ .strict()
96
+ .optional(),
97
+ }).strict();
88
98
  export type WorkspaceSessionKillArguments = z.infer<typeof WorkspaceSessionKillArgumentsSchemaZ>;
89
99
 
90
100
  export const WorkspaceRenameArgumentsSchemaZ = z.discriminatedUnion("scope", [
@@ -34,6 +34,11 @@ export async function workspaceSessionCreateHandler(input, context) {
34
34
  message: "Fleet lifecycle is unavailable.",
35
35
  });
36
36
  const [operation, generation] = authorityContext(context);
37
+ if (input.expectedDaemonInstanceId && input.expectedDaemonInstanceId !== generation)
38
+ throw new ActionError({
39
+ code: "bad_request",
40
+ message: "The selected daemon has been replaced.",
41
+ });
37
42
  return await mapAuthority(() => context.fleetLifecycleBackend.createSession(operation, generation, input));
38
43
  }
39
44
  export async function fleetAgentMutateHandler(input, context) {
@@ -3,8 +3,9 @@ import { bodyLimit } from "hono/body-limit";
3
3
  import { stripVTControlCharacters } from "node:util";
4
4
  import { ownerAuthorityGate } from "../owner-authority.js";
5
5
  import { discoverLiveSessionSummaries } from "../discovery.js";
6
+ const cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
6
7
  export function createFleetPreviewCapture(run) {
7
- return async (liveSessionId, signal) => {
8
+ const snapshot = async (liveSessionId, signal, windowId) => {
8
9
  const readSessions = async () => {
9
10
  const raw = await run(["list-panes", "-a", "-F", "#{pid}\t#{session_id}\t#{session_created}\t#{session_name}"], signal);
10
11
  return discoverLiveSessionSummaries(() => raw);
@@ -12,36 +13,99 @@ export function createFleetPreviewCapture(run) {
12
13
  const session = (await readSessions()).find((s) => s.liveSessionId === liveSessionId);
13
14
  if (!session)
14
15
  return null;
15
- const panes = await run([
16
+ const readPanes = async () => (await run([
16
17
  "list-panes",
17
18
  "-s",
18
19
  "-t",
19
20
  `=${session.sessionName}`,
20
21
  "-F",
21
- "#{pane_id}\t#{window_active}\t#{pane_active}",
22
- ], signal);
23
- const rows = panes
22
+ "#{pane_id}\t#{window_active}\t#{pane_active}\t#{window_id}\t#{window_index}",
23
+ ], signal))
24
24
  .trim()
25
25
  .split("\n")
26
26
  .map((line) => line.split("\t"));
27
- const pane = rows.find((row) => row[1] === "1" && row[2] === "1")?.[0] ?? rows[0]?.[0];
27
+ const rows = await readPanes();
28
+ // Names are untrusted display text. Keep them out of the pane membership
29
+ // format so embedded newlines cannot manufacture a capture target.
30
+ const names = new Map();
31
+ const windowNames = await run(["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id}\t#{window_name}"], signal);
32
+ for (const line of windowNames.split("\n")) {
33
+ const tab = line.indexOf("\t");
34
+ if (tab > 0)
35
+ names.set(line.slice(0, tab), cleanText(line.slice(tab + 1))
36
+ .replace(/[\n\t]/gu, " ")
37
+ .slice(0, 80));
38
+ }
39
+ const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
40
+ const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
41
+ if (windowId && !windowRows.some((row) => row[3] === windowId))
42
+ return null;
43
+ const toWindow = (row) => ({
44
+ id: row[3],
45
+ index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
46
+ name: names.get(row[3]) ?? `Window ${row[4]}`,
47
+ active: row[1] === "1",
48
+ });
49
+ const windows = [];
50
+ for (const row of windowRows) {
51
+ if (windows.some((w) => w.id === row[3]))
52
+ continue;
53
+ if (windows.length >= 64)
54
+ break;
55
+ windows.push(toWindow(row));
56
+ }
57
+ // Preserve the actual active/requested target even on unusually large sessions.
58
+ if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
59
+ const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
60
+ if (selectedRow)
61
+ windows.splice(63, 1, toWindow(selectedRow));
62
+ }
63
+ let paneBudget = 256;
64
+ // The selected window gets the membership budget first; omitted membership
65
+ // remains explicitly unknown instead of reporting incomplete activity counts.
66
+ for (const window of [...windows].sort((a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId))) {
67
+ const ids = [
68
+ ...new Set(windowRows
69
+ .filter((row) => row[3] === window.id && /^%\d+$/u.test(row[0] ?? ""))
70
+ .map((row) => row[0])),
71
+ ];
72
+ if (ids.length <= paneBudget) {
73
+ window.paneIds = ids;
74
+ paneBudget -= ids.length;
75
+ }
76
+ }
77
+ windows.sort((a, b) => a.index - b.index);
78
+ const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
79
+ const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
28
80
  if (!pane || !/^%\d+$/u.test(pane))
29
81
  return null;
30
- // Read-only capture: no attach, select, resize, input or ownership acquisition.
82
+ // Passive capture never changes active windows, size, input or terminal ownership.
31
83
  const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
32
84
  if (!(await readSessions()).some((s) => s.liveSessionId === liveSessionId))
33
85
  return null;
34
- return stripVTControlCharacters(captured)
35
- .replace(/[^\P{Cc}\n\t]/gu, "")
36
- .split("\n")
37
- .slice(-24)
38
- .map((line) => line.slice(0, 180))
39
- .join("\n")
40
- .slice(0, 8192);
86
+ if (selectedWindowId &&
87
+ !(await readPanes()).some((r) => r[3] === selectedWindowId && r[0] === pane))
88
+ return null;
89
+ return {
90
+ windows,
91
+ selectedWindowId,
92
+ text: cleanText(captured)
93
+ .split("\n")
94
+ .slice(-24)
95
+ .map((line) => line.slice(0, 180))
96
+ .join("\n")
97
+ .slice(0, 8192),
98
+ };
41
99
  };
100
+ const capture = async (liveSessionId, signal) => (await snapshot(liveSessionId, signal))?.text ?? null;
101
+ return Object.assign(capture, { snapshot });
42
102
  }
43
103
  const requestSchema = z.strictObject({
44
104
  expectedInstanceId: z.uuid(),
105
+ windowId: z
106
+ .string()
107
+ .regex(/^@\d+$/u)
108
+ .optional(),
45
109
  liveSessionId: z.string().regex(/^live-session\.[a-f0-9]{20}$/u),
46
110
  });
47
111
  export function mountFleetPreviewRoute(app, options) {
@@ -83,10 +147,27 @@ export function mountFleetPreviewRoute(app, options) {
83
147
  next = now + 250;
84
148
  pending = true;
85
149
  try {
86
- const text = await options.capture(input.data.liveSessionId, AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]));
150
+ const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
151
+ if (input.data.windowId && !options.capture.snapshot)
152
+ return c.json({ error: "Window preview unavailable" }, 503);
153
+ const snapshot = options.capture.snapshot
154
+ ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId)
155
+ : null;
156
+ // Older capture providers remain valid for the original text-only contract.
157
+ const legacy = !options.capture.snapshot
158
+ ? await options.capture(input.data.liveSessionId, signal)
159
+ : null;
160
+ const text = snapshot?.text ?? legacy;
87
161
  return text === null
88
162
  ? c.json({ error: "Session changed" }, 409)
89
- : c.json({ daemon: options.daemon, liveSessionId: input.data.liveSessionId, text });
163
+ : c.json({
164
+ daemon: options.daemon,
165
+ liveSessionId: input.data.liveSessionId,
166
+ text,
167
+ ...(snapshot
168
+ ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId }
169
+ : {}),
170
+ });
90
171
  }
91
172
  catch {
92
173
  return c.json({ error: "Preview unavailable" }, 503);
@@ -0,0 +1 @@
1
+ export {};