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

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 (45) hide show
  1. package/bin/cli.js +3 -4
  2. package/package.json +1 -1
  3. package/packages/daemon/dist/command-center/tmux-servers.js +3 -1
  4. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +19 -12
  5. package/packages/daemon/dist/tui/mirror/runtime/application-shell-home.jsx +48 -15
  6. package/packages/daemon/dist/tui/mirror/ui/badge.jsx +1 -1
  7. package/packages/daemon/dist/tui/mirror/ui/detail-row.jsx +22 -0
  8. package/packages/daemon/dist/tui/mirror/ui/home-wordmark.js +11 -0
  9. package/packages/daemon/dist/tui/mirror/ui/key-hint.jsx +8 -2
  10. package/packages/daemon/dist/tui/mirror/ui/menu.jsx +8 -8
  11. package/packages/daemon/dist/tui/mirror/ui/overlay-frame.jsx +9 -7
  12. package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +2 -2
  13. package/packages/daemon/dist/tui/mirror/ui/overlay-model.js +10 -0
  14. package/packages/daemon/dist/tui/mirror/ui/status-bar.jsx +2 -2
  15. package/packages/daemon/dist/tui/mirror/ui/surface.jsx +1 -1
  16. package/packages/daemon/dist/tui/mirror/ui/tabs.jsx +1 -3
  17. package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +45 -0
  18. package/packages/daemon/src/command-center/tmux-servers.ts +3 -2
  19. package/packages/daemon/src/tui/mirror/__snapshots__/shell-chrome-renderer.test.tsx.snap +3 -3
  20. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +7 -7
  21. package/packages/daemon/src/tui/mirror/runtime/application-add-machine-dialog.tsx +8 -1
  22. package/packages/daemon/src/tui/mirror/runtime/application-fleet-session-actions.tsx +5 -0
  23. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +29 -18
  24. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +147 -73
  25. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +3 -1
  26. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +1 -1
  27. package/packages/daemon/src/tui/mirror/runtime/application-shell-home.tsx +86 -19
  28. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +12 -2
  29. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +3 -0
  30. package/packages/daemon/src/tui/mirror/shell-chrome-view.tsx +50 -86
  31. package/packages/daemon/src/tui/mirror/ui/badge.tsx +1 -1
  32. package/packages/daemon/src/tui/mirror/ui/detail-row.tsx +51 -0
  33. package/packages/daemon/src/tui/mirror/ui/home-wordmark.ts +12 -0
  34. package/packages/daemon/src/tui/mirror/ui/key-hint.tsx +17 -2
  35. package/packages/daemon/src/tui/mirror/ui/menu.tsx +16 -15
  36. package/packages/daemon/src/tui/mirror/ui/overlay-frame.tsx +19 -11
  37. package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +2 -2
  38. package/packages/daemon/src/tui/mirror/ui/overlay-model.ts +11 -0
  39. package/packages/daemon/src/tui/mirror/ui/status-bar.tsx +2 -2
  40. package/packages/daemon/src/tui/mirror/ui/surface.tsx +1 -1
  41. package/packages/daemon/src/tui/mirror/ui/tabs.tsx +1 -4
  42. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/application-shell-renderer.test.tsx.snap +3 -3
  43. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/terminal-pane-header-polish-renderer.test.tsx.snap +20 -20
  44. package/packages/daemon/src/tui/mirror/workspace/application-shell-view.tsx +4 -1
  45. package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +52 -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.30",
11282
+ version: "2.9.0-beta.32",
11283
11283
  description: "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
11284
11284
  type: "module",
11285
11285
  bin: {
@@ -71620,9 +71620,8 @@ function mountTmuxServerRoutes(app, options) {
71620
71620
  (row) => row.sessionName === workspace.sessionName && row.liveSessionId === c.req.query("liveSessionId")
71621
71621
  ))
71622
71622
  throw new TmuxServerScopeError("stale-generation");
71623
- const snapshot2 = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
71624
- workspace.sessionName,
71625
- c.req.raw.signal
71623
+ const snapshot2 = await owner.terminalInventoryRuntime.discoverApplicationShellSession(
71624
+ workspace.sessionName
71626
71625
  );
71627
71626
  const currentSessions = await owner.catalog();
71628
71627
  if (!currentSessions.some(
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tmux-ide",
3
- "version": "2.9.0-beta.30",
3
+ "version": "2.9.0-beta.32",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -181,7 +181,9 @@ export function mountTmuxServerRoutes(app, options) {
181
181
  if (!sessions.some((row) => row.sessionName === workspace.sessionName &&
182
182
  row.liveSessionId === c.req.query("liveSessionId")))
183
183
  throw new TmuxServerScopeError("stale-generation");
184
- const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(workspace.sessionName, c.req.raw.signal);
184
+ // Agent lists need the same status enrichment as pane headers. Raw
185
+ // terminal inventory omits these facts and falls back to process activity.
186
+ const snapshot = await owner.terminalInventoryRuntime.discoverApplicationShellSession(workspace.sessionName);
185
187
  // Discovery can yield while a same-name session is replaced. Revalidate
186
188
  // the captured incarnation before publishing agent metadata as current.
187
189
  const currentSessions = await owner.catalog();
@@ -140,15 +140,15 @@ export function HomeAgentRoster(props) {
140
140
  event.stopPropagation();
141
141
  props.onMove(direction === "up" ? -1 : 1);
142
142
  }} onMouseOut={() => setHovered(null)}>
143
+ <box height={searchRows()} flexShrink={0}/>
143
144
  <box width={width()} height={1} flexShrink={0} flexDirection="row">
144
145
  <Show when={props.snapshot.phase === "loading" || props.snapshot.loadingSessions > 0}>
145
146
  <ActivityIndicator theme={props.theme} active={props.inputActive}/>
146
147
  </Show>
147
- <text height={1} flexGrow={1} fg={props.theme.roles.text.primary}>
148
+ <text height={1} flexGrow={1} fg={props.theme.roles.text.muted}>
148
149
  {clipTerminal(title(), width())}
149
150
  </text>
150
151
  </box>
151
- <box height={searchRows()} flexShrink={0}/>
152
152
  <box width={width()} height={1} flexShrink={0} flexDirection="row">
153
153
  <text width={Math.max(0, width() - (props.onToggleAttention ? 16 : 0))} height={1} fg={props.theme.roles.text.secondary} onMouseDown={(event) => {
154
154
  if (!props.inputActive || event.button !== 0)
@@ -166,10 +166,17 @@ export function HomeAgentRoster(props) {
166
166
  }}/>
167
167
  </Show>
168
168
  </box>
169
- <Show when={props.snapshot.rows.length > 0} fallback={<box height={Math.max(0, visibleCount() * rowHeight() + 1)} flexShrink={0}/>}>
170
- <text width={width()} height={1} flexShrink={0} fg={props.theme.roles.text.muted}>
171
- {clipTerminal(` ${padCells("Agent", columns().agent)}${padCells(props.filterLabel ? "Workspace / machine" : "Session", columns().session)} ${padCells("Status", columns().status)}`, width())}
172
- </text>
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}/>
173
180
  <box height={visibleCount() * rowHeight()} width={width()} flexShrink={0} flexDirection="column" overflow="hidden">
174
181
  <For each={keys()}>
175
182
  {(key) => {
@@ -185,10 +192,10 @@ export function HomeAgentRoster(props) {
185
192
  props.onSelect(key);
186
193
  props.onOpen(row(), "mouse");
187
194
  }} onMouseOver={() => setHovered(key)} onMouseMove={() => setHovered(key)}>
188
- <NavigationRow theme={props.theme} id={`home-agent:${key}`} width={width()} label={padCells(row().name, columns().agent) +
189
- padCells(rowHeight() > 1
190
- ? row().sessionName
191
- : [row().machineLabel, row().serverLabel, row().sessionName]
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]
192
199
  .filter(Boolean)
193
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) => {
194
201
  if (!props.inputActive || row().paneId === null || stale(row()))
@@ -203,7 +210,7 @@ export function HomeAgentRoster(props) {
203
210
  selected: props.selection.selectedKey === key,
204
211
  disabled: row().paneId === null || stale(row()),
205
212
  }).background}>
206
- {clipTerminal(` ${padCells(row().harness, columns().agent)}${[row().machineLabel, row().serverLabel].filter(Boolean).join(" / ")}`, width())}
213
+ {clipTerminal(` ${[row().sessionName, row().machineLabel, row().serverLabel, row().harness].filter(Boolean).join(" · ")}`, width())}
207
214
  </text>
208
215
  </Show>
209
216
  </box>);
@@ -212,7 +219,7 @@ export function HomeAgentRoster(props) {
212
219
  </box>
213
220
  </Show>
214
221
  <Show when={props.onQueryChange}>
215
- {(onQueryChange) => (<box position="absolute" top={1} left={0} width={width()} height={1}>
222
+ {(onQueryChange) => (<box position="absolute" top={0} left={0} width={width()} height={1}>
216
223
  <HomeAgentSearch theme={props.theme} width={width()} query={props.query ?? ""} active={props.inputActive} onChange={onQueryChange()} onEdit={(event) => {
217
224
  const action = applicationPaneRenameKeyAction(event, props.query ?? "");
218
225
  if (action.kind === "update")
@@ -1,11 +1,13 @@
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) {
@@ -13,9 +15,15 @@ export function ApplicationHomeSurface(props) {
13
15
  const height = () => Math.max(0, Math.floor(props.height));
14
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
25
  const context = () => clipTerminal(props.branded && props.agentRoster
18
- ? "Across your machines"
26
+ ? "Your agents, across your machines"
19
27
  : `${props.session ?? "No session selected"} · ${props.status}`, bodyWidth());
20
28
  const summary = () => {
21
29
  if (!props.session || props.agents === undefined)
@@ -34,14 +42,31 @@ export function ApplicationHomeSurface(props) {
34
42
  (props.onCycleTheme ? naturalButtonWidth(themeLabel()) + 2 : 0) +
35
43
  (props.onOpenTutorial ? naturalButtonWidth(props.tutorialLabel ?? "Learn tmux-ide") + 2 : 0) +
36
44
  2;
37
- const reservedRows = () => (spacious() ? 4 : 2) +
45
+ const reservedRows = () => brandRows() -
46
+ 1 +
47
+ (spacious() ? 4 : 2) +
38
48
  (actionsInRow() ? 1 : 2 + (props.onCycleTheme ? 1 : 0) + (props.onOpenTutorial ? 1 : 0)) +
39
49
  (spacious() ? 1 : 0) +
40
50
  (props.note ? (spacious() ? 2 : 1) : 0);
41
- const recentActivity = () => (props.recentPaneActivity ?? []).slice(0, spacious()
42
- ? 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())))
43
68
  : 0);
44
- const activityHeight = () => (recentActivity().length > 0 ? recentActivity().length + 2 : 0);
69
+ const activityHeight = () => recentActivity().length > 0 ? recentActivity().length * activityRows() + 2 : 0;
45
70
  const paneLabel = (paneId) => {
46
71
  const matches = props.agentRoster?.rows.filter((row) => row.paneId === paneId) ?? [];
47
72
  return matches.length === 1 ? matches[0].name : paneId;
@@ -65,9 +90,19 @@ export function ApplicationHomeSurface(props) {
65
90
  : "timed out";
66
91
  const rosterHeight = () => Math.max(0, height() - reservedRows() - activityHeight());
67
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">
68
- <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.primary}>
69
- <strong>{clipTerminal(props.branded ? "Your agents" : props.project, bodyWidth())}</strong>
70
- </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>
71
106
  <box height={spacious() ? 1 : 0} flexShrink={0}/>
72
107
  <text width={bodyWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.secondary}>
73
108
  {context()}
@@ -86,12 +121,10 @@ export function ApplicationHomeSurface(props) {
86
121
  <Show when={activityHeight() > 0}>
87
122
  <box width={bodyWidth()} height={activityHeight()} flexShrink={0} flexDirection="column" overflow="hidden">
88
123
  <text height={1} width={bodyWidth()} fg={props.theme.roles.text.secondary}>
89
- {clipTerminal("Recent pane activity", bodyWidth())}
124
+ {clipTerminal(`${selectedAgent()?.name ?? "Agent"} · latest activity`, bodyWidth())}
90
125
  </text>
91
126
  <For each={recentActivity()}>
92
- {(receipt) => (<text height={1} width={bodyWidth()} fg={props.theme.roles.text.primary}>
93
- {clipTerminal(`${activityTime(receipt)} · ${interactionReceiptTargetLabel(receipt, paneLabel)} · ${activityPhase(receipt)}`, bodyWidth())}
94
- </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"}/>)}
95
128
  </For>
96
129
  <text height={1} width={bodyWidth()} fg={props.theme.roles.text.muted}>
97
130
  {clipTerminal("Activity reported through tmux-ide", bodyWidth())}
@@ -3,7 +3,7 @@ import { componentPalette } from "./state.js";
3
3
  export function Badge(props) {
4
4
  const palette = () => componentPalette(props.theme, props, props.tone);
5
5
  const background = () => props.surface === "header" && palette().state === "base"
6
- ? props.theme.roles.surfaces.header
6
+ ? props.theme.roles.surfaces.panel
7
7
  : palette().background;
8
8
  const width = () => Math.max(1, Math.floor(props.width ?? terminalDisplayWidth(props.label) + 2));
9
9
  const content = () => clipTerminal(props.presentation ?? ` ${props.marker ? `${props.marker} ` : ""}${props.label} `, width());
@@ -0,0 +1,22 @@
1
+ import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
2
+ /** Read-only information row: primary label left, secondary detail aligned right. */
3
+ export function DetailRow(props) {
4
+ const detailWidth = () => props.detail
5
+ ? Math.min(terminalDisplayWidth(props.detail), Math.floor(Math.max(0, props.width) / 2))
6
+ : 0;
7
+ const labelWidth = () => Math.max(0, props.width - detailWidth() - (detailWidth() ? 1 : 0));
8
+ return (<box width={props.width} height={props.description ? 2 : 1} flexShrink={0} flexDirection="column" overflow="hidden">
9
+ <box width={props.width} height={1} flexShrink={0} flexDirection="row">
10
+ <text width={labelWidth()} height={1} fg={props.attention ? props.theme.colors.status.blocked : props.theme.roles.text.primary}>
11
+ {clipTerminal(props.label, labelWidth())}
12
+ </text>
13
+ <box flexGrow={1}/>
14
+ <text width={detailWidth()} height={1} fg={props.attention ? props.theme.colors.status.blocked : props.theme.roles.text.muted}>
15
+ {clipTerminal(props.detail ?? "", detailWidth())}
16
+ </text>
17
+ </box>
18
+ {props.description ? (<text width={props.width} height={1} fg={props.theme.roles.text.muted}>
19
+ {clipTerminal(props.description, props.width)}
20
+ </text>) : null}
21
+ </box>);
22
+ }
@@ -0,0 +1,11 @@
1
+ // Exact terminal glyphs from docs/public/ascii-wordmark.svg.
2
+ export const APPLICATION_HOME_WORDMARK = [
3
+ " ░██ ░██ ░██",
4
+ " ░██ ░██",
5
+ "░████████ ░█████████████ ░██ ░██ ░██ ░██ ░██ ░████████ ░███████",
6
+ " ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██ ░██░██ ░██ ░██ ░██",
7
+ " ░██ ░██ ░██ ░██ ░██ ░██ ░█████ ░██░██ ░██ ░█████████",
8
+ " ░██ ░██ ░██ ░██ ░██ ░███ ░██ ░██ ░██░██ ░███ ░██",
9
+ " ░████ ░██ ░██ ░██ ░█████░██ ░██ ░██ ░██ ░█████░██ ░███████",
10
+ ];
11
+ export const APPLICATION_HOME_WORDMARK_WIDTH = 75;
@@ -6,7 +6,7 @@ import { useKeyboardRoute } from "./keyboard-router.jsx";
6
6
  export function KeyHint(props) {
7
7
  const palette = () => componentPalette(props.theme, props, "neutral");
8
8
  const background = () => props.quiet && !props.focused && !props.hovered
9
- ? props.theme.roles.surfaces.header
9
+ ? props.theme.roles.surfaces.panel
10
10
  : palette().background;
11
11
  const foreground = () => props.quiet && !props.focused && !props.hovered
12
12
  ? props.theme.roles.text.muted
@@ -39,7 +39,13 @@ export function KeyHint(props) {
39
39
  activate();
40
40
  }}>
41
41
  <text fg={foreground()} bg={background()}>
42
- {props.focused || props.selected ? <strong>{content()}</strong> : content()}
42
+ {props.focused || props.selected ? (<strong>{content()}</strong>) : props.quiet && !props.presentation ? (<>
43
+ {" "}
44
+ <span style={{ fg: props.theme.roles.text.primary }}>
45
+ {clipTerminal(props.keys, Math.max(0, width() - 1))}
46
+ </span>
47
+ {clipTerminal(props.label ? ` ${props.label} ` : " ", Math.max(0, width() - terminalDisplayWidth(props.keys) - 1))}
48
+ </>) : (content())}
43
49
  </text>
44
50
  </box>);
45
51
  }
@@ -2,17 +2,17 @@
2
2
  import { For } from "solid-js";
3
3
  import { OverlayFrame } from "./overlay-frame.jsx";
4
4
  import { OverlayListRow } from "./overlay-list-row.jsx";
5
- import { overlayFrameSize } from "./overlay-model.js";
5
+ import { overlaySurfaceMetrics } from "./overlay-model.js";
6
6
  export function Menu(props) {
7
- // Match OverlayFrame's clamped border + left padding budget, not the
8
- // requested width; otherwise right-aligned shortcuts disappear offscreen.
9
- const innerWidth = () => Math.max(1, overlayFrameSize({
7
+ const preferredHeight = () => props.items.length + (props.title ? 1 : 0) + (props.footer ? 1 : 0) + 2;
8
+ const metrics = () => overlaySurfaceMetrics({
10
9
  viewportWidth: props.viewportWidth ?? Math.max(props.width, props.left + props.width),
11
- viewportHeight: props.viewportHeight ?? Math.max(3, props.top + props.items.length + 4),
10
+ viewportHeight: props.viewportHeight ?? Math.max(preferredHeight(), props.top + preferredHeight()),
12
11
  preferredWidth: props.width,
13
- preferredHeight: props.items.length + 4,
14
- }).width - 4);
15
- return (<OverlayFrame theme={props.theme} viewportWidth={props.viewportWidth ?? Math.max(props.width, props.left + props.width)} viewportHeight={props.viewportHeight ?? Math.max(3, props.top + props.items.length + 3)} width={props.width} height={props.items.length + (props.title ? 3 : 2) + (props.footer ? 1 : 0)} placement="anchor" anchor={{ x: props.left, y: props.top }} zIndex={props.zIndex ?? 20} active={props.active} title={props.title} footer={props.footer} onDismiss={props.onDismiss}>
12
+ preferredHeight: preferredHeight(),
13
+ });
14
+ const innerWidth = () => metrics().contentWidth;
15
+ return (<OverlayFrame theme={props.theme} viewportWidth={props.viewportWidth ?? Math.max(props.width, props.left + props.width)} viewportHeight={props.viewportHeight ?? Math.max(preferredHeight(), props.top + preferredHeight())} width={props.width} height={preferredHeight()} placement="anchor" anchor={{ x: props.left, y: props.top }} zIndex={props.zIndex ?? 20} active={props.active} title={props.title} footer={props.footer} onDismiss={props.onDismiss}>
16
16
  <For each={props.items.map((item) => item.id)}>
17
17
  {(id) => {
18
18
  const item = () => props.items.find((item) => item.id === id);
@@ -13,9 +13,9 @@ export function OverlayFrame(props) {
13
13
  const width = () => size().width;
14
14
  const height = () => size().height;
15
15
  // Empty border sides below keep OpenTUI border styling from enabling a frame.
16
- const bordered = () => !props.surface && width() >= 4 && height() >= 3;
16
+ const bordered = () => props.surface === false && width() >= 4 && height() >= 3;
17
17
  const padding = () => overlaySurfacePadding(width(), height());
18
- const innerWidth = () => Math.max(1, width() - (props.surface ? padding().horizontal * 2 : bordered() ? 2 : 0));
18
+ const innerWidth = () => Math.max(1, width() - (props.surface !== false ? padding().horizontal * 2 : bordered() ? 2 : 0));
19
19
  const left = () => {
20
20
  if (props.placement === "anchor")
21
21
  return Math.max(0, Math.min(props.anchor?.x ?? 0, viewportWidth() - width()));
@@ -30,17 +30,19 @@ export function OverlayFrame(props) {
30
30
  return Math.min(1, viewportHeight() - height());
31
31
  return Math.max(0, Math.floor((viewportHeight() - height()) / 2));
32
32
  };
33
- const frame = () => (<box id="ui-overlay-frame" position="absolute" left={left()} top={top()} width={width()} height={height()} zIndex={props.zIndex ?? 100} border={bordered() ? true : []} borderStyle="rounded" borderColor={props.theme.roles.borders.focused} backgroundColor={props.surface ? props.theme.roles.surfaces.command : props.theme.roles.surfaces.panelRaised} flexDirection="column" paddingLeft={props.surface ? padding().horizontal : bordered() ? 1 : 0} paddingRight={props.surface ? padding().horizontal : 0} paddingTop={props.surface ? padding().vertical : 0} paddingBottom={props.surface ? padding().vertical : 0} overflow="hidden" onMouseDown={(event) => {
33
+ const frame = () => (<box id="ui-overlay-frame" position="absolute" left={left()} top={top()} width={width()} height={height()} zIndex={props.zIndex ?? 100} border={bordered() ? true : []} borderStyle="rounded" borderColor={props.theme.roles.borders.focused} backgroundColor={props.surface !== false
34
+ ? props.theme.roles.surfaces.command
35
+ : props.theme.roles.surfaces.panelRaised} flexDirection="column" paddingLeft={props.surface !== false ? padding().horizontal : bordered() ? 1 : 0} paddingRight={props.surface !== false ? padding().horizontal : 0} paddingTop={props.surface !== false ? padding().vertical : 0} paddingBottom={props.surface !== false ? padding().vertical : 0} overflow="hidden" onMouseDown={(event) => {
34
36
  event.preventDefault();
35
37
  event.stopPropagation();
36
38
  }}>
37
39
  {props.title && height() >= 4 ? (<box width={innerWidth()} height={1} flexShrink={0} flexDirection="row">
38
- <text width={Math.max(1, innerWidth() - (props.surface && innerWidth() >= 20 ? 4 : 0))} fg={props.theme.roles.text.primary} overflow="hidden">
40
+ <text width={Math.max(1, innerWidth() - (props.surface !== false && innerWidth() >= 20 ? 4 : 0))} fg={props.theme.roles.text.primary} overflow="hidden">
39
41
  <strong>
40
- {clipTerminal(props.title, Math.max(1, innerWidth() - (props.surface && innerWidth() >= 20 ? 4 : 0)))}
42
+ {clipTerminal(props.title, Math.max(1, innerWidth() - (props.surface !== false && innerWidth() >= 20 ? 4 : 0)))}
41
43
  </strong>
42
44
  </text>
43
- {props.surface && innerWidth() >= 20 ? (<text width={4} fg={props.theme.roles.text.muted} onMouseDown={(event) => {
45
+ {props.surface !== false && innerWidth() >= 20 ? (<text width={4} fg={props.theme.roles.text.muted} onMouseDown={(event) => {
44
46
  event.preventDefault();
45
47
  event.stopPropagation();
46
48
  if (event.button === 0 && props.active !== false)
@@ -51,7 +53,7 @@ export function OverlayFrame(props) {
51
53
  </text>) : null}
52
54
  </box>) : null}
53
55
  {props.children}
54
- {props.footer && height() >= 5 ? (<text width={innerWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.muted} bg={props.surface
56
+ {props.footer && height() >= 5 ? (<text width={innerWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.muted} bg={props.surface !== false
55
57
  ? props.theme.roles.surfaces.command
56
58
  : props.theme.roles.surfaces.panelRaised} overflow="hidden">
57
59
  {clipTerminal(props.footer, innerWidth())}
@@ -5,7 +5,7 @@ import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
5
5
  import { overlayRowPalette } from "./overlay-surface.js";
6
6
  import { componentPalette } from "./state.js";
7
7
  export function OverlayListRow(props) {
8
- const palette = () => props.surface
8
+ const palette = () => props.surface !== false
9
9
  ? overlayRowPalette(props.theme, {
10
10
  selected: props.selected,
11
11
  disabled: props.disabled,
@@ -18,7 +18,7 @@ export function OverlayListRow(props) {
18
18
  ? "● "
19
19
  : " "
20
20
  : props.selected
21
- ? props.surface
21
+ ? props.surface !== false
22
22
  ? " "
23
23
  : "› "
24
24
  : props.reserveMarker
@@ -50,3 +50,13 @@ export function overlaySurfacePadding(width, height) {
50
50
  vertical: height >= 8 ? 1 : 0,
51
51
  };
52
52
  }
53
+ /** Content geometry for the default calm overlay, after viewport clamping. */
54
+ export function overlaySurfaceMetrics(input) {
55
+ const size = overlayFrameSize(input);
56
+ const padding = overlaySurfacePadding(size.width, size.height);
57
+ return {
58
+ ...size,
59
+ contentWidth: Math.max(1, size.width - padding.horizontal * 2),
60
+ contentHeight: Math.max(0, size.height - padding.vertical * 2),
61
+ };
62
+ }
@@ -2,7 +2,7 @@ import { clipTerminal } from "../terminal-text.js";
2
2
  import { componentPalette } from "./state.js";
3
3
  import { useKeyboardRoute } from "./keyboard-router.jsx";
4
4
  export function StatusBar(props) {
5
- return (<box id="ui-status-bar" width={props.width} height={1} flexDirection="row" justifyContent="space-between" backgroundColor={props.theme.roles.surfaces.header} overflow="hidden">
5
+ return (<box id="ui-status-bar" width={props.width} height={1} flexDirection="row" justifyContent="space-between" backgroundColor={props.theme.roles.surfaces.panel} overflow="hidden">
6
6
  {props.children}
7
7
  </box>);
8
8
  }
@@ -58,7 +58,7 @@ export function StatusSegment(props) {
58
58
  loading: props.loading,
59
59
  empty: props.empty,
60
60
  }, props.tone);
61
- const background = () => palette().state === "base" ? props.theme.roles.surfaces.header : palette().background;
61
+ const background = () => palette().state === "base" ? props.theme.roles.surfaces.panel : palette().background;
62
62
  const foreground = () => {
63
63
  if (palette().state !== "base")
64
64
  return palette().foreground;
@@ -35,7 +35,7 @@ export function Surface(props) {
35
35
  case "raised":
36
36
  return local.theme.roles.surfaces.panelRaised;
37
37
  case "header":
38
- return local.theme.roles.surfaces.header;
38
+ return local.theme.roles.surfaces.panel;
39
39
  case "panel":
40
40
  default:
41
41
  return local.theme.roles.surfaces.panel;
@@ -19,9 +19,7 @@ export function Tabs(props) {
19
19
  const addWidth = () => (props.onAdd ? terminalDisplayWidth(props.addLabel ?? "+") + 2 : 0);
20
20
  const equalWidth = () => Math.max(1, Math.min(32, Math.floor(((props.width ?? 0) - addWidth()) / Math.max(1, props.items.length))));
21
21
  const itemWidth = (item) => props.fit === "equal" && props.width ? equalWidth() : terminalDisplayWidth(tabText(item));
22
- const background = () => props.variant === "header"
23
- ? props.theme.roles.surfaces.header
24
- : props.theme.roles.surfaces.panel;
22
+ const background = () => props.theme.roles.surfaces.panel;
25
23
  useKeyboardRoute((event) => {
26
24
  if (!props.focused || event.eventType !== "press")
27
25
  return false;
@@ -0,0 +1,45 @@
1
+ import { applicationCommandDescription } from "./application-command-description.js";
2
+ /** Display metadata only. Context-specific keyboard owners remain authoritative. */
3
+ export const APPLICATION_SHORTCUTS = [
4
+ ...["home", "terminals", "switch-session"].map((command) => {
5
+ const description = applicationCommandDescription(command);
6
+ return { category: "Application", label: description.label, keys: description.shortcut };
7
+ }),
8
+ { category: "Application", label: "Commands", keys: "F5" },
9
+ { category: "Application", label: "Agent attention", keys: "F7" },
10
+ { category: "Application", label: "Sidebar / Sessions when hidden", keys: "Ctrl+G" },
11
+ { category: "Home", label: "Find an agent or workspace", keys: "/" },
12
+ { category: "Home", label: "Cycle machine filter", keys: "f" },
13
+ { category: "Home", label: "Toggle attention filter", keys: "a" },
14
+ { category: "Home", label: "Open selected agent", keys: "Enter" },
15
+ { category: "Command and session menus", label: "Search actions or sessions", keys: "Type" },
16
+ { category: "Command and session menus", label: "Choose a result", keys: "↑ / ↓" },
17
+ { category: "Command and session menus", label: "Activate selected result", keys: "Enter" },
18
+ { category: "Command and session menus", label: "Search / navigation mode", keys: "Ctrl+Space" },
19
+ { category: "Menu navigation mode", label: "Move selection", keys: "j / k" },
20
+ { category: "Menu navigation mode", label: "First / last result", keys: "g / G" },
21
+ { category: "Menu navigation mode", label: "Return to search", keys: "i" },
22
+ {
23
+ category: "Command and session menus",
24
+ label: "Previous / next page",
25
+ keys: "PageUp / PageDown",
26
+ },
27
+ {
28
+ category: "Command and session menus",
29
+ label: "Previous / next half page",
30
+ keys: "Ctrl+U / Ctrl+D",
31
+ },
32
+ { category: "Sessions", label: "Local / all hosts", keys: "Ctrl+H" },
33
+ { category: "Sessions", label: "Favorite session", keys: "Ctrl+F" },
34
+ { category: "Sessions", label: "Browse windows without activating", keys: "Ctrl+← / Ctrl+→" },
35
+ { category: "Sessions", label: "Show / hide preview", keys: "Ctrl+P" },
36
+ { category: "Sessions", label: "Expand / restore preview", keys: "Ctrl+E" },
37
+ { category: "Sessions", label: "New session", keys: "Ctrl+N" },
38
+ { category: "Sessions", label: "Confirm close session", keys: "Ctrl+X" },
39
+ { category: "Sessions", label: "Retry selected host", keys: "Ctrl+R" },
40
+ ...["shortcuts", "whats-new"].map((command) => {
41
+ const description = applicationCommandDescription(command);
42
+ return { category: "Help in menus", label: description.label, keys: description.shortcut };
43
+ }),
44
+ { category: "Help in menus", label: "Back / dismiss", keys: "Esc" },
45
+ ];
@@ -258,9 +258,10 @@ export function mountTmuxServerRoutes(app: Hono, options: TmuxServerRoutesOption
258
258
  )
259
259
  )
260
260
  throw new TmuxServerScopeError("stale-generation");
261
- const snapshot = await owner.terminalInventoryRuntime.discoverTerminalRuntimeSession(
261
+ // Agent lists need the same status enrichment as pane headers. Raw
262
+ // terminal inventory omits these facts and falls back to process activity.
263
+ const snapshot = await owner.terminalInventoryRuntime.discoverApplicationShellSession(
262
264
  workspace.sessionName,
263
- c.req.raw.signal,
264
265
  );
265
266
  // Discovery can yield while a same-name session is replaced. Revalidate
266
267
  // the captured incarnation before publishing agent metadata as current.
@@ -24,7 +24,7 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s compact shell
24
24
  │ │
25
25
  │ │
26
26
  └──────────────────────────────────────────────────────────┘
27
- F5 · ^q quit web Live F5"
27
+ F5 · ^q quit F6 Sessions F7 Attention F5 Commands"
28
28
  `;
29
29
 
30
30
  exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell chrome 1`] = `
@@ -67,7 +67,7 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s standard shell
67
67
  │ │
68
68
  │ │
69
69
  └──────────────────────────────────────────────────────────────────────────────────────────┘
70
- F5 palette · ^q quit web Live F5 Commands"
70
+ F5 palette · ^q quit F6 Sessions F7 Attention F5 Commands"
71
71
  `;
72
72
 
73
73
  exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s wide shell chrome 1`] = `
@@ -130,5 +130,5 @@ exports[`ShellChrome OpenTUI renderer renders deterministic %sx%s wide shell chr
130
130
  │ │
131
131
  │ │
132
132
  └──────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────┘
133
- F5 palette · ^q quit tmux-ide web Claude Code Live F5 Commands"
133
+ F5 palette · ^q quit F6 Sessions F7 Attention F5 Commands"
134
134
  `;
@@ -13,7 +13,7 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
13
13
 
14
14
  …
15
15
 
16
- main Live F5"
16
+ F6 Sessions F7 Attention F5 Commands"
17
17
  `;
18
18
 
19
19
  exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 2`] = `
@@ -29,7 +29,7 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
29
29
 
30
30
  …
31
31
 
32
- main Live F5 Commands"
32
+ F6 Sessions F7 Attention F5 Commands"
33
33
  `;
34
34
 
35
35
  exports[`production ApplicationShellView retains shell, window strip, pane chrome, and canvas at %sx%s 3`] = `
@@ -45,24 +45,24 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
45
45
 
46
46
  …
47
47
 
48
- tmux-ide main Live F5 Commands"
48
+ F6 Sessions F7 Attention F5 Commands"
49
49
  `;
50
50
 
51
51
  exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 1`] = `
52
52
  " ⌂ ●❯ live
53
- Y Command palette esc
53
+ t Command palette esc
54
54
  m Search commands…
55
55
  2 Home F1
56
56
  C Terminals F2 …
57
57
  New terminal wind…
58
58
 
59
59
 
60
- main Live F5"
60
+ ↑↓ Select F5 Commands"
61
61
  `;
62
62
 
63
63
  exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 2`] = `
64
64
  " ⌂ ●❯ live
65
- YCommand palette
65
+ tCommand palette
66
66
  m …rch commands…
67
67
  2 Home F1w
68
68
  C …
@@ -83,5 +83,5 @@ exports[`production ApplicationShellView preserves canonical attention while pri
83
83
 
84
84
  …
85
85
 
86
- main Live F5 Commands"
86
+ F6 Sessions F7 Attention F5 Commands"
87
87
  `;
@@ -2,6 +2,7 @@
2
2
  import { Show } from "solid-js";
3
3
  import type { SemanticThemeSnapshot } from "../theme.ts";
4
4
  import { clipTerminal } from "../terminal-text.ts";
5
+ import { overlaySurfaceMetrics } from "../ui/overlay-model.ts";
5
6
  import { Dialog } from "../ui/dialog.tsx";
6
7
  import { TuiButton } from "../ui/button.tsx";
7
8
 
@@ -20,7 +21,13 @@ export interface ApplicationAddMachineDialogProps {
20
21
  /** Collects an SSH target only; the owner validates and opens the connection. */
21
22
  export function ApplicationAddMachineDialog(props: ApplicationAddMachineDialogProps) {
22
23
  const width = () => Math.max(1, Math.min(58, props.width - (props.width >= 8 ? 4 : 0)));
23
- const contentWidth = () => Math.max(1, width() - 4);
24
+ const contentWidth = () =>
25
+ overlaySurfaceMetrics({
26
+ viewportWidth: props.width,
27
+ viewportHeight: props.height,
28
+ preferredWidth: width(),
29
+ preferredHeight: 10,
30
+ }).contentWidth;
24
31
  return (
25
32
  <Show when={props.open}>
26
33
  <Dialog