tmux-ide 2.9.0-beta.39 → 2.9.0-beta.41

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 (39) hide show
  1. package/README.md +5 -0
  2. package/bin/cli.js +6 -3
  3. package/package.json +6 -3
  4. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-tabs.js +29 -2
  5. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +10 -13
  6. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +1 -0
  7. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +45 -7
  8. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +44 -44
  9. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +2 -2
  10. package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
  11. package/packages/daemon/dist/tui/mirror/ui/agent-status-marker.js +6 -5
  12. package/packages/daemon/dist/tui/mirror/ui/icons.js +9 -0
  13. package/packages/daemon/dist/tui/mirror/ui/index.js +3 -0
  14. package/packages/daemon/dist/tui/mirror/ui/session-row.jsx +71 -0
  15. package/packages/daemon/dist/tui/mirror/ui/working-sessions.jsx +61 -0
  16. package/packages/daemon/dist/tui/mirror/workspace/application-action-descriptions.js +51 -0
  17. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +10 -3
  18. package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +15 -7
  19. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +1 -1
  20. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +34 -2
  21. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +14 -18
  22. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +1 -0
  23. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +44 -7
  24. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +70 -80
  25. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +5 -3
  26. package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
  27. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +80 -5
  28. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +6 -2
  29. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +6 -5
  30. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -2
  31. package/packages/daemon/src/tui/mirror/ui/agent-status-marker.ts +7 -5
  32. package/packages/daemon/src/tui/mirror/ui/icons.ts +9 -0
  33. package/packages/daemon/src/tui/mirror/ui/index.ts +4 -0
  34. package/packages/daemon/src/tui/mirror/ui/session-row.tsx +122 -0
  35. package/packages/daemon/src/tui/mirror/ui/working-sessions.tsx +99 -0
  36. package/packages/daemon/src/tui/mirror/workspace/application-action-descriptions.ts +52 -0
  37. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +12 -3
  38. package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +19 -7
  39. package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +1 -1
@@ -0,0 +1,71 @@
1
+ import { Show } from "solid-js";
2
+ import { clipTerminal } from "../terminal-text.js";
3
+ import { NavigationRow } from "./navigation-row.jsx";
4
+ import { TuiButton } from "./button.jsx";
5
+ import { componentPalette } from "./state.js";
6
+ import { createAgentStatusMarker } from "./agent-status-marker.js";
7
+ import { UI_ICONS } from "./icons.js";
8
+ export function sessionRowStatus(row) {
9
+ if (!row.available)
10
+ return "offline";
11
+ if (row.attention || row.activity === "waiting")
12
+ return "needs input";
13
+ if (row.activity === "failed")
14
+ return "failed";
15
+ if (row.activity === "running")
16
+ return "working";
17
+ if (row.unread)
18
+ return "new result";
19
+ if (!row.activity || row.activity === "disconnected")
20
+ return "unknown";
21
+ return "idle";
22
+ }
23
+ /** Shared two-line session identity. Pure presentation: opening/closing belongs to its owner. */
24
+ export function SessionRow(props) {
25
+ const marker = createAgentStatusMarker({
26
+ theme: () => props.theme,
27
+ status: () => (sessionRowStatus(props.row) === "idle" ? "idle" : props.row.activity),
28
+ attention: () => !!props.row.attention,
29
+ unavailable: () => !props.row.available,
30
+ });
31
+ const tone = () => {
32
+ const status = sessionRowStatus(props.row);
33
+ return status === "needs input" || status === "failed"
34
+ ? "blocked"
35
+ : status === "working"
36
+ ? "working"
37
+ : status === "new result"
38
+ ? "done"
39
+ : "neutral";
40
+ };
41
+ const glyph = () => !props.row.available
42
+ ? UI_ICONS.offline
43
+ : sessionRowStatus(props.row) === "new result"
44
+ ? UI_ICONS.newResult
45
+ : marker();
46
+ const palette = () => componentPalette(props.theme, {
47
+ selected: props.selected,
48
+ focused: props.focused,
49
+ disabled: !props.row.available,
50
+ });
51
+ const detail = () => `${props.row.hostLabel} · ${props.row.serverLabel ?? "default"}${props.row.active ? " · current" : ""}`;
52
+ return (<box id={`session-row:${props.row.key}`} width={props.width} height={2} flexDirection="column" overflow="hidden">
53
+ <box height={1} flexDirection="row">
54
+ <NavigationRow theme={props.theme} id={`fleet-tab:${props.row.key}`} width={Math.max(1, props.width - (props.onClose ? 3 : 0))} label={props.row.label} tone={tone()} marker={glyph()} detail={sessionRowStatus(props.row)} selected={props.selected} focused={props.focused} disabled={!props.row.available} onActivate={props.onOpen}/>
55
+ <Show when={props.onClose}>
56
+ <TuiButton theme={props.theme} label={UI_ICONS.close} size="compact" width={3} onPress={props.onClose}/>
57
+ </Show>
58
+ </box>
59
+ <box height={1} backgroundColor={palette().background} onMouseDown={(event) => {
60
+ if (event.button !== 0 || !props.row.available)
61
+ return;
62
+ event.preventDefault();
63
+ event.stopPropagation();
64
+ props.onOpen();
65
+ }}>
66
+ <text fg={props.selected || props.focused ? palette().foreground : props.theme.roles.text.muted}>
67
+ {clipTerminal(` ${detail()}`, props.width)}
68
+ </text>
69
+ </box>
70
+ </box>);
71
+ }
@@ -0,0 +1,61 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
3
+ import { SessionRow } from "./session-row.jsx";
4
+ import { useKeyboardRoute } from "./keyboard-router.jsx";
5
+ /** Bounded working-set list, independent of fleet discovery and terminal ownership. */
6
+ export function WorkingSessions(props) {
7
+ const [selected, setSelected] = createSignal(null);
8
+ const index = () => Math.max(0, props.rows.findIndex((row) => row.key === selected()));
9
+ const capacity = () => Math.max(0, Math.floor((props.height - 1) / 3));
10
+ const start = () => Math.max(0, index() - capacity() + 1);
11
+ const visible = () => props.rows.slice(start(), start() + capacity()).map((row) => row.key);
12
+ createEffect(() => {
13
+ if (!props.rows.some((row) => row.key === selected()))
14
+ setSelected((props.rows.find((row) => row.active) ?? props.rows[0])?.key ?? null);
15
+ if (!props.focused)
16
+ setSelected((props.rows.find((row) => row.active) ?? props.rows[index()])?.key ?? null);
17
+ });
18
+ const open = (key) => {
19
+ setSelected(key);
20
+ props.onFocus?.();
21
+ if (props.rows.find((row) => row.key === key)?.available)
22
+ props.onOpen(key);
23
+ };
24
+ useKeyboardRoute((event) => {
25
+ if (!props.focused || event.eventType !== "press" || event.ctrl || event.meta)
26
+ return false;
27
+ const key = event.name.toLowerCase();
28
+ if (!["up", "down", "j", "k", "home", "end", "enter", "return", "space"].includes(key))
29
+ return false;
30
+ event.preventDefault();
31
+ event.stopPropagation();
32
+ if (!props.rows.length || !capacity())
33
+ return true;
34
+ if (["enter", "return", "space"].includes(key))
35
+ open(props.rows[index()].key);
36
+ else {
37
+ const next = key === "home"
38
+ ? 0
39
+ : key === "end"
40
+ ? props.rows.length - 1
41
+ : Math.max(0, Math.min(props.rows.length - 1, index() + (["up", "k"].includes(key) ? -1 : 1)));
42
+ setSelected(props.rows[next].key);
43
+ }
44
+ return true;
45
+ });
46
+ return (<box width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
47
+ <text height={1} fg={props.theme.roles.text.secondary}>
48
+ {props.focused ? <strong> Working sessions</strong> : " Working sessions"}
49
+ </text>
50
+ <Show when={props.rows.length} fallback={<text fg={props.theme.roles.text.muted}> No working sessions yet.</text>}>
51
+ <For each={visible()}>
52
+ {(key) => {
53
+ const row = createMemo((previous) => props.rows.find((row) => row.key === key) ?? previous, props.rows.find((row) => row.key === key));
54
+ return (<box height={3} flexShrink={0} flexDirection="column">
55
+ <SessionRow theme={props.theme} row={row()} width={props.width} selected={props.focused ? selected() === key : row().active} onOpen={() => open(key)} onClose={props.onClose ? () => props.onClose?.(key) : undefined}/>
56
+ </box>);
57
+ }}
58
+ </For>
59
+ </Show>
60
+ </box>);
61
+ }
@@ -0,0 +1,51 @@
1
+ /** Shared presentation/binding metadata. Context owners still decide availability and execution. */
2
+ export const HOME_ACTIONS = {
3
+ search: { key: "/", keys: "/", label: "Search", description: "Find an agent or workspace" },
4
+ machine: { key: "f", keys: "f", label: "Machines", description: "Cycle machine filter" },
5
+ all: { key: "0", keys: "0", label: "All", description: "Show all agent activity", value: "all" },
6
+ working: {
7
+ key: "w",
8
+ keys: "w",
9
+ label: "Working",
10
+ description: "Show working agents",
11
+ value: "working",
12
+ },
13
+ attention: {
14
+ key: "a",
15
+ keys: "a",
16
+ label: "Needs attention",
17
+ description: "Toggle attention filter",
18
+ value: "attention",
19
+ },
20
+ open: { key: "enter", keys: "Enter", label: "open", description: "Open selected agent" },
21
+ };
22
+ export const HOME_ACTIVITY_ACTIONS = [
23
+ HOME_ACTIONS.all,
24
+ HOME_ACTIONS.working,
25
+ HOME_ACTIONS.attention,
26
+ ];
27
+ export const CHROME_ACTIONS = {
28
+ home: { keys: "F1", label: "Home" },
29
+ terminals: { keys: "F2", label: "Terminals" },
30
+ commands: { keys: "F5", label: "Commands" },
31
+ sessions: { keys: "F6", label: "Sessions" },
32
+ attention: { keys: "F7", label: "Attention" },
33
+ tabs: { keys: "F9", label: "Tabs" },
34
+ sidebar: { keys: "F10", label: "Show / hide sidebar" },
35
+ };
36
+ export const SIDEBAR_ACTIONS = {
37
+ help: { key: "?", keys: "?", label: "Help", description: "Using tmux-ide" },
38
+ retry: {
39
+ key: "r",
40
+ keys: "R",
41
+ label: "Retry connection",
42
+ description: "Retry selected machine connection",
43
+ },
44
+ disconnect: {
45
+ key: "d",
46
+ keys: "D",
47
+ label: "Disconnect",
48
+ description: "Disconnect selected machine",
49
+ },
50
+ add: { key: "a", keys: "A", label: "Add machine", description: "Add an SSH machine" },
51
+ };
@@ -1,3 +1,4 @@
1
+ import { CHROME_ACTIONS } from "./application-action-descriptions.js";
1
2
  import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
2
3
  export { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
3
4
  import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
@@ -7,18 +8,20 @@ export const PALETTE_REFERENCE_COMMANDS = {
7
8
  };
8
9
  /** Presentation only: execution remains in the existing application owners. */
9
10
  export function applicationCommandDescription(command) {
11
+ if (command === "help")
12
+ return { id: command, label: "Using tmux-ide", detail: "Help and getting started" };
10
13
  if (command === "hide-sidebar" || command === "show-sidebar")
11
14
  return {
12
15
  id: command,
13
16
  label: command === "hide-sidebar" ? "Hide sidebar" : "Show sidebar",
14
17
  detail: "Terminal layout",
15
- shortcut: "F10",
18
+ shortcut: CHROME_ACTIONS.sidebar.keys,
16
19
  };
17
20
  if (command === "switch-session")
18
21
  return {
19
22
  id: command,
20
23
  label: "Switch session",
21
- shortcut: "F6",
24
+ shortcut: CHROME_ACTIONS.sessions.keys,
22
25
  detail: "Sessions across machines",
23
26
  };
24
27
  if (command === "shortcuts" || command === "whats-new")
@@ -71,7 +74,11 @@ export function applicationCommandDescription(command) {
71
74
  ? "Zoom / unzoom pane"
72
75
  : undefined) ??
73
76
  (command === "home" ? "Home" : command === "terminals" ? "Terminals" : "New terminal window"),
74
- shortcut: command === "home" ? "F1" : command === "terminals" ? "F2" : undefined,
77
+ shortcut: command === "home"
78
+ ? CHROME_ACTIONS.home.keys
79
+ : command === "terminals"
80
+ ? CHROME_ACTIONS.terminals.keys
81
+ : undefined,
75
82
  detail: command === "home"
76
83
  ? "sessions and agent state"
77
84
  : command === "terminals"
@@ -1,3 +1,4 @@
1
+ import { HOME_ACTIONS, CHROME_ACTIONS, SIDEBAR_ACTIONS, } from "./application-action-descriptions.js";
1
2
  import { applicationCommandDescription } from "./application-command-description.js";
2
3
  /** Display metadata only. Context-specific keyboard owners remain authoritative. */
3
4
  export const APPLICATION_SHORTCUTS = [
@@ -5,16 +6,23 @@ export const APPLICATION_SHORTCUTS = [
5
6
  const description = applicationCommandDescription(command);
6
7
  return { category: "Application", label: description.label, keys: description.shortcut };
7
8
  }),
8
- { category: "Application", label: "Commands", keys: "F5" },
9
- { category: "Application", label: "Show / hide sidebar", keys: "F10" },
10
- { category: "Application", label: "Agent attention", keys: "F7" },
9
+ { category: "Application", ...CHROME_ACTIONS.commands },
10
+ { category: "Application", ...CHROME_ACTIONS.sidebar },
11
+ { category: "Application", ...CHROME_ACTIONS.attention },
11
12
  { category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
12
13
  { category: "Terminals", label: "Open link", keys: "Shift+click" },
13
14
  { category: "Terminals", label: "Select and copy text", keys: "Shift+drag" },
14
- { category: "Home", label: "Find an agent or workspace", keys: "/" },
15
- { category: "Home", label: "Cycle machine filter", keys: "f" },
16
- { category: "Home", label: "Toggle attention filter", keys: "a" },
17
- { category: "Home", label: "Open selected agent", keys: "Enter" },
15
+ ...Object.values(HOME_ACTIONS).map((action) => ({
16
+ category: "Home",
17
+ label: action.description,
18
+ keys: action.keys,
19
+ })),
20
+ { category: "Sidebar (when focused)", label: "Switch working sessions / machines", keys: "Tab" },
21
+ ...Object.values(SIDEBAR_ACTIONS).map((action) => ({
22
+ category: "Sidebar (when focused)",
23
+ label: action.description,
24
+ keys: action.keys,
25
+ })),
18
26
  { category: "Command and session menus", label: "Search actions or sessions", keys: "Type" },
19
27
  { category: "Command and session menus", label: "Choose a result", keys: "↑ / ↓" },
20
28
  { category: "Command and session menus", label: "Activate selected result", keys: "Enter" },
@@ -13,7 +13,7 @@ export function paletteSection(command) {
13
13
  return "Appearance";
14
14
  if (command === "appearance")
15
15
  return "Appearance";
16
- if (command === "shortcuts" || command === "whats-new")
16
+ if (command === "help" || command === "shortcuts" || command === "whats-new")
17
17
  return "Help";
18
18
  return "Panes";
19
19
  }
@@ -6,6 +6,7 @@ export interface FleetTabTarget {
6
6
  readonly server?: TmuxServerScope;
7
7
  readonly label: string;
8
8
  readonly hostLabel: string;
9
+ readonly serverLabel?: string;
9
10
  }
10
11
  /** Retained targets, with at most one active terminal owner. Suspended tabs own no streams. */
11
12
  export function createFleetTabs(options: {
@@ -16,19 +17,45 @@ export function createFleetTabs(options: {
16
17
  unavailable(): void;
17
18
  }) {
18
19
  const tabs = new Map<string, FleetTabTarget>();
20
+ const unread = new Set<string>();
21
+ const observations = new Map<string, Map<string, string>>();
19
22
  let active: string | null = null;
20
23
  let pending: string | null = null;
21
24
  let epoch = 0;
22
25
  let disposed = false;
23
26
  return {
24
- snapshot: () => ({ tabs: [...tabs.values()], active }),
27
+ snapshot: () => ({ tabs: [...tabs.values()], active, unread: [...unread] }),
28
+ observe(key: string, agents: readonly { id: string; activity: string }[] | null) {
29
+ if (disposed || !tabs.has(key)) return;
30
+ if (!agents) {
31
+ observations.delete(key);
32
+ return;
33
+ }
34
+ const previous = observations.get(key);
35
+ observations.set(key, new Map(agents.map((agent) => [agent.id, agent.activity])));
36
+ if (
37
+ active !== key &&
38
+ !unread.has(key) &&
39
+ agents.some(
40
+ (agent) => agent.activity === "complete" && previous?.get(agent.id) === "running",
41
+ )
42
+ ) {
43
+ unread.add(key);
44
+ options.publish();
45
+ }
46
+ },
25
47
  remember(target: FleetTabTarget) {
26
48
  if (disposed) return;
27
49
  if (!tabs.has(target.key) && tabs.size >= 8) {
28
50
  const removable = [...tabs.keys()].find((key) => key !== active);
29
- if (removable) tabs.delete(removable);
51
+ if (removable) {
52
+ tabs.delete(removable);
53
+ unread.delete(removable);
54
+ observations.delete(removable);
55
+ }
30
56
  }
31
57
  tabs.set(target.key, Object.freeze({ ...target }));
58
+ unread.delete(target.key);
32
59
  active = target.key;
33
60
  options.publish();
34
61
  },
@@ -52,6 +79,7 @@ export function createFleetTabs(options: {
52
79
  options.unavailable();
53
80
  return false;
54
81
  }
82
+ unread.delete(key);
55
83
  active = key;
56
84
  options.publish();
57
85
  return true;
@@ -65,6 +93,8 @@ export function createFleetTabs(options: {
65
93
  pending = null;
66
94
  }
67
95
  tabs.delete(key);
96
+ unread.delete(key);
97
+ observations.delete(key);
68
98
  options.publish();
69
99
  },
70
100
  suspend() {
@@ -80,6 +110,8 @@ export function createFleetTabs(options: {
80
110
  disposed = true;
81
111
  epoch++;
82
112
  tabs.clear();
113
+ unread.clear();
114
+ observations.clear();
83
115
  active = null;
84
116
  },
85
117
  };
@@ -1,4 +1,8 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
+ import {
3
+ HOME_ACTIONS,
4
+ HOME_ACTIVITY_ACTIONS,
5
+ } from "../workspace/application-action-descriptions.ts";
2
6
  import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
3
7
  import type { SemanticThemeSnapshot } from "../theme.ts";
4
8
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.ts";
@@ -151,13 +155,13 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
151
155
  const key = event.name.toLowerCase();
152
156
  const filter =
153
157
  !editing() &&
154
- (key === "f"
158
+ (key === HOME_ACTIONS.machine.key
155
159
  ? props.onCycleMachine
156
- : key === "a"
160
+ : key === HOME_ACTIONS.attention.key
157
161
  ? props.onToggleAttention
158
- : key === "w" && props.onSetActivityFilter
162
+ : key === HOME_ACTIONS.working.key && props.onSetActivityFilter
159
163
  ? () => props.onSetActivityFilter?.("working")
160
- : key === "0" && props.onSetActivityFilter
164
+ : key === HOME_ACTIONS.all.key && props.onSetActivityFilter
161
165
  ? () => props.onSetActivityFilter?.("all")
162
166
  : undefined);
163
167
  if (filter) {
@@ -232,7 +236,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
232
236
  >
233
237
  <KeyHint
234
238
  theme={props.theme}
235
- keys="f"
239
+ keys={HOME_ACTIONS.machine.keys}
236
240
  label={props.filterLabel?.split(" · ")[0] ?? "All machines"}
237
241
  width={
238
242
  props.onSetActivityFilter && filterRows() === 1
@@ -259,7 +263,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
259
263
  fallback={
260
264
  <KeyHint
261
265
  theme={props.theme}
262
- keys="a"
266
+ keys={HOME_ACTIONS.attention.keys}
263
267
  label="Attention"
264
268
  quiet
265
269
  button
@@ -269,19 +273,11 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
269
273
  />
270
274
  }
271
275
  >
272
- <For
273
- each={
274
- [
275
- { key: "0", label: "All", value: "all" },
276
- { key: "w", label: "Working", value: "working" },
277
- { key: "a", label: "Needs attention", value: "attention" },
278
- ] as const
279
- }
280
- >
276
+ <For each={HOME_ACTIVITY_ACTIONS}>
281
277
  {(filter) => (
282
278
  <KeyHint
283
279
  theme={props.theme}
284
- keys={filter.key}
280
+ keys={filter.keys}
285
281
  label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label}
286
282
  quiet
287
283
  button
@@ -463,8 +459,8 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
463
459
  <Show when={footer().endsWith("Enter open") && width() >= 20}>
464
460
  <KeyHint
465
461
  theme={props.theme}
466
- keys="Enter"
467
- label="open"
462
+ keys={HOME_ACTIONS.open.keys}
463
+ label={HOME_ACTIONS.open.label}
468
464
  quiet
469
465
  button
470
466
  onPress={() => openSelected("mouse")}
@@ -307,6 +307,7 @@ export function createApplicationHomeNavigationOwner(options: {
307
307
  command === "appearance" ||
308
308
  command === "hide-sidebar" ||
309
309
  command === "show-sidebar" ||
310
+ command === "help" ||
310
311
  command === "shortcuts" ||
311
312
  command === "whats-new" ||
312
313
  command === "switch-session"
@@ -9,7 +9,7 @@ import {
9
9
  createApplicationMachineAgents,
10
10
  type ApplicationMachineAgent,
11
11
  } from "./application-machine-agents.ts";
12
- import { createSignal, onCleanup } from "solid-js";
12
+ import { createEffect, createSignal, onCleanup } from "solid-js";
13
13
  import { applicationMachineAuthorityManager as manager } from "./application-machine-authority.ts";
14
14
  import { createApplicationMachineCatalog } from "./application-machine-catalog.ts";
15
15
  import { ephemeralMachineProfile } from "./application-machine-startup.ts";
@@ -195,7 +195,8 @@ export function createApplicationMachineNavigation(options: {
195
195
  machineId: id,
196
196
  liveSessionId: session.liveSessionId,
197
197
  label: session.name,
198
- hostLabel: `${manager.getMachine(id)?.label ?? id}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
198
+ hostLabel: manager.getMachine(id)?.label ?? id,
199
+ serverLabel: session.serverLabel ?? "default",
199
200
  });
200
201
  preferences.change({ type: "visit", key: session.id });
201
202
  if (remember && history[historyIndex] !== session.id) {
@@ -246,6 +247,27 @@ export function createApplicationMachineNavigation(options: {
246
247
  const isDefaultSession = (machineId: string, session: { server?: TmuxServerScope }) =>
247
248
  !session.server ||
248
249
  session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
250
+ const tabAgents = (tab: FleetTabTarget) => {
251
+ const group = agentGroups().find((value) => value.machineId === tab.machineId);
252
+ // This observer covers only the default tmux server; never guess for other scopes.
253
+ if (!resolveTab(tab) || !group?.available || !isDefaultSession(tab.machineId, tab)) return null;
254
+ const observation = group.observation;
255
+ if (
256
+ observation &&
257
+ (observation.loadingSessions > 0 ||
258
+ observation.unavailableSessions > 0 ||
259
+ observation.truncatedSessions > 0 ||
260
+ observation.refreshingSessionKeys.length > 0)
261
+ )
262
+ return null;
263
+ const rows = group.agents.filter((agent) => agent.liveSessionId === tab.liveSessionId);
264
+ if (rows.some((agent) => agent.disabled)) return null;
265
+ return rows;
266
+ };
267
+ createEffect(() => {
268
+ tabRevision();
269
+ for (const tab of tabs.snapshot().tabs) tabs.observe(tab.key, tabAgents(tab));
270
+ });
249
271
  const sidebar: ApplicationMachineSidebarModel = {
250
272
  groups: () =>
251
273
  snapshot().groups.map((group) => ({
@@ -269,11 +291,26 @@ export function createApplicationMachineNavigation(options: {
269
291
  tabs: () => {
270
292
  tabRevision();
271
293
  const value = tabs.snapshot();
272
- return value.tabs.map((tab) => ({
273
- ...tab,
274
- active: value.active === tab.key,
275
- available: !!resolveTab(tab),
276
- }));
294
+ return value.tabs.map((tab) => {
295
+ const agents = tabAgents(tab);
296
+ const activity = !agents
297
+ ? undefined
298
+ : agents.some((agent) => agent.activity === "failed")
299
+ ? ("failed" as const)
300
+ : agents.some((agent) => agent.attention || agent.activity === "waiting")
301
+ ? ("waiting" as const)
302
+ : agents.some((agent) => agent.activity === "running")
303
+ ? ("running" as const)
304
+ : ("idle" as const);
305
+ return {
306
+ ...tab,
307
+ active: value.active === tab.key,
308
+ available: !!resolveTab(tab),
309
+ activity,
310
+ attention: agents?.some((agent) => agent.attention || agent.activity === "waiting"),
311
+ unread: value.unread.includes(tab.key),
312
+ };
313
+ });
277
314
  },
278
315
  onOpenTab: (key) => {
279
316
  void tabs.activate(key);