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

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 (67) hide show
  1. package/bin/cli.js +170 -26
  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 +102 -17
  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 +192 -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 +106 -69
  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 +71 -1
  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/keyboard-router.jsx +22 -0
  27. package/packages/daemon/dist/tui/mirror/ui/navigation-row.jsx +3 -1
  28. package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +25 -2
  29. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +46 -11
  30. package/packages/daemon/dist/tui/team/fuzzy.js +36 -15
  31. package/packages/daemon/src/command-center/actions/handlers/fleet-lifecycle.ts +5 -0
  32. package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +141 -35
  33. package/packages/daemon/src/lib/fleet-preview-model.ts +13 -0
  34. package/packages/daemon/src/lib/workspace-multiplexer-verbs.ts +41 -3
  35. package/packages/daemon/src/terminal/pane-stream/pane-stream-websocket.ts +33 -2
  36. package/packages/daemon/src/terminal/session-runtime/multiplexer-backend.ts +14 -1
  37. package/packages/daemon/src/terminal/session-runtime/registry.ts +6 -1
  38. package/packages/daemon/src/terminal/session-runtime/semantic-mutation-executor.ts +4 -1
  39. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +84 -14
  40. package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +218 -9
  41. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +253 -0
  42. package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +87 -113
  43. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +17 -1
  44. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +8 -0
  45. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +3 -0
  46. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +111 -75
  47. package/packages/daemon/src/tui/mirror/runtime/application-machine-overlays.tsx +8 -1
  48. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +84 -8
  49. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +16 -0
  50. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +235 -0
  51. package/packages/daemon/src/tui/mirror/runtime/application-palette-search-owner.ts +89 -1
  52. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +34 -1
  53. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +33 -2
  54. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +204 -56
  55. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +27 -1
  56. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +66 -1
  57. package/packages/daemon/src/tui/mirror/runtime/fleet-presentation.ts +33 -0
  58. package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +2 -0
  59. package/packages/daemon/src/tui/mirror/theme.ts +10 -0
  60. package/packages/daemon/src/tui/mirror/ui/badge.tsx +10 -1
  61. package/packages/daemon/src/tui/mirror/ui/keyboard-router.tsx +22 -0
  62. package/packages/daemon/src/tui/mirror/ui/navigation-row.tsx +10 -1
  63. package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +30 -2
  64. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +71 -12
  65. package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +1 -0
  66. package/packages/daemon/src/tui/team/fuzzy.ts +38 -14
  67. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-switcher.jsx +0 -88
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.18",
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,83 @@ 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
18437
- );
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];
18448
+ )).trim().split("\n").map((line) => line.split(" "));
18449
+ const names = /* @__PURE__ */ new Map();
18450
+ const [rows, windowNames] = await Promise.all([
18451
+ readPanes(),
18452
+ run(
18453
+ ["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id} #{window_name}"],
18454
+ signal
18455
+ )
18456
+ ]);
18457
+ for (const line of windowNames.split("\n")) {
18458
+ const tab = line.indexOf(" ");
18459
+ if (tab > 0)
18460
+ names.set(
18461
+ line.slice(0, tab),
18462
+ cleanText(line.slice(tab + 1)).replace(/[\n\t]/gu, " ").slice(0, 80)
18463
+ );
18464
+ }
18465
+ const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
18466
+ const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
18467
+ if (windowId && !windowRows.some((row) => row[3] === windowId)) return null;
18468
+ const toWindow = (row) => ({
18469
+ id: row[3],
18470
+ index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
18471
+ name: names.get(row[3]) ?? `Window ${row[4]}`,
18472
+ active: row[1] === "1"
18473
+ });
18474
+ const windows = [];
18475
+ for (const row of windowRows) {
18476
+ if (windows.some((w) => w.id === row[3])) continue;
18477
+ if (windows.length >= 64) break;
18478
+ windows.push(toWindow(row));
18479
+ }
18480
+ if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
18481
+ const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
18482
+ if (selectedRow) windows.splice(63, 1, toWindow(selectedRow));
18483
+ }
18484
+ let paneBudget = 256;
18485
+ for (const window2 of [...windows].sort(
18486
+ (a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId)
18487
+ )) {
18488
+ const ids = [
18489
+ ...new Set(
18490
+ windowRows.filter((row) => row[3] === window2.id && /^%\d+$/u.test(row[0] ?? "")).map((row) => row[0])
18491
+ )
18492
+ ];
18493
+ if (ids.length <= paneBudget) {
18494
+ window2.paneIds = ids;
18495
+ paneBudget -= ids.length;
18496
+ }
18497
+ }
18498
+ windows.sort((a, b) => a.index - b.index);
18499
+ const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
18500
+ const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
18440
18501
  if (!pane || !/^%\d+$/u.test(pane)) return null;
18441
18502
  const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
18442
- 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);
18503
+ const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
18504
+ if (!currentSessions.some((s) => s.liveSessionId === liveSessionId2)) return null;
18505
+ if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
18506
+ return null;
18507
+ return {
18508
+ windows,
18509
+ selectedWindowId,
18510
+ text: cleanText(captured).split("\n").slice(-24).map((line) => line.slice(0, 180)).join("\n").slice(0, 8192)
18511
+ };
18444
18512
  };
18513
+ const capture = async (liveSessionId2, signal) => (await snapshot(liveSessionId2, signal))?.text ?? null;
18514
+ return Object.assign(capture, { snapshot });
18445
18515
  }
18446
18516
  function mountFleetPreviewRoute(app, options) {
18447
18517
  const authorize = ownerAuthorityGate(options.ownerToken, {
@@ -18476,11 +18546,18 @@ function mountFleetPreviewRoute(app, options) {
18476
18546
  next = now + 250;
18477
18547
  pending = true;
18478
18548
  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 });
18549
+ const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
18550
+ if (input.data.windowId && !options.capture.snapshot)
18551
+ return c.json({ error: "Window preview unavailable" }, 503);
18552
+ const snapshot = options.capture.snapshot ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId) : null;
18553
+ const legacy = !options.capture.snapshot ? await options.capture(input.data.liveSessionId, signal) : null;
18554
+ const text = snapshot?.text ?? legacy;
18555
+ return text === null ? c.json({ error: "Session changed" }, 409) : c.json({
18556
+ daemon: options.daemon,
18557
+ liveSessionId: input.data.liveSessionId,
18558
+ text,
18559
+ ...snapshot ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId } : {}
18560
+ });
18484
18561
  } catch {
18485
18562
  return c.json({ error: "Preview unavailable" }, 503);
18486
18563
  } finally {
@@ -18488,14 +18565,16 @@ function mountFleetPreviewRoute(app, options) {
18488
18565
  }
18489
18566
  });
18490
18567
  }
18491
- var requestSchema;
18568
+ var cleanText, requestSchema;
18492
18569
  var init_fleet_preview_route = __esm({
18493
18570
  "packages/daemon/src/command-center/resources/fleet-preview-route.ts"() {
18494
18571
  "use strict";
18495
18572
  init_owner_authority();
18496
18573
  init_discovery();
18574
+ cleanText = (value) => stripVTControlCharacters(value).replace(/[^\P{Cc}\n\t]/gu, "");
18497
18575
  requestSchema = z71.strictObject({
18498
18576
  expectedInstanceId: z71.uuid(),
18577
+ windowId: z71.string().regex(/^@\d+$/u).optional(),
18499
18578
  liveSessionId: z71.string().regex(/^live-session\.[a-f0-9]{20}$/u)
18500
18579
  });
18501
18580
  }
@@ -33670,6 +33749,7 @@ var CREATION_OPTION2, SEMANTIC_PANE_OPTION4, SEMANTIC_WINDOW_OPTION3, DISPLAY_TI
33670
33749
  var init_workspace_multiplexer_verbs = __esm({
33671
33750
  "packages/daemon/src/lib/workspace-multiplexer-verbs.ts"() {
33672
33751
  "use strict";
33752
+ init_discovery();
33673
33753
  init_src();
33674
33754
  init_src2();
33675
33755
  init_workspace_pane_creation2();
@@ -33888,6 +33968,37 @@ var init_workspace_multiplexer_verbs = __esm({
33888
33968
  operationId: request.operationId
33889
33969
  });
33890
33970
  }
33971
+ if (request.intent.verb === "workspace.session.kill" && request.intent.fleetTarget) {
33972
+ const target = request.intent.fleetTarget;
33973
+ if (target.daemonInstanceId !== this.#daemonInstanceId)
33974
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
33975
+ operationId: request.operationId
33976
+ });
33977
+ const raw2 = this.#io.runTmux([
33978
+ "list-panes",
33979
+ "-a",
33980
+ "-F",
33981
+ "#{pid} #{session_id} #{session_created} #{session_name}"
33982
+ ]);
33983
+ const match = raw2.split("\n").find((line) => {
33984
+ const session = discoverLiveSessionSummaries(() => line)[0];
33985
+ return session?.liveSessionId === target.liveSessionId && session.sessionName === target.sessionName;
33986
+ });
33987
+ if (!match)
33988
+ throw new WorkspaceMultiplexerError("workspace_not_found", {
33989
+ reason: "session_incarnation_changed"
33990
+ });
33991
+ const runtimeId = match.split(" ")[1];
33992
+ return this.#killSession(
33993
+ target.sessionName,
33994
+ {
33995
+ operationId: request.operationId,
33996
+ daemonInstanceId: this.#daemonInstanceId,
33997
+ workspaceName: request.intent.workspaceName
33998
+ },
33999
+ runtimeId
34000
+ );
34001
+ }
33891
34002
  const workspace = this.#registry.get(request.intent.workspaceName);
33892
34003
  if (!workspace) {
33893
34004
  throw new WorkspaceMultiplexerError("workspace_not_found", {
@@ -34128,19 +34239,19 @@ var init_workspace_multiplexer_verbs = __esm({
34128
34239
  remainingWindowCount: windowIdsOf(after).length
34129
34240
  };
34130
34241
  }
34131
- #killSession(sessionName, envelope) {
34242
+ #killSession(sessionName, envelope, exactTarget = `=${sessionName}`) {
34132
34243
  let existed = true;
34133
34244
  try {
34134
- this.#io.runTmux(["has-session", "-t", `=${sessionName}`]);
34245
+ this.#io.runTmux(["has-session", "-t", exactTarget]);
34135
34246
  } catch (error) {
34136
34247
  if (!this.#io.isMissingTmuxTarget(error)) throw error;
34137
34248
  existed = false;
34138
34249
  }
34139
34250
  if (existed) {
34140
- this.#io.runTmux(["kill-session", "-t", `=${sessionName}`]);
34251
+ this.#io.runTmux(["kill-session", "-t", exactTarget]);
34141
34252
  let stillPresent = true;
34142
34253
  try {
34143
- this.#io.runTmux(["has-session", "-t", `=${sessionName}`]);
34254
+ this.#io.runTmux(["has-session", "-t", exactTarget]);
34144
34255
  } catch {
34145
34256
  stillPresent = false;
34146
34257
  }
@@ -39533,7 +39644,7 @@ var init_semantic_mutation_executor = __esm({
39533
39644
  intent = { ...intent, origin: authority.origin };
39534
39645
  }
39535
39646
  const authenticatedSourceSemanticPaneId = authority.authenticatedSourceSemanticPaneId ?? null;
39536
- const session = this.#options.resolveSession(intent.workspaceName);
39647
+ const session = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.sessionName : this.#options.resolveSession(intent.workspaceName);
39537
39648
  const ledger2 = this.#ledger(session ?? MISSING_SESSION_LEDGER);
39538
39649
  const origin = authority.origin;
39539
39650
  const fingerprint2 = JSON.stringify([intent, authenticatedSourceSemanticPaneId, origin]);
@@ -56807,7 +56918,7 @@ var init_registry2 = __esm({
56807
56918
  if (this.#disposed) return Promise.reject(new Error("SessionRuntimeRegistry is disposed"));
56808
56919
  runtime.assertController(lease);
56809
56920
  let intent = SessionRuntimeSemanticIntentSchemaZ.parse(rawIntent);
56810
- const resolvedSession = this.#resolveSession?.(intent.workspaceName) ?? null;
56921
+ const resolvedSession = intent.verb === "workspace.session.kill" && intent.fleetTarget ? intent.fleetTarget.daemonInstanceId === this.generation ? intent.fleetTarget.sessionName : null : this.#resolveSession?.(intent.workspaceName) ?? null;
56811
56922
  if (resolvedSession !== runtime.session) {
56812
56923
  return Promise.reject(
56813
56924
  new SessionRuntimeControllerLeaseError(
@@ -64732,6 +64843,14 @@ var init_pane_stream_websocket = __esm({
64732
64843
  this.close(1011, "stream-unavailable");
64733
64844
  return;
64734
64845
  }
64846
+ const visibleAtOpen = new Set(
64847
+ stagedAuthority.layouts.flatMap(
64848
+ (layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
64849
+ )
64850
+ );
64851
+ channels.sort(
64852
+ (left, right) => Number(visibleAtOpen.has(right.semanticPaneId)) - Number(visibleAtOpen.has(left.semanticPaneId))
64853
+ );
64735
64854
  const openings = channels.map(async (channel) => {
64736
64855
  const pending = [];
64737
64856
  let ready = false;
@@ -64802,7 +64921,15 @@ var init_pane_stream_websocket = __esm({
64802
64921
  });
64803
64922
  layoutActivated = true;
64804
64923
  this.#recordDiagnosticLifecycle("pane-stream-delivery-open");
64805
- for (const { channel, delivery, pending, markReady } of opened) {
64924
+ const visibleAtDelivery = new Set(
64925
+ authority.layouts.flatMap(
64926
+ (layout) => layout.currentWindow ? layout.panes.map((pane) => pane.semanticPaneId) : []
64927
+ )
64928
+ );
64929
+ opened.sort(
64930
+ (left, right) => Number(visibleAtDelivery.has(right.channel.semanticPaneId)) - Number(visibleAtDelivery.has(left.channel.semanticPaneId))
64931
+ );
64932
+ for (const { channel, delivery } of opened) {
64806
64933
  if (this.#closed || channel.closed) {
64807
64934
  await delivery.close();
64808
64935
  continue;
@@ -64824,6 +64951,9 @@ var init_pane_stream_websocket = __esm({
64824
64951
  pane: channel.semanticPaneId,
64825
64952
  negotiation: delivery.negotiation
64826
64953
  });
64954
+ }
64955
+ for (const { channel, pending, markReady } of opened) {
64956
+ if (this.#closed || channel.closed) continue;
64827
64957
  markReady();
64828
64958
  for (const message of pending)
64829
64959
  await this.#sendTerminalDelivery(channel.semanticPaneId, message);
@@ -65895,7 +66025,16 @@ function createSessionRuntimeMultiplexerBackend(options) {
65895
66025
  };
65896
66026
  return {
65897
66027
  mutate: async (request, authenticatedHostClientId, sourcePaneCredential, ownerAuthorized = false) => {
65898
- const session = options.resolveSession(request.intent.workspaceName);
66028
+ const fleetTarget = request.intent.verb === "workspace.session.kill" ? request.intent.fleetTarget : void 0;
66029
+ if (fleetTarget && (!ownerAuthorized || authenticatedHostClientId || sourcePaneCredential))
66030
+ throw new WorkspaceMultiplexerError("operation_conflict", {
66031
+ reason: "fleet_close_requires_explicit_owner"
66032
+ });
66033
+ if (fleetTarget && fleetTarget.daemonInstanceId !== options.registry.generation)
66034
+ throw new WorkspaceMultiplexerError("daemon_instance_mismatch", {
66035
+ operationId: request.operationId
66036
+ });
66037
+ const session = fleetTarget && ownerAuthorized && !authenticatedHostClientId && !sourcePaneCredential ? fleetTarget.sessionName : options.resolveSession(request.intent.workspaceName);
65899
66038
  if (!session) {
65900
66039
  throw new Error(`Workspace ${request.intent.workspaceName} has no live tmux session`);
65901
66040
  }
@@ -68774,6 +68913,11 @@ async function workspaceSessionCreateHandler(input, context) {
68774
68913
  message: "Fleet lifecycle is unavailable."
68775
68914
  });
68776
68915
  const [operation, generation] = authorityContext(context);
68916
+ if (input.expectedDaemonInstanceId && input.expectedDaemonInstanceId !== generation)
68917
+ throw new ActionError({
68918
+ code: "bad_request",
68919
+ message: "The selected daemon has been replaced."
68920
+ });
68777
68921
  return await mapAuthority(
68778
68922
  () => context.fleetLifecycleBackend.createSession(operation, generation, input)
68779
68923
  );
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.18",
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,103 @@ 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
+ // Names are untrusted display text. Keep them out of the pane membership
28
+ // format so embedded newlines cannot manufacture a capture target.
29
+ const names = new Map();
30
+ // Independent metadata reads share one capture's deadline and run in two
31
+ // bounded lanes. Incarnation checks still surround the actual capture.
32
+ const [rows, windowNames] = await Promise.all([
33
+ readPanes(),
34
+ run(["list-windows", "-t", `=${session.sessionName}`, "-F", "#{window_id}\t#{window_name}"], signal),
35
+ ]);
36
+ for (const line of windowNames.split("\n")) {
37
+ const tab = line.indexOf("\t");
38
+ if (tab > 0)
39
+ names.set(line.slice(0, tab), cleanText(line.slice(tab + 1))
40
+ .replace(/[\n\t]/gu, " ")
41
+ .slice(0, 80));
42
+ }
43
+ const windowRows = rows.filter((row) => /^@\d+$/u.test(row[3] ?? ""));
44
+ const selectedWindowId = windowId ?? windowRows.find((row) => row[1] === "1")?.[3] ?? windowRows[0]?.[3] ?? null;
45
+ if (windowId && !windowRows.some((row) => row[3] === windowId))
46
+ return null;
47
+ const toWindow = (row) => ({
48
+ id: row[3],
49
+ index: Math.max(0, Number.parseInt(row[4] ?? "0", 10) || 0),
50
+ name: names.get(row[3]) ?? `Window ${row[4]}`,
51
+ active: row[1] === "1",
52
+ });
53
+ const windows = [];
54
+ for (const row of windowRows) {
55
+ if (windows.some((w) => w.id === row[3]))
56
+ continue;
57
+ if (windows.length >= 64)
58
+ break;
59
+ windows.push(toWindow(row));
60
+ }
61
+ // Preserve the actual active/requested target even on unusually large sessions.
62
+ if (selectedWindowId && !windows.some((w) => w.id === selectedWindowId)) {
63
+ const selectedRow = windowRows.find((row) => row[3] === selectedWindowId);
64
+ if (selectedRow)
65
+ windows.splice(63, 1, toWindow(selectedRow));
66
+ }
67
+ let paneBudget = 256;
68
+ // The selected window gets the membership budget first; omitted membership
69
+ // remains explicitly unknown instead of reporting incomplete activity counts.
70
+ for (const window of [...windows].sort((a, b) => Number(b.id === selectedWindowId) - Number(a.id === selectedWindowId))) {
71
+ const ids = [
72
+ ...new Set(windowRows
73
+ .filter((row) => row[3] === window.id && /^%\d+$/u.test(row[0] ?? ""))
74
+ .map((row) => row[0])),
75
+ ];
76
+ if (ids.length <= paneBudget) {
77
+ window.paneIds = ids;
78
+ paneBudget -= ids.length;
79
+ }
80
+ }
81
+ windows.sort((a, b) => a.index - b.index);
82
+ const candidates = selectedWindowId ? rows.filter((r) => r[3] === selectedWindowId) : rows;
83
+ const pane = candidates.find((r) => r[2] === "1")?.[0] ?? candidates[0]?.[0];
28
84
  if (!pane || !/^%\d+$/u.test(pane))
29
85
  return null;
30
- // Read-only capture: no attach, select, resize, input or ownership acquisition.
86
+ // Passive capture never changes active windows, size, input or terminal ownership.
31
87
  const captured = await run(["capture-pane", "-p", "-t", pane, "-S", "-24"], signal);
32
- if (!(await readSessions()).some((s) => s.liveSessionId === liveSessionId))
88
+ const [currentSessions, currentPanes] = await Promise.all([readSessions(), readPanes()]);
89
+ if (!currentSessions.some((s) => s.liveSessionId === liveSessionId))
33
90
  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);
91
+ if (selectedWindowId && !currentPanes.some((r) => r[3] === selectedWindowId && r[0] === pane))
92
+ return null;
93
+ return {
94
+ windows,
95
+ selectedWindowId,
96
+ text: cleanText(captured)
97
+ .split("\n")
98
+ .slice(-24)
99
+ .map((line) => line.slice(0, 180))
100
+ .join("\n")
101
+ .slice(0, 8192),
102
+ };
41
103
  };
104
+ const capture = async (liveSessionId, signal) => (await snapshot(liveSessionId, signal))?.text ?? null;
105
+ return Object.assign(capture, { snapshot });
42
106
  }
43
107
  const requestSchema = z.strictObject({
44
108
  expectedInstanceId: z.uuid(),
109
+ windowId: z
110
+ .string()
111
+ .regex(/^@\d+$/u)
112
+ .optional(),
45
113
  liveSessionId: z.string().regex(/^live-session\.[a-f0-9]{20}$/u),
46
114
  });
47
115
  export function mountFleetPreviewRoute(app, options) {
@@ -83,10 +151,27 @@ export function mountFleetPreviewRoute(app, options) {
83
151
  next = now + 250;
84
152
  pending = true;
85
153
  try {
86
- const text = await options.capture(input.data.liveSessionId, AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]));
154
+ const signal = AbortSignal.any([c.req.raw.signal, AbortSignal.timeout(1500)]);
155
+ if (input.data.windowId && !options.capture.snapshot)
156
+ return c.json({ error: "Window preview unavailable" }, 503);
157
+ const snapshot = options.capture.snapshot
158
+ ? await options.capture.snapshot(input.data.liveSessionId, signal, input.data.windowId)
159
+ : null;
160
+ // Older capture providers remain valid for the original text-only contract.
161
+ const legacy = !options.capture.snapshot
162
+ ? await options.capture(input.data.liveSessionId, signal)
163
+ : null;
164
+ const text = snapshot?.text ?? legacy;
87
165
  return text === null
88
166
  ? c.json({ error: "Session changed" }, 409)
89
- : c.json({ daemon: options.daemon, liveSessionId: input.data.liveSessionId, text });
167
+ : c.json({
168
+ daemon: options.daemon,
169
+ liveSessionId: input.data.liveSessionId,
170
+ text,
171
+ ...(snapshot
172
+ ? { windows: snapshot.windows, selectedWindowId: snapshot.selectedWindowId }
173
+ : {}),
174
+ });
90
175
  }
91
176
  catch {
92
177
  return c.json({ error: "Preview unavailable" }, 503);
@@ -0,0 +1 @@
1
+ export {};