tmux-ide 2.9.0-beta.19 → 2.9.0-beta.21

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 (90) hide show
  1. package/bin/cli.js +1979 -783
  2. package/package.json +4 -2
  3. package/packages/contracts/src/__tests__/daemon-wire.test.ts +32 -0
  4. package/packages/contracts/src/daemon-events.ts +12 -12
  5. package/packages/contracts/src/daemon-wire.ts +30 -0
  6. package/packages/daemon/dist/command-center/agent-status-watch.js +9 -2
  7. package/packages/daemon/dist/command-center/diagnostics.js +65 -0
  8. package/packages/daemon/dist/command-center/log-stream.js +9 -0
  9. package/packages/daemon/dist/command-center/resources/fleet-preview-route.js +22 -6
  10. package/packages/daemon/dist/command-center/server.js +7 -0
  11. package/packages/daemon/dist/doctor.js +62 -0
  12. package/packages/daemon/dist/lib/__tests__/installed-recovery-fixture.js +400 -0
  13. package/packages/daemon/dist/lib/app-config.js +4 -2
  14. package/packages/daemon/dist/lib/canonical-daemon.js +1 -0
  15. package/packages/daemon/dist/lib/daemon-embed.js +24 -0
  16. package/packages/daemon/dist/lib/daemon-provenance.js +575 -0
  17. package/packages/daemon/dist/lib/headless-daemon.js +1 -0
  18. package/packages/daemon/dist/lib/log-sanitize.js +199 -0
  19. package/packages/daemon/dist/lib/log.js +123 -13
  20. package/packages/daemon/dist/lib/soak-diagnostics.js +124 -0
  21. package/packages/daemon/dist/lib/soak-verdict.js +472 -0
  22. package/packages/daemon/dist/lib/terminal-host-color.js +33 -0
  23. package/packages/daemon/dist/lib/tmux-external-interaction-observer.js +187 -39
  24. package/packages/daemon/dist/lib/tmux-interaction-retention.js +21 -0
  25. package/packages/daemon/dist/lib/workspace-promotion.js +55 -37
  26. package/packages/daemon/dist/terminal/mirror/session-channel.js +2 -1
  27. package/packages/daemon/dist/tui/mirror/automatic-contrast.js +161 -0
  28. package/packages/daemon/dist/tui/mirror/open-tui-workspace-runtime-port.js +9 -3
  29. package/packages/daemon/dist/tui/mirror/pane-surface.jsx +7 -5
  30. package/packages/daemon/dist/tui/mirror/resize-transaction.js +48 -20
  31. package/packages/daemon/dist/tui/mirror/runtime/application-appearance-owner.js +40 -6
  32. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-preview.js +4 -1
  33. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +63 -42
  34. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-interaction-controller.js +138 -67
  35. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-palette-owner.js +44 -28
  36. package/packages/daemon/dist/tui/mirror/runtime/application-terminal-workspace.jsx +49 -11
  37. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +140 -2
  38. package/packages/daemon/dist/tui/mirror/runtime/workspace-terminal-fast-lane.js +3 -1
  39. package/packages/daemon/dist/tui/mirror/semantic-pane-render-source.js +2 -1
  40. package/packages/daemon/dist/tui/mirror/theme.js +4 -31
  41. package/packages/daemon/dist/tui/mirror/workspace/terminal-pane-header.jsx +15 -8
  42. package/packages/daemon/dist/tui/team/wait-receipts.js +112 -33
  43. package/packages/daemon/src/command-center/agent-status-watch.ts +8 -2
  44. package/packages/daemon/src/command-center/diagnostics.ts +75 -0
  45. package/packages/daemon/src/command-center/log-stream.ts +8 -0
  46. package/packages/daemon/src/command-center/resources/fleet-preview-route.ts +27 -5
  47. package/packages/daemon/src/command-center/server.ts +8 -0
  48. package/packages/daemon/src/doctor.ts +70 -0
  49. package/packages/daemon/src/lib/app-config.ts +11 -3
  50. package/packages/daemon/src/lib/canonical-daemon.ts +1 -0
  51. package/packages/daemon/src/lib/daemon-embed.ts +30 -0
  52. package/packages/daemon/src/lib/daemon-provenance.ts +769 -0
  53. package/packages/daemon/src/lib/fleet-preview-model.ts +1 -0
  54. package/packages/daemon/src/lib/headless-daemon.ts +1 -0
  55. package/packages/daemon/src/lib/log-sanitize.ts +248 -0
  56. package/packages/daemon/src/lib/log.ts +165 -14
  57. package/packages/daemon/src/lib/soak-diagnostics.ts +183 -0
  58. package/packages/daemon/src/lib/soak-verdict.ts +785 -0
  59. package/packages/daemon/src/lib/terminal-host-color.ts +43 -0
  60. package/packages/daemon/src/lib/tmux-external-interaction-observer.ts +233 -37
  61. package/packages/daemon/src/lib/tmux-interaction-retention.ts +24 -0
  62. package/packages/daemon/src/lib/workspace-promotion.ts +74 -45
  63. package/packages/daemon/src/terminal/mirror/session-channel.ts +2 -1
  64. package/packages/daemon/src/tui/mirror/automatic-contrast.ts +180 -0
  65. package/packages/daemon/src/tui/mirror/open-tui-workspace-runtime-port.ts +26 -5
  66. package/packages/daemon/src/tui/mirror/pane-surface.tsx +9 -8
  67. package/packages/daemon/src/tui/mirror/resize-transaction.ts +46 -22
  68. package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +45 -6
  69. package/packages/daemon/src/tui/mirror/runtime/application-fleet-preview.ts +4 -0
  70. package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +2 -1
  71. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +99 -75
  72. package/packages/daemon/src/tui/mirror/runtime/application-palette-preview.tsx +13 -3
  73. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +125 -0
  74. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +2 -1
  75. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +199 -157
  76. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  77. package/packages/daemon/src/tui/mirror/runtime/application-terminal-interaction-controller.ts +148 -69
  78. package/packages/daemon/src/tui/mirror/runtime/application-terminal-palette-owner.ts +55 -28
  79. package/packages/daemon/src/tui/mirror/runtime/application-terminal-workspace.tsx +59 -17
  80. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +151 -3
  81. package/packages/daemon/src/tui/mirror/runtime/workspace-terminal-fast-lane.ts +3 -1
  82. package/packages/daemon/src/tui/mirror/semantic-pane-render-source.ts +2 -1
  83. package/packages/daemon/src/tui/mirror/theme.ts +4 -39
  84. package/packages/daemon/src/tui/mirror/workspace/terminal-pane-header.tsx +21 -8
  85. package/packages/daemon/src/tui/team/wait-receipts.ts +115 -40
  86. package/packages/daemon-client/src/terminal-fast-lane.test.ts +18 -0
  87. package/packages/daemon-client/src/terminal-fast-lane.ts +7 -2
  88. package/packages/tmux-bridge/src/index.ts +2 -0
  89. package/packages/tmux-bridge/src/runner.test.ts +41 -0
  90. package/packages/tmux-bridge/src/runner.ts +41 -2
@@ -104,6 +104,7 @@ function sameTarget(
104
104
  ): boolean {
105
105
  return (
106
106
  state.operationId === observation.operationId &&
107
+ state.authorityGeneration === observation.authorityGeneration &&
107
108
  state.workspaceName === observation.workspaceName &&
108
109
  state.semanticPaneId === observation.semanticPaneId &&
109
110
  state.axis === observation.axis
@@ -113,9 +114,9 @@ function sameTarget(
113
114
  /**
114
115
  * Renderer-local pane-resize transaction.
115
116
  *
116
- * Pointer motion only changes the preview state. Release authors exactly one
117
- * semantic intent and keeps that preview visible until the matching observed
118
- * operation settles or rejects. The controller owns no timers, transport, or
117
+ * Pointer motion submits the first target immediately, then retains only the
118
+ * latest desired size while receipt and canonical layout settle the pending
119
+ * operation. Release closes the gesture and flushes that latest target. The controller owns no timers, transport, or
119
120
  * rendering effects beyond the functions supplied by its caller.
120
121
  */
121
122
  export class ResizeTransactionController {
@@ -129,6 +130,8 @@ export class ResizeTransactionController {
129
130
  #projection: OptimisticProjectionState<number, number> | null = null;
130
131
  #revision = 0;
131
132
  #disposed = false;
133
+ #dragOpen = false;
134
+ #submittedCells: number | null = null;
132
135
 
133
136
  constructor(options: ResizeTransactionControllerOptions) {
134
137
  if (!Number.isFinite(options.timeoutMs) || options.timeoutMs <= 0) {
@@ -150,6 +153,8 @@ export class ResizeTransactionController {
150
153
  const workspaceName = nonEmpty(input.workspaceName, "workspaceName");
151
154
  const semanticPaneId = nonEmpty(input.semanticPaneId, "semanticPaneId");
152
155
  const canonicalCells = positiveCells(input.canonicalCells, "canonicalCells");
156
+ this.#dragOpen = true;
157
+ this.#submittedCells = null;
153
158
  this.#projection = createOptimisticProjection<number, number>({
154
159
  generation: authorityGeneration,
155
160
  revision: this.#revision,
@@ -168,28 +173,31 @@ export class ResizeTransactionController {
168
173
  return true;
169
174
  }
170
175
 
171
- /** Purely local preview update. It never calls submit or schedules work. */
176
+ /** First/latest coalescing: at most one mutation awaits canonical settlement. */
172
177
  move(previewCells: number): boolean {
173
- if (this.#state.phase !== "dragging") return false;
178
+ if (!this.#dragOpen || this.#state.phase === "idle") return false;
174
179
  const next = positiveCells(previewCells, "previewCells");
175
180
  if (next === this.#state.previewCells) return false;
176
181
  this.#emit({ ...this.#state, previewCells: next });
182
+ if (this.#state.phase === "dragging") this.#submit();
177
183
  return true;
178
184
  }
179
185
 
180
- /**
181
- * Submit once. Duplicate physical release/up/drop events return the existing
182
- * operation id without authoring another mutation.
183
- */
186
+ /** Duplicate releases never author another mutation. */
184
187
  release(): string | null {
188
+ this.#dragOpen = false;
185
189
  if (this.#state.phase === "pending") return this.#state.operationId;
186
190
  if (this.#state.phase !== "dragging") return null;
187
- const dragging = this.#state;
188
- if (dragging.previewCells === dragging.canonicalCells) {
189
- this.#emit({ phase: "idle", canonicalCells: dragging.canonicalCells, outcome: null });
190
- return null;
191
- }
191
+ const canonicalCells = this.#state.canonicalCells;
192
+ this.#emit({ phase: "idle", canonicalCells, outcome: null });
193
+ return null;
194
+ }
192
195
 
196
+ #submit(): string | null {
197
+ if (this.#state.phase !== "dragging") return null;
198
+ const dragging = this.#state;
199
+ if (dragging.previewCells === dragging.canonicalCells) return null;
200
+ this.#submittedCells = dragging.previewCells;
193
201
  const operationId = nonEmpty(this.#options.operationId(), "operationId");
194
202
  const submittedAt = this.#options.now();
195
203
  if (!this.#projection) throw new Error("resize projection is unavailable");
@@ -245,9 +253,14 @@ export class ResizeTransactionController {
245
253
  });
246
254
  }
247
255
 
248
- /** Escape/cancel is only meaningful before release. */
256
+ /** Discard queued motion; an already dispatched mutation cannot be undone. */
249
257
  cancelDrag(): boolean {
250
- if (this.#state.phase !== "dragging") return false;
258
+ if (!this.#dragOpen || this.#state.phase === "idle") return false;
259
+ this.#dragOpen = false;
260
+ if (this.#state.phase === "pending") {
261
+ this.#emit({ ...this.#state, previewCells: this.#submittedCells! });
262
+ return true;
263
+ }
251
264
  const canonicalCells = this.#state.canonicalCells;
252
265
  this.#emit({ phase: "idle", canonicalCells, outcome: null });
253
266
  return true;
@@ -260,6 +273,8 @@ export class ResizeTransactionController {
260
273
 
261
274
  /** Retire one runtime lane without permanently disposing the controller. */
262
275
  retire(): void {
276
+ this.#dragOpen = false;
277
+ this.#submittedCells = null;
263
278
  this.#clearTimeout();
264
279
  this.#projection = null;
265
280
  if (this.#state.phase !== "idle" || this.#state.canonicalCells !== null) {
@@ -277,7 +292,8 @@ export class ResizeTransactionController {
277
292
  )
278
293
  return false;
279
294
  const cells = positiveCells(observation.cells, "observed cells");
280
- const operationId = this.#state.operationId;
295
+ const pending = this.#state;
296
+ const operationId = pending.operationId;
281
297
  if (!this.#projection) return false;
282
298
  this.#revision += 1;
283
299
  this.#projection = replaceCommittedProjection(
@@ -290,11 +306,18 @@ export class ResizeTransactionController {
290
306
  { observedOperationIds: [operationId], nowMs: this.#options.now() },
291
307
  );
292
308
  this.#clearTimeout();
293
- this.#emit({
294
- phase: "idle",
295
- canonicalCells: cells,
296
- outcome: { kind: "settled", operationId, source, cells },
297
- });
309
+ const hasLatest =
310
+ pending.previewCells !== this.#submittedCells && pending.previewCells !== cells;
311
+ if (this.#dragOpen || hasLatest) {
312
+ this.#emit({ ...pending, phase: "dragging", canonicalCells: cells });
313
+ if (hasLatest) this.#submit();
314
+ } else {
315
+ this.#emit({
316
+ phase: "idle",
317
+ canonicalCells: cells,
318
+ outcome: { kind: "settled", operationId, source, cells },
319
+ });
320
+ }
298
321
  return true;
299
322
  }
300
323
 
@@ -321,6 +344,7 @@ export class ResizeTransactionController {
321
344
  );
322
345
  }
323
346
  const canonicalCells = this.#state.canonicalCells;
347
+ this.#dragOpen = false;
324
348
  this.#clearTimeout();
325
349
  this.#emit({
326
350
  phase: "idle",
@@ -1,3 +1,5 @@
1
+ import type { CliRenderer } from "@opentui/core";
2
+ import { createAutomaticContrastPass } from "../automatic-contrast.ts";
1
3
  import { VISUAL_THEME_PRESETS, findVisualThemePreset } from "@tmux-ide/contracts";
2
4
  import { batch, createSignal, type Accessor } from "solid-js";
3
5
 
@@ -26,6 +28,8 @@ export interface ApplicationAppearanceOwner {
26
28
  readonly palette: Accessor<TerminalPaletteProjection>;
27
29
  readonly hostPalette: Accessor<ApplicationTerminalPaletteSnapshot>;
28
30
  readonly setTransientNote: (note: string | null) => void;
31
+ readonly automaticContrast: Accessor<boolean>;
32
+ readonly toggleAutomaticContrast: () => void;
29
33
  readonly pickerOpen: Accessor<boolean>;
30
34
  readonly pickerError: Accessor<string | null>;
31
35
  readonly openPicker: () => void;
@@ -54,15 +58,36 @@ export interface ApplicationAppearanceSnapshot {
54
58
  }
55
59
 
56
60
  function hostDefaults(snapshot: ApplicationTerminalPaletteSnapshot) {
57
- if (snapshot.availability !== "available") return null;
58
- return deriveSystemVisualHostDefaults(snapshot);
61
+ return (
62
+ deriveSystemVisualHostDefaults(snapshot) ?? { appearance: snapshot.detectedMode, overrides: {} }
63
+ );
59
64
  }
60
65
 
61
66
  export function createAppearanceOwner(
62
67
  config: AppConfig,
63
- renderer: ThemeModeSource,
68
+ renderer: ThemeModeSource &
69
+ Partial<Pick<CliRenderer, "addPostProcessFn" | "removePostProcessFn" | "requestRender">>,
64
70
  terminalPaletteOwner: ApplicationTerminalPaletteOwner,
65
71
  ): ApplicationAppearanceOwner {
72
+ const [automaticContrast, setAutomaticContrast] = createSignal(
73
+ config.theme.automaticContrast ?? true,
74
+ );
75
+ const correct = createAutomaticContrastPass();
76
+ const postProcess: Parameters<CliRenderer["addPostProcessFn"]>[0] = (buffer) => {
77
+ if (automaticContrast()) correct(buffer);
78
+ };
79
+ renderer.addPostProcessFn?.(postProcess);
80
+ const setContrast = (enabled: boolean) => {
81
+ if (enabled === automaticContrast()) return;
82
+ setAutomaticContrast(enabled);
83
+ renderer.requestRender?.();
84
+ };
85
+ const toggleAutomaticContrast = () => {
86
+ if (pickerOpen()) {
87
+ setContrast(!automaticContrast());
88
+ setPickerError(null);
89
+ }
90
+ };
66
91
  const initialHostPalette = terminalPaletteOwner.getSnapshot();
67
92
  const store = createSemanticThemeStore(config.theme, {
68
93
  rendererMode: renderer.themeMode,
@@ -128,10 +153,17 @@ export function createAppearanceOwner(
128
153
  const apply = (id: string) => {
129
154
  const preset = findVisualThemePreset(id);
130
155
  const mode = preset?.appearance ?? (id as ThemeModeSetting);
131
- store.configure({ ...config.theme, mode, preset: preset?.id });
156
+ const wasSystem = theme().setting === "system";
157
+ store.configure({
158
+ ...config.theme,
159
+ mode,
160
+ preset: preset?.id,
161
+ });
162
+ if (mode === "system" && !wasSystem) void terminalPaletteOwner.refresh();
132
163
  setPickerSelection(id);
133
164
  };
134
165
  let originalSelection = pickerSelection();
166
+ let originalContrast = automaticContrast();
135
167
  const preview = (id: string): void => {
136
168
  if (!pickerOpen()) return;
137
169
  setPickerError(null);
@@ -139,6 +171,7 @@ export function createAppearanceOwner(
139
171
  };
140
172
  const cancelPicker = (): void => {
141
173
  if (!pickerOpen()) return;
174
+ setContrast(originalContrast);
142
175
  apply(originalSelection);
143
176
  setPickerOpen(false);
144
177
  setPickerError(null);
@@ -148,6 +181,7 @@ export function createAppearanceOwner(
148
181
  try {
149
182
  updateAppConfig({
150
183
  theme: {
184
+ automaticContrast: automaticContrast(),
151
185
  mode: theme().setting,
152
186
  preset: findVisualThemePreset(pickerSelection())?.id ?? "",
153
187
  },
@@ -165,7 +199,7 @@ export function createAppearanceOwner(
165
199
  const order: readonly ThemeModeSetting[] = ["dark", "light", "system"];
166
200
  const next = order[(order.indexOf(theme().setting) + 1) % order.length]!;
167
201
  apply(next);
168
- updateAppConfig({ theme: { mode: next, preset: "" } });
202
+ updateAppConfig({ theme: { mode: next, preset: "", automaticContrast: automaticContrast() } });
169
203
  notice.publish(`theme → ${next}`);
170
204
  };
171
205
  return {
@@ -176,6 +210,8 @@ export function createAppearanceOwner(
176
210
  palette,
177
211
  hostPalette,
178
212
  setTransientNote: notice.publish,
213
+ automaticContrast,
214
+ toggleAutomaticContrast,
179
215
  pickerOpen,
180
216
  pickerError,
181
217
  pickerQuery,
@@ -184,6 +220,7 @@ export function createAppearanceOwner(
184
220
  openPicker() {
185
221
  if (pickerOpen()) return;
186
222
  originalSelection = pickerSelection();
223
+ originalContrast = automaticContrast();
187
224
  setPickerQuery("");
188
225
  setPickerError(null);
189
226
  setPickerOpen(true);
@@ -196,7 +233,8 @@ export function createAppearanceOwner(
196
233
  if (event.eventType === "release" || event.repeated) return true;
197
234
  const name = event.name.toLowerCase();
198
235
  const choices = pickerOptions();
199
- if (name === "escape") cancelPicker();
236
+ if (event.ctrl && name === "a") toggleAutomaticContrast();
237
+ else if (name === "escape") cancelPicker();
200
238
  else if (name === "return" || name === "enter") savePicker();
201
239
  else if (name === "up" || name === "down" || name === "tab") {
202
240
  if (choices.length) {
@@ -219,6 +257,7 @@ export function createAppearanceOwner(
219
257
  },
220
258
  cycleTheme,
221
259
  dispose() {
260
+ renderer.removePostProcessFn?.(postProcess);
222
261
  stopTheme();
223
262
  stopRendererTheme();
224
263
  stopHostPalette();
@@ -14,6 +14,7 @@ export async function readFleetWindowPreview(
14
14
  liveSessionId: string,
15
15
  signal: AbortSignal,
16
16
  windowId?: string,
17
+ paneId?: string,
17
18
  ): Promise<FleetPreviewResult> {
18
19
  const daemon = handle.read();
19
20
  const epoch = handle.endpoint().epoch;
@@ -32,6 +33,7 @@ export async function readFleetWindowPreview(
32
33
  body: JSON.stringify({
33
34
  expectedInstanceId: daemon.instanceId,
34
35
  liveSessionId,
36
+ ...(paneId ? { paneId } : {}),
35
37
  ...(windowId ? { windowId } : {}),
36
38
  }),
37
39
  },
@@ -63,6 +65,7 @@ export async function readFleetWindowPreview(
63
65
  body.daemon?.instanceId !== daemon.instanceId ||
64
66
  body.daemon?.startedAt !== daemon.startedAt ||
65
67
  body.liveSessionId !== liveSessionId ||
68
+ (paneId && body.selectedPaneId !== paneId) ||
66
69
  typeof body.text !== "string" ||
67
70
  body.text.length > 8192
68
71
  )
@@ -103,6 +106,7 @@ export async function readFleetWindowPreview(
103
106
  status: "ready",
104
107
  snapshot: {
105
108
  text: clean(body.text),
109
+ ...(body.selectedPaneId ? { selectedPaneId: body.selectedPaneId } : {}),
106
110
  selectedWindowId,
107
111
  windows: windows.map((w) => ({
108
112
  id: w.id,
@@ -83,7 +83,8 @@ export function ApplicationFleetSwitcher(props: {
83
83
  if (!props.open || props.active === false || modal()) return false;
84
84
  const key = event.name.toLowerCase();
85
85
  // Preview and action components own these chords, without text or terminal leakage.
86
- if (event.ctrl && ["left", "right", "p", "e", "n", "x", "r"].includes(key)) return false;
86
+ if (event.ctrl && ["left", "right", "p", "e", "n", "x", "r", "k", "b"].includes(key))
87
+ return false;
87
88
  if (event.ctrl && key === "f") {
88
89
  event.preventDefault();
89
90
  event.stopPropagation();
@@ -175,6 +175,7 @@ export function ApplicationMachineSidebar(props: {
175
175
  : []),
176
176
  ]),
177
177
  );
178
+ const rowsByKey = createMemo(() => new Map(rows().map((row) => [row.key, row])));
178
179
  const index = () =>
179
180
  Math.max(
180
181
  0,
@@ -437,83 +438,106 @@ export function ApplicationMachineSidebar(props: {
437
438
  scrollY={true}
438
439
  horizontalScrollbarOptions={{ visible: false }}
439
440
  >
440
- <For each={rows()}>
441
- {(row) => (
442
- <box
443
- width={Math.max(1, props.width - 1)}
444
- height={row.agent ? (row.agentHeading ? 3 : 2) : 1}
445
- flexShrink={0}
446
- flexDirection="column"
447
- >
448
- <Show when={row.agentHeading}>
449
- <text height={1} fg={props.theme.roles.text.secondary}>
450
- {" "}
451
- Agents
452
- </text>
453
- </Show>
454
- <NavigationRow
455
- theme={props.theme}
456
- id={`machine:${row.key}`}
441
+ <For each={rows().map((row) => row.key)}>
442
+ {(key) => {
443
+ // The semantic key owns the native row; fresh snapshots update its
444
+ // props instead of destroying and recreating identical renderables.
445
+ const current = createMemo<Row>(
446
+ (previous) => rowsByKey().get(key) ?? previous,
447
+ rowsByKey().get(key)!,
448
+ );
449
+ const row: Row = {
450
+ key,
451
+ get group() {
452
+ return current().group;
453
+ },
454
+ get session() {
455
+ return current().session;
456
+ },
457
+ get agent() {
458
+ return current().agent;
459
+ },
460
+ get agentHeading() {
461
+ return current().agentHeading;
462
+ },
463
+ };
464
+ return (
465
+ <box
457
466
  width={Math.max(1, props.width - 1)}
458
- labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined}
459
- label={
460
- row.agent
461
- ? row.agent.name
462
- : row.session
463
- ? friendlySessionLabel(row.session.name)
464
- : row.group.label
465
- }
466
- marker={
467
- row.agent
468
- ? active(row)
469
- ? row.agent.attention
470
- ? "›!"
471
- : "›"
472
- : row.agent.attention
473
- ? "!"
474
- : "•"
475
- : row.session
476
- ? props.model.favorites?.().includes(row.session.id)
477
- ? " ★"
478
- : active(row)
479
- ? " ›"
480
- : " "
481
- : collapsed().has(preferenceKey(row.group))
482
- ? "▸"
483
- : "▾"
484
- }
485
- detail={
486
- row.agent
487
- ? row.group.state !== "ready" || row.agent.disabled
488
- ? "unavailable"
489
- : `[${terminalAgentStatusLabel(row.agent.activity)}]`
490
- : row.session
491
- ? row.group.state !== "ready" || row.session.disabled
467
+ height={row.agent ? (row.agentHeading ? 3 : 2) : 1}
468
+ flexShrink={0}
469
+ flexDirection="column"
470
+ >
471
+ <Show when={row.agentHeading}>
472
+ <text height={1} fg={props.theme.roles.text.secondary}>
473
+ {" "}
474
+ Agents
475
+ </text>
476
+ </Show>
477
+ <NavigationRow
478
+ theme={props.theme}
479
+ id={`machine:${row.key}`}
480
+ width={Math.max(1, props.width - 1)}
481
+ labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined}
482
+ label={
483
+ row.agent
484
+ ? row.agent.name
485
+ : row.session
486
+ ? friendlySessionLabel(row.session.name)
487
+ : row.group.label
488
+ }
489
+ marker={
490
+ row.agent
491
+ ? active(row)
492
+ ? row.agent.attention
493
+ ? "›!"
494
+ : "›"
495
+ : row.agent.attention
496
+ ? "!"
497
+ : "•"
498
+ : row.session
499
+ ? props.model.favorites?.().includes(row.session.id)
500
+ ? " ★"
501
+ : active(row)
502
+ ? " ›"
503
+ : " "
504
+ : collapsed().has(preferenceKey(row.group))
505
+ ? "▸"
506
+ : "▾"
507
+ }
508
+ detail={
509
+ row.agent
510
+ ? row.group.state !== "ready" || row.agent.disabled
492
511
  ? "unavailable"
493
- : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
494
- : row.group.agents?.length && row.group.state === "ready"
495
- ? activity(row).label
496
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
497
- }
498
- selected={Boolean((row.session || row.agent) && active(row))}
499
- focused={Boolean(focused() && row.key === selectedKey())}
500
- attention={row.agent?.attention ?? activity(row).kind === "attention"}
501
- onActivate={(source) => activate(row, source)}
502
- />
503
- <Show when={row.agent}>
504
- {(agent) => (
505
- <text
506
- height={1}
507
- fg={props.theme.roles.text.muted}
508
- content={clipTerminal(
509
- ` ${friendlySessionLabel(agent().sessionName)}`,
510
- Math.max(1, props.width - 1),
511
- )}
512
- />
513
- )}
514
- </Show>
515
- </box>
516
- )}
512
+ : `[${terminalAgentStatusLabel(row.agent.activity)}]`
513
+ : row.session
514
+ ? row.group.state !== "ready" || row.session.disabled
515
+ ? "unavailable"
516
+ : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
517
+ : row.group.agents?.length && row.group.state === "ready"
518
+ ? activity(row).label
519
+ : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
520
+ }
521
+ selected={Boolean((row.session || row.agent) && active(row))}
522
+ focused={Boolean(focused() && row.key === selectedKey())}
523
+ attention={row.agent?.attention ?? activity(row).kind === "attention"}
524
+ onActivate={(source) => activate(row, source)}
525
+ />
526
+ <Show when={row.agent}>
527
+ {(agent) => (
528
+ <text
529
+ height={1}
530
+ fg={props.theme.roles.text.muted}
531
+ content={clipTerminal(
532
+ ` ${friendlySessionLabel(agent().sessionName)}`,
533
+ Math.max(1, props.width - 1),
534
+ )}
535
+ />
536
+ )}
537
+ </Show>
538
+ </box>
539
+ );
540
+ }}
517
541
  </For>
518
542
  </scrollbox>
519
543
  <For each={agentHeight() > 0 ? [true] : []}>
@@ -45,15 +45,24 @@ export function ApplicationPalettePreview(props: {
45
45
  const owner = createAdaptiveFleetPreviewOwner(setState, { cache: fleetPreviewMemory });
46
46
  const stop = applicationMachineAuthorityManager.subscribe(() => setRevision((v) => v + 1));
47
47
  const command = () => (typeof props.command === "object" ? props.command : undefined);
48
+ const paneId = () => {
49
+ const c = command();
50
+ return c?.kind === "jump-agent" ? c.paneId : undefined;
51
+ };
48
52
  const target = () => command()?.fleet;
49
53
  const identity = () =>
50
- JSON.stringify([target()?.machineId, target()?.liveSessionId, target()?.daemonInstanceId]);
54
+ JSON.stringify([
55
+ target()?.machineId,
56
+ target()?.liveSessionId,
57
+ target()?.daemonInstanceId,
58
+ paneId(),
59
+ ]);
51
60
  let previousIdentity = "";
52
61
  createEffect(() => {
53
62
  const next = identity();
54
63
  if (next !== previousIdentity) {
55
64
  previousIdentity = next;
56
- setWindowId(rememberedWindows.get(next));
65
+ setWindowId(paneId() ? undefined : rememberedWindows.get(next));
57
66
  }
58
67
  });
59
68
  createEffect(() => {
@@ -78,6 +87,7 @@ export function ApplicationPalettePreview(props: {
78
87
  fleet.liveSessionId,
79
88
  signal,
80
89
  selected,
90
+ selected ? undefined : paneId(),
81
91
  );
82
92
  // A remembered window may have been closed since the last visit.
83
93
  // Fall back to the session's current window instead of retrying it forever.
@@ -135,7 +145,7 @@ export function ApplicationPalettePreview(props: {
135
145
  const heading = () => {
136
146
  const snapshot = state().snapshot;
137
147
  const window = snapshot?.windows.find((w) => w.id === snapshot.selectedWindowId);
138
- return `${target()?.hostLabel ?? "Session preview"} · ${command()?.sessionName ?? "Select a session"}${window ? ` · ${window.index}: ${window.name}` : ""}`;
148
+ return `${paneId() ? command()?.label + " · " : ""}${target()?.hostLabel ?? "Session preview"} · ${command()?.sessionName ?? "Select a session"}${window ? ` · ${window.index}: ${window.name}` : ""}`;
139
149
  };
140
150
  const hostColor = () => {
141
151
  revision();