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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/bin/cli.js +1 -1
  2. package/package.json +1 -1
  3. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +90 -21
  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 +11 -9
  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/home-agent-search.jsx +66 -0
  15. package/packages/daemon/dist/tui/mirror/ui/key-hint.jsx +8 -2
  16. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +6 -0
  17. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +2 -0
  18. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +2 -2
  19. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +6 -6
  20. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +143 -27
  21. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +18 -3
  22. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +19 -1
  23. package/packages/daemon/src/tui/mirror/runtime/application-home-fleet.ts +21 -1
  24. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +24 -19
  25. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +6 -0
  26. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +17 -6
  27. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +12 -9
  28. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +27 -5
  29. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +8 -5
  30. package/packages/daemon/src/tui/mirror/runtime/application-shell-viewport.ts +2 -1
  31. package/packages/daemon/src/tui/mirror/runtime/semantic-shell-viewport-resize.ts +2 -1
  32. package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +22 -16
  33. package/packages/daemon/src/tui/mirror/shell-chrome.ts +1 -0
  34. package/packages/daemon/src/tui/mirror/ui/activity-indicator.tsx +19 -0
  35. package/packages/daemon/src/tui/mirror/ui/home-agent-search.tsx +81 -0
  36. package/packages/daemon/src/tui/mirror/ui/key-hint.tsx +11 -2
  37. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +2 -2
  38. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +8 -0
  39. 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.30",
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.30",
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,89 @@ 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}/>}>
143
+ <box width={width()} height={1} flexShrink={0} flexDirection="row">
144
+ <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
145
+ <ActivityIndicator theme={props.theme} active={props.inputActive}/>
146
+ </Show>
147
+ <text height={1} flexGrow={1} fg={props.theme.roles.text.primary}>
148
+ {clipTerminal(title(), width())}
149
+ </text>
150
+ </box>
151
+ <box height={searchRows()} flexShrink={0}/>
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)))}
161
+ </text>
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}/>}>
135
170
  <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())}
171
+ {clipTerminal(` ${padCells("Agent", columns().agent)}${padCells(props.filterLabel ? "Workspace / machine" : "Session", columns().session)} ${padCells("Status", columns().status)}`, width())}
137
172
  </text>
138
- <box height={visibleCount()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
173
+ <box height={visibleCount() * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
139
174
  <For each={keys()}>
140
175
  {(key) => {
141
176
  const row = () => byKey().get(key);
142
- return (<box height={1} width={width()} flexShrink={0} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
177
+ return (<box height={rowHeight()} flexDirection="column" backgroundColor={componentPalette(props.theme, {
178
+ selected: props.selection.selectedKey === key,
179
+ disabled: row().paneId === null || stale(row()),
180
+ }).background} width={width()} flexShrink={0} onMouseDown={(event) => {
181
+ if (!props.inputActive || event.button !== 0 || !row().paneId || stale(row()))
182
+ return;
183
+ event.preventDefault();
184
+ event.stopPropagation();
185
+ props.onSelect(key);
186
+ props.onOpen(row(), "mouse");
187
+ }} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
143
188
  <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) => {
189
+ padCells(rowHeight() > 1
190
+ ? row().sessionName
191
+ : [row().machineLabel, row().serverLabel, row().sessionName]
192
+ .filter(Boolean)
193
+ .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
194
  if (!props.inputActive || row().paneId === null || stale(row()))
148
195
  return;
149
196
  props.onSelect(key);
150
197
  props.onOpen(row(), source);
151
198
  }}/>
199
+ <Show when={rowHeight() > 1}>
200
+ <text width={width()} height={1} fg={props.selection.selectedKey === key
201
+ ? props.theme.roles.selection.selectionText
202
+ : props.theme.roles.text.muted} bg={componentPalette(props.theme, {
203
+ selected: props.selection.selectedKey === key,
204
+ disabled: row().paneId === null || stale(row()),
205
+ }).background}>
206
+ {clipTerminal(` ${padCells(row().harness, columns().agent)}${[row().machineLabel, row().serverLabel].filter(Boolean).join(" / ")}`, width())}
207
+ </text>
208
+ </Show>
152
209
  </box>);
153
210
  }}
154
211
  </For>
155
212
  </box>
156
213
  </Show>
214
+ <Show when={props.onQueryChange}>
215
+ {(onQueryChange) => (<box position="absolute" top={1} left={0} width={width()} height={1}>
216
+ <HomeAgentSearch theme={props.theme} width={width()} query={props.query ?? ""} active={props.inputActive} onChange={onQueryChange()} onEdit={(event) => {
217
+ const action = applicationPaneRenameKeyAction(event, props.query ?? "");
218
+ if (action.kind === "update")
219
+ onQueryChange()(action.value);
220
+ }} onPaste={(bytes) => onQueryChange()(applicationPaneRenamePaste(props.query ?? "", bytes))} onEditing={setEditing} onSubmit={openSelected}/>
221
+ </box>)}
222
+ </Show>
223
+ <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
224
+ {clipTerminal(coverage(), width())}
225
+ </text>
157
226
  <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
158
227
  {clipTerminal(footer(), width())}
159
228
  </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?.();
@@ -11,10 +11,12 @@ export function applicationHomeBrandVariant(_width, _height) {
11
11
  export function ApplicationHomeSurface(props) {
12
12
  const width = () => Math.max(0, Math.floor(props.width));
13
13
  const height = () => Math.max(0, Math.floor(props.height));
14
- const inset = () => (width() >= 40 ? 2 : width() >= 12 ? 1 : 0);
14
+ const inset = () => Math.max(width() >= 40 ? 2 : width() >= 12 ? 1 : 0, props.branded ? Math.floor((width() - 96) / 2) : 0);
15
15
  const bodyWidth = () => Math.max(0, width() - inset() * 2);
16
16
  const spacious = () => height() >= 14;
17
- const context = () => clipTerminal(`${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
17
+ const context = () => clipTerminal(props.branded && props.agentRoster
18
+ ? "Across your machines"
19
+ : `${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
18
20
  const summary = () => {
19
21
  if (!props.session || props.agents === undefined)
20
22
  return "Agent signals unavailable";
@@ -24,7 +26,7 @@ export function ApplicationHomeSurface(props) {
24
26
  };
25
27
  const themeLabel = () => `Theme: ${props.theme.setting}`;
26
28
  // 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;
29
+ const naturalButtonWidth = (label, shortcut) => terminalDisplayWidth(label) + (shortcut ? terminalDisplayWidth(shortcut) + 1 : 0) + 2;
28
30
  const buttonWidth = (label, shortcut) => Math.min(bodyWidth(), naturalButtonWidth(label, shortcut));
29
31
  const actionsInRow = () => bodyWidth() >=
30
32
  naturalButtonWidth("Open terminals", "F2") +
@@ -64,7 +66,7 @@ export function ApplicationHomeSurface(props) {
64
66
  const rosterHeight = () => Math.max(0, height() - reservedRows() - activityHeight());
65
67
  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
68
  <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
67
- <strong>{clipTerminal(props.branded ? "tmux-ide" : props.project, bodyWidth())}</strong>
69
+ <strong>{clipTerminal(props.branded ? "Your agents" : props.project, bodyWidth())}</strong>
68
70
  </text>
69
71
  <box height={spacious() ? 1 : 0} flexShrink={0}/>
70
72
  <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
@@ -79,7 +81,7 @@ export function ApplicationHomeSurface(props) {
79
81
  {clipTerminal(summary(), bodyWidth())}
80
82
  </text>
81
83
  </>}>
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}/>)}
84
+ {(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
85
  </Show>
84
86
  <Show when={activityHeight() > 0}>
85
87
  <box width={bodyWidth()} height={activityHeight()} flexShrink={0} flexDirection="column" overflow="hidden">
@@ -98,13 +100,13 @@ export function ApplicationHomeSurface(props) {
98
100
  </Show>
99
101
  <box height={spacious() ? 1 : 0} flexShrink={0}/>
100
102
  <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}/>
103
+ <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}/>
104
+ <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
105
  <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()}/>)}
106
+ {(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
107
  </Show>
106
108
  <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()}/>)}
109
+ {(onCycleTheme) => (<TuiButton theme={props.theme} label={themeLabel()} width={buttonWidth(themeLabel())} size="compact" variant="ghost" background={props.theme.roles.surfaces.canvas} onPress={onCycleTheme()}/>)}
108
110
  </Show>
109
111
  </box>
110
112
  </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")
@@ -0,0 +1,16 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { createEffect, createSignal, onCleanup } from "solid-js";
3
+ /** One small indicator for a visible pending region, never a timer per agent. */
4
+ export function ActivityIndicator(props) {
5
+ const [frame, setFrame] = createSignal(0);
6
+ const frames = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
7
+ createEffect(() => {
8
+ if (!props.active || props.theme.accessibility.reducedMotion)
9
+ return;
10
+ const timer = setInterval(() => setFrame((value) => (value + 1) % frames.length), 100);
11
+ onCleanup(() => clearInterval(timer));
12
+ });
13
+ return (<text width={2} height={1} fg={props.theme.roles.text.muted}>
14
+ {props.active && !props.theme.accessibility.reducedMotion ? frames[frame()] : "…"}{" "}
15
+ </text>);
16
+ }
@@ -0,0 +1,66 @@
1
+ /* @jsxImportSource @opentui/solid */
2
+ import { createSignal } from "solid-js";
3
+ import { clipTerminalEnd } from "../terminal-text.js";
4
+ import { useKeyboardRoute, usePasteRoute } from "./keyboard-router.jsx";
5
+ /** Editing is local to Home; the resident fleet owner owns query and selected identity. */
6
+ export function HomeAgentSearch(props) {
7
+ const [editing, setEditing] = createSignal(false);
8
+ const edit = (value) => {
9
+ setEditing(value);
10
+ props.onEditing(value);
11
+ };
12
+ useKeyboardRoute((event) => {
13
+ if (!props.active || event.eventType === "release")
14
+ return false;
15
+ const key = event.name.toLowerCase();
16
+ if (!editing()) {
17
+ if (key !== "/" || event.ctrl || event.meta)
18
+ return false;
19
+ edit(true);
20
+ }
21
+ else {
22
+ if (["up", "down", "pageup", "pagedown", "home", "end"].includes(key))
23
+ return false;
24
+ if (/^f\d+$/.test(key) || event.meta || (event.ctrl && key !== "u"))
25
+ return false;
26
+ if (key === "escape") {
27
+ if (props.query)
28
+ props.onChange("");
29
+ else
30
+ edit(false);
31
+ }
32
+ else if (key === "enter" || key === "return") {
33
+ if (event.eventType !== "repeat")
34
+ props.onSubmit();
35
+ }
36
+ else if (key === "tab")
37
+ edit(false);
38
+ else
39
+ props.onEdit(event);
40
+ }
41
+ event.preventDefault();
42
+ event.stopPropagation();
43
+ return true;
44
+ });
45
+ usePasteRoute((bytes) => {
46
+ if (!props.active || !editing())
47
+ return false;
48
+ props.onPaste(bytes);
49
+ return true;
50
+ });
51
+ return (<box width={props.width} height={1} flexShrink={0} paddingLeft={1} paddingRight={1} backgroundColor={props.theme.roles.surfaces.panel} onMouseDown={(event) => {
52
+ if (!props.active || event.button !== 0)
53
+ return;
54
+ event.preventDefault();
55
+ event.stopPropagation();
56
+ edit(true);
57
+ }}>
58
+ <text height={1} width={Math.max(0, props.width - 2)} fg={props.query ? props.theme.roles.text.primary : props.theme.roles.text.muted}>
59
+ {clipTerminalEnd(props.query
60
+ ? `${props.query}${editing() && props.active ? "▏" : ""}`
61
+ : editing() && props.active
62
+ ? "▏Find an agent or workspace"
63
+ : "/ Find an agent or workspace", Math.max(0, props.width - 2))}
64
+ </text>
65
+ </box>);
66
+ }