tmux-ide 2.9.0-beta.29 → 2.9.0-beta.31

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 (67) 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 +99 -23
  4. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +18 -6
  5. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +9 -0
  6. package/packages/daemon/dist/tui/mirror/runtime/application-home-fleet.js +18 -1
  7. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +16 -16
  8. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +5 -0
  9. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +57 -22
  10. package/packages/daemon/dist/tui/mirror/runtime/application-shell-viewport.js +2 -2
  11. package/packages/daemon/dist/tui/mirror/runtime/semantic-shell-viewport-resize.js +2 -2
  12. package/packages/daemon/dist/tui/mirror/shell-chrome.js +2 -0
  13. package/packages/daemon/dist/tui/mirror/ui/activity-indicator.jsx +16 -0
  14. package/packages/daemon/dist/tui/mirror/ui/badge.jsx +1 -1
  15. package/packages/daemon/dist/tui/mirror/ui/detail-row.jsx +22 -0
  16. package/packages/daemon/dist/tui/mirror/ui/home-agent-search.jsx +66 -0
  17. package/packages/daemon/dist/tui/mirror/ui/home-wordmark.js +11 -0
  18. package/packages/daemon/dist/tui/mirror/ui/key-hint.jsx +15 -3
  19. package/packages/daemon/dist/tui/mirror/ui/menu.jsx +8 -8
  20. package/packages/daemon/dist/tui/mirror/ui/overlay-frame.jsx +9 -7
  21. package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +2 -2
  22. package/packages/daemon/dist/tui/mirror/ui/overlay-model.js +10 -0
  23. package/packages/daemon/dist/tui/mirror/ui/status-bar.jsx +2 -2
  24. package/packages/daemon/dist/tui/mirror/ui/surface.jsx +1 -1
  25. package/packages/daemon/dist/tui/mirror/ui/tabs.jsx +1 -3
  26. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +6 -0
  27. package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +45 -0
  28. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +2 -0
  29. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +3 -3
  30. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +6 -6
  31. package/packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog.tsx +8 -1
  32. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +5 -0
  33. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +163 -36
  34. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +18 -3
  35. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +19 -1
  36. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +21 -1
  37. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +24 -19
  38. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +6 -0
  39. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +147 -73
  40. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +20 -7
  41. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +13 -10
  42. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +111 -22
  43. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +12 -2
  44. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +11 -5
  45. package/packages/daemon/src/tui/mirror/runtime/application-shell-viewport.ts +2 -1
  46. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +2 -1
  47. package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +50 -80
  48. package/packages/daemon/src/tui/mirror/shell-chrome.ts +1 -0
  49. package/packages/daemon/src/tui/mirror/ui/activity-indicator.tsx +19 -0
  50. package/packages/daemon/src/tui/mirror/ui/badge.tsx +1 -1
  51. package/packages/daemon/src/tui/mirror/ui/detail-row.tsx +51 -0
  52. package/packages/daemon/src/tui/mirror/ui/home-agent-search.tsx +81 -0
  53. package/packages/daemon/src/tui/mirror/ui/home-wordmark.ts +12 -0
  54. package/packages/daemon/src/tui/mirror/ui/key-hint.tsx +27 -3
  55. package/packages/daemon/src/tui/mirror/ui/menu.tsx +16 -15
  56. package/packages/daemon/src/tui/mirror/ui/overlay-frame.tsx +19 -11
  57. package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +2 -2
  58. package/packages/daemon/src/tui/mirror/ui/overlay-model.ts +11 -0
  59. package/packages/daemon/src/tui/mirror/ui/status-bar.tsx +2 -2
  60. package/packages/daemon/src/tui/mirror/ui/surface.tsx +1 -1
  61. package/packages/daemon/src/tui/mirror/ui/tabs.tsx +1 -4
  62. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
  63. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +20 -20
  64. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +8 -0
  65. package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +4 -1
  66. package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +52 -0
  67. package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +1 -0
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.29",
11282
+ version: "2.9.0-beta.31",
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.29",
3
+ "version": "2.9.0-beta.31",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -4,6 +4,10 @@ import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
4
4
  import { NavigationRow } from "../ui/navigation-row.jsx";
5
5
  import { TuiButton } from "../ui/button.jsx";
6
6
  import { useKeyboardRoute } from "../ui/keyboard-router.jsx";
7
+ import { applicationPaneRenameKeyAction, applicationPaneRenamePaste, } from "./application-pane-rename-input.js";
8
+ import { HomeAgentSearch } from "../ui/home-agent-search.jsx";
9
+ import { ActivityIndicator } from "../ui/activity-indicator.jsx";
10
+ import { componentPalette } from "../ui/state.js";
7
11
  import { homeAgentStatusLabel, } from "./application-home-agents.js";
8
12
  function padCells(text, width) {
9
13
  const clipped = clipTerminal(text, Math.max(0, width));
@@ -21,6 +25,7 @@ function statusTone(row) {
21
25
  /** A flat projected roster; no subscriptions, navigation effects, or terminal input ingress. */
22
26
  export function HomeAgentRoster(props) {
23
27
  const [hovered, setHovered] = createSignal(null);
28
+ const [editing, setEditing] = createSignal(false);
24
29
  const width = () => Math.max(0, Math.floor(props.width));
25
30
  const height = () => Math.max(0, Math.floor(props.height));
26
31
  const stale = (row) => row.disabled ||
@@ -29,7 +34,14 @@ export function HomeAgentRoster(props) {
29
34
  const showRecovery = () => Boolean(((props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
30
35
  props.onRetry) ||
31
36
  (props.snapshot.truncatedSessions > 0 && props.onLoadMore));
32
- const visibleCount = () => Math.max(0, height() - 5 - (showRecovery() ? 1 : 0));
37
+ const rowHeight = () => (width() >= 60 && height() >= 16 ? 2 : 1);
38
+ const searchRows = () => (props.onQueryChange ? 2 : 0);
39
+ const visibleCount = () => Math.max(0, Math.floor((height() - 5 - searchRows() - (showRecovery() ? 1 : 0)) / rowHeight()));
40
+ const openSelected = () => {
41
+ const row = props.snapshot.rows.find((row) => row.key === props.selection.selectedKey);
42
+ if (props.inputActive && visibleCount() > 0 && row?.paneId && !stale(row))
43
+ props.onOpen(row, "keyboard");
44
+ };
33
45
  createEffect(() => props.onViewport(visibleCount()));
34
46
  const offset = () => Math.max(0, Math.min(props.selection.scrollOffset, Math.max(0, props.snapshot.rows.length - visibleCount())));
35
47
  const visible = createMemo(() => props.snapshot.rows.slice(offset(), offset() + visibleCount()));
@@ -48,6 +60,8 @@ export function HomeAgentRoster(props) {
48
60
  return "Agent overview unavailable";
49
61
  if (props.snapshot.phase === "loading" && props.snapshot.rows.length === 0)
50
62
  return "Loading agent overview…";
63
+ if (props.query && props.snapshot.rows.length === 0)
64
+ return "No matching agents";
51
65
  if (props.snapshot.phase === "live" && props.snapshot.rows.length === 0)
52
66
  return "No agents reported";
53
67
  const count = props.snapshot.rows.length;
@@ -59,6 +73,8 @@ export function HomeAgentRoster(props) {
59
73
  const footer = () => {
60
74
  if (props.snapshot.note)
61
75
  return props.snapshot.note;
76
+ if (props.query && props.snapshot.rows.length === 0)
77
+ return "Search covers loaded observations · Esc clears search";
62
78
  if (props.snapshot.rows.length === 0) {
63
79
  if (props.snapshot.phase === "live")
64
80
  return "Observed sessions have no agent entries. F2 opens terminals.";
@@ -79,17 +95,19 @@ export function HomeAgentRoster(props) {
79
95
  if (!props.inputActive || event.eventType !== "press" || event.ctrl || event.meta)
80
96
  return false;
81
97
  const key = event.name.toLowerCase();
82
- const filter = key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined;
98
+ const filter = !editing() &&
99
+ (key === "f" ? props.onCycleMachine : key === "a" ? props.onToggleAttention : undefined);
83
100
  if (filter) {
84
101
  event.preventDefault();
85
102
  event.stopPropagation();
86
103
  filter();
87
104
  return true;
88
105
  }
89
- const retry = key === "r" &&
106
+ const retry = !editing() &&
107
+ key === "r" &&
90
108
  (props.snapshot.phase === "unavailable" || props.snapshot.unavailableSessions > 0) &&
91
109
  props.onRetry;
92
- const more = key === "m" && props.snapshot.truncatedSessions > 0 && props.onLoadMore;
110
+ const more = !editing() && key === "m" && props.snapshot.truncatedSessions > 0 && props.onLoadMore;
93
111
  const recovery = retry || more;
94
112
  if (recovery) {
95
113
  event.preventDefault();
@@ -112,7 +130,7 @@ export function HomeAgentRoster(props) {
112
130
  props.onMove(delta);
113
131
  return true;
114
132
  });
115
- return (<box width={width()} height={height()} flexShrink={0} flexDirection="column" overflow="hidden" onMouseScroll={(event) => {
133
+ return (<box position="relative" width={width()} height={height()} flexShrink={0} flexDirection="column" overflow="hidden" onMouseScroll={(event) => {
116
134
  if (!props.inputActive)
117
135
  return;
118
136
  const direction = event.scroll?.direction;
@@ -122,38 +140,96 @@ export function HomeAgentRoster(props) {
122
140
  event.stopPropagation();
123
141
  props.onMove(direction === "up" ? -1 : 1);
124
142
  }} onMouseOut={() => setHovered(null)}>
125
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
126
- {clipTerminal(title(), width())}
127
- </text>
128
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
129
- {clipTerminal(coverage(), width())}
130
- </text>
131
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
132
- {clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine · a attention` : "", width())}
133
- </text>
134
- <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() + 1)} flexShrink={0}/>}>
135
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
136
- {clipTerminal(` ${padCells("AGENT", columns().agent)}${padCells(props.filterLabel ? "MACHINE / SERVER / SESSION" : "SESSION", columns().session)} ${padCells("STATUS", columns().status)}`, width())}
143
+ <box height={searchRows()} flexShrink={0}/>
144
+ <box width={width()} height={1} flexShrink={0} flexDirection="row">
145
+ <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
146
+ <ActivityIndicator theme={props.theme} active={props.inputActive}/>
147
+ </Show>
148
+ <text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
149
+ {clipTerminal(title(), width())}
150
+ </text>
151
+ </box>
152
+ <box width={width()} height={1} flexShrink={0} flexDirection="row">
153
+ <text width={Math.max(0, width() - (props.onToggleAttention ? 16 : 0))} height={1} fg={props.theme.roles.text.secondary} onMouseDown={(event) => {
154
+ if (!props.inputActive || event.button !== 0)
155
+ return;
156
+ event.preventDefault();
157
+ event.stopPropagation();
158
+ props.onCycleMachine?.();
159
+ }}>
160
+ {clipTerminal(props.filterLabel ? `${props.filterLabel} · f machine` : "", Math.max(0, width() - (props.onToggleAttention ? 16 : 0)))}
137
161
  </text>
138
- <box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
162
+ <Show when={props.onToggleAttention}>
163
+ <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
+ if (props.inputActive)
165
+ props.onToggleAttention?.();
166
+ }}/>
167
+ </Show>
168
+ </box>
169
+ <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0} flexDirection="column" overflow="hidden">
170
+ <Show when={props.snapshot.phase === "live" && !props.query}>
171
+ <text height={1} width={width()} fg={props.theme.roles.text.primary}>
172
+ {clipTerminal("Your next session starts here", width())}
173
+ </text>
174
+ <text height={1} width={width()} fg={props.theme.roles.text.muted}>
175
+ {clipTerminal("Open terminals, or use Commands to connect a machine.", width())}
176
+ </text>
177
+ </Show>
178
+ </box>}>
179
+ <box height={1} flexShrink={0}/>
180
+ <box height={visibleCount() * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
139
181
  <For each={keys()}>
140
182
  {(key) => {
141
183
  const row = () => byKey().get(key);
142
- return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
143
- <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
144
- padCells([row().machineLabel, row().serverLabel, row().sessionName]
145
- .filter(Boolean)
146
- .join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${stale(row()) ? "last seen" : homeAgentStatusLabel(row().activity)}`, columns().status)} detailAlign="end" marker={props.selection.selectedKey === key ? "›" : " "} selected={props.selection.selectedKey === key} focused={props.inputActive && props.selection.selectedKey === key} hovered={hovered() === key} attention={!stale(row()) && row().attention} status={stale(row()) ? "unknown" : statusTone(row())} disabled={row().paneId === null || stale(row())} onActivate={(source) => {
184
+ return (<box height={rowHeight()} flexDirection="column" backgroundColor={componentPalette(props.theme, {
185
+ selected: props.selection.selectedKey === key,
186
+ disabled: row().paneId === null || stale(row()),
187
+ }).background} width={width()} flexShrink={0} onMouseDown={(event) => {
188
+ if (!props.inputActive || event.button !== 0 || !row().paneId || stale(row()))
189
+ return;
190
+ event.preventDefault();
191
+ event.stopPropagation();
192
+ props.onSelect(key);
193
+ props.onOpen(row(), "mouse");
194
+ }} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
195
+ <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={rowHeight() > 1
196
+ ? row().name
197
+ : padCells(row().name, columns().agent) +
198
+ padCells([row().machineLabel, row().serverLabel, row().sessionName]
199
+ .filter(Boolean)
200
+ .join(" / "), columns().session)} detail={padCells(`${!stale(row()) && row().attention ? "! " : ""}${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) => {
147
201
  if (!props.inputActive || row().paneId === null || stale(row()))
148
202
  return;
149
203
  props.onSelect(key);
150
204
  props.onOpen(row(), source);
151
205
  }}/>
206
+ <Show when={rowHeight() > 1}>
207
+ <text width={width()} height={1} fg={props.selection.selectedKey === key
208
+ ? props.theme.roles.selection.selectionText
209
+ : props.theme.roles.text.muted} bg={componentPalette(props.theme, {
210
+ selected: props.selection.selectedKey === key,
211
+ disabled: row().paneId === null || stale(row()),
212
+ }).background}>
213
+ {clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
214
+ </text>
215
+ </Show>
152
216
  </box>);
153
217
  }}
154
218
  </For>
155
219
  </box>
156
220
  </Show>
221
+ <Show when={props.onQueryChange}>
222
+ {(onQueryChange) => (<box position="absolute" top={0} left={0} width={width()} height={1}>
223
+ <HomeAgentSearch theme={props.theme} width={width()} query={props.query ?? ""} active={props.inputActive} onChange={onQueryChange()} onEdit={(event) => {
224
+ const action = applicationPaneRenameKeyAction(event, props.query ?? "");
225
+ if (action.kind === "update")
226
+ onQueryChange()(action.value);
227
+ }} onPaste={(bytes) => onQueryChange()(applicationPaneRenamePaste(props.query ?? "", bytes))} onEditing={setEditing} onSubmit={openSelected}/>
228
+ </box>)}
229
+ </Show>
230
+ <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
231
+ {clipTerminal(coverage(), width())}
232
+ </text>
157
233
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
158
234
  {clipTerminal(footer(), width())}
159
235
  </text>
@@ -189,12 +189,21 @@ export function createApplicationHomeNavigationOwner(options) {
189
189
  observer: options.observer,
190
190
  });
191
191
  const paletteCommands = createApplicationPaletteCommandOwner({
192
- commands: () => options.fleetCommands
193
- ? [
194
- ...applicationPaletteCommands(options.shell().semantic).filter((c) => typeof c === "string"),
195
- ...options.fleetCommands(),
196
- ]
197
- : applicationPaletteCommands(options.shell().semantic, options.catalog.sessionNames()),
192
+ commands: () => [
193
+ ...(options.fleetCommands
194
+ ? [
195
+ ...applicationPaletteCommands(options.shell().semantic).filter((c) => typeof c === "string"),
196
+ ...options.fleetCommands(),
197
+ ]
198
+ : applicationPaletteCommands(options.shell().semantic, options.catalog.sessionNames())),
199
+ ...(options.toggleSidebar
200
+ ? [
201
+ options.sidebarVisible?.() === false
202
+ ? "show-sidebar"
203
+ : "hide-sidebar",
204
+ ]
205
+ : []),
206
+ ],
198
207
  isOpen: () => Boolean(options.shell().semantic?.focus.palette.open ?? options.shell().localPaletteOpen),
199
208
  targetKey: () => `${applicationGenerationNavigationKey(options.sessionOwner()?.snapshot() ?? null)}:${options.focusedPane()}`,
200
209
  disabledReason: (command) => {
@@ -204,6 +213,8 @@ export function createApplicationHomeNavigationOwner(options) {
204
213
  command === "home" ||
205
214
  command === "terminals" ||
206
215
  command === "appearance" ||
216
+ command === "hide-sidebar" ||
217
+ command === "show-sidebar" ||
207
218
  command === "shortcuts" ||
208
219
  command === "whats-new" ||
209
220
  command === "switch-session")
@@ -218,6 +229,7 @@ export function createApplicationHomeNavigationOwner(options) {
218
229
  setSurface: options.setSurface,
219
230
  setNote: options.setNote,
220
231
  openAppearance: options.openAppearance,
232
+ toggleSidebar: options.toggleSidebar,
221
233
  openSessions: options.openSessions,
222
234
  zoomPane: options.zoomPane,
223
235
  newWindow: options.interaction.newWindow,
@@ -1,9 +1,18 @@
1
+ import { createEffect } from "solid-js";
1
2
  import { createApplicationHomeFleetOwner } from "./application-home-fleet.js";
2
3
  import { createApplicationHomeNavigationOwner } from "./application-home-agents-owner.js";
3
4
  import { createApplicationGuidedTourIntegration } from "./application-guided-tour-integration.jsx";
5
+ /** A late modal dismissal must not restore focus into Home's hidden sidebar. */
6
+ export function createHomeSidebarFocusGuard(surface, focused, blur, sidebarVisible = () => true) {
7
+ createEffect(() => {
8
+ if ((surface() === "home" || !sidebarVisible()) && focused())
9
+ blur();
10
+ });
11
+ }
4
12
  /** Compose Home's fleet, navigation and learning experience without additional transports. */
5
13
  export function createApplicationHomeExperience(options) {
6
14
  const { machines, appearance } = options;
15
+ createHomeSidebarFocusGuard(options.activeSurface, machines.focused, () => machines.sidebar.onBlur?.(), options.sidebarVisible);
7
16
  const paletteOpen = () => options.shell().semantic?.focus.palette.open ?? options.shell().localPaletteOpen;
8
17
  const fleetHome = createApplicationHomeFleetOwner({
9
18
  catalog: machines.catalog,
@@ -41,6 +41,17 @@ export function projectHomeFleet(catalog, groups, filter) {
41
41
  rows.sort((a, b) => a.key.localeCompare(b.key));
42
42
  const missing = machines.filter((machine) => machine.state !== "ready" ||
43
43
  !groups.find((group) => group.machineId === machine.id)?.available);
44
+ const query = filter.query?.trim().toLocaleLowerCase() ?? "";
45
+ const matching = query
46
+ ? rows.filter((row) => [
47
+ row.name,
48
+ row.harness,
49
+ row.projectName,
50
+ row.sessionName,
51
+ row.machineLabel,
52
+ row.serverLabel,
53
+ ].some((value) => value?.toLocaleLowerCase().includes(query)))
54
+ : rows;
44
55
  return {
45
56
  phase: missing.length
46
57
  ? rows.length || observedSessions
@@ -49,7 +60,7 @@ export function projectHomeFleet(catalog, groups, filter) {
49
60
  ? "loading"
50
61
  : "unavailable"
51
62
  : "live",
52
- rows,
63
+ rows: matching,
53
64
  observedSessions,
54
65
  totalSessions,
55
66
  loadingSessions,
@@ -92,6 +103,12 @@ export function createApplicationHomeFleetOwner(options) {
92
103
  selection.dispose();
93
104
  });
94
105
  const presentation = {
106
+ get agentQuery() {
107
+ return filter().query ?? "";
108
+ },
109
+ onAgentQueryChange(query) {
110
+ setFilter((value) => ({ ...value, query }));
111
+ },
95
112
  get agentRoster() {
96
113
  return snapshot();
97
114
  },
@@ -165,10 +165,13 @@ export function ApplicationMachineSidebar(props) {
165
165
  else
166
166
  toggle(row.group);
167
167
  };
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);
168
172
  const revealFocusedRow = () => {
169
173
  if (!focused() || !scroll)
170
174
  return;
171
- const rowHeight = (row) => row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1;
172
175
  const y = rows()
173
176
  .slice(0, index())
174
177
  .reduce((sum, row) => sum + rowHeight(row), 0);
@@ -342,7 +345,8 @@ export function ApplicationMachineSidebar(props) {
342
345
  return current().agentHeading;
343
346
  },
344
347
  };
345
- return (<box width={Math.max(1, props.width - 1)} height={row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1} flexShrink={0} flexDirection="column">
348
+ return (<box width={Math.max(1, props.width - 1)} height={rowHeight(row)} flexShrink={0} flexDirection="column">
349
+ <box height={sectionGap(row)} flexShrink={0}/>
346
350
  <Show when={row.serverHeading}>
347
351
  <NavigationRow theme={props.theme} width={Math.max(1, props.width - 1)} id={`server:${row.group.id}:${row.session?.server?.serverId}`} label={row.serverHeading ?? "Server"} marker=" ▾" onActivate={() => {
348
352
  if (row.session?.server)
@@ -364,19 +368,13 @@ export function ApplicationMachineSidebar(props) {
364
368
  : row.group.label} marker={row.server
365
369
  ? " "
366
370
  : row.agent
367
- ? active(row)
368
- ? row.agent.attention
369
- ? "›!"
370
- : "›"
371
- : row.agent.attention
372
- ? "!"
373
- : "•"
371
+ ? row.agent.attention
372
+ ? "!"
373
+ : " "
374
374
  : row.session
375
375
  ? props.model.favorites?.().includes(row.session.id)
376
376
  ? " ★"
377
- : active(row)
378
- ? " ›"
379
- : " "
377
+ : " "
380
378
  : collapsed().has(preferenceKey(row.group))
381
379
  ? "▸"
382
380
  : "▾"} detail={row.server
@@ -386,14 +384,16 @@ export function ApplicationMachineSidebar(props) {
386
384
  : row.agent
387
385
  ? row.group.state !== "ready" || row.agent.disabled
388
386
  ? "unavailable"
389
- : `[${terminalAgentStatusLabel(row.agent.activity)}]`
387
+ : terminalAgentStatusLabel(row.agent.activity).toLowerCase()
390
388
  : row.session
391
389
  ? row.group.state !== "ready" || row.session.disabled
392
390
  ? "unavailable"
393
391
  : `${row.session.paneCount}p${row.group.agents?.length ? ` ${activity(row).label}` : ""}`
394
392
  : row.group.agents?.length && row.group.state === "ready"
395
393
  ? activity(row).label
396
- : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`} selected={Boolean((row.session || row.agent) && active(row))} focused={Boolean(focused() && row.key === selectedKey())} attention={row.agent?.attention ?? activity(row).kind === "attention"} onActivate={(source) => activate(row, source)}/>
394
+ : `${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")
395
+ ? "blocked"
396
+ : undefined} onActivate={(source) => activate(row, source)}/>
397
397
  <Show when={row.agent}>
398
398
  {(agent) => (<text height={1} fg={props.theme.roles.text.muted} content={clipTerminal(` ${friendlySessionLabel(agent().sessionName)}`, Math.max(1, props.width - 1))}/>)}
399
399
  </Show>
@@ -412,7 +412,7 @@ export function ApplicationMachineSidebar(props) {
412
412
  </text>
413
413
  </Show>
414
414
  <Show when={props.model.onOpenSwitcher}>
415
- <NavigationRow theme={props.theme} id="machine:switcher" label="Search fleet (F6)" marker="/" width={props.width} onActivate={() => props.model.onOpenSwitcher?.()}/>
415
+ <NavigationRow theme={props.theme} id="machine:switcher" label="Sessions" detail="F6" marker=" " width={props.width} onActivate={() => props.model.onOpenSwitcher?.()}/>
416
416
  </Show>
417
417
  <Show when={props.model.onOpenAttention}>
418
418
  <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?.()}/>
@@ -429,7 +429,7 @@ export function ApplicationMachineSidebar(props) {
429
429
  </>)}
430
430
  </Show>
431
431
  <For each={props.model.onAddMachine ? [props.model.onAddMachine] : []}>
432
- {(add) => (<NavigationRow theme={props.theme} id="machine:add" label="Add machine (A)" marker="+" width={props.width} onActivate={add}/>)}
432
+ {(add) => (<NavigationRow theme={props.theme} id="machine:add" label="Add machine" detail="A" marker=" " width={props.width} onActivate={add}/>)}
433
433
  </For>
434
434
  </Surface>);
435
435
  }
@@ -41,6 +41,11 @@ export function createApplicationPaletteCommandOwner(options) {
41
41
  options.openSessions();
42
42
  return;
43
43
  }
44
+ if (command === "hide-sidebar" || command === "show-sidebar") {
45
+ setOpen(false, source);
46
+ options.toggleSidebar?.();
47
+ return;
48
+ }
44
49
  if (command === "appearance") {
45
50
  setOpen(false, source);
46
51
  options.openAppearance?.();
@@ -1,20 +1,30 @@
1
1
  import { interactionReceiptTargetLabel } from "@tmux-ide/core";
2
2
  import { For, Show } from "solid-js";
3
3
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
4
+ import { DetailRow } from "../ui/detail-row.jsx";
4
5
  import { TuiButton } from "../ui/button.jsx";
5
6
  import { HomeAgentRoster } from "./application-home-agent-roster.jsx";
6
- /** Home reserves its cells for workspace information at every terminal size. */
7
- export function applicationHomeBrandVariant(_width, _height) {
8
- return "wordmark";
7
+ import { APPLICATION_HOME_WORDMARK, APPLICATION_HOME_WORDMARK_WIDTH } from "../ui/home-wordmark.js";
8
+ /** Keep the marketing wordmark intact, falling back when the agent list needs the room. */
9
+ export function applicationHomeBrandVariant(width, height) {
10
+ return width >= APPLICATION_HOME_WORDMARK_WIDTH && height >= 28 ? "ascii" : "wordmark";
9
11
  }
10
12
  /** Presentation only: session data, commands, and keyboard admission stay with the shell. */
11
13
  export function ApplicationHomeSurface(props) {
12
14
  const width = () => Math.max(0, Math.floor(props.width));
13
15
  const height = () => Math.max(0, Math.floor(props.height));
14
- const inset = () => (width() >= 40 ? 2 : width() >= 12 ? 1 : 0);
16
+ const inset = () => Math.max(width() >= 40 ? 2 : width() >= 12 ? 1 : 0, props.branded ? Math.floor((width() - 96) / 2) : 0);
15
17
  const bodyWidth = () => Math.max(0, width() - inset() * 2);
18
+ const showAscii = () => props.branded &&
19
+ props.agentRoster?.phase === "live" &&
20
+ props.agentRoster.rows.length === 0 &&
21
+ !props.agentQuery &&
22
+ applicationHomeBrandVariant(bodyWidth(), height()) === "ascii";
23
+ const brandRows = () => (showAscii() ? APPLICATION_HOME_WORDMARK.length : 1);
16
24
  const spacious = () => height() >= 14;
17
- const context = () => clipTerminal(`${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
25
+ const context = () => clipTerminal(props.branded && props.agentRoster
26
+ ? "Your agents, across your machines"
27
+ : `${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
18
28
  const summary = () => {
19
29
  if (!props.session || props.agents === undefined)
20
30
  return "Agent signals unavailable";
@@ -24,7 +34,7 @@ export function ApplicationHomeSurface(props) {
24
34
  };
25
35
  const themeLabel = () => `Theme: ${props.theme.setting}`;
26
36
  // Use the existing TuiButton cell budget for both its label and hit target.
27
- const naturalButtonWidth = (label, shortcut) => terminalDisplayWidth(label) + (shortcut ? terminalDisplayWidth(shortcut) + 1 : 0) + 4;
37
+ const naturalButtonWidth = (label, shortcut) => terminalDisplayWidth(label) + (shortcut ? terminalDisplayWidth(shortcut) + 1 : 0) + 2;
28
38
  const buttonWidth = (label, shortcut) => Math.min(bodyWidth(), naturalButtonWidth(label, shortcut));
29
39
  const actionsInRow = () => bodyWidth() >=
30
40
  naturalButtonWidth("Open terminals", "F2") +
@@ -32,14 +42,31 @@ export function ApplicationHomeSurface(props) {
32
42
  (props.onCycleTheme ? naturalButtonWidth(themeLabel()) + 2 : 0) +
33
43
  (props.onOpenTutorial ? naturalButtonWidth(props.tutorialLabel ?? "Learn tmux-ide") + 2 : 0) +
34
44
  2;
35
- const reservedRows = () => (spacious() ? 4 : 2) +
45
+ const reservedRows = () => brandRows() -
46
+ 1 +
47
+ (spacious() ? 4 : 2) +
36
48
  (actionsInRow() ? 1 : 2 + (props.onCycleTheme ? 1 : 0) + (props.onOpenTutorial ? 1 : 0)) +
37
49
  (spacious() ? 1 : 0) +
38
50
  (props.note ? (spacious() ? 2 : 1) : 0);
39
- const recentActivity = () => (props.recentPaneActivity ?? []).slice(0, spacious()
40
- ? Math.max(0, Math.min(3, height() - reservedRows() - (props.agentRoster ? 4 : 2) - 2))
51
+ const activityRows = () => (height() >= 24 && bodyWidth() >= 48 ? 2 : 1);
52
+ const selectedAgent = () => props.agentRoster?.rows.find((row) => row.key === props.agentSelection?.selectedKey);
53
+ // Receipts lack a fleet-wide machine identity. Never attribute a same-named
54
+ // workspace/pane collision to the selected agent.
55
+ const selectedActivity = () => {
56
+ const selected = selectedAgent();
57
+ if (!selected?.paneId || selected.daemonInstanceId !== props.activityDaemonId)
58
+ return [];
59
+ const matches = props.agentRoster?.rows.filter((row) => row.sessionName === selected.sessionName && row.paneId === selected.paneId);
60
+ if (matches?.length !== 1)
61
+ return [];
62
+ return (props.recentPaneActivity ?? []).filter((receipt) => receipt.workspaceName === selected.sessionName &&
63
+ receipt.target.kind === "pane" &&
64
+ receipt.target.semanticPaneId === selected.paneId);
65
+ };
66
+ const recentActivity = () => selectedActivity().slice(0, spacious()
67
+ ? Math.max(0, Math.min(1, Math.floor((height() - reservedRows() - (props.agentRoster ? 12 : 2) - 2) / activityRows())))
41
68
  : 0);
42
- const activityHeight = () => (recentActivity().length > 0 ? recentActivity().length + 2 : 0);
69
+ const activityHeight = () => recentActivity().length > 0 ? recentActivity().length * activityRows() + 2 : 0;
43
70
  const paneLabel = (paneId) => {
44
71
  const matches = props.agentRoster?.rows.filter((row) => row.paneId === paneId) ?? [];
45
72
  return matches.length === 1 ? matches[0].name : paneId;
@@ -63,9 +90,19 @@ export function ApplicationHomeSurface(props) {
63
90
  : "timed out";
64
91
  const rosterHeight = () => Math.max(0, height() - reservedRows() - activityHeight());
65
92
  return (<box id="application-home" width={width()} height={height()} paddingLeft={inset()} paddingRight={inset()} paddingTop={spacious() ? 1 : 0} flexDirection="column" alignItems="flex-start" backgroundColor={props.theme.roles.surfaces.canvas} overflow="hidden">
66
- <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
67
- <strong>{clipTerminal(props.branded ? "tmux-ide" : props.project, bodyWidth())}</strong>
68
- </text>
93
+ <box height={brandRows()} width={bodyWidth()} flexShrink={0} flexDirection="column">
94
+ <Show when={showAscii()} fallback={<text width={bodyWidth()} height={1} fg={props.theme.roles.text.primary}>
95
+ <strong>
96
+ {clipTerminal(props.branded ? "tmux-ide" : props.project, bodyWidth())}
97
+ </strong>
98
+ </text>}>
99
+ <For each={APPLICATION_HOME_WORDMARK}>
100
+ {(line) => (<text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
101
+ {" ".repeat(Math.max(0, Math.floor((bodyWidth() - APPLICATION_HOME_WORDMARK_WIDTH) / 2))) + line}
102
+ </text>)}
103
+ </For>
104
+ </Show>
105
+ </box>
69
106
  <box height={spacious() ? 1 : 0} flexShrink={0}/>
70
107
  <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
71
108
  {context()}
@@ -79,17 +116,15 @@ export function ApplicationHomeSurface(props) {
79
116
  {clipTerminal(summary(), bodyWidth())}
80
117
  </text>
81
118
  </>}>
82
- {(snapshot) => (<HomeAgentRoster 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}/>)}
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}/>)}
83
120
  </Show>
84
121
  <Show when={activityHeight() > 0}>
85
122
  <box width={bodyWidth()} height={activityHeight()} flexShrink={0} flexDirection="column" overflow="hidden">
86
123
  <text height={1} width={bodyWidth()} fg={props.theme.roles.text.secondary}>
87
- {clipTerminal("Recent pane activity", bodyWidth())}
124
+ {clipTerminal(`${selectedAgent()?.name ?? "Agent"} · latest activity`, bodyWidth())}
88
125
  </text>
89
126
  <For each={recentActivity()}>
90
- {(receipt) => (<text height={1} width={bodyWidth()} fg={props.theme.roles.text.primary}>
91
- {clipTerminal(`${activityTime(receipt)} · ${interactionReceiptTargetLabel(receipt, paneLabel)} · ${activityPhase(receipt)}`, bodyWidth())}
92
- </text>)}
127
+ {(receipt) => (<DetailRow theme={props.theme} width={bodyWidth()} label={interactionReceiptTargetLabel(receipt, paneLabel)} detail={activityPhase(receipt)} description={activityRows() === 2 ? activityTime(receipt) : undefined} attention={receipt.phase === "rejected" || receipt.phase === "timed-out"}/>)}
93
128
  </For>
94
129
  <text height={1} width={bodyWidth()} fg={props.theme.roles.text.muted}>
95
130
  {clipTerminal("Activity reported through tmux-ide", bodyWidth())}
@@ -98,13 +133,13 @@ export function ApplicationHomeSurface(props) {
98
133
  </Show>
99
134
  <box height={spacious() ? 1 : 0} flexShrink={0}/>
100
135
  <box width={bodyWidth()} flexShrink={0} flexDirection={actionsInRow() ? "row" : "column"} alignItems="flex-start" gap={actionsInRow() ? 2 : 0}>
101
- <TuiButton theme={props.theme} label="Open terminals" shortcut="F2" width={buttonWidth("Open terminals", "F2")} variant="primary" onPress={props.onOpenTerminals}/>
102
- <TuiButton theme={props.theme} label="Commands" shortcut="F5" width={buttonWidth("Commands", "F5")} onPress={props.onOpenCommands}/>
136
+ <TuiButton theme={props.theme} label="Open terminals" shortcut="F2" width={buttonWidth("Open terminals", "F2")} size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} onPress={props.onOpenTerminals}/>
137
+ <TuiButton theme={props.theme} label="Commands" size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} shortcut="F5" width={buttonWidth("Commands", "F5")} onPress={props.onOpenCommands}/>
103
138
  <Show when={props.onOpenTutorial}>
104
- {(open) => (<TuiButton theme={props.theme} label={props.tutorialLabel ?? "Learn tmux-ide"} width={buttonWidth(props.tutorialLabel ?? "Learn tmux-ide")} onPress={open()}/>)}
139
+ {(open) => (<TuiButton theme={props.theme} label={props.tutorialLabel ?? "Learn tmux-ide"} size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} width={buttonWidth(props.tutorialLabel ?? "Learn tmux-ide")} onPress={open()}/>)}
105
140
  </Show>
106
141
  <Show when={props.onCycleTheme}>
107
- {(onCycleTheme) => (<TuiButton theme={props.theme} label={themeLabel()} width={buttonWidth(themeLabel())} variant="ghost" background={props.theme.roles.surfaces.canvas} onPress={onCycleTheme()}/>)}
142
+ {(onCycleTheme) => (<TuiButton theme={props.theme} label={themeLabel()} width={buttonWidth(themeLabel())} size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} onPress={onCycleTheme()}/>)}
108
143
  </Show>
109
144
  </box>
110
145
  </Show>
@@ -1,12 +1,12 @@
1
1
  import { shellChromeLayout } from "../shell-chrome.js";
2
2
  /** The one physical terminal viewport after production shell chrome. */
3
- export function applicationShellViewport(dimensions, hasSemanticShell) {
3
+ export function applicationShellViewport(dimensions, hasSemanticShell, sidebarVisible = true) {
4
4
  if (!hasSemanticShell)
5
5
  return {
6
6
  width: Math.max(1, dimensions.width),
7
7
  height: Math.max(2, dimensions.height - 2),
8
8
  };
9
- const shell = shellChromeLayout(dimensions.width, dimensions.height, 28);
9
+ const shell = shellChromeLayout(dimensions.width, dimensions.height, sidebarVisible ? 28 : 0);
10
10
  return {
11
11
  width: Math.max(1, shell.main.width),
12
12
  height: Math.max(2, shell.main.height - shell.status.height - 1),
@@ -3,7 +3,7 @@ import { applicationShellViewport } from "./application-shell-viewport.js";
3
3
  * Owns the one semantic-shell viewport resize for the current generation.
4
4
  * Provisional local chrome is intentionally not terminal geometry authority.
5
5
  */
6
- export function createSemanticShellViewportResizeOwner(getLayout = () => ({ current: { paneBorderStatus: "top" } })) {
6
+ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ current: { paneBorderStatus: "top" } }), sidebarVisible = () => true) {
7
7
  let disposed = false;
8
8
  let applied = null;
9
9
  let pending = null;
@@ -118,7 +118,7 @@ export function createSemanticShellViewportResizeOwner(getLayout = () => ({ curr
118
118
  retry?.();
119
119
  }) ?? null;
120
120
  }
121
- const viewport = applicationShellViewport(dimensions, true);
121
+ const viewport = applicationShellViewport(dimensions, true, sidebarVisible());
122
122
  const lane = generation.fastLane;
123
123
  const target = Object.freeze({
124
124
  lane,
@@ -15,6 +15,8 @@ export function shellOverlayWidth(terminalWidth, variant, kind) {
15
15
  return Math.min(safe, Math.max(1, Math.min(preferred, safe - margin)));
16
16
  }
17
17
  export function shellSidebarWidth(terminalWidth, preferredWidth, variant = shellChromeVariant(terminalWidth, 24)) {
18
+ if (preferredWidth === 0)
19
+ return 0;
18
20
  const safe = Math.max(0, Math.floor(terminalWidth));
19
21
  const preferred = Math.max(16, Math.min(48, Math.floor(preferredWidth)));
20
22
  if (variant === "compact")