tmux-ide 2.9.0-beta.40 → 2.9.0-beta.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (58) hide show
  1. package/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 +52 -98
  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 +99 -114
  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/runtime/application-shell-home.jsx +43 -13
  12. package/packages/daemon/dist/tui/mirror/shell-chrome.js +5 -7
  13. package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +40 -0
  14. package/packages/daemon/dist/tui/mirror/ui/agent-status-marker.js +6 -5
  15. package/packages/daemon/dist/tui/mirror/ui/home-agent-search.jsx +8 -5
  16. package/packages/daemon/dist/tui/mirror/ui/icons.js +9 -0
  17. package/packages/daemon/dist/tui/mirror/ui/index.js +3 -0
  18. package/packages/daemon/dist/tui/mirror/ui/input-surface.jsx +11 -0
  19. package/packages/daemon/dist/tui/mirror/ui/navigation-row.jsx +7 -2
  20. package/packages/daemon/dist/tui/mirror/ui/section-heading.jsx +6 -0
  21. package/packages/daemon/dist/tui/mirror/ui/session-row.jsx +71 -0
  22. package/packages/daemon/dist/tui/mirror/ui/tabs.jsx +22 -5
  23. package/packages/daemon/dist/tui/mirror/ui/working-sessions.jsx +61 -0
  24. package/packages/daemon/dist/tui/mirror/workspace/application-action-descriptions.js +51 -0
  25. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +10 -3
  26. package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +15 -7
  27. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +1 -1
  28. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +3 -3
  29. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +8 -8
  30. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +34 -2
  31. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +122 -190
  32. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +1 -0
  33. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +44 -7
  34. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +133 -169
  35. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +4 -2
  36. package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
  37. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +41 -8
  38. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +67 -17
  39. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +2 -1
  40. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  41. package/packages/daemon/src/tui/mirror/shell-chrome.ts +4 -5
  42. package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +92 -0
  43. package/packages/daemon/src/tui/mirror/ui/agent-status-marker.ts +7 -5
  44. package/packages/daemon/src/tui/mirror/ui/home-agent-search.tsx +17 -17
  45. package/packages/daemon/src/tui/mirror/ui/icons.ts +9 -0
  46. package/packages/daemon/src/tui/mirror/ui/index.ts +4 -0
  47. package/packages/daemon/src/tui/mirror/ui/input-surface.tsx +39 -0
  48. package/packages/daemon/src/tui/mirror/ui/navigation-row.tsx +8 -1
  49. package/packages/daemon/src/tui/mirror/ui/overlay-search-field.tsx +5 -12
  50. package/packages/daemon/src/tui/mirror/ui/section-heading.tsx +15 -0
  51. package/packages/daemon/src/tui/mirror/ui/session-row.tsx +122 -0
  52. package/packages/daemon/src/tui/mirror/ui/tabs.tsx +30 -7
  53. package/packages/daemon/src/tui/mirror/ui/working-sessions.tsx +99 -0
  54. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
  55. package/packages/daemon/src/tui/mirror/workspace/application-action-descriptions.ts +52 -0
  56. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +12 -3
  57. package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +19 -7
  58. package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +1 -1
package/README.md CHANGED
@@ -146,6 +146,11 @@ environment.
146
146
 
147
147
  Regenerate the production-renderer demo with `pnpm demo:tui`.
148
148
 
149
+ Explore real Home, sidebar, Help, pane-header and footer components with `pnpm gallery:tui`. The
150
+ development gallery uses fixture data and local actions, so it does not connect
151
+ to your daemon or change live tmux sessions. See the
152
+ [gallery controls and checks](scripts/tui-gallery/README.md).
153
+
149
154
  - [Documentation](https://github.com/wavyrai/tmux-ide/tree/main/docs)
150
155
  - [Contributing](CONTRIBUTING.md)
151
156
  - [Release checklist](RELEASE.md)
package/bin/cli.js CHANGED
@@ -11279,7 +11279,7 @@ var require_package = __commonJS({
11279
11279
  "package.json"(exports, module) {
11280
11280
  module.exports = {
11281
11281
  name: "tmux-ide",
11282
- version: "2.9.0-beta.40",
11282
+ version: "2.9.0-beta.44",
11283
11283
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11284
11284
  type: "module",
11285
11285
  bin: {
@@ -11339,7 +11339,7 @@ var require_package = __commonJS({
11339
11339
  "test:pack-installed": "node scripts/pack-check-run.mjs",
11340
11340
  "release:opentui:check": "node scripts/opentui-release-check.mjs",
11341
11341
  "check:native-deps": "node packages/daemon/scripts/check-native-deps.mjs",
11342
- check: "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
11342
+ check: "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run typecheck:tui-gallery && pnpm run test:tui-gallery && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
11343
11343
  postinstall: "node scripts/postinstall.js",
11344
11344
  docs: "turbo run dev --filter=@tmux-ide/docs",
11345
11345
  "demo:tui": "bun --preload @opentui/solid/preload docs/scripts/render-tui-demo.tsx",
@@ -11374,7 +11374,10 @@ var require_package = __commonJS({
11374
11374
  "test:postinstall": "node --test scripts/postinstall-daemon-upgrade.test.mjs",
11375
11375
  "test:development-isolation": "tsx scripts/lib/development-isolation-qualification.ts",
11376
11376
  "test:development-isolation-unit": "node --test scripts/lib/development-isolation-resources.test.mjs",
11377
- "test:development-ci": "node --test scripts/lib/development-ci.test.mjs"
11377
+ "test:development-ci": "node --test scripts/lib/development-ci.test.mjs",
11378
+ "gallery:tui": "bun --preload @opentui/solid/preload scripts/tui-gallery/index.tsx",
11379
+ "test:tui-gallery": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./scripts/tui-gallery/gallery.test.tsx",
11380
+ "typecheck:tui-gallery": "tsc --noEmit -p scripts/tsconfig.tui-gallery.json"
11378
11381
  },
11379
11382
  keywords: [
11380
11383
  "tmux",
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.40",
3
+ "version": "2.9.0-beta.44",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -60,7 +60,7 @@
60
60
  "test:pack-installed": "node scripts/pack-check-run.mjs",
61
61
  "release:opentui:check": "node scripts/opentui-release-check.mjs",
62
62
  "check:native-deps": "node packages/daemon/scripts/check-native-deps.mjs",
63
- "check": "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
63
+ "check": "pnpm run lint:workspace && pnpm run check:control-bytes && pnpm run format:check && pnpm run typecheck:workspace && pnpm run test:postinstall && pnpm run test:portable-release-contract && pnpm run test:benchmark-comparative && pnpm run test:contracts-initializer-purity && pnpm run test:unit && pnpm run test:tui-testdrive && pnpm run test:product-test-rig && pnpm run test:daemon-bun && pnpm run test:tui-renderer && pnpm run typecheck:tui-gallery && pnpm run test:tui-gallery && pnpm run test:workbench-dock-package && pnpm run test:pane-frame-package && pnpm run docs:build && pnpm run pack:check && pnpm run test:pack-installed && pnpm run check:native-deps && pnpm run smoke:desktop",
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",
@@ -95,7 +95,10 @@
95
95
  "test:postinstall": "node --test scripts/postinstall-daemon-upgrade.test.mjs",
96
96
  "test:development-isolation": "tsx scripts/lib/development-isolation-qualification.ts",
97
97
  "test:development-isolation-unit": "node --test scripts/lib/development-isolation-resources.test.mjs",
98
- "test:development-ci": "node --test scripts/lib/development-ci.test.mjs"
98
+ "test:development-ci": "node --test scripts/lib/development-ci.test.mjs",
99
+ "gallery:tui": "bun --preload @opentui/solid/preload scripts/tui-gallery/index.tsx",
100
+ "test:tui-gallery": "bun test --preload @opentui/solid/preload --preload ./packages/daemon/test-support/opentui-renderer-preload.ts ./scripts/tui-gallery/gallery.test.tsx",
101
+ "typecheck:tui-gallery": "tsc --noEmit -p scripts/tsconfig.tui-gallery.json"
99
102
  },
100
103
  "keywords": [
101
104
  "tmux",
@@ -1,21 +1,43 @@
1
1
  /** Retained targets, with at most one active terminal owner. Suspended tabs own no streams. */
2
2
  export function createFleetTabs(options) {
3
3
  const tabs = new Map();
4
+ const unread = new Set();
5
+ const observations = new Map();
4
6
  let active = null;
5
7
  let pending = null;
6
8
  let epoch = 0;
7
9
  let disposed = false;
8
10
  return {
9
- snapshot: () => ({ tabs: [...tabs.values()], active }),
11
+ snapshot: () => ({ tabs: [...tabs.values()], active, unread: [...unread] }),
12
+ observe(key, agents) {
13
+ if (disposed || !tabs.has(key))
14
+ return;
15
+ if (!agents) {
16
+ observations.delete(key);
17
+ return;
18
+ }
19
+ const previous = observations.get(key);
20
+ observations.set(key, new Map(agents.map((agent) => [agent.id, agent.activity])));
21
+ if (active !== key &&
22
+ !unread.has(key) &&
23
+ agents.some((agent) => agent.activity === "complete" && previous?.get(agent.id) === "running")) {
24
+ unread.add(key);
25
+ options.publish();
26
+ }
27
+ },
10
28
  remember(target) {
11
29
  if (disposed)
12
30
  return;
13
31
  if (!tabs.has(target.key) && tabs.size >= 8) {
14
32
  const removable = [...tabs.keys()].find((key) => key !== active);
15
- if (removable)
33
+ if (removable) {
16
34
  tabs.delete(removable);
35
+ unread.delete(removable);
36
+ observations.delete(removable);
37
+ }
17
38
  }
18
39
  tabs.set(target.key, Object.freeze({ ...target }));
40
+ unread.delete(target.key);
19
41
  active = target.key;
20
42
  options.publish();
21
43
  },
@@ -41,6 +63,7 @@ export function createFleetTabs(options) {
41
63
  options.unavailable();
42
64
  return false;
43
65
  }
66
+ unread.delete(key);
44
67
  active = key;
45
68
  options.publish();
46
69
  return true;
@@ -55,6 +78,8 @@ export function createFleetTabs(options) {
55
78
  pending = null;
56
79
  }
57
80
  tabs.delete(key);
81
+ unread.delete(key);
82
+ observations.delete(key);
58
83
  options.publish();
59
84
  },
60
85
  suspend() {
@@ -70,6 +95,8 @@ export function createFleetTabs(options) {
70
95
  disposed = true;
71
96
  epoch++;
72
97
  tabs.clear();
98
+ unread.clear();
99
+ observations.clear();
73
100
  active = null;
74
101
  },
75
102
  };
@@ -1,29 +1,14 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
+ import { HOME_ACTIONS, HOME_ACTIVITY_ACTIONS, } from "../workspace/application-action-descriptions.js";
2
3
  import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
3
4
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
4
- import { NavigationRow } from "../ui/navigation-row.jsx";
5
+ import { AgentRow } from "../ui/agent-row.jsx";
5
6
  import { KeyHint } from "../ui/key-hint.jsx";
6
7
  import { TuiButton } from "../ui/button.jsx";
7
8
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
8
9
  import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
9
10
  import { HomeAgentSearch } from "../ui/home-agent-search.jsx";
10
- import { createAgentStatusMarker } from "../ui/agent-status-marker.js";
11
11
  import { ActivityIndicator } from "../ui/activity-indicator.jsx";
12
- import { componentPalette } from "../ui/state.js";
13
- import { homeAgentStatusLabel, } from "./application-home-agents.js";
14
- function padCells(text, width) {
15
- const clipped = clipTerminal(text, Math.max(0, width));
16
- return clipped + " ".repeat(Math.max(0, width - terminalDisplayWidth(clipped)));
17
- }
18
- function statusTone(row) {
19
- if (row.activity === "waiting" || row.activity === "failed")
20
- return "blocked";
21
- if (row.activity === "running")
22
- return "working";
23
- if (row.activity === "complete")
24
- return "done";
25
- return row.activity === "idle" ? "idle" : "unknown";
26
- }
27
12
  /** A flat projected roster; no subscriptions, navigation effects, or terminal input ingress. */
28
13
  export function HomeAgentRoster(props) {
29
14
  const [hovered, setHovered] = createSignal(null);
@@ -36,10 +21,12 @@ export function HomeAgentRoster(props) {
36
21
  const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
37
22
  props.onRetry) ||
38
23
  (props.snapshot.truncatedSessions > 0 && props.onLoadMore));
39
- const rowHeight = () => (width() >= 60 && height() >= 16 ? 2 : 1);
40
- const filterRows = () => (props.onSetActivityFilter && width() < 70 ? 2 : 1);
41
- const searchRows = () => (props.onQueryChange ? 2 : 0);
42
- const visibleCount = () => Math.max(0, Math.floor((height() - 4 - filterRows() - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight()));
24
+ const rowHeight = () => (height() >= 14 ? 2 : 1);
25
+ const compact = () => height() < 8;
26
+ const filterRows = () => (compact() ? 0 : props.onSetActivityFilter && width() < 70 ? 2 : 1);
27
+ const searchRows = () => (props.onQueryChange ? (compact() ? 1 : 2) : 0);
28
+ const visibleCount = () => Math.max(0, Math.floor((height() - (compact() ? 1 : 4) - filterRows() - searchRows() - (showRecovery() ? 1 : 0)) /
29
+ rowHeight()));
43
30
  const openSelected = (source = "keyboard") => {
44
31
  const row = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
45
32
  if (props.inputActive && visibleCount() > 0 && row?.paneId && !stale(row))
@@ -52,12 +39,6 @@ export function HomeAgentRoster(props) {
52
39
  const keys = createMemo(() => visible().map((row) => row.key), undefined, {
53
40
  equals: (a, b) => a.length === b.length && a.every((key, index) => key === b[index]),
54
41
  });
55
- const columns = () => {
56
- const status = Math.min(12, Math.max(0, width() - 4));
57
- const label = Math.max(0, width() - status - 3);
58
- const session = width() >= 44 ? Math.floor(label * 0.43) : 0;
59
- return { status, agent: label - session, session };
60
- };
61
42
  const filtered = () => Boolean(props.query || (props.activityFilter && props.activityFilter !== "all"));
62
43
  const title = () => {
63
44
  if (props.snapshot.phase === "unavailable")
@@ -100,13 +81,13 @@ export function HomeAgentRoster(props) {
100
81
  return false;
101
82
  const key = event.name.toLowerCase();
102
83
  const filter = !editing() &&
103
- (key === "f"
84
+ (key === HOME_ACTIONS.machine.key
104
85
  ? props.onCycleMachine
105
- : key === "a"
86
+ : key === HOME_ACTIONS.attention.key
106
87
  ? props.onToggleAttention
107
- : key === "w" && props.onSetActivityFilter
88
+ : key === HOME_ACTIONS.working.key && props.onSetActivityFilter
108
89
  ? () => props.onSetActivityFilter?.("working")
109
- : key === "0" && props.onSetActivityFilter
90
+ : key === HOME_ACTIONS.all.key && props.onSetActivityFilter
110
91
  ? () => props.onSetActivityFilter?.("all")
111
92
  : undefined);
112
93
  if (filter) {
@@ -142,7 +123,9 @@ export function HomeAgentRoster(props) {
142
123
  props.onMove(delta);
143
124
  return true;
144
125
  });
145
- return (<box position="relative" width={width()} height={height()} flexShrink={0} flexDirection="column" overflow="hidden" onMouseScroll={(event) => {
126
+ return (<box position="relative" width={width()} height={props.fitContent && props.snapshot.rows.length > 0
127
+ ? height() - (visibleCount() - visible().length) * rowHeight()
128
+ : height()} flexShrink={0} flexDirection="column" overflow="hidden" onMouseScroll={(event) => {
146
129
  if (!props.inputActive)
147
130
  return;
148
131
  const direction = event.scroll?.direction;
@@ -153,16 +136,19 @@ export function HomeAgentRoster(props) {
153
136
  props.onMove(direction === "up" ? -1 : 1);
154
137
  }} onMouseOut={() => setHovered(null)}>
155
138
  <box height={searchRows()} flexShrink={0}/>
156
- <box width={width()} height={1} flexShrink={0} flexDirection="row">
157
- <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
158
- <ActivityIndicator theme={props.theme} active={props.inputActive}/>
159
- </Show>
160
- <text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
161
- {clipTerminal(title(), width())}
162
- </text>
163
- </box>
164
- <box width={width()} height={filterRows()} flexShrink={0} flexDirection={filterRows() > 1 ? "column" : "row"} overflow="hidden">
165
- <KeyHint theme={props.theme} keys="f" label={props.filterLabel?.split(" · ")[0] ?? "All machines"} width={props.onSetActivityFilter && filterRows() === 1
139
+ <Show when={!compact()}>
140
+ <box width={width()} height={1} flexShrink={0} flexDirection="row">
141
+ <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
142
+ <ActivityIndicator theme={props.theme} active={props.inputActive}/>
143
+ </Show>
144
+ <text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
145
+ {clipTerminal(title(), width())}
146
+ </text>
147
+ </box>
148
+ </Show>
149
+ <Show when={!compact()}>
150
+ <box width={width()} height={filterRows()} flexShrink={0} flexDirection={filterRows() > 1 ? "column" : "row"} overflow="hidden">
151
+ <KeyHint theme={props.theme} keys={HOME_ACTIONS.machine.keys} label={props.filterLabel?.split(" · ")[0] ?? "All machines"} width={props.onSetActivityFilter && filterRows() === 1
166
152
  ? Math.max(1, width() - 47)
167
153
  : props.onSetActivityFilter
168
154
  ? Math.min(width(), 28)
@@ -170,24 +156,21 @@ export function HomeAgentRoster(props) {
170
156
  if (props.inputActive)
171
157
  props.onCycleMachine?.();
172
158
  }}/>
173
- <box height={1} flexShrink={0} flexDirection="row" overflow="hidden" width={Math.min(width(), 47)}>
174
- <Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys="a" label="Attention" quiet button onPress={() => {
159
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden" width={Math.min(width(), 47)}>
160
+ <Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys={HOME_ACTIONS.attention.keys} label="Attention" quiet button onPress={() => {
175
161
  if (props.inputActive)
176
162
  props.onToggleAttention?.();
177
163
  }}/>}>
178
- <For each={[
179
- { key: "0", label: "All", value: "all" },
180
- { key: "w", label: "Working", value: "working" },
181
- { key: "a", label: "Needs attention", value: "attention" },
182
- ]}>
183
- {(filter) => (<KeyHint theme={props.theme} keys={filter.key} label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label} quiet button selected={(props.activityFilter ?? "all") === filter.value} onPress={() => {
164
+ <For each={HOME_ACTIVITY_ACTIONS}>
165
+ {(filter) => (<KeyHint theme={props.theme} keys={filter.keys} label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label} quiet button selected={(props.activityFilter ?? "all") === filter.value} onPress={() => {
184
166
  if (props.inputActive)
185
167
  props.onSetActivityFilter?.(filter.value);
186
168
  }}/>)}
187
- </For>
188
- </Show>
169
+ </For>
170
+ </Show>
171
+ </box>
189
172
  </box>
190
- </box>
173
+ </Show>
191
174
  <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0} flexDirection="column" overflow="hidden">
192
175
  <Show when={props.snapshot.phase === "live" && !props.query}>
193
176
  <text height={1} width={width()} fg={props.theme.roles.text.primary}>
@@ -200,51 +183,20 @@ export function HomeAgentRoster(props) {
200
183
  </text>
201
184
  </Show>
202
185
  </box>}>
203
- <box height={1} flexShrink={0}/>
204
- <box height={visibleCount() * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
186
+ <box height={compact() ? 0 : 1} flexShrink={0}/>
187
+ <box height={(props.fitContent ? visible().length : visibleCount()) * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
205
188
  <For each={keys()}>
206
189
  {(key) => {
207
190
  const row = () => byKey().get(key);
208
- const marker = createAgentStatusMarker({
209
- theme: () => props.theme,
210
- status: () => row().activity,
211
- attention: () => row().attention,
212
- unavailable: () => stale(row()),
213
- });
214
- return (<box height={rowHeight()} flexDirection="column" backgroundColor={componentPalette(props.theme, {
215
- selected: props.selection.selectedKey === key,
216
- hovered: hovered() === key,
217
- disabled: row().paneId === null || stale(row()),
218
- }).background} width={width()} flexShrink={0} onMouseDown={(event) => {
219
- if (!props.inputActive || event.button !== 0 || !row().paneId || stale(row()))
220
- return;
221
- event.preventDefault();
222
- event.stopPropagation();
223
- props.onSelect(key);
224
- props.onOpen(row(), "mouse");
225
- }} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
226
- <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={rowHeight() > 1
227
- ? row().name
228
- : padCells(row().name, columns().agent) +
229
- padCells([row().machineLabel, row().serverLabel, row().sessionName]
230
- .filter(Boolean)
231
- .join(" / "), columns().session)} detail={padCells(`${marker()} ${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity).toLowerCase()}`, columns().status)} detailAlign="end" marker=" " selected={props.selection.selectedKey === key} focused={props.inputActive && !editing() && props.selection.selectedKey === key} hovered={hovered() === key} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
191
+ return (<box height={rowHeight()} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
192
+ <AgentRow surface="canvas" theme={props.theme} id={`home-agent:${key}`} name={row().name} context={[row().machineLabel, row().serverLabel, row().sessionName]
193
+ .filter(Boolean)
194
+ .join(" · ")} width={width()} compact={rowHeight() === 1} activity={row().activity} attention={row().attention} unavailable={row().paneId === null || stale(row())} selected={props.selection.selectedKey === key} focused={props.inputActive && !editing() && props.selection.selectedKey === key} hovered={hovered() === key} onOpen={(source) => {
232
195
  if (!props.inputActive || row().paneId === null || stale(row()))
233
196
  return;
234
197
  props.onSelect(key);
235
198
  props.onOpen(row(), source);
236
199
  }}/>
237
- <Show when={rowHeight() > 1}>
238
- <text width={width()} height={1} fg={props.selection.selectedKey === key
239
- ? props.theme.roles.selection.selectionText
240
- : props.theme.roles.text.muted} bg={componentPalette(props.theme, {
241
- selected: props.selection.selectedKey === key,
242
- hovered: hovered() === key,
243
- disabled: row().paneId === null || stale(row()),
244
- }).background}>
245
- {clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
246
- </text>
247
- </Show>
248
200
  </box>);
249
201
  }}
250
202
  </For>
@@ -262,14 +214,16 @@ export function HomeAgentRoster(props) {
262
214
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
263
215
  {clipTerminal(coverage(), width())}
264
216
  </text>
265
- <box width={width()} height={1} flexShrink={0} flexDirection="row" overflow="hidden">
266
- <text width={Math.min(Math.max(0, width() - (footer().endsWith("Enter open") && width() >= 20 ? 12 : 0)), terminalDisplayWidth(footer().replace(/ Enter open$/u, "")))} fg={props.theme.roles.text.muted}>
267
- {clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
268
- </text>
269
- <Show when={footer().endsWith("Enter open") && width() >= 20}>
270
- <KeyHint theme={props.theme} keys="Enter" label="open" quiet button onPress={() => openSelected("mouse")}/>
271
- </Show>
272
- </box>
217
+ <Show when={!compact()}>
218
+ <box width={width()} height={1} flexShrink={0} flexDirection="row" overflow="hidden">
219
+ <text width={Math.min(Math.max(0, width() - (footer().endsWith("Enter open") && width() >= 20 ? 12 : 0)), terminalDisplayWidth(footer().replace(/ Enter open$/u, "")))} fg={props.theme.roles.text.muted}>
220
+ {clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
221
+ </text>
222
+ <Show when={footer().endsWith("Enter open") && width() >= 20}>
223
+ <KeyHint theme={props.theme} keys={HOME_ACTIONS.open.keys} label={HOME_ACTIONS.open.label} quiet button onPress={() => openSelected("mouse")}/>
224
+ </Show>
225
+ </box>
226
+ </Show>
273
227
  <Show when={showRecovery()}>
274
228
  <box height={1} width={width()} flexShrink={0} flexDirection="row" gap={1}>
275
229
  <Show when={(props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
@@ -215,6 +215,7 @@ export function createApplicationHomeNavigationOwner(options) {
215
215
  command === "appearance" ||
216
216
  command === "hide-sidebar" ||
217
217
  command === "show-sidebar" ||
218
+ command === "help" ||
218
219
  command === "shortcuts" ||
219
220
  command === "whats-new" ||
220
221
  command === "switch-session")
@@ -3,7 +3,7 @@ import { createFleetTabs } from "./application-fleet-tabs.js";
3
3
  import { saveMachineProfiles } from "../../../lib/local-fleet-request.js";
4
4
  import { createApplicationFleetPreferences } from "./application-fleet-preferences.js";
5
5
  import { createApplicationMachineAgents, } from "./application-machine-agents.js";
6
- import { createSignal, onCleanup } from "solid-js";
6
+ import { createEffect, createSignal, onCleanup } from "solid-js";
7
7
  import { applicationMachineAuthorityManager as manager } from "./application-machine-authority.js";
8
8
  import { createApplicationMachineCatalog } from "./application-machine-catalog.js";
9
9
  import { ephemeralMachineProfile } from "./application-machine-startup.js";
@@ -159,7 +159,8 @@ export function createApplicationMachineNavigation(options) {
159
159
  machineId: id,
160
160
  liveSessionId: session.liveSessionId,
161
161
  label: session.name,
162
- hostLabel: `${manager.getMachine(id)?.label ?? id}${session.serverLabel ? ` / ${session.serverLabel}` : ""}`,
162
+ hostLabel: manager.getMachine(id)?.label ?? id,
163
+ serverLabel: session.serverLabel ?? "default",
163
164
  });
164
165
  preferences.change({ type: "visit", key: session.id });
165
166
  if (remember && history[historyIndex] !== session.id) {
@@ -198,6 +199,28 @@ export function createApplicationMachineNavigation(options) {
198
199
  });
199
200
  const isDefaultSession = (machineId, session) => !session.server ||
200
201
  session.server.generation === manager.getMachine(machineId)?.read()?.instanceId;
202
+ const tabAgents = (tab) => {
203
+ const group = agentGroups().find((value) => value.machineId === tab.machineId);
204
+ // This observer covers only the default tmux server; never guess for other scopes.
205
+ if (!resolveTab(tab) || !group?.available || !isDefaultSession(tab.machineId, tab))
206
+ return null;
207
+ const observation = group.observation;
208
+ if (observation &&
209
+ (observation.loadingSessions > 0 ||
210
+ observation.unavailableSessions > 0 ||
211
+ observation.truncatedSessions > 0 ||
212
+ observation.refreshingSessionKeys.length > 0))
213
+ return null;
214
+ const rows = group.agents.filter((agent) => agent.liveSessionId === tab.liveSessionId);
215
+ if (rows.some((agent) => agent.disabled))
216
+ return null;
217
+ return rows;
218
+ };
219
+ createEffect(() => {
220
+ tabRevision();
221
+ for (const tab of tabs.snapshot().tabs)
222
+ tabs.observe(tab.key, tabAgents(tab));
223
+ });
201
224
  const sidebar = {
202
225
  groups: () => snapshot().groups.map((group) => ({
203
226
  ...group,
@@ -214,11 +237,26 @@ export function createApplicationMachineNavigation(options) {
214
237
  tabs: () => {
215
238
  tabRevision();
216
239
  const value = tabs.snapshot();
217
- return value.tabs.map((tab) => ({
218
- ...tab,
219
- active: value.active === tab.key,
220
- available: !!resolveTab(tab),
221
- }));
240
+ return value.tabs.map((tab) => {
241
+ const agents = tabAgents(tab);
242
+ const activity = !agents
243
+ ? undefined
244
+ : agents.some((agent) => agent.activity === "failed")
245
+ ? "failed"
246
+ : agents.some((agent) => agent.attention || agent.activity === "waiting")
247
+ ? "waiting"
248
+ : agents.some((agent) => agent.activity === "running")
249
+ ? "running"
250
+ : "idle";
251
+ return {
252
+ ...tab,
253
+ active: value.active === tab.key,
254
+ available: !!resolveTab(tab),
255
+ activity,
256
+ attention: agents?.some((agent) => agent.attention || agent.activity === "waiting"),
257
+ unread: value.unread.includes(tab.key),
258
+ };
259
+ });
222
260
  },
223
261
  onOpenTab: (key) => {
224
262
  void tabs.activate(key);