tmux-ide 2.9.0-beta.22 → 2.9.0-beta.26

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 (48) hide show
  1. package/bin/cli.js +22 -3
  2. package/package.json +2 -2
  3. package/packages/contracts/src/fleet-lifecycle.ts +5 -0
  4. package/packages/daemon/dist/command-center/tmux-servers.js +6 -0
  5. package/packages/daemon/dist/lib/fleet-lifecycle-authority.js +15 -1
  6. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-integration.jsx +119 -0
  7. package/packages/daemon/dist/tui/mirror/runtime/application-guided-tour-owner.js +132 -0
  8. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +21 -9
  9. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-transport.js +63 -1
  10. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +8 -2
  11. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents.js +3 -1
  12. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +40 -0
  13. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +132 -0
  14. package/packages/daemon/dist/tui/mirror/runtime/application-machine-agents.js +9 -0
  15. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +21 -0
  16. package/packages/daemon/dist/tui/mirror/runtime/application-performance-log.js +20 -7
  17. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +6 -2
  18. package/packages/daemon/dist/tui/mirror/runtime/application-theme-repaint.js +9 -0
  19. package/packages/daemon/dist/tui/mirror/runtime/fleet-lifecycle-client.js +3 -1
  20. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-coach.jsx +54 -0
  21. package/packages/daemon/dist/tui/mirror/runtime/guided-tour-storage.js +67 -0
  22. package/packages/daemon/dist/tui/mirror/runtime/guided-tour.js +113 -0
  23. package/packages/daemon/dist/tui/mirror/runtime/resize-terminal-pane.js +4 -0
  24. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +12 -4
  25. package/packages/daemon/src/command-center/tmux-servers.ts +11 -0
  26. package/packages/daemon/src/lib/fleet-lifecycle-authority.ts +17 -1
  27. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-integration.tsx +199 -0
  28. package/packages/daemon/src/tui/mirror/runtime/application-guided-tour-owner.ts +162 -0
  29. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +36 -10
  30. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-transport.ts +69 -0
  31. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +12 -2
  32. package/packages/daemon/src/tui/mirror/runtime/application-home-agents.ts +13 -2
  33. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +59 -0
  34. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +175 -0
  35. package/packages/daemon/src/tui/mirror/runtime/application-machine-agents.ts +9 -1
  36. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +22 -0
  37. package/packages/daemon/src/tui/mirror/runtime/application-performance-log.ts +18 -6
  38. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +16 -16
  39. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  40. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +20 -1
  41. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -0
  42. package/packages/daemon/src/tui/mirror/runtime/application-theme-repaint.ts +19 -0
  43. package/packages/daemon/src/tui/mirror/runtime/fleet-lifecycle-client.ts +3 -0
  44. package/packages/daemon/src/tui/mirror/runtime/guided-tour-coach.tsx +118 -0
  45. package/packages/daemon/src/tui/mirror/runtime/guided-tour-storage.ts +72 -0
  46. package/packages/daemon/src/tui/mirror/runtime/guided-tour.ts +147 -0
  47. package/packages/daemon/src/tui/mirror/runtime/resize-terminal-pane.ts +3 -0
  48. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +11 -4
package/bin/cli.js CHANGED
@@ -4960,6 +4960,7 @@ var SafeDisplayNameSchemaZ, OwnerPathSchemaZ, WorkspaceSessionCreateArgumentsSch
4960
4960
  var init_fleet_lifecycle = __esm({
4961
4961
  "packages/contracts/src/fleet-lifecycle.ts"() {
4962
4962
  "use strict";
4963
+ init_workspace_catalog_resource();
4963
4964
  init_fleet_catalog();
4964
4965
  init_desktop_workspace_name();
4965
4966
  init_semantic_identity();
@@ -4973,6 +4974,8 @@ var init_fleet_lifecycle = __esm({
4973
4974
  OwnerPathSchemaZ = /* @__PURE__ */ (() => z34.string().min(1).max(4096).refine((value) => !value.includes("\0")))();
4974
4975
  WorkspaceSessionCreateArgumentsSchemaZ = /* @__PURE__ */ (() => z34.object({
4975
4976
  displayName: SafeDisplayNameSchemaZ,
4977
+ /** Opt in to an incarnation-bearing receipt; legacy clients keep their strict result shape. */
4978
+ includeLiveSessionId: z34.boolean().optional(),
4976
4979
  cwd: OwnerPathSchemaZ.optional(),
4977
4980
  expectedDaemonInstanceId: z34.uuid().optional()
4978
4981
  }).strict())();
@@ -4980,6 +4983,8 @@ var init_fleet_lifecycle = __esm({
4980
4983
  operationId: z34.uuid(),
4981
4984
  daemonInstanceId: z34.uuid(),
4982
4985
  outcome: z34.enum(["created", "adopted", "replayed"]),
4986
+ /** Captured atomically by new-session; absent from older daemons and adopted routes. */
4987
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ.optional(),
4983
4988
  fleetSessionId: FleetSessionIdSchemaZ,
4984
4989
  /** Canonical registered route; displayName is presentation only. */
4985
4990
  workspaceName: DesktopWorkspaceNameSchemaZ,
@@ -11274,7 +11279,7 @@ var require_package = __commonJS({
11274
11279
  "package.json"(exports, module) {
11275
11280
  module.exports = {
11276
11281
  name: "tmux-ide",
11277
- version: "2.9.0-beta.22",
11282
+ version: "2.9.0-beta.26",
11278
11283
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11279
11284
  type: "module",
11280
11285
  bin: {
@@ -11338,7 +11343,7 @@ var require_package = __commonJS({
11338
11343
  postinstall: "node scripts/postinstall.js",
11339
11344
  docs: "turbo run dev --filter=@tmux-ide/docs",
11340
11345
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
11341
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./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",
11346
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./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/guided-tour-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",
11342
11347
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
11343
11348
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
11344
11349
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
@@ -24855,6 +24860,7 @@ var MAX_REPLAY_OPERATIONS, sleep2, FleetLifecycleAuthorityError, FleetLifecycleA
24855
24860
  var init_fleet_lifecycle_authority = __esm({
24856
24861
  "packages/daemon/src/lib/fleet-lifecycle-authority.ts"() {
24857
24862
  "use strict";
24863
+ init_live_session_identity();
24858
24864
  init_src();
24859
24865
  init_discovery();
24860
24866
  init_fleet_catalog2();
@@ -24934,10 +24940,14 @@ var init_fleet_lifecycle_authority = __esm({
24934
24940
  }
24935
24941
  }
24936
24942
  let created = false;
24943
+ let liveSessionId;
24937
24944
  try {
24938
- this.#runTmux([
24945
+ const createdId = this.#runTmux([
24939
24946
  "new-session",
24940
24947
  "-d",
24948
+ "-P",
24949
+ "-F",
24950
+ "#{pid} #{session_id} #{session_created}",
24941
24951
  ...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
24942
24952
  "-s",
24943
24953
  identity2.sessionName,
@@ -24947,6 +24957,9 @@ var init_fleet_lifecycle_authority = __esm({
24947
24957
  cwd,
24948
24958
  TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND
24949
24959
  ]);
24960
+ const [serverPid, sessionId, sessionCreated] = createdId.trim().split(" ");
24961
+ if (serverPid && /^\d+$/u.test(serverPid) && sessionId && /^\$\d+$/u.test(sessionId) && sessionCreated && /^\d+$/u.test(sessionCreated))
24962
+ liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
24950
24963
  created = true;
24951
24964
  prepareTmuxTruecolorEnvironment(this.#runTmux, identity2.sessionName);
24952
24965
  this.#runTmux(["set-environment", "-t", identity2.sessionName, "TMUX_IDE", "1"]);
@@ -24980,6 +24993,7 @@ var init_fleet_lifecycle_authority = __esm({
24980
24993
  operationId,
24981
24994
  daemonInstanceId: this.#daemonInstanceId,
24982
24995
  outcome: "created",
24996
+ ...input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {},
24983
24997
  fleetSessionId: fleetSessionIdForName(identity2.sessionName),
24984
24998
  workspaceName: identity2.workspaceName,
24985
24999
  displayName: input.displayName
@@ -71501,6 +71515,11 @@ function mountTmuxServerRoutes(app, options) {
71501
71515
  workspace.sessionName,
71502
71516
  c.req.raw.signal
71503
71517
  );
71518
+ const currentSessions = await owner.catalog();
71519
+ if (!currentSessions.some(
71520
+ (row) => row.sessionName === workspace.sessionName && row.liveSessionId === c.req.query("liveSessionId")
71521
+ ))
71522
+ throw new TmuxServerScopeError("stale-generation");
71504
71523
  return snapshot2 ? projectApplicationShellResource(snapshot2) : null;
71505
71524
  });
71506
71525
  return resource3 ? c.json({ version: 1, server, resource: resource3 }) : c.json({ error: { code: "workspace-not-found" } }, 404);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.22",
3
+ "version": "2.9.0-beta.26",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -64,7 +64,7 @@
64
64
  "postinstall": "node scripts/postinstall.js",
65
65
  "docs": "turbo run dev --filter=@tmux-ide/docs",
66
66
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
67
- "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./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",
67
+ "test:tui-renderer": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./packages/daemon/src/tui/mirror/automatic-contrast-renderer.test.tsx ./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/guided-tour-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",
68
68
  "test:tui-smoke": "bun scripts/smoke-tui-missions.mjs",
69
69
  "test:tui-live": "node scripts/tui-testdrive.mjs smoke",
70
70
  "test:tui-testdrive": "node --test scripts/lib/tui-testdrive-clipboard-hook.test.mjs scripts/lib/tui-testdrive-input.test.mjs",
@@ -1,3 +1,4 @@
1
+ import { WorkspaceCatalogLiveSessionIdSchemaZ } from "./workspace-catalog-resource.ts";
1
2
  import { z } from "zod";
2
3
  import {
3
4
  FleetAgentIdSchemaZ,
@@ -29,6 +30,8 @@ const OwnerPathSchemaZ = z
29
30
  export const WorkspaceSessionCreateArgumentsSchemaZ = z
30
31
  .object({
31
32
  displayName: SafeDisplayNameSchemaZ,
33
+ /** Opt in to an incarnation-bearing receipt; legacy clients keep their strict result shape. */
34
+ includeLiveSessionId: z.boolean().optional(),
32
35
  cwd: OwnerPathSchemaZ.optional(),
33
36
  expectedDaemonInstanceId: z.uuid().optional(),
34
37
  })
@@ -42,6 +45,8 @@ export const WorkspaceSessionCreateResultSchemaZ = z
42
45
  operationId: z.uuid(),
43
46
  daemonInstanceId: z.uuid(),
44
47
  outcome: z.enum(["created", "adopted", "replayed"]),
48
+ /** Captured atomically by new-session; absent from older daemons and adopted routes. */
49
+ liveSessionId: WorkspaceCatalogLiveSessionIdSchemaZ.optional(),
45
50
  fleetSessionId: FleetSessionIdSchemaZ,
46
51
  /** Canonical registered route; displayName is presentation only. */
47
52
  workspaceName: DesktopWorkspaceNameSchemaZ,
@@ -182,6 +182,12 @@ export function mountTmuxServerRoutes(app, options) {
182
182
  row.liveSessionId === c.req.query("liveSessionId")))
183
183
  throw new TmuxServerScopeError("stale-generation");
184
184
  const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(workspace.sessionName, c.req.raw.signal);
185
+ // Discovery can yield while a same-name session is replaced. Revalidate
186
+ // the captured incarnation before publishing agent metadata as current.
187
+ const currentSessions = await owner.catalog();
188
+ if (!currentSessions.some((row) => row.sessionName === workspace.sessionName &&
189
+ row.liveSessionId === c.req.query("liveSessionId")))
190
+ throw new TmuxServerScopeError("stale-generation");
185
191
  return snapshot ? projectApplicationShellResource(snapshot) : null;
186
192
  });
187
193
  return resource
@@ -1,3 +1,4 @@
1
+ import { liveSessionIdForNativeIdentity } from "../terminal/protocol/live-session-identity.js";
1
2
  import { createHash, randomUUID } from "node:crypto";
2
3
  import { isAbsolute, resolve } from "node:path";
3
4
  import { realpath, stat } from "node:fs/promises";
@@ -79,10 +80,14 @@ export class FleetLifecycleAuthority {
79
80
  }
80
81
  }
81
82
  let created = false;
83
+ let liveSessionId;
82
84
  try {
83
- this.#runTmux([
85
+ const createdId = this.#runTmux([
84
86
  "new-session",
85
87
  "-d",
88
+ "-P",
89
+ "-F",
90
+ "#{pid}\t#{session_id}\t#{session_created}",
86
91
  ...TMUX_TRUECOLOR_ENVIRONMENT_ARGS,
87
92
  "-s",
88
93
  identity.sessionName,
@@ -92,6 +97,14 @@ export class FleetLifecycleAuthority {
92
97
  cwd,
93
98
  TMUX_TRUECOLOR_INTERACTIVE_SHELL_COMMAND,
94
99
  ]);
100
+ const [serverPid, sessionId, sessionCreated] = createdId.trim().split("\t");
101
+ if (serverPid &&
102
+ /^\d+$/u.test(serverPid) &&
103
+ sessionId &&
104
+ /^\$\d+$/u.test(sessionId) &&
105
+ sessionCreated &&
106
+ /^\d+$/u.test(sessionCreated))
107
+ liveSessionId = liveSessionIdForNativeIdentity(serverPid, sessionId, sessionCreated);
95
108
  created = true;
96
109
  prepareTmuxTruecolorEnvironment(this.#runTmux, identity.sessionName);
97
110
  this.#runTmux(["set-environment", "-t", identity.sessionName, "TMUX_IDE", "1"]);
@@ -129,6 +142,7 @@ export class FleetLifecycleAuthority {
129
142
  operationId,
130
143
  daemonInstanceId: this.#daemonInstanceId,
131
144
  outcome: "created",
145
+ ...(input.includeLiveSessionId && liveSessionId ? { liveSessionId } : {}),
132
146
  fleetSessionId: fleetSessionIdForName(identity.sessionName),
133
147
  workspaceName: identity.workspaceName,
134
148
  displayName: input.displayName,
@@ -0,0 +1,119 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { randomUUID } from "node:crypto";
3
+ import { createEffect, createMemo, Show } from "solid-js";
4
+ import { createApplicationGuidedTourOwner } from "./application-guided-tour-owner.js";
5
+ import { GuidedTourCoach } from "./guided-tour-coach.jsx";
6
+ import { createFleetSession } from "./fleet-lifecycle-client.js";
7
+ import { applicationMachineAuthorityManager } from "./application-machine-authority.js";
8
+ export function createApplicationGuidedTourIntegration(options) {
9
+ const { machines, lifecycle, generation, generationMachineId, layoutSnapshot, sessionName, focusedPane, activeSurface, appearance, paletteOpen, paletteCommands, dimensions, blocked, } = options;
10
+ const practiceRows = () => machines.catalog
11
+ .getSnapshot()
12
+ .groups.filter((group) => group.id === "local" || group.routeIds?.includes("local"))
13
+ .flatMap((group) => group.sessions)
14
+ .filter((row) => !row.disabled);
15
+ const practiceIdentity = (row) => {
16
+ const daemon = applicationMachineAuthorityManager.getMachine("local")?.read();
17
+ if (!row.liveSessionId || !daemon)
18
+ return null;
19
+ return {
20
+ machineId: "local",
21
+ serverId: row.server?.serverId ?? "default",
22
+ generation: row.server?.generation ?? daemon.instanceId,
23
+ sessionId: row.liveSessionId,
24
+ sessionName: row.name,
25
+ };
26
+ };
27
+ const tour = createApplicationGuidedTourOwner({
28
+ async createPractice() {
29
+ const handle = applicationMachineAuthorityManager.getMachine("local");
30
+ if (!handle || handle.endpoint().state !== "ready")
31
+ throw new Error("Connect this machine before creating a practice session.");
32
+ const created = await createFleetSession(handle, `tmux-ide-practice-${randomUUID().slice(0, 8)}`, undefined, { includeLiveSessionId: true });
33
+ if (!created || created.outcome !== "created")
34
+ throw new Error("Could not create a fresh practice session. Try again.");
35
+ if (!created.liveSessionId)
36
+ throw new Error("Update the local tmux-ide daemon to use the guided practice session, then restart the tour.");
37
+ return await new Promise((resolve, reject) => {
38
+ let done = false;
39
+ let stop = () => { };
40
+ const abort = () => {
41
+ clearTimeout(timer);
42
+ stop();
43
+ reject(new Error("Tour closed."));
44
+ };
45
+ const timer = setTimeout(() => {
46
+ lifecycle.signal.removeEventListener("abort", abort);
47
+ stop();
48
+ reject(new Error("Practice session was created but has not appeared yet. Find it in the local session list."));
49
+ }, 10_000);
50
+ lifecycle.signal.addEventListener("abort", abort, { once: true });
51
+ const check = () => {
52
+ const rows = practiceRows().filter((row) => row.name === created.workspaceName && row.liveSessionId === created.liveSessionId);
53
+ const identity = rows.length === 1 ? practiceIdentity(rows[0]) : null;
54
+ if (!identity || identity.generation !== created.daemonInstanceId)
55
+ return;
56
+ done = true;
57
+ lifecycle.signal.removeEventListener("abort", abort);
58
+ clearTimeout(timer);
59
+ stop();
60
+ resolve(identity);
61
+ };
62
+ stop = machines.catalog.subscribe(check);
63
+ if (done)
64
+ stop();
65
+ else if (lifecycle.signal.aborted)
66
+ abort();
67
+ else
68
+ check();
69
+ });
70
+ },
71
+ openPractice(practice) {
72
+ if (practice.machineId !== "local")
73
+ throw new Error("Practice sessions must belong to this machine. Restart the tour.");
74
+ const row = practiceRows().find((row) => {
75
+ const identity = practiceIdentity(row);
76
+ return (identity &&
77
+ identity.serverId === practice.serverId &&
78
+ identity.generation === practice.generation &&
79
+ identity.sessionId === practice.sessionId);
80
+ });
81
+ if (!row)
82
+ throw new Error("That practice session is unavailable or has been replaced. Replay the tour to create a new one.");
83
+ machines.sidebar.onOpen("local", row.name, "mouse", row.id);
84
+ },
85
+ });
86
+ const committedTourTheme = createMemo((previous) => appearance.pickerOpen()
87
+ ? (previous ?? JSON.stringify(appearance.theme().canonical))
88
+ : JSON.stringify(appearance.theme().canonical));
89
+ createEffect(() => {
90
+ const current = generation();
91
+ const connection = current?.status === "live" ? current.connection : null;
92
+ const machineId = generationMachineId();
93
+ const practice = connection?.liveSessionId && current?.daemonGeneration && machineId
94
+ ? {
95
+ machineId,
96
+ serverId: connection.server?.serverId ?? "default",
97
+ generation: connection.server?.generation ?? current.daemonGeneration,
98
+ sessionId: connection.liveSessionId,
99
+ sessionName: sessionName() ?? "",
100
+ }
101
+ : null;
102
+ tour.observe({
103
+ practice,
104
+ panes: (layoutSnapshot().current?.panes ?? []).flatMap((pane) => pane.pane ? [{ id: pane.pane, width: pane.width, height: pane.height }] : []),
105
+ focusedPane: focusedPane(),
106
+ surface: activeSurface(),
107
+ paletteOpen: paletteOpen(),
108
+ theme: committedTourTheme(),
109
+ });
110
+ });
111
+ return {
112
+ ...tour,
113
+ Coach: () => (<Show when={tour.state().active && !blocked()}>
114
+ <box position="absolute" right={1} bottom={2} width={Math.max(1, Math.min(68, dimensions().width - 2))} zIndex={30}>
115
+ <GuidedTourCoach height={Math.max(4, Math.min(10, dimensions().height - 4))} state={tour.state()} theme={appearance.theme()} width={Math.max(1, Math.min(68, dimensions().width - 2))} shortcuts={{ commands: "F5", home: "F1" }} error={tour.error()} busy={tour.busy()} onPause={tour.pause} onReplay={tour.replay} onWelcomeRead={tour.welcomeRead} onCreatePractice={() => void tour.createPractice()} onOpenCommands={() => paletteCommands.setOpen(true, "mouse")} onOpenAppearance={appearance.openPicker} onOpenHome={() => paletteCommands.openSurface("home", "mouse")} onOpenPractice={() => void tour.openPractice()}/>
116
+ </box>
117
+ </Show>),
118
+ };
119
+ }
@@ -0,0 +1,132 @@
1
+ import { createSignal, onCleanup } from "solid-js";
2
+ import { initialGuidedTourState, reduceGuidedTour, sameGuidedTourPractice, } from "./guided-tour.js";
3
+ import { readGuidedTourState, writeGuidedTourState } from "./guided-tour-storage.js";
4
+ /** Compare confirmed snapshots; unrelated sessions and request attempts never count. */
5
+ export function observedGuidedTourEvent(state, previous, current) {
6
+ if (!state.active || !previous)
7
+ return null;
8
+ if (state.step === "commands" && current.paletteOpen && !previous.paletteOpen)
9
+ return { type: "commands-opened" };
10
+ if (state.step === "theme" && current.theme !== previous.theme)
11
+ return { type: "theme-changed" };
12
+ if (state.step === "leave" && current.surface === "home")
13
+ return { type: "home-opened" };
14
+ if (!current.practice ||
15
+ !sameGuidedTourPractice(state.practice, current.practice) ||
16
+ current.surface !== "terminals")
17
+ return null;
18
+ if (state.step === "reopen" &&
19
+ (previous.surface !== "terminals" ||
20
+ !sameGuidedTourPractice(previous.practice, current.practice)))
21
+ return { type: "reopened", practice: current.practice };
22
+ if (!previous.practice || !sameGuidedTourPractice(previous.practice, current.practice))
23
+ return null;
24
+ if (state.step === "split" &&
25
+ previous.panes.length > 0 &&
26
+ current.panes.length > previous.panes.length &&
27
+ previous.panes.every((old) => current.panes.some((p) => p.id === old.id)) &&
28
+ current.panes.some((p) => !previous.panes.some((old) => old.id === p.id)))
29
+ return { type: "split-confirmed", practice: current.practice };
30
+ if (state.step === "focus" &&
31
+ previous.focusedPane &&
32
+ current.focusedPane &&
33
+ previous.focusedPane !== current.focusedPane &&
34
+ current.panes.some((p) => p.id === current.focusedPane) &&
35
+ current.panes.some((p) => p.id === previous.focusedPane))
36
+ return { type: "focus-confirmed", practice: current.practice };
37
+ // A divider reallocates space between the same panes: one grows while
38
+ // another shrinks on the same axis. Whole-app growth/shrink is not practice.
39
+ const samePanes = current.panes.length >= 2 &&
40
+ current.panes.length === previous.panes.length &&
41
+ previous.panes.every((old) => current.panes.some((pane) => pane.id === old.id));
42
+ const reallocated = ["width", "height"].some((axis) => {
43
+ const changes = current.panes.map((pane) => pane[axis] - (previous.panes.find((old) => old.id === pane.id)?.[axis] ?? pane[axis]));
44
+ return changes.some((change) => change > 0) && changes.some((change) => change < 0);
45
+ });
46
+ if (state.step === "resize" && samePanes && reallocated)
47
+ return { type: "resize-confirmed", practice: current.practice };
48
+ return null;
49
+ }
50
+ export function createApplicationGuidedTourOwner(options) {
51
+ const [state, setState] = createSignal((options.read ?? readGuidedTourState)());
52
+ const [busy, setBusy] = createSignal(false);
53
+ const [error, setError] = createSignal(null);
54
+ let previous = null;
55
+ let disposed = false;
56
+ onCleanup(() => {
57
+ disposed = true;
58
+ });
59
+ const send = (event) => {
60
+ const next = reduceGuidedTour(state(), event);
61
+ if (next === state())
62
+ return;
63
+ setState(next);
64
+ if (!(options.write ?? writeGuidedTourState)(next))
65
+ setError("Progress could not be saved. You can still continue this tour.");
66
+ };
67
+ return {
68
+ state,
69
+ busy,
70
+ error,
71
+ label: () => state().step === "complete"
72
+ ? "Replay walkthrough"
73
+ : state().step === initialGuidedTourState().step
74
+ ? "Learn tmux-ide"
75
+ : "Resume walkthrough",
76
+ open() {
77
+ previous = null;
78
+ send({ type: state().step === "complete" ? "replay" : "resume" });
79
+ },
80
+ replay() {
81
+ previous = null;
82
+ send({ type: "replay" });
83
+ },
84
+ pause() {
85
+ send({ type: "pause" });
86
+ },
87
+ welcomeRead() {
88
+ send({ type: "welcome-read" });
89
+ },
90
+ observe(current) {
91
+ const event = observedGuidedTourEvent(state(), previous, current);
92
+ previous = current;
93
+ if (event)
94
+ send(event);
95
+ },
96
+ async createPractice() {
97
+ if (busy() || state().step !== "practice")
98
+ return;
99
+ setBusy(true);
100
+ setError(null);
101
+ try {
102
+ const practice = await options.createPractice();
103
+ if (disposed)
104
+ return;
105
+ send({ type: "practice-created", practice });
106
+ if (state().active)
107
+ await options.openPractice(practice);
108
+ }
109
+ catch (cause) {
110
+ if (!disposed)
111
+ setError(cause instanceof Error ? cause.message : "Could not create the practice session.");
112
+ }
113
+ finally {
114
+ if (!disposed)
115
+ setBusy(false);
116
+ }
117
+ },
118
+ async openPractice() {
119
+ const practice = state().practice;
120
+ if (!practice || busy())
121
+ return;
122
+ setError(null);
123
+ try {
124
+ if (state().active)
125
+ await options.openPractice(practice);
126
+ }
127
+ catch (cause) {
128
+ setError(cause instanceof Error ? cause.message : "Practice session is unavailable.");
129
+ }
130
+ },
131
+ };
132
+ }
@@ -23,7 +23,8 @@ export function HomeAgentRoster(props) {
23
23
  const [hovered, setHovered] = createSignal(null);
24
24
  const width = () => Math.max(0, Math.floor(props.width));
25
25
  const height = () => Math.max(0, Math.floor(props.height));
26
- const stale = (row) => (props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
26
+ const stale = (row) => row.disabled ||
27
+ (props.snapshot.refreshingSessionKeys ?? []).includes(row.sessionKey) ||
27
28
  (props.snapshot.unavailableSessionKeys ?? []).includes(row.sessionKey);
28
29
  const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
29
30
  props.onRetry) ||
@@ -50,8 +51,8 @@ export function HomeAgentRoster(props) {
50
51
  if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
51
52
  return "No agents reported";
52
53
  const count = props.snapshot.rows.length;
53
- const attention = props.snapshot.rows.filter((row) => row.attention || row.activity === "waiting" || row.activity === "failed").length;
54
- const working = props.snapshot.rows.filter((row) => row.activity === "running").length;
54
+ const attention = props.snapshot.rows.filter((row) => !stale(row) && (row.attention || row.activity === "waiting" || row.activity === "failed")).length;
55
+ const working = props.snapshot.rows.filter((row) => !stale(row) && row.activity === "running").length;
55
56
  return `${count} observed ${count === 1 ? "agent" : "agents"} · ${attention} ${attention === 1 ? "needs" : "need"} attention · ${working} working`;
56
57
  };
57
58
  const coverage = () => `Scope: ${props.snapshot.observedSessions} of ${props.snapshot.totalSessions} sessions observed${props.snapshot.phase === "partial" ? " · partial" : ""}${props.snapshot.loadingSessions ? ` · ${props.snapshot.loadingSessions} loading` : ""}${props.snapshot.unavailableSessions ? ` · ${props.snapshot.unavailableSessions} unavailable` : ""}${props.snapshot.truncatedSessions ? ` · ${props.snapshot.truncatedSessions} not loaded` : ""}`;
@@ -69,15 +70,22 @@ export function HomeAgentRoster(props) {
69
70
  return "Enlarge the terminal to view agents.";
70
71
  const selected = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
71
72
  if (selected && stale(selected))
72
- return `${selected.sessionName} · last observed; waiting for fresh signals`;
73
- if (width() < 44 && selected)
74
- return `${selected.sessionName} · Enter open`;
73
+ return `${[selected.machineLabel, selected.serverLabel, selected.sessionName].filter(Boolean).join(" / ")} · last observed; waiting for fresh signals`;
74
+ if (selected?.machineLabel || (width() < 44 && selected))
75
+ return `${[selected?.machineLabel, selected?.serverLabel, selected?.sessionName].filter(Boolean).join(" / ")} · Enter open`;
75
76
  return `${offset() + 1}–${Math.min(props.snapshot.rows.length, offset() + visibleCount())} of ${props.snapshot.rows.length} · ↑↓ select · Enter open`;
76
77
  };
77
78
  useKeyboardRoute((event) => {
78
79
  if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta)
79
80
  return false;
80
81
  const key = event.name.toLowerCase();
82
+ const filter = key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
83
+ if (filter) {
84
+ event.preventDefault();
85
+ event.stopPropagation();
86
+ filter();
87
+ return true;
88
+ }
81
89
  const retry = key === "r" &&
82
90
  (props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
83
91
  props.onRetry;
@@ -120,10 +128,12 @@ export function HomeAgentRoster(props) {
120
128
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
121
129
  {clipTerminal(coverage(), width())}
122
130
  </text>
123
- <box height={1} flexShrink={0}/>
131
+ <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
132
+ {clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "", width())}
133
+ </text>
124
134
  <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0}/>}>
125
135
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
126
- {clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells("SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
136
+ {clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
127
137
  </text>
128
138
  <box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
129
139
  <For each={keys()}>
@@ -131,7 +141,9 @@ export function HomeAgentRoster(props) {
131
141
  const row = () => byKey().get(key);
132
142
  return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
133
143
  <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
134
- padCells(row().sessionName, columns().session)} detail={padCells(`${row().attention ? "! " : ""}${homeAgentStatusLabel(row().activity)}${stale(row()) ? "*" : ""}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={row().attention} status={statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
144
+ padCells([row().machineLabel, row().serverLabel, row().sessionName]
145
+ .filter(Boolean)
146
+ .join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={!stale(row()) && row().attention} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
135
147
  if (!props.inputActive || row().paneId === null || stale(row()))
136
148
  return;
137
149
  props.onSelect(key);