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
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.39",
11282
+ version: "2.9.0-beta.41",
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.39",
3
+ "version": "2.9.0-beta.41",
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,4 +1,5 @@
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
5
  import { NavigationRow } from "../ui/navigation-row.jsx";
@@ -100,13 +101,13 @@ export function HomeAgentRoster(props) {
100
101
  return false;
101
102
  const key = event.name.toLowerCase();
102
103
  const filter = !editing() &&
103
- (key === "f"
104
+ (key === HOME_ACTIONS.machine.key
104
105
  ? props.onCycleMachine
105
- : key === "a"
106
+ : key === HOME_ACTIONS.attention.key
106
107
  ? props.onToggleAttention
107
- : key === "w" && props.onSetActivityFilter
108
+ : key === HOME_ACTIONS.working.key && props.onSetActivityFilter
108
109
  ? () => props.onSetActivityFilter?.("working")
109
- : key === "0" && props.onSetActivityFilter
110
+ : key === HOME_ACTIONS.all.key && props.onSetActivityFilter
110
111
  ? () => props.onSetActivityFilter?.("all")
111
112
  : undefined);
112
113
  if (filter) {
@@ -162,7 +163,7 @@ export function HomeAgentRoster(props) {
162
163
  </text>
163
164
  </box>
164
165
  <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
166
+ <KeyHint theme={props.theme} keys={HOME_ACTIONS.machine.keys} label={props.filterLabel?.split(" · ")[0] ?? "All machines"} width={props.onSetActivityFilter && filterRows() === 1
166
167
  ? Math.max(1, width() - 47)
167
168
  : props.onSetActivityFilter
168
169
  ? Math.min(width(), 28)
@@ -171,16 +172,12 @@ export function HomeAgentRoster(props) {
171
172
  props.onCycleMachine?.();
172
173
  }}/>
173
174
  <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={() => {
175
+ <Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys={HOME_ACTIONS.attention.keys} label="Attention" quiet button onPress={() => {
175
176
  if (props.inputActive)
176
177
  props.onToggleAttention?.();
177
178
  }}/>}>
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={() => {
179
+ <For each={HOME_ACTIVITY_ACTIONS}>
180
+ {(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
181
  if (props.inputActive)
185
182
  props.onSetActivityFilter?.(filter.value);
186
183
  }}/>)}
@@ -267,7 +264,7 @@ export function HomeAgentRoster(props) {
267
264
  {clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
268
265
  </text>
269
266
  <Show when={footer().endsWith("Enter open") && width() >= 20}>
270
- <KeyHint theme={props.theme} keys="Enter" label="open" quiet button onPress={() => openSelected("mouse")}/>
267
+ <KeyHint theme={props.theme} keys={HOME_ACTIONS.open.keys} label={HOME_ACTIONS.open.label} quiet button onPress={() => openSelected("mouse")}/>
271
268
  </Show>
272
269
  </box>
273
270
  <Show when={showRecovery()}>
@@ -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);
@@ -1,6 +1,7 @@
1
+ import { CHROME_ACTIONS, SIDEBAR_ACTIONS } from "../workspace/application-action-descriptions.js";
1
2
  import { createAgentStatusMarker } from "../ui/agent-status-marker.js";
2
3
  import { fleetHostColor, summarizeFleetActivity } from "./fleet-presentation.js";
3
- import { TuiButton } from "../ui/button.jsx";
4
+ import { WorkingSessions } from "../ui/working-sessions.jsx";
4
5
  /* @jsxImportSource @opentui/solid */
5
6
  import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connection-status";
6
7
  import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.js";
@@ -12,7 +13,6 @@ import { Surface } from "../ui/surface.jsx";
12
13
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
13
14
  /** Pure machine navigation. All connection and session authority stays with the caller. */
14
15
  export function ApplicationMachineSidebar(props) {
15
- const [showHelp, setShowHelp] = createSignal(false);
16
16
  const [localFocused, setLocalFocused] = createSignal(false);
17
17
  const focused = () => props.model.focused?.() ?? localFocused();
18
18
  const [localCollapsed, setCollapsed] = createSignal(new Set());
@@ -24,21 +24,18 @@ export function ApplicationMachineSidebar(props) {
24
24
  : 0;
25
25
  const controlsHeight = () => (props.height >= 8 && controlGroup() ? 4 : 0);
26
26
  const searchHeight = () => (props.model.onOpenSwitcher ? 1 : 0) + (props.model.onOpenAttention ? 1 : 0);
27
- const tabHeight = () => Math.min(props.model.tabs?.().length ?? 0, Math.max(0, Math.floor(props.height / 3)));
28
- const visibleTabs = () => {
29
- const tabs = props.model.tabs?.() ?? [];
30
- const active = tabs.findIndex((tab) => tab.active);
31
- const start = Math.max(0, active - tabHeight() + 1);
32
- return tabs.slice(start, start + tabHeight());
33
- };
34
- const helpHeight = () => showHelp() ? Math.min(5, Math.max(0, Math.floor(props.height / 3))) : 0;
27
+ const [workingFocused, setWorkingFocused] = createSignal(true);
28
+ const tabHeight = () => (props.model.tabs?.().length ?? 0) > 0 && props.height >= 12
29
+ ? 1 +
30
+ Math.min(props.model.tabs().length, Math.max(1, Math.floor((props.height / 2 - 1) / 3))) *
31
+ 3
32
+ : 0;
35
33
  const machineHeight = () => Math.max(0, props.height -
36
34
  1 -
37
35
  (props.model.onAddMachine ? 1 : 0) -
38
36
  agentHeight() -
39
37
  controlsHeight() -
40
38
  searchHeight() -
41
- helpHeight() -
42
39
  tabHeight());
43
40
  const active = (row) => row.group.id === props.model.activeMachineId() &&
44
41
  (row.agent
@@ -136,6 +133,7 @@ export function ApplicationMachineSidebar(props) {
136
133
  props.model.onCollapse?.(preferenceKey(group), value);
137
134
  };
138
135
  const activate = (row, source) => {
136
+ setWorkingFocused(false);
139
137
  setSelectedKey(row.key);
140
138
  setLocalFocused(true);
141
139
  props.model.onFocus?.();
@@ -172,7 +170,7 @@ export function ApplicationMachineSidebar(props) {
172
170
  : 0;
173
171
  const rowHeight = (row) => (row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1) + sectionGap(row);
174
172
  const revealFocusedRow = () => {
175
- if (!focused() || !scroll)
173
+ if (!focused() || (workingFocused() && tabHeight()) || !scroll)
176
174
  return;
177
175
  const y = rows()
178
176
  .slice(0, index())
@@ -194,6 +192,14 @@ export function ApplicationMachineSidebar(props) {
194
192
  if (!focused() || event.eventType !== "press" || event.meta)
195
193
  return false;
196
194
  let key = event.name.toLowerCase();
195
+ if (key === "tab" && tabHeight()) {
196
+ setWorkingFocused((value) => !value);
197
+ event.preventDefault();
198
+ event.stopPropagation();
199
+ return true;
200
+ }
201
+ if (workingFocused() && tabHeight() && !["escape", "?", "question", "/", "a"].includes(key))
202
+ return false;
197
203
  if (event.ctrl && key === "d")
198
204
  key = "halfdown";
199
205
  else if (event.ctrl && key === "u")
@@ -207,7 +213,7 @@ export function ApplicationMachineSidebar(props) {
207
213
  l: "right",
208
214
  g: event.shift ? "end" : "home",
209
215
  "/": "search",
210
- "?": "help",
216
+ [SIDEBAR_ACTIONS.help.key]: "help",
211
217
  question: "help",
212
218
  }[key] ?? key;
213
219
  if (event.ctrl && !["halfdown", "halfup"].includes(key))
@@ -229,16 +235,16 @@ export function ApplicationMachineSidebar(props) {
229
235
  "return",
230
236
  "space",
231
237
  "escape",
232
- "a",
233
- "r",
234
- "d",
238
+ SIDEBAR_ACTIONS.add.key,
239
+ SIDEBAR_ACTIONS.retry.key,
240
+ SIDEBAR_ACTIONS.disconnect.key,
235
241
  "f",
236
242
  ].includes(key))
237
243
  return false;
238
244
  event.preventDefault();
239
245
  event.stopPropagation();
240
246
  if (key === "help") {
241
- setShowHelp(!showHelp());
247
+ props.onHelp?.("keyboard");
242
248
  return true;
243
249
  }
244
250
  if (key === "search") {
@@ -246,15 +252,11 @@ export function ApplicationMachineSidebar(props) {
246
252
  return true;
247
253
  }
248
254
  if (key === "escape") {
249
- if (showHelp()) {
250
- setShowHelp(false);
251
- return true;
252
- }
253
255
  setLocalFocused(false);
254
256
  props.model.onBlur?.();
255
257
  return true;
256
258
  }
257
- if (key === "a") {
259
+ if (key === SIDEBAR_ACTIONS.add.key) {
258
260
  props.model.onAddMachine?.();
259
261
  return true;
260
262
  }
@@ -267,9 +269,9 @@ export function ApplicationMachineSidebar(props) {
267
269
  props.model.onFavorite?.(row.session.id, !props.model.favorites?.().includes(row.session.id));
268
270
  return true;
269
271
  }
270
- if (key === "r" || key === "d") {
272
+ if (key === SIDEBAR_ACTIONS.retry.key || key === SIDEBAR_ACTIONS.disconnect.key) {
271
273
  if (row.group.id !== "local") {
272
- if (key === "r")
274
+ if (key === SIDEBAR_ACTIONS.retry.key)
273
275
  props.model.onRetryMachine?.(row.group.id);
274
276
  else
275
277
  props.model.onDisconnectMachine?.(row.group.id);
@@ -304,19 +306,22 @@ export function ApplicationMachineSidebar(props) {
304
306
  });
305
307
  return (<Surface id="application-machine-sidebar" theme={props.theme} variant="panel" width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
306
308
  <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
307
- <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
309
+ <text fg={props.theme.roles.text.secondary}>
310
+ {tabHeight() ? " Workspace" : " Machines"}
311
+ </text>
308
312
  <box flexGrow={1}/>
309
313
  <Show when={props.model.tabs?.().length && props.width >= 28}>
310
- <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet/>
314
+ <KeyHint theme={props.theme} keys={CHROME_ACTIONS.tabs.keys} label={CHROME_ACTIONS.tabs.label} quiet/>
311
315
  </Show>
312
- <KeyHint theme={props.theme} keys="?" label={props.width >= 20 ? "Help" : undefined} quiet button onPress={() => setShowHelp(!showHelp())}/>
316
+ <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.help.keys} label={props.width >= 20 ? SIDEBAR_ACTIONS.help.label : undefined} quiet button onPress={() => props.onHelp?.("mouse")}/>
313
317
  </box>
314
- <For each={visibleTabs()}>
315
- {(tab) => (<box height={1} flexDirection="row">
316
- <NavigationRow theme={props.theme} width={Math.max(1, props.width - 4)} id={`fleet-tab:${tab.key}`} label={`${tab.hostLabel} / ${tab.label}`} marker={tab.active ? "●" : "○"} detail={tab.available ? "" : "offline"} focused={false} onActivate={() => props.model.onOpenTab?.(tab.key)}/>
317
- <TuiButton theme={props.theme} label="×" size="compact" width={3} onPress={() => props.model.onCloseTab?.(tab.key)}/>
318
- </box>)}
319
- </For>
318
+ <Show when={tabHeight() > 0}>
319
+ <WorkingSessions theme={props.theme} rows={props.model.tabs?.() ?? []} width={props.width} height={tabHeight()} focused={focused() && workingFocused()} onFocus={() => {
320
+ setWorkingFocused(true);
321
+ setLocalFocused(true);
322
+ props.model.onFocus?.();
323
+ }} onOpen={(key) => props.model.onOpenTab?.(key)} onClose={props.model.onCloseTab}/>
324
+ </Show>
320
325
  <scrollbox ref={(value) => {
321
326
  scroll = value;
322
327
  const resized = value.content.onSizeChange;
@@ -401,7 +406,7 @@ export function ApplicationMachineSidebar(props) {
401
406
  : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
402
407
  : row.group.agents?.length && row.group.state === "ready"
403
408
  ? activity(row).label
404
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} status={(row.agent?.attention ?? activity(row).kind === "attention")
409
+ : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && !(workingFocused() && tabHeight()) && row.key === selectedKey())} status={(row.agent?.attention ?? activity(row).kind === "attention")
405
410
  ? "blocked"
406
411
  : undefined} onActivate={(source) => activate(row, source)}/>
407
412
  <Show when={row.agent}>
@@ -416,16 +421,11 @@ export function ApplicationMachineSidebar(props) {
416
421
  {props.agents}
417
422
  </scrollbox>)}
418
423
  </For>
419
- <Show when={helpHeight() > 0}>
420
- <text height={helpHeight()} fg={props.theme.roles.text.secondary}>
421
- {"j/k ↑↓ move · g/G ends\nPgUp/Dn · Ctrl-U/D half\nh/l ←→ collapse/expand\nEnter open · / search\nf favorite · Esc terminal"}
422
- </text>
423
- </Show>
424
424
  <Show when={props.model.onOpenSwitcher}>
425
- <KeyHint theme={props.theme} keys="F6" label="Sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
425
+ <KeyHint theme={props.theme} keys={CHROME_ACTIONS.sessions.keys} label="Browse all sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
426
426
  </Show>
427
427
  <Show when={props.model.onOpenAttention}>
428
- <KeyHint theme={props.theme} keys="F7" label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`} width={props.width} quiet button onPress={() => props.model.onOpenAttention?.()}/>
428
+ <KeyHint theme={props.theme} keys={CHROME_ACTIONS.attention.keys} label={`${CHROME_ACTIONS.attention.label} (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`} width={props.width} quiet button onPress={() => props.model.onOpenAttention?.()}/>
429
429
  </Show>
430
430
  <Show when={controlsHeight() > 0 && controlGroup()}>
431
431
  {(group) => (<>
@@ -434,12 +434,12 @@ export function ApplicationMachineSidebar(props) {
434
434
  ? fleetConnectionMessage(group().diagnostic)
435
435
  : connectionDetail(group())}
436
436
  </text>
437
- <KeyHint theme={props.theme} keys="R" label="Retry connection" width={props.width} quiet button onPress={() => props.model.onRetryMachine?.(group().id)}/>
438
- <KeyHint theme={props.theme} keys="D" label="Disconnect" width={props.width} quiet button onPress={() => props.model.onDisconnectMachine?.(group().id)}/>
437
+ <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.retry.keys} label={SIDEBAR_ACTIONS.retry.label} width={props.width} quiet button onPress={() => props.model.onRetryMachine?.(group().id)}/>
438
+ <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.disconnect.keys} label={SIDEBAR_ACTIONS.disconnect.label} width={props.width} quiet button onPress={() => props.model.onDisconnectMachine?.(group().id)}/>
439
439
  </>)}
440
440
  </Show>
441
441
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
442
- {(add) => (<KeyHint theme={props.theme} keys="A" label="Add machine" width={props.width} quiet button onPress={add}/>)}
442
+ {(add) => (<KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.add.keys} label={SIDEBAR_ACTIONS.add.label} width={props.width} quiet button onPress={add}/>)}
443
443
  </For>
444
444
  </Surface>);
445
445
  }
@@ -27,9 +27,9 @@ export function createApplicationPaletteCommandOwner(options) {
27
27
  const activate = (command, source, confirmed = false) => {
28
28
  if (busy())
29
29
  return;
30
- if (command === "shortcuts" || command === "whats-new") {
30
+ if (command === "help" || command === "shortcuts" || command === "whats-new") {
31
31
  setCloseArmed(false);
32
- setReferencePage(command === "shortcuts" ? "shortcuts" : "changes");
32
+ setReferencePage(command === "help" ? "help" : command === "shortcuts" ? "shortcuts" : "changes");
33
33
  return;
34
34
  }
35
35
  if (command === "switch-session") {
@@ -10,6 +10,7 @@ const BASE_COMMANDS = [
10
10
  "close-pane",
11
11
  "zoom-pane",
12
12
  "appearance",
13
+ "help",
13
14
  "shortcuts",
14
15
  "whats-new",
15
16
  "switch-session",
@@ -1,4 +1,5 @@
1
1
  import { batch, createEffect, createSignal, onCleanup } from "solid-js";
2
+ import { UI_ICONS } from "./icons.js";
2
3
  const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
3
4
  const listeners = new Set();
4
5
  let timer;
@@ -37,25 +38,25 @@ export function createAgentStatusMarker(options) {
37
38
  });
38
39
  return () => {
39
40
  if (options.unavailable?.())
40
- return "·";
41
+ return UI_ICONS.unknown;
41
42
  if (options.attention?.())
42
- return "!";
43
+ return UI_ICONS.attention;
43
44
  switch (options.status()) {
44
45
  case "running":
45
46
  case "working":
46
47
  return animated() ? FRAMES[current()] : options.theme().glyphs.active;
47
48
  case "waiting":
48
49
  case "blocked":
49
- return "!";
50
+ return UI_ICONS.attention;
50
51
  case "complete":
51
52
  case "done":
52
53
  return options.theme().glyphs.check;
53
54
  case "failed":
54
- return "×";
55
+ return UI_ICONS.failed;
55
56
  case "idle":
56
57
  return options.theme().glyphs.inactive;
57
58
  default:
58
- return "·";
59
+ return UI_ICONS.unknown;
59
60
  }
60
61
  };
61
62
  }
@@ -0,0 +1,9 @@
1
+ /** Shared semantic glyphs; theme-specific active/idle/check glyphs stay in the theme. */
2
+ export const UI_ICONS = {
3
+ attention: "!",
4
+ failed: "×",
5
+ unknown: "·",
6
+ offline: "○",
7
+ newResult: "•",
8
+ close: "×",
9
+ };
@@ -14,3 +14,6 @@ export { componentPalette, } from "./state.js";
14
14
  export { Tabs } from "./tabs.jsx";
15
15
  export { Surface } from "./surface.jsx";
16
16
  export { StatusBar, StatusBarAction, StatusBarGroup, StatusBarSegment, StatusSegment, } from "./status-bar.jsx";
17
+ export { UI_ICONS } from "./icons.js";
18
+ export { SessionRow, sessionRowStatus } from "./session-row.jsx";
19
+ export { WorkingSessions } from "./working-sessions.jsx";