tmux-ide 2.9.0-beta.41 → 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 (28) hide show
  1. package/bin/cli.js +1 -1
  2. package/package.json +1 -1
  3. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +47 -90
  4. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +82 -109
  5. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +43 -13
  6. package/packages/daemon/dist/tui/mirror/shell-chrome.js +5 -7
  7. package/packages/daemon/dist/tui/mirror/ui/agent-row.jsx +40 -0
  8. package/packages/daemon/dist/tui/mirror/ui/home-agent-search.jsx +8 -5
  9. package/packages/daemon/dist/tui/mirror/ui/input-surface.jsx +11 -0
  10. package/packages/daemon/dist/tui/mirror/ui/navigation-row.jsx +7 -2
  11. package/packages/daemon/dist/tui/mirror/ui/section-heading.jsx +6 -0
  12. package/packages/daemon/dist/tui/mirror/ui/tabs.jsx +22 -5
  13. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +3 -3
  14. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +8 -8
  15. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +114 -178
  16. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +108 -148
  17. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +9 -0
  18. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +67 -17
  19. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +2 -0
  20. package/packages/daemon/src/tui/mirror/shell-chrome.ts +4 -5
  21. package/packages/daemon/src/tui/mirror/ui/agent-row.tsx +92 -0
  22. package/packages/daemon/src/tui/mirror/ui/home-agent-search.tsx +17 -17
  23. package/packages/daemon/src/tui/mirror/ui/input-surface.tsx +39 -0
  24. package/packages/daemon/src/tui/mirror/ui/navigation-row.tsx +8 -1
  25. package/packages/daemon/src/tui/mirror/ui/overlay-search-field.tsx +5 -12
  26. package/packages/daemon/src/tui/mirror/ui/section-heading.tsx +15 -0
  27. package/packages/daemon/src/tui/mirror/ui/tabs.tsx +30 -7
  28. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
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.41",
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: {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.41",
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": {
@@ -2,29 +2,13 @@
2
2
  import { HOME_ACTIONS, HOME_ACTIVITY_ACTIONS, } from "../workspace/application-action-descriptions.js";
3
3
  import { createEffect, createMemo, createSignal, For, Show } from "solid-js";
4
4
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
5
- import { NavigationRow } from "../ui/navigation-row.jsx";
5
+ import { AgentRow } from "../ui/agent-row.jsx";
6
6
  import { KeyHint } from "../ui/key-hint.jsx";
7
7
  import { TuiButton } from "../ui/button.jsx";
8
8
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
9
9
  import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
10
10
  import { HomeAgentSearch } from "../ui/home-agent-search.jsx";
11
- import { createAgentStatusMarker } from "../ui/agent-status-marker.js";
12
11
  import { ActivityIndicator } from "../ui/activity-indicator.jsx";
13
- import { componentPalette } from "../ui/state.js";
14
- import { homeAgentStatusLabel, } from "./application-home-agents.js";
15
- function padCells(text, width) {
16
- const clipped = clipTerminal(text, Math.max(0, width));
17
- return clipped + " ".repeat(Math.max(0, width - terminalDisplayWidth(clipped)));
18
- }
19
- function statusTone(row) {
20
- if (row.activity === "waiting" || row.activity === "failed")
21
- return "blocked";
22
- if (row.activity === "running")
23
- return "working";
24
- if (row.activity === "complete")
25
- return "done";
26
- return row.activity === "idle" ? "idle" : "unknown";
27
- }
28
12
  /** A flat projected roster; no subscriptions, navigation effects, or terminal input ingress. */
29
13
  export function HomeAgentRoster(props) {
30
14
  const [hovered, setHovered] = createSignal(null);
@@ -37,10 +21,12 @@ export function HomeAgentRoster(props) {
37
21
  const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
38
22
  props.onRetry) ||
39
23
  (props.snapshot.truncatedSessions > 0 && props.onLoadMore));
40
- const rowHeight = () => (width() >= 60 && height() >= 16 ? 2 : 1);
41
- const filterRows = () => (props.onSetActivityFilter && width() < 70 ? 2 : 1);
42
- const searchRows = () => (props.onQueryChange ? 2 : 0);
43
- 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()));
44
30
  const openSelected = (source = "keyboard") => {
45
31
  const row = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
46
32
  if (props.inputActive && visibleCount() > 0 && row?.paneId && !stale(row))
@@ -53,12 +39,6 @@ export function HomeAgentRoster(props) {
53
39
  const keys = createMemo(() => visible().map((row) => row.key), undefined, {
54
40
  equals: (a, b) => a.length === b.length && a.every((key, index) => key === b[index]),
55
41
  });
56
- const columns = () => {
57
- const status = Math.min(12, Math.max(0, width() - 4));
58
- const label = Math.max(0, width() - status - 3);
59
- const session = width() >= 44 ? Math.floor(label * 0.43) : 0;
60
- return { status, agent: label - session, session };
61
- };
62
42
  const filtered = () => Boolean(props.query || (props.activityFilter && props.activityFilter !== "all"));
63
43
  const title = () => {
64
44
  if (props.snapshot.phase === "unavailable")
@@ -143,7 +123,9 @@ export function HomeAgentRoster(props) {
143
123
  props.onMove(delta);
144
124
  return true;
145
125
  });
146
- 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) => {
147
129
  if (!props.inputActive)
148
130
  return;
149
131
  const direction = event.scroll?.direction;
@@ -154,16 +136,19 @@ export function HomeAgentRoster(props) {
154
136
  props.onMove(direction === "up" ? -1 : 1);
155
137
  }} onMouseOut={() => setHovered(null)}>
156
138
  <box height={searchRows()} flexShrink={0}/>
157
- <box width={width()} height={1} flexShrink={0} flexDirection="row">
158
- <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
159
- <ActivityIndicator theme={props.theme} active={props.inputActive}/>
160
- </Show>
161
- <text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
162
- {clipTerminal(title(), width())}
163
- </text>
164
- </box>
165
- <box width={width()} height={filterRows()} flexShrink={0} flexDirection={filterRows() > 1 ? "column" : "row"} overflow="hidden">
166
- <KeyHint theme={props.theme} keys={HOME_ACTIONS.machine.keys} 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
167
152
  ? Math.max(1, width() - 47)
168
153
  : props.onSetActivityFilter
169
154
  ? Math.min(width(), 28)
@@ -171,20 +156,21 @@ export function HomeAgentRoster(props) {
171
156
  if (props.inputActive)
172
157
  props.onCycleMachine?.();
173
158
  }}/>
174
- <box height={1} flexShrink={0} flexDirection="row" overflow="hidden" width={Math.min(width(), 47)}>
175
- <Show when={props.onSetActivityFilter} fallback={<KeyHint theme={props.theme} keys={HOME_ACTIONS.attention.keys} 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={() => {
176
161
  if (props.inputActive)
177
162
  props.onToggleAttention?.();
178
163
  }}/>}>
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={() => {
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={() => {
181
166
  if (props.inputActive)
182
167
  props.onSetActivityFilter?.(filter.value);
183
168
  }}/>)}
184
- </For>
185
- </Show>
169
+ </For>
170
+ </Show>
171
+ </box>
186
172
  </box>
187
- </box>
173
+ </Show>
188
174
  <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0} flexDirection="column" overflow="hidden">
189
175
  <Show when={props.snapshot.phase === "live" && !props.query}>
190
176
  <text height={1} width={width()} fg={props.theme.roles.text.primary}>
@@ -197,51 +183,20 @@ export function HomeAgentRoster(props) {
197
183
  </text>
198
184
  </Show>
199
185
  </box>}>
200
- <box height={1} flexShrink={0}/>
201
- <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">
202
188
  <For each={keys()}>
203
189
  {(key) => {
204
190
  const row = () => byKey().get(key);
205
- const marker = createAgentStatusMarker({
206
- theme: () => props.theme,
207
- status: () => row().activity,
208
- attention: () => row().attention,
209
- unavailable: () => stale(row()),
210
- });
211
- return (<box height={rowHeight()} flexDirection="column" backgroundColor={componentPalette(props.theme, {
212
- selected: props.selection.selectedKey === key,
213
- hovered: hovered() === key,
214
- disabled: row().paneId === null || stale(row()),
215
- }).background} width={width()} flexShrink={0} onMouseDown={(event) => {
216
- if (!props.inputActive || event.button !== 0 || !row().paneId || stale(row()))
217
- return;
218
- event.preventDefault();
219
- event.stopPropagation();
220
- props.onSelect(key);
221
- props.onOpen(row(), "mouse");
222
- }} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
223
- <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={rowHeight() > 1
224
- ? row().name
225
- : padCells(row().name, columns().agent) +
226
- padCells([row().machineLabel, row().serverLabel, row().sessionName]
227
- .filter(Boolean)
228
- .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) => {
229
195
  if (!props.inputActive || row().paneId === null || stale(row()))
230
196
  return;
231
197
  props.onSelect(key);
232
198
  props.onOpen(row(), source);
233
199
  }}/>
234
- <Show when={rowHeight() > 1}>
235
- <text width={width()} height={1} fg={props.selection.selectedKey === key
236
- ? props.theme.roles.selection.selectionText
237
- : props.theme.roles.text.muted} bg={componentPalette(props.theme, {
238
- selected: props.selection.selectedKey === key,
239
- hovered: hovered() === key,
240
- disabled: row().paneId === null || stale(row()),
241
- }).background}>
242
- {clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
243
- </text>
244
- </Show>
245
200
  </box>);
246
201
  }}
247
202
  </For>
@@ -259,14 +214,16 @@ export function HomeAgentRoster(props) {
259
214
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
260
215
  {clipTerminal(coverage(), width())}
261
216
  </text>
262
- <box width={width()} height={1} flexShrink={0} flexDirection="row" overflow="hidden">
263
- <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}>
264
- {clipTerminal(footer().replace(/ Enter open$/u, ""), width())}
265
- </text>
266
- <Show when={footer().endsWith("Enter open") && width() >= 20}>
267
- <KeyHint theme={props.theme} keys={HOME_ACTIONS.open.keys} label={HOME_ACTIONS.open.label} quiet button onPress={() => openSelected("mouse")}/>
268
- </Show>
269
- </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>
270
227
  <Show when={showRecovery()}>
271
228
  <box height={1} width={width()} flexShrink={0} flexDirection="row" gap={1}>
272
229
  <Show when={(props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
@@ -1,12 +1,10 @@
1
1
  import { CHROME_ACTIONS, SIDEBAR_ACTIONS } from "../workspace/application-action-descriptions.js";
2
- import { createAgentStatusMarker } from "../ui/agent-status-marker.js";
3
- import { fleetHostColor, summarizeFleetActivity } from "./fleet-presentation.js";
4
- import { WorkingSessions } from "../ui/working-sessions.jsx";
2
+ import { AgentRow } from "../ui/agent-row.jsx";
3
+ import { fleetHostColor } from "./fleet-presentation.js";
5
4
  /* @jsxImportSource @opentui/solid */
6
5
  import { fleetConnectionMessage, } from "@tmux-ide/daemon-client/fleet-connection-status";
7
- import { terminalAgentStatusLabel } from "./application-terminal-workspace-policy.js";
8
6
  import { For, Show, createEffect, createMemo, createSignal, } from "solid-js";
9
- import { clipTerminal, friendlySessionLabel } from "../terminal-text.js";
7
+ import { friendlySessionLabel } from "../terminal-text.js";
10
8
  import { KeyHint } from "../ui/key-hint.jsx";
11
9
  import { NavigationRow } from "../ui/navigation-row.jsx";
12
10
  import { Surface } from "../ui/surface.jsx";
@@ -24,22 +22,16 @@ export function ApplicationMachineSidebar(props) {
24
22
  : 0;
25
23
  const controlsHeight = () => (props.height >= 8 && controlGroup() ? 4 : 0);
26
24
  const searchHeight = () => (props.model.onOpenSwitcher ? 1 : 0) + (props.model.onOpenAttention ? 1 : 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;
33
25
  const machineHeight = () => Math.max(0, props.height -
34
26
  1 -
35
27
  (props.model.onAddMachine ? 1 : 0) -
36
28
  agentHeight() -
37
29
  controlsHeight() -
38
- searchHeight() -
39
- tabHeight());
30
+ searchHeight());
40
31
  const active = (row) => row.group.id === props.model.activeMachineId() &&
41
32
  (row.agent
42
33
  ? row.group.state === "ready" &&
34
+ row.group.agentsAvailable !== false &&
43
35
  !row.agent.disabled &&
44
36
  row.agent.sessionName === props.model.activeSessionName() &&
45
37
  (!props.model.activeSessionKey ||
@@ -52,44 +44,50 @@ export function ApplicationMachineSidebar(props) {
52
44
  ? row.session?.id === props.model.activeSessionKey()
53
45
  : row.session?.name === props.model.activeSessionName());
54
46
  const preferenceKey = (group) => group.environmentId ?? group.id;
55
- const rows = createMemo(() => props.model.groups().flatMap((group) => [
56
- { key: JSON.stringify([group.id]), group },
57
- ...group.sessions
58
- .filter((session) => !collapsed().has(preferenceKey(group)) ||
59
- props.model.favorites?.().includes(session.id) ||
60
- (group.id === props.model.activeMachineId() &&
61
- (props.model.activeSessionKey
62
- ? session.id === props.model.activeSessionKey()
63
- : session.name === props.model.activeSessionName())))
64
- .map((session, index, sessions) => ({
65
- serverHeading: (group.servers?.length ??
66
- new Set(group.sessions.map((row) => row.server?.serverId)).size) > 1 &&
67
- session.server &&
68
- (index === 0 || sessions[index - 1]?.server?.serverId !== session.server.serverId)
69
- ? `${session.serverLabel ?? "Server"} · ${session.server.serverId.slice(-6)}`
70
- : undefined,
71
- key: JSON.stringify([group.id, "session", session.id]),
47
+ const rows = createMemo(() => [
48
+ ...props.model.groups().flatMap((group) => (group.agents ?? []).map((agent) => ({
49
+ key: JSON.stringify([
50
+ group.id,
51
+ "agent",
52
+ agent.server?.serverId,
53
+ agent.server?.generation,
54
+ agent.id,
55
+ ]),
72
56
  group,
73
- session,
74
- })),
75
- ...(collapsed().has(preferenceKey(group))
76
- ? []
77
- : (group.servers ?? [])
78
- .filter((server) => !group.sessions.some((session) => session.server?.serverId === server.serverId))
79
- .map((server) => ({
80
- key: JSON.stringify([group.id, "server", server.serverId]),
81
- group,
82
- server,
83
- }))),
84
- ...(!collapsed().has(preferenceKey(group))
85
- ? (group.agents ?? []).map((agent, index) => ({
86
- key: JSON.stringify([group.id, "agent", agent.id]),
57
+ agent,
58
+ }))),
59
+ ...props.model.groups().flatMap((group) => [
60
+ { key: JSON.stringify([group.id]), group },
61
+ ...group.sessions
62
+ .filter((session) => !collapsed().has(preferenceKey(group)) ||
63
+ props.model.favorites?.().includes(session.id) ||
64
+ (group.id === props.model.activeMachineId() &&
65
+ (props.model.activeSessionKey
66
+ ? session.id === props.model.activeSessionKey()
67
+ : session.name === props.model.activeSessionName())))
68
+ .map((session, index, sessions) => ({
69
+ serverHeading: (group.servers?.length ??
70
+ new Set(group.sessions.map((row) => row.server?.serverId)).size) > 1 &&
71
+ session.server &&
72
+ (index === 0 || sessions[index - 1]?.server?.serverId !== session.server.serverId)
73
+ ? `${session.serverLabel ?? "Server"} · ${session.server.serverId.slice(-6)}`
74
+ : undefined,
75
+ key: JSON.stringify([group.id, "session", session.id]),
87
76
  group,
88
- agent,
89
- agentHeading: index === 0,
90
- }))
91
- : []),
92
- ]));
77
+ session,
78
+ })),
79
+ ...(collapsed().has(preferenceKey(group))
80
+ ? []
81
+ : (group.servers ?? [])
82
+ .filter((server) => !group.sessions.some((session) => session.server?.serverId === server.serverId))
83
+ .map((server) => ({
84
+ key: JSON.stringify([group.id, "server", server.serverId]),
85
+ group,
86
+ server,
87
+ }))),
88
+ ]),
89
+ ]);
90
+ const hasAgents = createMemo(() => rows().some((row) => row.agent));
93
91
  const rowsByKey = createMemo(() => new Map(rows().map((row) => [row.key, row])));
94
92
  const index = () => Math.max(0, rows().findIndex((row) => row.key === selectedKey()));
95
93
  const controlGroup = () => {
@@ -101,10 +99,6 @@ export function ApplicationMachineSidebar(props) {
101
99
  ? group
102
100
  : null;
103
101
  };
104
- const activity = (row) => summarizeFleetActivity((row.group.agents ?? []).filter((agent) => !row.session ||
105
- (agent.sessionName === row.session.name &&
106
- agent.server?.serverId === row.session.server?.serverId &&
107
- agent.server?.generation === row.session.server?.generation)), row.group.state === "ready" && row.group.agentsAvailable !== false && !row.session?.disabled);
108
102
  const connectionDetail = (group) => {
109
103
  const diagnostic = group.diagnostic;
110
104
  if (!diagnostic)
@@ -133,7 +127,6 @@ export function ApplicationMachineSidebar(props) {
133
127
  props.model.onCollapse?.(preferenceKey(group), value);
134
128
  };
135
129
  const activate = (row, source) => {
136
- setWorkingFocused(false);
137
130
  setSelectedKey(row.key);
138
131
  setLocalFocused(true);
139
132
  props.model.onFocus?.();
@@ -147,6 +140,7 @@ export function ApplicationMachineSidebar(props) {
147
140
  }
148
141
  if (row.agent) {
149
142
  if (row.group.state === "ready" &&
143
+ row.group.agentsAvailable !== false &&
150
144
  !row.agent.disabled &&
151
145
  row.agent.paneId &&
152
146
  props.model.onOpenAgent) {
@@ -165,12 +159,12 @@ export function ApplicationMachineSidebar(props) {
165
159
  else
166
160
  toggle(row.group);
167
161
  };
168
- const sectionGap = (row) => !row.session && !row.agent && !row.server && row.group.id !== props.model.groups()[0]?.id
169
- ? 1
170
- : 0;
171
- const rowHeight = (row) => (row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1) + sectionGap(row);
162
+ const sectionGap = (row) => (!row.session && !row.agent && !row.server ? 1 : 0);
163
+ const rowHeight = (row) => (row.agent ? 2 : row.serverHeading ? 2 : 1) +
164
+ sectionGap(row) +
165
+ (hasAgents() && row.key === JSON.stringify([props.model.groups()[0]?.id]) ? 1 : 0);
172
166
  const revealFocusedRow = () => {
173
- if (!focused() || (workingFocused() && tabHeight()) || !scroll)
167
+ if (!focused() || !scroll)
174
168
  return;
175
169
  const y = rows()
176
170
  .slice(0, index())
@@ -192,14 +186,16 @@ export function ApplicationMachineSidebar(props) {
192
186
  if (!focused() || event.eventType !== "press" || event.meta)
193
187
  return false;
194
188
  let key = event.name.toLowerCase();
195
- if (key === "tab" && tabHeight()) {
196
- setWorkingFocused((value) => !value);
189
+ if (key === "tab") {
190
+ const target = rows()[index()]?.agent
191
+ ? rows().find((row) => !row.agent)
192
+ : rows().find((row) => row.agent);
193
+ if (target)
194
+ setSelectedKey(target.key);
197
195
  event.preventDefault();
198
196
  event.stopPropagation();
199
197
  return true;
200
198
  }
201
- if (workingFocused() && tabHeight() && !["escape", "?", "question", "/", "a"].includes(key))
202
- return false;
203
199
  if (event.ctrl && key === "d")
204
200
  key = "halfdown";
205
201
  else if (event.ctrl && key === "u")
@@ -306,22 +302,10 @@ export function ApplicationMachineSidebar(props) {
306
302
  });
307
303
  return (<Surface id="application-machine-sidebar" theme={props.theme} variant="panel" width={props.width} height={props.height} flexShrink={0} flexDirection="column" overflow="hidden">
308
304
  <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
309
- <text fg={props.theme.roles.text.secondary}>
310
- {tabHeight() ? " Workspace" : " Machines"}
311
- </text>
305
+ <text fg={props.theme.roles.text.secondary}>{hasAgents() ? " Agents" : " Machines"}</text>
312
306
  <box flexGrow={1}/>
313
- <Show when={props.model.tabs?.().length && props.width >= 28}>
314
- <KeyHint theme={props.theme} keys={CHROME_ACTIONS.tabs.keys} label={CHROME_ACTIONS.tabs.label} quiet/>
315
- </Show>
316
307
  <KeyHint theme={props.theme} keys={SIDEBAR_ACTIONS.help.keys} label={props.width >= 20 ? SIDEBAR_ACTIONS.help.label : undefined} quiet button onPress={() => props.onHelp?.("mouse")}/>
317
308
  </box>
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>
325
309
  <scrollbox ref={(value) => {
326
310
  scroll = value;
327
311
  const resized = value.content.onSizeChange;
@@ -352,16 +336,7 @@ export function ApplicationMachineSidebar(props) {
352
336
  get serverHeading() {
353
337
  return current().serverHeading;
354
338
  },
355
- get agentHeading() {
356
- return current().agentHeading;
357
- },
358
339
  };
359
- const agentMarker = createAgentStatusMarker({
360
- theme: () => props.theme,
361
- status: () => row.agent?.activity,
362
- attention: () => Boolean(row.agent?.attention),
363
- unavailable: () => row.group.state !== "ready" || Boolean(row.agent?.disabled),
364
- });
365
340
  return (<box width={Math.max(1, props.width - 1)} height={rowHeight(row)} flexShrink={0} flexDirection="column">
366
341
  <box height={sectionGap(row)} flexShrink={0}/>
367
342
  <Show when={row.serverHeading}>
@@ -370,22 +345,18 @@ export function ApplicationMachineSidebar(props) {
370
345
  props.model.onSelectServer?.(row.group.id, row.session.server);
371
346
  }}/>
372
347
  </Show>
373
- <Show when={row.agentHeading}>
348
+ <Show when={hasAgents() && row.key === JSON.stringify([props.model.groups()[0]?.id])}>
374
349
  <text height={1} fg={props.theme.roles.text.secondary}>
375
350
  {" "}
376
- Agents
351
+ Machines
377
352
  </text>
378
353
  </Show>
379
- <NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.agent && !row.session ? fleetHostColor(row.group) : undefined} label={row.server
380
- ? ` ${row.server.label} · ${row.server.serverId.slice(-6)}`
381
- : row.agent
382
- ? row.agent.name
354
+ <Show when={row.agent} fallback={<NavigationRow theme={props.theme} id={`machine:${row.key}`} width={Math.max(1, props.width - 1)} labelColor={!row.session ? fleetHostColor(row.group) : undefined} label={row.server
355
+ ? ` ${row.server.label} · ${row.server.serverId.slice(-6)}`
383
356
  : row.session
384
357
  ? friendlySessionLabel(row.session.name)
385
358
  : row.group.label} marker={row.server
386
- ? " "
387
- : row.agent
388
- ? agentMarker()
359
+ ? " "
389
360
  : row.session
390
361
  ? props.model.favorites?.().includes(row.session.id)
391
362
  ? " ★"
@@ -393,24 +364,26 @@ export function ApplicationMachineSidebar(props) {
393
364
  : collapsed().has(preferenceKey(row.group))
394
365
  ? "▸"
395
366
  : "▾"} detail={row.server
396
- ? row.server.state === "offline"
397
- ? "offline"
398
- : "empty"
399
- : row.agent
400
- ? row.group.state !== "ready" || row.agent.disabled
401
- ? "unavailable"
402
- : terminalAgentStatusLabel(row.agent.activity).toLowerCase()
367
+ ? row.server.state === "offline"
368
+ ? "offline"
369
+ : "empty"
403
370
  : row.session
404
371
  ? row.group.state !== "ready" || row.session.disabled
405
372
  ? "unavailable"
406
- : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
407
- : row.group.agents?.length && row.group.state === "ready"
408
- ? activity(row).label
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")
410
- ? "blocked"
411
- : undefined} onActivate={(source) => activate(row, source)}/>
412
- <Show when={row.agent}>
413
- {(agent) => (<text height={1} fg={props.theme.roles.text.muted} content={clipTerminal(` ${friendlySessionLabel(agent().sessionName)}`, Math.max(1, props.width - 1))}/>)}
373
+ : `${row.session.paneCount}p`
374
+ : row.group.state === "ready"
375
+ ? undefined
376
+ : connectionDetail(row.group)} selected={Boolean(row.session && active(row))} focused={focused() && row.key === selectedKey()} onActivate={(source) => activate(row, source)}/>}>
377
+ {(agent) => (<AgentRow theme={props.theme} id={`machine:${row.key}`} name={agent().name} context={[
378
+ row.group.label,
379
+ row.group.sessions.find((session) => session.server?.serverId === agent().server?.serverId)?.serverLabel,
380
+ friendlySessionLabel(agent().sessionName),
381
+ ]
382
+ .filter(Boolean)
383
+ .join(" · ")} width={Math.max(1, props.width - 1)} activity={agent().activity} attention={agent().attention} unavailable={row.group.state !== "ready" ||
384
+ row.group.agentsAvailable === false ||
385
+ !!agent().disabled ||
386
+ !agent().paneId} selected={active(row)} focused={focused() && row.key === selectedKey()} onOpen={(source) => activate(row, source)}/>)}
414
387
  </Show>
415
388
  </box>);
416
389
  }}