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

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.
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.37",
11282
+ version: "2.9.0-beta.39",
11283
11283
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11284
11284
  type: "module",
11285
11285
  bin: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.37",
3
+ "version": "2.9.0-beta.39",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -2,6 +2,7 @@
2
2
  import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
3
3
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
4
4
  import { NavigationRow } from "../ui/navigation-row.jsx";
5
+ import { KeyHint } from "../ui/key-hint.jsx";
5
6
  import { TuiButton } from "../ui/button.jsx";
6
7
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
7
8
  import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
@@ -36,12 +37,13 @@ export function HomeAgentRoster(props) {
36
37
  props.onRetry) ||
37
38
  (props.snapshot.truncatedSessions > 0 && props.onLoadMore));
38
39
  const rowHeight = () => (width() >= 60 && height() >= 16 ? 2 : 1);
40
+ const filterRows = () => (props.onSetActivityFilter && width() < 70 ? 2 : 1);
39
41
  const searchRows = () => (props.onQueryChange ? 2 : 0);
40
- const visibleCount = () => Math.max(0, Math.floor((height() - 5 - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight()));
41
- const openSelected = () => {
42
+ const visibleCount = () => Math.max(0, Math.floor((height() - 4 - filterRows() - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight()));
43
+ const openSelected = (source = "keyboard") => {
42
44
  const row = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
43
45
  if (props.inputActive && visibleCount() > 0 && row?.paneId && !stale(row))
44
- props.onOpen(row, "keyboard");
46
+ props.onOpen(row, source);
45
47
  };
46
48
  createEffect(() => props.onViewport(visibleCount()));
47
49
  const offset = () => Math.max(0, Math.min(props.selection.scrollOffset, Math.max(0, props.snapshot.rows.length - visibleCount())));
@@ -56,12 +58,13 @@ export function HomeAgentRoster(props) {
56
58
  const session = width() >= 44 ? Math.floor(label * 0.43) : 0;
57
59
  return { status, agent: label - session, session };
58
60
  };
61
+ const filtered = () => Boolean(props.query || (props.activityFilter && props.activityFilter !== "all"));
59
62
  const title = () => {
60
63
  if (props.snapshot.phase === "unavailable")
61
64
  return "Agent overview unavailable";
62
65
  if (props.snapshot.phase === "loading" && props.snapshot.rows.length === 0)
63
66
  return "Loading agent overview…";
64
- if (props.query && props.snapshot.rows.length === 0)
67
+ if (filtered() && props.snapshot.rows.length === 0)
65
68
  return "No matching agents";
66
69
  if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
67
70
  return "No agents reported";
@@ -97,7 +100,15 @@ export function HomeAgentRoster(props) {
97
100
  return false;
98
101
  const key = event.name.toLowerCase();
99
102
  const filter = !editing() &&
100
- (key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined);
103
+ (key === "f"
104
+ ? props.onCycleMachine
105
+ : key === "a"
106
+ ? props.onToggleAttention
107
+ : key === "w" && props.onSetActivityFilter
108
+ ? () => props.onSetActivityFilter?.("working")
109
+ : key === "0" && props.onSetActivityFilter
110
+ ? () => props.onSetActivityFilter?.("all")
111
+ : undefined);
101
112
  if (filter) {
102
113
  event.preventDefault();
103
114
  event.stopPropagation();
@@ -150,30 +161,42 @@ export function HomeAgentRoster(props) {
150
161
  {clipTerminal(title(), width())}
151
162
  </text>
152
163
  </box>
153
- <box width={width()} height={1} flexShrink={0} flexDirection="row">
154
- <text width={Math.max(0, width() - (props.onToggleAttention ? 16 : 0))} height={1} fg={props.theme.roles.text.secondary} onMouseDown={(event) => {
155
- if (!props.inputActive || event.button !== 0)
156
- return;
157
- event.preventDefault();
158
- event.stopPropagation();
159
- props.onCycleMachine?.();
160
- }}>
161
- {clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine` : "", Math.max(0, width() - (props.onToggleAttention ? 16 : 0)))}
162
- </text>
163
- <Show when={props.onToggleAttention}>
164
- <TuiButton theme={props.theme} label="Attention" shortcut="a" size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} width={Math.min(16, width())} onPress={() => {
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
166
+ ? Math.max(1, width() - 47)
167
+ : props.onSetActivityFilter
168
+ ? Math.min(width(), 28)
169
+ : Math.max(1, Math.min(28, width() - 16))} quiet button onPress={() => {
165
170
  if (props.inputActive)
166
- props.onToggleAttention?.();
171
+ props.onCycleMachine?.();
167
172
  }}/>
168
- </Show>
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={() => {
175
+ if (props.inputActive)
176
+ props.onToggleAttention?.();
177
+ }}/>}>
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={() => {
184
+ if (props.inputActive)
185
+ props.onSetActivityFilter?.(filter.value);
186
+ }}/>)}
187
+ </For>
188
+ </Show>
189
+ </box>
169
190
  </box>
170
191
  <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0} flexDirection="column" overflow="hidden">
171
192
  <Show when={props.snapshot.phase === "live" && !props.query}>
172
193
  <text height={1} width={width()} fg={props.theme.roles.text.primary}>
173
- {clipTerminal("Your next session starts here", width())}
194
+ {clipTerminal(filtered() ? "No agents match these filters" : "Your next session starts here", width())}
174
195
  </text>
175
196
  <text height={1} width={width()} fg={props.theme.roles.text.muted}>
176
- {clipTerminal("Open terminals, or use Commands to connect a machine.", width())}
197
+ {clipTerminal(filtered()
198
+ ? "Choose All or change the machine filter to see more agents."
199
+ : "Open terminals, or use Commands to connect a machine.", width())}
177
200
  </text>
178
201
  </Show>
179
202
  </box>}>
@@ -190,6 +213,7 @@ export function HomeAgentRoster(props) {
190
213
  });
191
214
  return (<box height={rowHeight()} flexDirection="column" backgroundColor={componentPalette(props.theme, {
192
215
  selected: props.selection.selectedKey === key,
216
+ hovered: hovered() === key,
193
217
  disabled: row().paneId === null || stale(row()),
194
218
  }).background} width={width()} flexShrink={0} onMouseDown={(event) => {
195
219
  if (!props.inputActive || event.button !== 0 || !row().paneId || stale(row()))
@@ -215,6 +239,7 @@ export function HomeAgentRoster(props) {
215
239
  ? props.theme.roles.selection.selectionText
216
240
  : props.theme.roles.text.muted} bg={componentPalette(props.theme, {
217
241
  selected: props.selection.selectedKey === key,
242
+ hovered: hovered() === key,
218
243
  disabled: row().paneId === null || stale(row()),
219
244
  }).background}>
220
245
  {clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
@@ -231,15 +256,20 @@ export function HomeAgentRoster(props) {
231
256
  const action = applicationPaneRenameKeyAction(event, props.query ?? "");
232
257
  if (action.kind === "update")
233
258
  onQueryChange()(action.value);
234
- }} onPaste={(bytes) => onQueryChange()(applicationPaneRenamePaste(props.query ?? "", bytes))} onEditing={setEditing} onSubmit={openSelected}/>
259
+ }} onPaste={(bytes) => onQueryChange()(applicationPaneRenamePaste(props.query ?? "", bytes))} onEditing={setEditing} onSubmit={() => openSelected()}/>
235
260
  </box>)}
236
261
  </Show>
237
262
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
238
263
  {clipTerminal(coverage(), width())}
239
264
  </text>
240
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
241
- {clipTerminal(footer(), width())}
242
- </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>
243
273
  <Show when={showRecovery()}>
244
274
  <box height={1} width={width()} flexShrink={0} flexDirection="row" gap={1}>
245
275
  <Show when={(props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
@@ -28,6 +28,13 @@ export function projectHomeFleet(catalog, groups, filter) {
28
28
  if (filter.attentionOnly &&
29
29
  !(agent.attention || agent.activity === "waiting" || agent.activity === "failed"))
30
30
  continue;
31
+ if (filter.workingOnly &&
32
+ (agent.disabled ||
33
+ group?.available === false ||
34
+ unavailable.has(sessionKey) ||
35
+ machine.state !== "ready" ||
36
+ agent.activity !== "running"))
37
+ continue;
31
38
  const paneKey = agent.paneId
32
39
  ? JSON.stringify([machine.id, agent.server?.serverId, agent.daemonInstanceId, agent.paneId])
33
40
  : agent.id;
@@ -119,7 +126,7 @@ export function createApplicationHomeFleetOwner(options) {
119
126
  return options.inputActive();
120
127
  },
121
128
  get agentFilterLabel() {
122
- return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : "All agents"}`;
129
+ return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : filter().workingOnly ? "Working" : "All agents"}`;
123
130
  },
124
131
  onCycleAgentMachine() {
125
132
  const ids = [null, ...catalog().groups.map((group) => group.id)];
@@ -128,8 +135,18 @@ export function createApplicationHomeFleetOwner(options) {
128
135
  machineId: ids[(ids.indexOf(value.machineId) + 1) % ids.length],
129
136
  }));
130
137
  },
138
+ get agentActivityFilter() {
139
+ return filter().attentionOnly ? "attention" : filter().workingOnly ? "working" : "all";
140
+ },
141
+ onSetAgentActivityFilter(activity) {
142
+ setFilter((value) => ({
143
+ ...value,
144
+ attentionOnly: activity === "attention",
145
+ workingOnly: activity === "working",
146
+ }));
147
+ },
131
148
  onToggleAgentAttention() {
132
- setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly }));
149
+ setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly, workingOnly: false }));
133
150
  },
134
151
  onRetryAgents: () => options.agents.retry?.(),
135
152
  onLoadMoreAgents: () => options.agents.loadMore?.(),
@@ -6,6 +6,7 @@ import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connectio
6
6
  import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.js";
7
7
  import { For, Show, createEffect, createMemo, createSignal, } from "solid-js";
8
8
  import { clipTerminal, friendlySessionLabel } from "../terminal-text.js";
9
+ import { KeyHint } from "../ui/key-hint.jsx";
9
10
  import { NavigationRow } from "../ui/navigation-row.jsx";
10
11
  import { Surface } from "../ui/surface.jsx";
11
12
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
@@ -302,10 +303,14 @@ export function ApplicationMachineSidebar(props) {
302
303
  return true;
303
304
  });
304
305
  return (<Surface id="application-machine-sidebar" theme={props.theme} variant="panel" width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
305
- <text height={1} fg={props.theme.roles.text.secondary}>
306
- {" "}
307
- {props.model.tabs?.().length ? "Machines · F9 tabs · ?" : "Machines · ? help"}
308
- </text>
306
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
307
+ <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
308
+ <box flexGrow={1}/>
309
+ <Show when={props.model.tabs?.().length && props.width >= 28}>
310
+ <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet/>
311
+ </Show>
312
+ <KeyHint theme={props.theme} keys="?" label={props.width >= 20 ? "Help" : undefined} quiet button onPress={() => setShowHelp(!showHelp())}/>
313
+ </box>
309
314
  <For each={visibleTabs()}>
310
315
  {(tab) => (<box height={1} flexDirection="row">
311
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)}/>
@@ -417,10 +422,10 @@ export function ApplicationMachineSidebar(props) {
417
422
  </text>
418
423
  </Show>
419
424
  <Show when={props.model.onOpenSwitcher}>
420
- <NavigationRow theme={props.theme} id="machine:switcher" label="Sessions" detail="F6" marker=" " width={props.width} onActivate={() => props.model.onOpenSwitcher?.()}/>
425
+ <KeyHint theme={props.theme} keys="F6" label="Sessions" width={props.width} quiet button onPress={() => props.model.onOpenSwitcher?.()}/>
421
426
  </Show>
422
427
  <Show when={props.model.onOpenAttention}>
423
- <NavigationRow theme={props.theme} id="machine:attention" label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)}) · F7`} marker="!" width={props.width} onActivate={() => 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?.()}/>
424
429
  </Show>
425
430
  <Show when={controlsHeight() > 0 && controlGroup()}>
426
431
  {(group) => (<>
@@ -429,12 +434,12 @@ export function ApplicationMachineSidebar(props) {
429
434
  ? fleetConnectionMessage(group().diagnostic)
430
435
  : connectionDetail(group())}
431
436
  </text>
432
- <NavigationRow theme={props.theme} id="machine:retry" label="Retry connection (R)" marker="↻" width={props.width} onActivate={() => props.model.onRetryMachine?.(group().id)}/>
433
- <NavigationRow theme={props.theme} id="machine:disconnect" label="Disconnect (D)" marker="×" width={props.width} onActivate={() => props.model.onDisconnectMachine?.(group().id)}/>
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)}/>
434
439
  </>)}
435
440
  </Show>
436
441
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
437
- {(add) => (<NavigationRow theme={props.theme} id="machine:add" label="Add machine" detail="A" marker=" " width={props.width} onActivate={add}/>)}
442
+ {(add) => (<KeyHint theme={props.theme} keys="A" label="Add machine" width={props.width} quiet button onPress={add}/>)}
438
443
  </For>
439
444
  </Surface>);
440
445
  }
@@ -19,6 +19,7 @@ export function ApplicationHomeSurface(props) {
19
19
  props.agentRoster?.phase === "live" &&
20
20
  props.agentRoster.rows.length === 0 &&
21
21
  !props.agentQuery &&
22
+ (!props.agentActivityFilter || props.agentActivityFilter === "all") &&
22
23
  applicationHomeBrandVariant(bodyWidth(), height()) === "ascii";
23
24
  const brandRows = () => (showAscii() ? APPLICATION_HOME_WORDMARK.length : 1);
24
25
  const spacious = () => height() >= 14;
@@ -116,7 +117,7 @@ export function ApplicationHomeSurface(props) {
116
117
  {clipTerminal(summary(), bodyWidth())}
117
118
  </text>
118
119
  </>}>
119
- {(snapshot) => (<HomeAgentRoster query={props.agentQuery} onQueryChange={props.onAgentQueryChange} filterLabel={props.agentFilterLabel} onCycleMachine={props.onCycleAgentMachine} onToggleAttention={props.onToggleAgentAttention} theme={props.theme} width={bodyWidth()} height={rosterHeight()} snapshot={snapshot()} selection={props.agentSelection ?? { selectedKey: null, scrollOffset: 0 }} inputActive={props.agentInputActive ?? false} onSelect={(key) => props.onSelectAgent?.(key)} onMove={(delta) => props.onMoveAgent?.(delta)} onViewport={(rows) => props.onAgentViewport?.(rows)} onOpen={(row, source) => props.onOpenAgent?.(row, source)} onRetry={props.onRetryAgents} onLoadMore={props.onLoadMoreAgents}/>)}
120
+ {(snapshot) => (<HomeAgentRoster query={props.agentQuery} onQueryChange={props.onAgentQueryChange} filterLabel={props.agentFilterLabel} activityFilter={props.agentActivityFilter} onSetActivityFilter={props.onSetAgentActivityFilter} onCycleMachine={props.onCycleAgentMachine} onToggleAttention={props.onToggleAgentAttention} theme={props.theme} width={bodyWidth()} height={rosterHeight()} snapshot={snapshot()} selection={props.agentSelection ?? { selectedKey: null, scrollOffset: 0 }} inputActive={props.agentInputActive ?? false} onSelect={(key) => props.onSelectAgent?.(key)} onMove={(delta) => props.onMoveAgent?.(delta)} onViewport={(rows) => props.onAgentViewport?.(rows)} onOpen={(row, source) => props.onOpenAgent?.(row, source)} onRetry={props.onRetryAgents} onLoadMore={props.onLoadMoreAgents}/>)}
120
121
  </Show>
121
122
  <Show when={activityHeight() > 0}>
122
123
  <box width={bodyWidth()} height={activityHeight()} flexShrink={0} flexDirection="column" overflow="hidden">
@@ -6,13 +6,17 @@ import { useKeyboardRoute } from "./keyboard-router.jsx";
6
6
  export function KeyHint(props) {
7
7
  const palette = () => componentPalette(props.theme, props, "neutral");
8
8
  const background = () => props.button
9
- ? props.theme.roles.surfaces.panelRaised
9
+ ? props.selected
10
+ ? props.theme.roles.selection.selection
11
+ : props.theme.roles.surfaces.panelRaised
10
12
  : props.quiet && !props.focused && !props.hovered
11
13
  ? props.theme.roles.surfaces.panel
12
14
  : palette().background;
13
- const foreground = () => props.quiet && !props.focused && !props.hovered
14
- ? props.theme.roles.text.muted
15
- : palette().foreground;
15
+ const foreground = () => props.selected
16
+ ? props.theme.roles.selection.selectionText
17
+ : props.quiet && !props.focused && !props.hovered
18
+ ? props.theme.roles.text.muted
19
+ : palette().foreground;
16
20
  const text = () => props.presentation ?? `${props.keys}${props.label ? ` ${props.label}` : ""}`;
17
21
  const width = () => Math.max(1, Math.floor(props.width ?? terminalDisplayWidth(text()) + 2));
18
22
  const content = () => clipTerminal(props.presentation ?? ` ${text()} `, width());
@@ -3,6 +3,7 @@ import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
3
3
  import type { SemanticThemeSnapshot } from "../theme.ts";
4
4
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.ts";
5
5
  import { NavigationRow } from "../ui/navigation-row.tsx";
6
+ import { KeyHint } from "../ui/key-hint.tsx";
6
7
  import { TuiButton } from "../ui/button.tsx";
7
8
  import { useKeyboardRoute } from "../ui/keyboard-router.tsx";
8
9
  import {
@@ -24,6 +25,8 @@ export interface HomeAgentRosterProps {
24
25
  readonly query?: string;
25
26
  readonly onQueryChange?: (query: string) => void;
26
27
  readonly filterLabel?: string;
28
+ readonly activityFilter?: "all" | "working" | "attention";
29
+ readonly onSetActivityFilter?: (value: "all" | "working" | "attention") => void;
27
30
  readonly onCycleMachine?: () => void;
28
31
  readonly onToggleAttention?: () => void;
29
32
  readonly theme: SemanticThemeSnapshot;
@@ -69,13 +72,19 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
69
72
  (props.snapshot.truncatedSessions > 0 && props.onLoadMore),
70
73
  );
71
74
  const rowHeight = () => (width() >= 60 && height() >= 16 ? 2 : 1);
75
+ const filterRows = () => (props.onSetActivityFilter && width() < 70 ? 2 : 1);
72
76
  const searchRows = () => (props.onQueryChange ? 2 : 0);
73
77
  const visibleCount = () =>
74
- Math.max(0, Math.floor((height() - 5 - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight()));
75
- const openSelected = () => {
78
+ Math.max(
79
+ 0,
80
+ Math.floor(
81
+ (height() - 4 - filterRows() - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight(),
82
+ ),
83
+ );
84
+ const openSelected = (source: "keyboard" | "mouse" = "keyboard") => {
76
85
  const row = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
77
86
  if (props.inputActive && visibleCount() > 0 && row?.paneId && !stale(row))
78
- props.onOpen(row, "keyboard");
87
+ props.onOpen(row, source);
79
88
  };
80
89
  createEffect(() => props.onViewport(visibleCount()));
81
90
  const offset = () =>
@@ -97,11 +106,13 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
97
106
  const session = width() >= 44 ? Math.floor(label * 0.43) : 0;
98
107
  return { status, agent: label - session, session };
99
108
  };
109
+ const filtered = () =>
110
+ Boolean(props.query || (props.activityFilter && props.activityFilter !== "all"));
100
111
  const title = () => {
101
112
  if (props.snapshot.phase === "unavailable") return "Agent overview unavailable";
102
113
  if (props.snapshot.phase === "loading" && props.snapshot.rows.length === 0)
103
114
  return "Loading agent overview…";
104
- if (props.query && props.snapshot.rows.length === 0) return "No matching agents";
115
+ if (filtered() && props.snapshot.rows.length === 0) return "No matching agents";
105
116
  if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
106
117
  return "No agents reported";
107
118
  const count = props.snapshot.rows.length;
@@ -140,7 +151,15 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
140
151
  const key = event.name.toLowerCase();
141
152
  const filter =
142
153
  !editing() &&
143
- (key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined);
154
+ (key === "f"
155
+ ? props.onCycleMachine
156
+ : key === "a"
157
+ ? props.onToggleAttention
158
+ : key === "w" && props.onSetActivityFilter
159
+ ? () => props.onSetActivityFilter?.("working")
160
+ : key === "0" && props.onSetActivityFilter
161
+ ? () => props.onSetActivityFilter?.("all")
162
+ : undefined);
144
163
  if (filter) {
145
164
  event.preventDefault();
146
165
  event.stopPropagation();
@@ -204,37 +223,77 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
204
223
  {clipTerminal(title(), width())}
205
224
  </text>
206
225
  </box>
207
- <box width={width()} height={1} flexShrink={0} flexDirection="row">
208
- <text
209
- width={Math.max(0, width() - (props.onToggleAttention ? 16 : 0))}
210
- height={1}
211
- fg={props.theme.roles.text.secondary}
212
- onMouseDown={(event) => {
213
- if (!props.inputActive || event.button !== 0) return;
214
- event.preventDefault();
215
- event.stopPropagation();
216
- props.onCycleMachine?.();
226
+ <box
227
+ width={width()}
228
+ height={filterRows()}
229
+ flexShrink={0}
230
+ flexDirection={filterRows() > 1 ? "column" : "row"}
231
+ overflow="hidden"
232
+ >
233
+ <KeyHint
234
+ theme={props.theme}
235
+ keys="f"
236
+ label={props.filterLabel?.split(" · ")[0] ?? "All machines"}
237
+ width={
238
+ props.onSetActivityFilter && filterRows() === 1
239
+ ? Math.max(1, width() - 47)
240
+ : props.onSetActivityFilter
241
+ ? Math.min(width(), 28)
242
+ : Math.max(1, Math.min(28, width() - 16))
243
+ }
244
+ quiet
245
+ button
246
+ onPress={() => {
247
+ if (props.inputActive) props.onCycleMachine?.();
217
248
  }}
249
+ />
250
+ <box
251
+ height={1}
252
+ flexShrink={0}
253
+ flexDirection="row"
254
+ overflow="hidden"
255
+ width={Math.min(width(), 47)}
218
256
  >
219
- {clipTerminal(
220
- props.filterLabel ? `${props.filterLabel} · f machine` : "",
221
- Math.max(0, width() - (props.onToggleAttention ? 16 : 0)),
222
- )}
223
- </text>
224
- <Show when={props.onToggleAttention}>
225
- <TuiButton
226
- theme={props.theme}
227
- label="Attention"
228
- shortcut="a"
229
- size="compact"
230
- variant="ghost"
231
- background={props.theme.roles.surfaces.canvas}
232
- width={Math.min(16, width())}
233
- onPress={() => {
234
- if (props.inputActive) props.onToggleAttention?.();
235
- }}
236
- />
237
- </Show>
257
+ <Show
258
+ when={props.onSetActivityFilter}
259
+ fallback={
260
+ <KeyHint
261
+ theme={props.theme}
262
+ keys="a"
263
+ label="Attention"
264
+ quiet
265
+ button
266
+ onPress={() => {
267
+ if (props.inputActive) props.onToggleAttention?.();
268
+ }}
269
+ />
270
+ }
271
+ >
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
+ >
281
+ {(filter) => (
282
+ <KeyHint
283
+ theme={props.theme}
284
+ keys={filter.key}
285
+ label={filter.value === "attention" && width() < 47 ? "Attention" : filter.label}
286
+ quiet
287
+ button
288
+ selected={(props.activityFilter ?? "all") === filter.value}
289
+ onPress={() => {
290
+ if (props.inputActive) props.onSetActivityFilter?.(filter.value);
291
+ }}
292
+ />
293
+ )}
294
+ </For>
295
+ </Show>
296
+ </box>
238
297
  </box>
239
298
  <Show
240
299
  when={props.snapshot.rows.length > 0}
@@ -247,10 +306,18 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
247
306
  >
248
307
  <Show when={props.snapshot.phase === "live" && !props.query}>
249
308
  <text height={1} width={width()} fg={props.theme.roles.text.primary}>
250
- {clipTerminal("Your next session starts here", width())}
309
+ {clipTerminal(
310
+ filtered() ? "No agents match these filters" : "Your next session starts here",
311
+ width(),
312
+ )}
251
313
  </text>
252
314
  <text height={1} width={width()} fg={props.theme.roles.text.muted}>
253
- {clipTerminal("Open terminals, or use Commands to connect a machine.", width())}
315
+ {clipTerminal(
316
+ filtered()
317
+ ? "Choose All or change the machine filter to see more agents."
318
+ : "Open terminals, or use Commands to connect a machine.",
319
+ width(),
320
+ )}
254
321
  </text>
255
322
  </Show>
256
323
  </box>
@@ -280,6 +347,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
280
347
  backgroundColor={
281
348
  componentPalette(props.theme, {
282
349
  selected: props.selection.selectedKey === key,
350
+ hovered: hovered() === key,
283
351
  disabled: row().paneId === null || stale(row()),
284
352
  }).background
285
353
  }
@@ -340,6 +408,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
340
408
  bg={
341
409
  componentPalette(props.theme, {
342
410
  selected: props.selection.selectedKey === key,
411
+ hovered: hovered() === key,
343
412
  disabled: row().paneId === null || stale(row()),
344
413
  }).background
345
414
  }
@@ -373,7 +442,7 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
373
442
  onQueryChange()(applicationPaneRenamePaste(props.query ?? "", bytes))
374
443
  }
375
444
  onEditing={setEditing}
376
- onSubmit={openSelected}
445
+ onSubmit={() => openSelected()}
377
446
  />
378
447
  </box>
379
448
  )}
@@ -381,9 +450,27 @@ export function HomeAgentRoster(props: HomeAgentRosterProps) {
381
450
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
382
451
  {clipTerminal(coverage(), width())}
383
452
  </text>
384
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
385
- {clipTerminal(footer(), width())}
386
- </text>
453
+ <box width={width()} height={1} flexShrink={0} flexDirection="row" overflow="hidden">
454
+ <text
455
+ width={Math.min(
456
+ Math.max(0, width() - (footer().endsWith("Enter open") && width() >= 20 ? 12 : 0)),
457
+ terminalDisplayWidth(footer().replace(/ Enter open$/u, "")),
458
+ )}
459
+ fg={props.theme.roles.text.muted}
460
+ >
461
+ {clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
462
+ </text>
463
+ <Show when={footer().endsWith("Enter open") && width() >= 20}>
464
+ <KeyHint
465
+ theme={props.theme}
466
+ keys="Enter"
467
+ label="open"
468
+ quiet
469
+ button
470
+ onPress={() => openSelected("mouse")}
471
+ />
472
+ </Show>
473
+ </box>
387
474
  <Show when={showRecovery()}>
388
475
  <box height={1} width={width()} flexShrink={0} flexDirection="row" gap={1}>
389
476
  <Show
@@ -30,6 +30,8 @@ export type ApplicationHomeAgentPresentation = Pick<
30
30
  | "agentQuery"
31
31
  | "onAgentQueryChange"
32
32
  | "agentFilterLabel"
33
+ | "agentActivityFilter"
34
+ | "onSetAgentActivityFilter"
33
35
  | "onCycleAgentMachine"
34
36
  | "onToggleAgentAttention"
35
37
  | "agentRoster"
@@ -11,6 +11,7 @@ import type { ApplicationHomeAgentPresentation } from "./application-home-agents
11
11
  export interface HomeFleetFilter {
12
12
  readonly machineId: string | null;
13
13
  readonly attentionOnly: boolean;
14
+ readonly workingOnly?: boolean;
14
15
  readonly query?: string;
15
16
  }
16
17
  export function projectHomeFleet(
@@ -54,6 +55,15 @@ export function projectHomeFleet(
54
55
  !(agent.attention || agent.activity === "waiting" || agent.activity === "failed")
55
56
  )
56
57
  continue;
58
+ if (
59
+ filter.workingOnly &&
60
+ (agent.disabled ||
61
+ group?.available === false ||
62
+ unavailable.has(sessionKey) ||
63
+ machine.state !== "ready" ||
64
+ agent.activity !== "running")
65
+ )
66
+ continue;
57
67
  const paneKey = agent.paneId
58
68
  ? JSON.stringify([machine.id, agent.server?.serverId, agent.daemonInstanceId, agent.paneId])
59
69
  : agent.id;
@@ -166,7 +176,7 @@ export function createApplicationHomeFleetOwner(options: {
166
176
  return options.inputActive();
167
177
  },
168
178
  get agentFilterLabel() {
169
- return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : "All agents"}`;
179
+ return `${catalog().groups.find((group) => group.id === filter().machineId)?.label ?? "All machines"} · ${filter().attentionOnly ? "Needs attention" : filter().workingOnly ? "Working" : "All agents"}`;
170
180
  },
171
181
  onCycleAgentMachine() {
172
182
  const ids = [null, ...catalog().groups.map((group) => group.id)];
@@ -175,8 +185,18 @@ export function createApplicationHomeFleetOwner(options: {
175
185
  machineId: ids[(ids.indexOf(value.machineId) + 1) % ids.length]!,
176
186
  }));
177
187
  },
188
+ get agentActivityFilter() {
189
+ return filter().attentionOnly ? "attention" : filter().workingOnly ? "working" : "all";
190
+ },
191
+ onSetAgentActivityFilter(activity) {
192
+ setFilter((value) => ({
193
+ ...value,
194
+ attentionOnly: activity === "attention",
195
+ workingOnly: activity === "working",
196
+ }));
197
+ },
178
198
  onToggleAgentAttention() {
179
- setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly }));
199
+ setFilter((value) => ({ ...value, attentionOnly: !value.attentionOnly, workingOnly: false }));
180
200
  },
181
201
  onRetryAgents: () => options.agents.retry?.(),
182
202
  onLoadMoreAgents: () => options.agents.loadMore?.(),
@@ -21,6 +21,7 @@ import {
21
21
  } from "solid-js";
22
22
  import { clipTerminal, friendlySessionLabel } from "../terminal-text.ts";
23
23
  import type { SemanticThemeSnapshot } from "../theme.ts";
24
+ import { KeyHint } from "../ui/key-hint.tsx";
24
25
  import { NavigationRow } from "../ui/navigation-row.tsx";
25
26
  import { Surface } from "../ui/surface.tsx";
26
27
  import { useKeyboardRoute } from "../ui/keyboard-router.tsx";
@@ -458,10 +459,21 @@ export function ApplicationMachineSidebar(props: {
458
459
  flexDirection="column"
459
460
  overflow="hidden"
460
461
  >
461
- <text height={1} fg={props.theme.roles.text.secondary}>
462
- {" "}
463
- {props.model.tabs?.().length ? "Machines · F9 tabs · ?" : "Machines · ? help"}
464
- </text>
462
+ <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
463
+ <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
464
+ <box flexGrow={1} />
465
+ <Show when={props.model.tabs?.().length && props.width >= 28}>
466
+ <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet />
467
+ </Show>
468
+ <KeyHint
469
+ theme={props.theme}
470
+ keys="?"
471
+ label={props.width >= 20 ? "Help" : undefined}
472
+ quiet
473
+ button
474
+ onPress={() => setShowHelp(!showHelp())}
475
+ />
476
+ </box>
465
477
  <For each={visibleTabs()}>
466
478
  {(tab) => (
467
479
  <box height={1} flexDirection="row">
@@ -653,24 +665,25 @@ export function ApplicationMachineSidebar(props: {
653
665
  </text>
654
666
  </Show>
655
667
  <Show when={props.model.onOpenSwitcher}>
656
- <NavigationRow
668
+ <KeyHint
657
669
  theme={props.theme}
658
- id="machine:switcher"
670
+ keys="F6"
659
671
  label="Sessions"
660
- detail="F6"
661
- marker=" "
662
672
  width={props.width}
663
- onActivate={() => props.model.onOpenSwitcher?.()}
673
+ quiet
674
+ button
675
+ onPress={() => props.model.onOpenSwitcher?.()}
664
676
  />
665
677
  </Show>
666
678
  <Show when={props.model.onOpenAttention}>
667
- <NavigationRow
679
+ <KeyHint
668
680
  theme={props.theme}
669
- id="machine:attention"
670
- label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)}) · F7`}
671
- marker="!"
681
+ keys="F7"
682
+ label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`}
672
683
  width={props.width}
673
- onActivate={() => props.model.onOpenAttention?.()}
684
+ quiet
685
+ button
686
+ onPress={() => props.model.onOpenAttention?.()}
674
687
  />
675
688
  </Show>
676
689
  <Show when={controlsHeight() > 0 && controlGroup()}>
@@ -681,35 +694,37 @@ export function ApplicationMachineSidebar(props: {
681
694
  ? fleetConnectionMessage(group().diagnostic!)
682
695
  : connectionDetail(group())}
683
696
  </text>
684
- <NavigationRow
697
+ <KeyHint
685
698
  theme={props.theme}
686
- id="machine:retry"
687
- label="Retry connection (R)"
688
- marker="↻"
699
+ keys="R"
700
+ label="Retry connection"
689
701
  width={props.width}
690
- onActivate={() => props.model.onRetryMachine?.(group().id)}
702
+ quiet
703
+ button
704
+ onPress={() => props.model.onRetryMachine?.(group().id)}
691
705
  />
692
- <NavigationRow
706
+ <KeyHint
693
707
  theme={props.theme}
694
- id="machine:disconnect"
695
- label="Disconnect (D)"
696
- marker="×"
708
+ keys="D"
709
+ label="Disconnect"
697
710
  width={props.width}
698
- onActivate={() => props.model.onDisconnectMachine?.(group().id)}
711
+ quiet
712
+ button
713
+ onPress={() => props.model.onDisconnectMachine?.(group().id)}
699
714
  />
700
715
  </>
701
716
  )}
702
717
  </Show>
703
718
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
704
719
  {(add) => (
705
- <NavigationRow
720
+ <KeyHint
706
721
  theme={props.theme}
707
- id="machine:add"
722
+ keys="A"
708
723
  label="Add machine"
709
- detail="A"
710
- marker=" "
711
724
  width={props.width}
712
- onActivate={add}
725
+ quiet
726
+ button
727
+ onPress={add}
713
728
  />
714
729
  )}
715
730
  </For>
@@ -44,6 +44,8 @@ export interface ApplicationHomeSurfaceProps {
44
44
  readonly agentQuery?: string;
45
45
  readonly onAgentQueryChange?: (query: string) => void;
46
46
  readonly agentFilterLabel?: string;
47
+ readonly agentActivityFilter?: "all" | "working" | "attention";
48
+ readonly onSetAgentActivityFilter?: (value: "all" | "working" | "attention") => void;
47
49
  readonly onCycleAgentMachine?: () => void;
48
50
  readonly onToggleAgentAttention?: () => void;
49
51
  readonly agentRoster?: HomeAgentSnapshot;
@@ -74,6 +76,7 @@ export function ApplicationHomeSurface(props: ApplicationHomeSurfaceProps): JSX.
74
76
  props.agentRoster?.phase === "live" &&
75
77
  props.agentRoster.rows.length === 0 &&
76
78
  !props.agentQuery &&
79
+ (!props.agentActivityFilter || props.agentActivityFilter === "all") &&
77
80
  applicationHomeBrandVariant(bodyWidth(), height()) === "ascii";
78
81
  const brandRows = () => (showAscii() ? APPLICATION_HOME_WORDMARK.length : 1);
79
82
  const spacious = () => height() >= 14;
@@ -241,6 +244,8 @@ export function ApplicationHomeSurface(props: ApplicationHomeSurfaceProps): JSX.
241
244
  query={props.agentQuery}
242
245
  onQueryChange={props.onAgentQueryChange}
243
246
  filterLabel={props.agentFilterLabel}
247
+ activityFilter={props.agentActivityFilter}
248
+ onSetActivityFilter={props.onSetAgentActivityFilter}
244
249
  onCycleMachine={props.onCycleAgentMachine}
245
250
  onToggleAttention={props.onToggleAgentAttention}
246
251
  theme={props.theme}
@@ -395,6 +395,16 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
395
395
  }}
396
396
  >
397
397
  <ApplicationShell
398
+ onFooterAction={(key) => {
399
+ props.machineSidebar?.onBlur?.();
400
+ if (key === "F6") props.machineSidebar?.onOpenSwitcher?.();
401
+ else if (key === "F7") props.machineSidebar?.onOpenAttention?.();
402
+ else
403
+ props.onPaletteActivate?.(
404
+ props.sidebarVisible === false ? "show-sidebar" : "hide-sidebar",
405
+ "mouse",
406
+ );
407
+ }}
398
408
  footerContext={props.surface() === "home" ? "home" : "terminals"}
399
409
  scrollback={props.surface() === "terminals" && scrollback()}
400
410
  rightChips={
@@ -164,6 +164,7 @@ export interface ShellStatusStripProps {
164
164
  tool?: string | null;
165
165
  dockMode?: string | null;
166
166
  focus?: string | null;
167
+ onFooterAction?: (key: "F6" | "F7" | "F10") => void;
167
168
  scrollback?: boolean;
168
169
  notification: string | null;
169
170
  transient?: string | null;
@@ -219,7 +220,26 @@ export function ContextStatusBar(props: ShellStatusStripProps) {
219
220
  when={showMessage()}
220
221
  fallback={
221
222
  <For each={hints()}>
222
- {(hint) => <KeyHint theme={props.theme} keys={hint.keys} label={hint.label} quiet />}
223
+ {(hint) => {
224
+ const action = () =>
225
+ hint.keys === "F6" || hint.keys === "F7" || hint.keys === "F10"
226
+ ? hint.keys
227
+ : null;
228
+ return (
229
+ <KeyHint
230
+ theme={props.theme}
231
+ keys={hint.keys}
232
+ label={hint.label}
233
+ quiet
234
+ button={action() !== null}
235
+ onPress={
236
+ action() && props.onFooterAction
237
+ ? () => props.onFooterAction?.(action()!)
238
+ : undefined
239
+ }
240
+ />
241
+ );
242
+ }}
223
243
  </For>
224
244
  }
225
245
  >
@@ -23,14 +23,18 @@ export function KeyHint(props: KeyHintProps) {
23
23
  const palette = () => componentPalette(props.theme, props, "neutral");
24
24
  const background = () =>
25
25
  props.button
26
- ? props.theme.roles.surfaces.panelRaised
26
+ ? props.selected
27
+ ? props.theme.roles.selection.selection
28
+ : props.theme.roles.surfaces.panelRaised
27
29
  : props.quiet && !props.focused && !props.hovered
28
30
  ? props.theme.roles.surfaces.panel
29
31
  : palette().background;
30
32
  const foreground = () =>
31
- props.quiet && !props.focused && !props.hovered
32
- ? props.theme.roles.text.muted
33
- : palette().foreground;
33
+ props.selected
34
+ ? props.theme.roles.selection.selectionText
35
+ : props.quiet && !props.focused && !props.hovered
36
+ ? props.theme.roles.text.muted
37
+ : palette().foreground;
34
38
  const text = () => props.presentation ?? `${props.keys}${props.label ? ` ${props.label}` : ""}`;
35
39
  const width = () => Math.max(1, Math.floor(props.width ?? terminalDisplayWidth(text()) + 2));
36
40
  const content = () => clipTerminal(props.presentation ?? ` ${text()} `, width());
@@ -11,6 +11,7 @@ import type { SemanticThemeSnapshot } from "../theme.ts";
11
11
  import type { ApplicationShellProjection } from "./application-shell.ts";
12
12
 
13
13
  export interface ApplicationShellProps {
14
+ onFooterAction?: (key: "F6" | "F7" | "F10") => void;
14
15
  scrollback?: boolean;
15
16
  footerContext?: "home" | "terminals";
16
17
  theme: SemanticThemeSnapshot;
@@ -145,6 +146,7 @@ export function ApplicationShell(props: ApplicationShellProps) {
145
146
  props.showToolStatus === false ? null : props.projection.semantic.bottomDock.mode
146
147
  }
147
148
  focus={props.focusLabel ?? applicationShellFocusLabel(props.projection)}
149
+ onFooterAction={props.onFooterAction}
148
150
  scrollback={props.scrollback}
149
151
  notification={props.projection.semantic.statusStrip.message}
150
152
  transient={props.note}