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

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-appearance-owner.js +13 -4
  4. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +5 -1
  5. package/packages/daemon/dist/tui/mirror/runtime/application-home-experience.js +1 -0
  6. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +18 -0
  7. package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +4 -1
  8. package/packages/daemon/dist/tui/mirror/ui/dialog.jsx +1 -1
  9. package/packages/daemon/dist/tui/mirror/ui/overlay-frame.jsx +24 -8
  10. package/packages/daemon/dist/tui/mirror/ui/overlay-list-row.jsx +34 -5
  11. package/packages/daemon/dist/tui/mirror/ui/overlay-model.js +7 -0
  12. package/packages/daemon/dist/tui/mirror/ui/overlay-surface.js +13 -0
  13. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +17 -7
  14. package/packages/daemon/dist/tui/mirror/workspace/command-palette-surface-view.jsx +22 -16
  15. package/packages/daemon/dist/tui/mirror/workspace/command-palette-surface.js +9 -14
  16. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +47 -0
  17. package/packages/daemon/src/tui/mirror/runtime/__snapshots__/application-shell-view-renderer.test.tsx.snap +12 -12
  18. package/packages/daemon/src/tui/mirror/runtime/application-appearance-owner.ts +12 -4
  19. package/packages/daemon/src/tui/mirror/runtime/application-fleet-switcher.tsx +1 -3
  20. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +6 -1
  21. package/packages/daemon/src/tui/mirror/runtime/application-home-experience.ts +1 -0
  22. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +19 -0
  23. package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +4 -1
  24. package/packages/daemon/src/tui/mirror/runtime/application-root-v2.tsx +2 -0
  25. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +4 -0
  26. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +174 -140
  27. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -0
  28. package/packages/daemon/src/tui/mirror/runtime/palette-production-overlay.tsx +5 -8
  29. package/packages/daemon/src/tui/mirror/ui/dialog.tsx +2 -0
  30. package/packages/daemon/src/tui/mirror/ui/overlay-frame.tsx +49 -14
  31. package/packages/daemon/src/tui/mirror/ui/overlay-list-row.tsx +46 -9
  32. package/packages/daemon/src/tui/mirror/ui/overlay-model.ts +8 -0
  33. package/packages/daemon/src/tui/mirror/ui/overlay-search-field.tsx +34 -0
  34. package/packages/daemon/src/tui/mirror/ui/overlay-surface.ts +19 -0
  35. package/packages/daemon/src/tui/mirror/workspace/__snapshots__/command-palette-surface-renderer.test.tsx.snap +168 -168
  36. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +27 -8
  37. package/packages/daemon/src/tui/mirror/workspace/command-palette-surface-view.tsx +24 -16
  38. package/packages/daemon/src/tui/mirror/workspace/command-palette-surface.ts +9 -14
  39. package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +55 -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.28",
11282
+ version: "2.9.0-beta.29",
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.28",
3
+ "version": "2.9.0-beta.29",
4
4
  "description": "A visual, agent-aware IDE for any tmux session, with optional workspace presets",
5
5
  "type": "module",
6
6
  "bin": {
@@ -77,7 +77,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
77
77
  { id: "system", name: "System · follow terminal" },
78
78
  { id: "dark", name: "Dark" },
79
79
  { id: "light", name: "Light" },
80
- ...VISUAL_THEME_PRESETS,
80
+ ...[...VISUAL_THEME_PRESETS].sort((a, b) => a.name.localeCompare(b.name)),
81
81
  ];
82
82
  const pickerOptions = () => options.filter((p) => `${p.id} ${p.name}`.toLowerCase().includes(pickerQuery().toLowerCase()));
83
83
  const apply = (id) => {
@@ -102,6 +102,14 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
102
102
  if (options.some((p) => p.id === id))
103
103
  apply(id);
104
104
  };
105
+ const filterPicker = (query) => {
106
+ setPickerQuery(query);
107
+ const first = pickerOptions()[0];
108
+ if (!query)
109
+ preview(originalSelection);
110
+ else if (first)
111
+ preview(first.id);
112
+ };
105
113
  const cancelPicker = () => {
106
114
  if (!pickerOpen())
107
115
  return;
@@ -111,7 +119,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
111
119
  setPickerError(null);
112
120
  };
113
121
  const savePicker = () => {
114
- if (!pickerOpen())
122
+ if (!pickerOpen() || !pickerOptions().some((option) => option.id === pickerSelection()))
115
123
  return;
116
124
  try {
117
125
  updateAppConfig({
@@ -150,6 +158,7 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
150
158
  pickerError,
151
159
  pickerQuery,
152
160
  pickerSelection,
161
+ pickerOriginalSelection: () => originalSelection,
153
162
  pickerOptions,
154
163
  openPicker() {
155
164
  if (pickerOpen())
@@ -188,12 +197,12 @@ export function createAppearanceOwner(config, renderer, terminalPaletteOwner) {
188
197
  }
189
198
  }
190
199
  else if (name === "backspace") {
191
- setPickerQuery((q) => q.slice(0, -1));
200
+ filterPicker(pickerQuery().slice(0, -1));
192
201
  }
193
202
  else if (!event.ctrl && !event.meta) {
194
203
  const text = event.sequence ?? (name.length === 1 ? name : "");
195
204
  if (text.length === 1 && text >= " ")
196
- setPickerQuery((q) => (q + text).slice(0, 80));
205
+ filterPicker((pickerQuery() + text).slice(0, 80));
197
206
  }
198
207
  return true;
199
208
  },
@@ -203,7 +203,10 @@ export function createApplicationHomeNavigationOwner(options) {
203
203
  if (typeof command === "object" ||
204
204
  command === "home" ||
205
205
  command === "terminals" ||
206
- command === "appearance")
206
+ command === "appearance" ||
207
+ command === "shortcuts" ||
208
+ command === "whats-new" ||
209
+ command === "switch-session")
207
210
  return null;
208
211
  if (options.sessionOwner()?.snapshot()?.status !== "live")
209
212
  return "Open a live session first";
@@ -215,6 +218,7 @@ export function createApplicationHomeNavigationOwner(options) {
215
218
  setSurface: options.setSurface,
216
219
  setNote: options.setNote,
217
220
  openAppearance: options.openAppearance,
221
+ openSessions: options.openSessions,
218
222
  zoomPane: options.zoomPane,
219
223
  newWindow: options.interaction.newWindow,
220
224
  splitPane: options.interaction.splitPane,
@@ -23,6 +23,7 @@ export function createApplicationHomeExperience(options) {
23
23
  fleetHome: fleetHome.presentation,
24
24
  fleetCommands: machines.paletteCommands,
25
25
  openFleet: machines.openPalette,
26
+ openSessions: () => machines.showSwitcher(false),
26
27
  });
27
28
  const tour = createApplicationGuidedTourIntegration({
28
29
  ...options,
@@ -2,6 +2,7 @@ import { createSignal } from "solid-js";
2
2
  import { applicationPaletteCommands } from "./application-palette-input.js";
3
3
  import { createApplicationPaletteSearchOwner } from "./application-palette-search-owner.js";
4
4
  export function createApplicationPaletteCommandOwner(options) {
5
+ const [referencePage, setReferencePage] = createSignal();
5
6
  const [closeArmed, setCloseArmed] = createSignal(false);
6
7
  const [busy, setBusy] = createSignal(false);
7
8
  let armedTarget;
@@ -14,6 +15,7 @@ export function createApplicationPaletteCommandOwner(options) {
14
15
  onChange: () => setCloseArmed(false),
15
16
  });
16
17
  const setOpen = (open, source) => {
18
+ setReferencePage(undefined);
17
19
  openRevision += 1;
18
20
  setCloseArmed(false);
19
21
  if (open)
@@ -25,6 +27,20 @@ export function createApplicationPaletteCommandOwner(options) {
25
27
  const activate = (command, source, confirmed = false) => {
26
28
  if (busy())
27
29
  return;
30
+ if (command === "shortcuts" || command === "whats-new") {
31
+ setCloseArmed(false);
32
+ setReferencePage(command === "shortcuts" ? "shortcuts" : "changes");
33
+ return;
34
+ }
35
+ if (command === "switch-session") {
36
+ if (!options.openSessions) {
37
+ options.setNote("Session switching is unavailable.");
38
+ return;
39
+ }
40
+ setOpen(false, source);
41
+ options.openSessions();
42
+ return;
43
+ }
28
44
  if (command === "appearance") {
29
45
  setOpen(false, source);
30
46
  options.openAppearance?.();
@@ -124,6 +140,8 @@ export function createApplicationPaletteCommandOwner(options) {
124
140
  };
125
141
  return {
126
142
  ...search,
143
+ referencePage,
144
+ setReferencePage,
127
145
  busy,
128
146
  disabledReason: options.disabledReason ?? (() => null),
129
147
  closeArmed: () => closeArmed() && armedTarget === options.targetKey?.(),
@@ -8,8 +8,11 @@ const BASE_COMMANDS = [
8
8
  "split-right",
9
9
  "split-down",
10
10
  "close-pane",
11
- "appearance",
12
11
  "zoom-pane",
12
+ "appearance",
13
+ "shortcuts",
14
+ "whats-new",
15
+ "switch-session",
13
16
  ];
14
17
  /** Agent commands are derived from the same semantic rows as the sidebar. */
15
18
  export function applicationPaletteCommands(semantic, liveSessions = []) {
@@ -1,6 +1,6 @@
1
1
  import { OverlayFrame } from "./overlay-frame.jsx";
2
2
  export function Dialog(props) {
3
- return (<OverlayFrame theme={props.theme} viewportWidth={props.viewportWidth} viewportHeight={props.viewportHeight} width={props.width} height={props.height} {...(props.title ? { title: props.title } : {})} {...(props.footer ? { footer: props.footer } : {})} {...(props.placement ? { placement: props.placement } : {})} {...(props.active !== undefined ? { active: props.active } : {})} {...(props.zIndex !== undefined ? { zIndex: props.zIndex } : {})} {...(props.dismissOnOutsidePress !== undefined
3
+ return (<OverlayFrame surface={props.surface} theme={props.theme} viewportWidth={props.viewportWidth} viewportHeight={props.viewportHeight} width={props.width} height={props.height} {...(props.title ? { title: props.title } : {})} {...(props.footer ? { footer: props.footer } : {})} {...(props.placement ? { placement: props.placement } : {})} {...(props.active !== undefined ? { active: props.active } : {})} {...(props.zIndex !== undefined ? { zIndex: props.zIndex } : {})} {...(props.dismissOnOutsidePress !== undefined
4
4
  ? { dismissOnOutsidePress: props.dismissOnOutsidePress }
5
5
  : {})} onDismiss={props.onDismiss}>
6
6
  {props.children}
@@ -1,5 +1,5 @@
1
1
  import { clipTerminal } from "../terminal-text.js";
2
- import { overlayFrameSize } from "./overlay-model.js";
2
+ import { overlayFrameSize, overlaySurfacePadding } from "./overlay-model.js";
3
3
  /** Owns modal geometry and pointer capture without owning commands or renderer lifecycle. */
4
4
  export function OverlayFrame(props) {
5
5
  const viewportWidth = () => Math.max(1, Math.floor(props.viewportWidth));
@@ -13,8 +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 = () => width() >= 4 && height() >= 3;
17
- const innerWidth = () => Math.max(1, width() - (bordered() ? 2 : 0));
16
+ const bordered = () => !props.surface && width() >= 4 && height() >= 3;
17
+ const padding = () => overlaySurfacePadding(width(), height());
18
+ const innerWidth = () => Math.max(1, width() - (props.surface ? padding().horizontal * 2 : bordered() ? 2 : 0));
18
19
  const left = () => {
19
20
  if (props.placement === "anchor")
20
21
  return Math.max(0, Math.min(props.anchor?.x ?? 0, viewportWidth() - width()));
@@ -29,15 +30,30 @@ export function OverlayFrame(props) {
29
30
  return Math.min(1, viewportHeight() - height());
30
31
  return Math.max(0, Math.floor((viewportHeight() - height()) / 2));
31
32
  };
32
- 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.theme.roles.surfaces.panelRaised} flexDirection="column" paddingLeft={bordered() ? 1 : 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 ? 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
34
  event.preventDefault();
34
35
  event.stopPropagation();
35
36
  }}>
36
- {props.title && height() >= 4 ? (<text width={innerWidth()} fg={props.theme.roles.text.primary} bg={props.theme.roles.surfaces.panelRaised} overflow="hidden">
37
- <strong>{clipTerminal(props.title, innerWidth())}</strong>
38
- </text>) : null}
37
+ {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">
39
+ <strong>
40
+ {clipTerminal(props.title, Math.max(1, innerWidth() - (props.surface && innerWidth() >= 20 ? 4 : 0)))}
41
+ </strong>
42
+ </text>
43
+ {props.surface && innerWidth() >= 20 ? (<text width={4} fg={props.theme.roles.text.muted} onMouseDown={(event) => {
44
+ event.preventDefault();
45
+ event.stopPropagation();
46
+ if (event.button === 0 && props.active !== false)
47
+ props.onDismiss?.();
48
+ }}>
49
+ {" "}
50
+ esc
51
+ </text>) : null}
52
+ </box>) : null}
39
53
  {props.children}
40
- {props.footer && height() >= 5 ? (<text width={innerWidth()} fg={props.theme.roles.text.muted} bg={props.theme.roles.surfaces.panelRaised} overflow="hidden">
54
+ {props.footer && height() >= 5 ? (<text width={innerWidth()} height={1} flexShrink={0} fg={props.theme.roles.text.muted} bg={props.surface
55
+ ? props.theme.roles.surfaces.command
56
+ : props.theme.roles.surfaces.panelRaised} overflow="hidden">
41
57
  {clipTerminal(props.footer, innerWidth())}
42
58
  </text>) : null}
43
59
  </box>);
@@ -2,22 +2,46 @@
2
2
  import { For, createMemo } from "solid-js";
3
3
  import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
4
4
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.js";
5
+ import { overlayRowPalette } from "./overlay-surface.js";
5
6
  import { componentPalette } from "./state.js";
6
7
  export function OverlayListRow(props) {
7
- const palette = () => componentPalette(props.theme, { selected: props.selected, disabled: props.disabled, attention: props.danger }, props.danger ? "blocked" : "neutral");
8
+ const palette = () => props.surface
9
+ ? overlayRowPalette(props.theme, {
10
+ selected: props.selected,
11
+ disabled: props.disabled,
12
+ attention: props.danger,
13
+ })
14
+ : componentPalette(props.theme, { selected: props.selected, disabled: props.disabled, attention: props.danger }, props.danger ? "blocked" : "neutral");
8
15
  const content = () => {
9
- const shortcut = props.shortcut ? ` ${props.shortcut}` : "";
10
- const prefix = props.selected ? "› " : props.reserveMarker ? " " : "";
16
+ const prefix = props.current !== undefined
17
+ ? props.current
18
+ ? "● "
19
+ : " "
20
+ : props.selected
21
+ ? props.surface
22
+ ? " "
23
+ : "› "
24
+ : props.reserveMarker
25
+ ? " "
26
+ : "";
27
+ const shortcut = props.shortcut &&
28
+ props.width >=
29
+ terminalDisplayWidth(prefix) +
30
+ terminalDisplayWidth(props.label) +
31
+ terminalDisplayWidth(props.shortcut) +
32
+ 2
33
+ ? ` ${props.shortcut}`
34
+ : "";
11
35
  const available = Math.max(1, props.width - terminalDisplayWidth(prefix) - terminalDisplayWidth(shortcut));
12
36
  const label = clipTerminal(props.label, available);
13
37
  const gap = Math.max(0, props.width -
14
38
  terminalDisplayWidth(prefix) -
15
39
  terminalDisplayWidth(label) -
16
40
  terminalDisplayWidth(shortcut));
17
- return clipTerminal(`${prefix}${label}${" ".repeat(gap)}${shortcut}`, props.width);
41
+ return { body: clipTerminal(`${prefix}${label}${" ".repeat(gap)}`, props.width), shortcut };
18
42
  };
19
43
  const segments = createMemo(() => {
20
- const value = content();
44
+ const value = content().body;
21
45
  const matches = new Set(props.query ? (commandSearchMatch(value, props.query)?.indices ?? []) : []);
22
46
  const runs = [];
23
47
  for (const [index, character] of Array.from(value).entries()) {
@@ -48,5 +72,10 @@ export function OverlayListRow(props) {
48
72
  <u>{segment.text}</u>
49
73
  </strong>) : (segment.text)}
50
74
  </For>
75
+ <span style={{
76
+ fg: props.selected && !props.disabled ? palette().foreground : props.theme.roles.text.muted,
77
+ }}>
78
+ {content().shortcut}
79
+ </span>
51
80
  </text>);
52
81
  }
@@ -43,3 +43,10 @@ export function createOverlayFocusCoordinator(options) {
43
43
  },
44
44
  };
45
45
  }
46
+ /** Cell padding collapses before labels become unusable in tiny terminals. */
47
+ export function overlaySurfacePadding(width, height) {
48
+ return {
49
+ horizontal: height < 6 ? 0 : width >= 48 ? 3 : width >= 12 ? 2 : 0,
50
+ vertical: height >= 8 ? 1 : 0,
51
+ };
52
+ }
@@ -0,0 +1,13 @@
1
+ import { componentPalette } from "./state.js";
2
+ /** Shared surface treatment for command and appearance lists. */
3
+ export function overlayRowPalette(theme, state = {}) {
4
+ const palette = componentPalette(theme, state);
5
+ return {
6
+ ...palette,
7
+ // Current is a marker, not a second selection background.
8
+ background: state.selected || state.disabled || state.attention
9
+ ? palette.background
10
+ : theme.roles.surfaces.command,
11
+ accent: state.selected && !state.disabled ? palette.foreground : palette.accent,
12
+ };
13
+ }
@@ -1,8 +1,21 @@
1
1
  import { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
2
2
  export { fuzzyTermsMatch as commandSearchMatch } from "../../team/fuzzy.js";
3
3
  import { PANE_ACTION_MENU_ITEMS } from "./pane-action-menu-model.js";
4
+ export const PALETTE_REFERENCE_COMMANDS = {
5
+ shortcuts: { label: "Keyboard shortcuts", shortcut: "Ctrl+K", key: "k", page: "shortcuts" },
6
+ "whats-new": { label: "What's new", shortcut: "Ctrl+B", key: "b", page: "changes" },
7
+ };
4
8
  /** Presentation only: execution remains in the existing application owners. */
5
9
  export function applicationCommandDescription(command) {
10
+ if (command === "switch-session")
11
+ return {
12
+ id: command,
13
+ label: "Switch session",
14
+ shortcut: "F6",
15
+ detail: "Sessions across machines",
16
+ };
17
+ if (command === "shortcuts" || command === "whats-new")
18
+ return { id: command, ...PALETTE_REFERENCE_COMMANDS[command], detail: "Help and reference" };
6
19
  if (typeof command === "object" && command.kind === "open-machine")
7
20
  return {
8
21
  id: JSON.stringify([
@@ -50,11 +63,8 @@ export function applicationCommandDescription(command) {
50
63
  : command === "zoom-pane"
51
64
  ? "Zoom / unzoom pane"
52
65
  : undefined) ??
53
- (command === "home"
54
- ? "F1 Home"
55
- : command === "terminals"
56
- ? "F2 Terminals"
57
- : "New terminal window"),
66
+ (command === "home" ? "Home" : command === "terminals" ? "Terminals" : "New terminal window"),
67
+ shortcut: command === "home" ? "F1" : command === "terminals" ? "F2" : undefined,
58
68
  detail: command === "home"
59
69
  ? "sessions and agent state"
60
70
  : command === "terminals"
@@ -65,8 +75,8 @@ export function applicationCommandDescription(command) {
65
75
  export function filterApplicationCommands(commands, query) {
66
76
  return commands
67
77
  .map((command, index) => {
68
- const { label, detail } = applicationCommandDescription(command);
69
- const score = commandSearchMatch(`${label} ${detail}`, query)?.score;
78
+ const { label, detail, shortcut } = applicationCommandDescription(command);
79
+ const score = commandSearchMatch(`${label} ${detail} ${shortcut ?? ""}`, query)?.score;
70
80
  const fleet = typeof command === "object" ? command.fleet : undefined;
71
81
  return {
72
82
  command,
@@ -1,7 +1,7 @@
1
1
  /* @jsxImportSource @opentui/solid */
2
2
  import { createMemo, For, Show } from "solid-js";
3
3
  import { terminalDisplayWidth } from "../panel-host.js";
4
- import { recipePalette } from "../recipes.js";
4
+ import { overlayRowPalette } from "../ui/overlay-surface.js";
5
5
  import { workspaceIcon } from "./icons.js";
6
6
  import { clipWorkspaceText } from "./text.js";
7
7
  function sameIds(left, right) {
@@ -14,7 +14,7 @@ function localY(projection, y) {
14
14
  return y - projection.overlay.y - (projection.bordered ? 1 : 0);
15
15
  }
16
16
  function commandRowPalette(theme, row) {
17
- return recipePalette(theme, {
17
+ return overlayRowPalette(theme, {
18
18
  disabled: row.disabled,
19
19
  selected: row.selected,
20
20
  focused: row.current,
@@ -27,7 +27,9 @@ function CommandRow(props) {
27
27
  <text fg={palette().accent}>{props.row.markerSpan.text}</text>
28
28
  </box>
29
29
  <box position="absolute" left={props.row.iconSpan.x - props.row.rect.x} top={0} width={props.row.iconSpan.width} height={1}>
30
- <text fg={props.row.current && !props.row.disabled ? props.theme.colors.focus : palette().accent}>
30
+ <text fg={props.row.current && !props.row.disabled && !props.row.selected
31
+ ? props.theme.colors.focus
32
+ : palette().accent}>
31
33
  {props.row.iconSpan.text}
32
34
  </text>
33
35
  </box>
@@ -36,15 +38,17 @@ function CommandRow(props) {
36
38
  </box>
37
39
  <Show when={props.row.trailingSpan !== null}>
38
40
  <box position="absolute" left={props.row.trailingSpan.x - props.row.rect.x} top={0} width={props.row.trailingSpan.width} height={1}>
39
- <text fg={props.row.disabled ? props.theme.colors.mutedForeground : props.theme.colors.accent}>
41
+ <text fg={props.row.selected && !props.row.disabled
42
+ ? palette().foreground
43
+ : props.theme.colors.mutedForeground}>
40
44
  {props.row.trailingSpan.text}
41
45
  </text>
42
46
  </box>
43
47
  </Show>
44
48
  <Show when={props.row.detailSpan !== null}>
45
49
  <box position="absolute" left={props.row.detailSpan.x - props.row.rect.x} top={1} width={props.row.detailSpan.width} height={1}>
46
- <text fg={props.row.disabled
47
- ? props.theme.colors.status.blocked
50
+ <text fg={props.row.selected && !props.row.disabled
51
+ ? palette().foreground
48
52
  : props.theme.colors.mutedForeground}>
49
53
  {props.row.detailSpan.text}
50
54
  </text>
@@ -53,7 +57,7 @@ function CommandRow(props) {
53
57
  </box>);
54
58
  }
55
59
  function StateRow(props) {
56
- const palette = () => recipePalette(props.theme, {
60
+ const palette = () => overlayRowPalette(props.theme, {
57
61
  loading: props.row.state === "loading",
58
62
  empty: props.row.state === "empty" || props.row.state === "no-match",
59
63
  attention: props.row.state === "error",
@@ -73,7 +77,7 @@ function StateRow(props) {
73
77
  </box>);
74
78
  }
75
79
  function Row(props) {
76
- return (<Show when={props.row.kind === "command"} fallback={<Show when={props.row.kind === "state"} fallback={<box position="absolute" left={localX(props.projection, props.row.rect.x)} top={localY(props.projection, props.row.rect.y)} width={props.row.rect.width} height={1} backgroundColor={props.theme.colors.surface} overflow="hidden">
80
+ return (<Show when={props.row.kind === "command"} fallback={<Show when={props.row.kind === "state"} fallback={<box position="absolute" left={localX(props.projection, props.row.rect.x)} top={localY(props.projection, props.row.rect.y)} width={props.row.rect.width} height={1} backgroundColor={props.theme.roles.surfaces.command} overflow="hidden">
77
81
  <box position="absolute" left={props.row.labelSpan.x - props.row.rect.x} top={0} width={props.row.labelSpan.width} height={1}>
78
82
  <text fg={props.theme.colors.mutedForeground}>{props.row.labelSpan.text}</text>
79
83
  </box>
@@ -106,9 +110,9 @@ function footerRight(projection) {
106
110
  export function CommandPaletteSurface(props) {
107
111
  const entries = createMemo(() => new Map(props.projection.rows.map((row) => [row.id, row])));
108
112
  const rowIds = createMemo(() => props.projection.rowIds, undefined, { equals: sameIds });
109
- const count = () => `${props.projection.commandCount}`;
113
+ const count = () => "esc";
110
114
  const countWidth = () => terminalDisplayWidth(count());
111
- const headerTitle = () => clipWorkspaceText(` ${workspaceIcon("command")} ${props.projection.title}`, Math.max(0, props.projection.header.width - countWidth() - 2));
115
+ const headerTitle = () => clipWorkspaceText(props.projection.title, Math.max(0, props.projection.header.width - countWidth() - 2));
112
116
  const leftFooter = () => clipWorkspaceText(footerLeft(props.projection), props.projection.footer.width);
113
117
  const rightFooter = () => {
114
118
  const leftWidth = terminalDisplayWidth(leftFooter());
@@ -116,17 +120,19 @@ export function CommandPaletteSurface(props) {
116
120
  };
117
121
  const footerRightWidth = () => terminalDisplayWidth(rightFooter());
118
122
  return (<box width={props.projection.width} height={props.projection.height} position="absolute" left={0} top={0} overflow="hidden">
119
- <box position="absolute" left={props.projection.overlay.x} top={props.projection.overlay.y} width={props.projection.overlay.width} height={props.projection.overlay.height} border={props.projection.bordered ? true : []} borderStyle="rounded" borderColor={props.theme.colors.focusBorder} backgroundColor={props.theme.colors.surface} overflow="hidden">
123
+ <box position="absolute" left={props.projection.overlay.x} top={props.projection.overlay.y} width={props.projection.overlay.width} height={props.projection.overlay.height} border={props.projection.bordered ? true : []} borderStyle="rounded" borderColor={props.theme.colors.focusBorder} backgroundColor={props.theme.roles.surfaces.command} overflow="hidden">
120
124
  <Show when={props.projection.header.height > 0}>
121
- <box position="absolute" left={localX(props.projection, props.projection.header.x)} top={localY(props.projection, props.projection.header.y)} width={props.projection.header.width} height={1} backgroundColor={props.theme.colors.surfaceRaised} overflow="hidden">
122
- <text fg={props.theme.colors.foreground}>{headerTitle()}</text>
125
+ <box position="absolute" left={localX(props.projection, props.projection.header.x)} top={localY(props.projection, props.projection.header.y)} width={props.projection.header.width} height={1} backgroundColor={props.theme.roles.surfaces.command} overflow="hidden">
126
+ <text fg={props.theme.colors.foreground}>
127
+ <strong>{headerTitle()}</strong>
128
+ </text>
123
129
  <box position="absolute" right={0} top={0} width={countWidth()} height={1}>
124
130
  <text fg={props.theme.colors.mutedForeground}>{count()}</text>
125
131
  </box>
126
132
  </box>
127
133
  </Show>
128
134
  <Show when={props.projection.query.height > 0}>
129
- <box position="absolute" left={localX(props.projection, props.projection.query.x)} top={localY(props.projection, props.projection.query.y)} width={props.projection.query.width} height={1} backgroundColor={props.theme.colors.background} overflow="hidden" flexDirection="row">
135
+ <box position="absolute" left={localX(props.projection, props.projection.query.x)} top={localY(props.projection, props.projection.query.y)} width={props.projection.query.width} height={1} backgroundColor={props.theme.roles.surfaces.panel} overflow="hidden" flexDirection="row">
130
136
  <text fg={props.theme.colors.accent}>{` ${workspaceIcon("search")} `}</text>
131
137
  <text fg={props.projection.queryText
132
138
  ? props.theme.colors.foreground
@@ -137,14 +143,14 @@ export function CommandPaletteSurface(props) {
137
143
  </Show>
138
144
  <Show when={props.projection.divider.height > 0}>
139
145
  <box position="absolute" left={localX(props.projection, props.projection.divider.x)} top={localY(props.projection, props.projection.divider.y)} width={props.projection.divider.width} height={1} overflow="hidden">
140
- <text fg={props.theme.colors.border}>{"─".repeat(props.projection.divider.width)}</text>
146
+ {/* Space separates search from results without a rule. */}
141
147
  </box>
142
148
  </Show>
143
149
  <For each={rowIds()}>
144
150
  {(rowId) => (<Row theme={props.theme} projection={props.projection} row={entries().get(rowId)}/>)}
145
151
  </For>
146
152
  <Show when={props.projection.footer.height > 0}>
147
- <box position="absolute" left={localX(props.projection, props.projection.footer.x)} top={localY(props.projection, props.projection.footer.y)} width={props.projection.footer.width} height={1} backgroundColor={props.theme.colors.surfaceRaised} overflow="hidden">
153
+ <box position="absolute" left={localX(props.projection, props.projection.footer.x)} top={localY(props.projection, props.projection.footer.y)} width={props.projection.footer.width} height={1} backgroundColor={props.theme.roles.surfaces.command} overflow="hidden">
148
154
  <text fg={props.theme.colors.mutedForeground}>{leftFooter()}</text>
149
155
  <Show when={footerRightWidth() > 0}>
150
156
  <box position="absolute" right={0} top={0} width={footerRightWidth()} height={1}>
@@ -1,3 +1,4 @@
1
+ import { overlaySurfacePadding } from "../ui/overlay-model.js";
1
2
  import { terminalDisplayWidth } from "../panel-host.js";
2
3
  import { workspaceIcon } from "./icons.js";
3
4
  import { clipWorkspaceText } from "./text.js";
@@ -29,13 +30,13 @@ function paletteGeometry(width, height, variant) {
29
30
  width: overlayWidth,
30
31
  height: overlayHeight,
31
32
  };
32
- const bordered = overlay.width >= 8 && overlay.height >= 6;
33
- const inset = bordered ? 1 : 0;
33
+ const bordered = false;
34
+ const padding = overlaySurfacePadding(overlay.width, overlay.height);
34
35
  const inner = {
35
- x: overlay.x + inset,
36
- y: overlay.y + inset,
37
- width: Math.max(0, overlay.width - inset * 2),
38
- height: Math.max(0, overlay.height - inset * 2),
36
+ x: overlay.x + padding.horizontal,
37
+ y: overlay.y + padding.vertical,
38
+ width: Math.max(0, overlay.width - padding.horizontal * 2),
39
+ height: Math.max(0, overlay.height - padding.vertical * 2),
39
40
  };
40
41
  const headerHeight = inner.height >= 3 ? 1 : 0;
41
42
  const queryHeight = inner.height >= 2 ? 1 : 0;
@@ -182,13 +183,7 @@ function projectCommandRow(candidate, rect, selectedCommandId) {
182
183
  const disabledReason = command.disabledReason?.trim() ?? "";
183
184
  const disabled = disabledReason.length > 0;
184
185
  const left = rect.x;
185
- const markerText = disabled
186
- ? "×"
187
- : selected
188
- ? workspaceIcon("command")
189
- : command.current
190
- ? "✓"
191
- : " ";
186
+ const markerText = disabled ? "×" : selected ? " " : command.current ? "✓" : " ";
192
187
  const markerSpan = span(markerText, left, rect.y, Math.min(1, rect.width));
193
188
  const iconX = left + Math.min(2, rect.width);
194
189
  const iconSpan = span(workspaceIcon(command.icon), iconX, rect.y, Math.max(0, rect.width - 2));
@@ -231,7 +226,7 @@ function projectCommandRow(candidate, rect, selectedCommandId) {
231
226
  }
232
227
  function projectStateRow(candidate, rect, selectedCommandId, retryCommandId) {
233
228
  const selected = candidate.actionable && selectedCommandId === retryCommandId;
234
- const prefix = `${selected ? workspaceIcon("command") : " "} ${candidate.icon} `;
229
+ const prefix = `${selected ? " " : " "} ${candidate.icon} `;
235
230
  const labelSpan = span(`${prefix}${candidate.title}`, rect.x, rect.y, rect.width);
236
231
  const detailSpan = rect.height > 1
237
232
  ? span(candidate.detail, rect.x + Math.min(4, rect.width), rect.y + 1, Math.max(0, rect.width - 4))
@@ -0,0 +1,47 @@
1
+ export function paletteSection(command) {
2
+ if (command === "switch-session")
3
+ return "Sessions";
4
+ if (typeof command === "object")
5
+ return command.kind === "jump-agent"
6
+ ? "Agents"
7
+ : command.kind === "open-machine"
8
+ ? "Machines"
9
+ : "Sessions";
10
+ if (command === "home" || command === "terminals")
11
+ return "Navigation";
12
+ if (command === "appearance")
13
+ return "Appearance";
14
+ if (command === "shortcuts" || command === "whats-new")
15
+ return "Help";
16
+ return "Panes";
17
+ }
18
+ /** Headings consume cells, never command indices or keyboard stops. */
19
+ export function paletteListLayout(commands, selected, capacity, grouped) {
20
+ const limit = Math.max(1, capacity);
21
+ const sections = commands.map(paletteSection);
22
+ const useGroups = grouped && limit >= 6;
23
+ const focus = Math.max(0, Math.min(selected, commands.length - 1));
24
+ let start = focus;
25
+ let used = useGroups ? 2 : 1;
26
+ while (start > 0) {
27
+ const cost = 1 + (useGroups && sections[start - 1] !== sections[start] ? 2 : 0);
28
+ if (used + cost > limit)
29
+ break;
30
+ used += cost;
31
+ start--;
32
+ }
33
+ const rows = [];
34
+ used = 0;
35
+ for (let index = start; index < commands.length; index++) {
36
+ const heading = useGroups && (index === start || sections[index] !== sections[index - 1])
37
+ ? sections[index]
38
+ : null;
39
+ const gap = Boolean(heading && index !== start);
40
+ const cost = 1 + (heading ? 1 : 0) + (gap ? 1 : 0);
41
+ if (used + cost > limit)
42
+ break;
43
+ used += cost;
44
+ rows.push({ command: commands[index], index, heading, gap });
45
+ }
46
+ return rows;
47
+ }
@@ -50,23 +50,23 @@ exports[`production ApplicationShellView retains shell, window strip, pane chrom
50
50
 
51
51
  exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 1`] = `
52
52
  " ⌂ ●❯ live
53
- t╭──────────────────────╮
54
- m│ Command palette │
55
- 2│ / Search commands… │
56
- C│ › F1 Home │…
57
- │ F2 Terminals │
58
- │ New terminal wind… │
59
- ╰──────────────────────╯
53
+ t Command palette esc
54
+ m Search commands…
55
+ 2 Home F1
56
+ C Terminals F2 …
57
+ New terminal wind…
58
+
59
+
60
60
  main Live F5"
61
61
  `;
62
62
 
63
63
  exports[`production ApplicationShellView reacts palette geometry through narrow live resizes 2`] = `
64
64
  " ⌂ ●❯ live
65
- t╭──────────────╮
66
- m│ Commands │
67
- 2│ / Search co… │w
68
- C│ › F1 Home │…
69
- ╰──────────────╯…
65
+ tCommand palette
66
+ m …rch commands…
67
+ 2 Home F1w
68
+ C …
69
+ …
70
70
  ○ Commands F5"
71
71
  `;
72
72