tmux-ide 2.9.0-beta.39 → 2.9.0-beta.41

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/README.md +5 -0
  2. package/bin/cli.js +6 -3
  3. package/package.json +6 -3
  4. package/packages/daemon/dist/tui/mirror/runtime/application-fleet-tabs.js +29 -2
  5. package/packages/daemon/dist/tui/mirror/runtime/application-home-agent-roster.jsx +10 -13
  6. package/packages/daemon/dist/tui/mirror/runtime/application-home-agents-owner.js +1 -0
  7. package/packages/daemon/dist/tui/mirror/runtime/application-machine-navigation.js +45 -7
  8. package/packages/daemon/dist/tui/mirror/runtime/application-machine-sidebar.jsx +44 -44
  9. package/packages/daemon/dist/tui/mirror/runtime/application-palette-command-owner.js +2 -2
  10. package/packages/daemon/dist/tui/mirror/runtime/application-palette-input.js +1 -0
  11. package/packages/daemon/dist/tui/mirror/ui/agent-status-marker.js +6 -5
  12. package/packages/daemon/dist/tui/mirror/ui/icons.js +9 -0
  13. package/packages/daemon/dist/tui/mirror/ui/index.js +3 -0
  14. package/packages/daemon/dist/tui/mirror/ui/session-row.jsx +71 -0
  15. package/packages/daemon/dist/tui/mirror/ui/working-sessions.jsx +61 -0
  16. package/packages/daemon/dist/tui/mirror/workspace/application-action-descriptions.js +51 -0
  17. package/packages/daemon/dist/tui/mirror/workspace/application-command-description.js +10 -3
  18. package/packages/daemon/dist/tui/mirror/workspace/application-shortcuts.js +15 -7
  19. package/packages/daemon/dist/tui/mirror/workspace/palette-list-layout.js +1 -1
  20. package/packages/daemon/src/tui/mirror/runtime/application-fleet-tabs.ts +34 -2
  21. package/packages/daemon/src/tui/mirror/runtime/application-home-agent-roster.tsx +14 -18
  22. package/packages/daemon/src/tui/mirror/runtime/application-home-agents-owner.ts +1 -0
  23. package/packages/daemon/src/tui/mirror/runtime/application-machine-navigation.ts +44 -7
  24. package/packages/daemon/src/tui/mirror/runtime/application-machine-sidebar.tsx +70 -80
  25. package/packages/daemon/src/tui/mirror/runtime/application-palette-command-owner.ts +5 -3
  26. package/packages/daemon/src/tui/mirror/runtime/application-palette-input.ts +1 -0
  27. package/packages/daemon/src/tui/mirror/runtime/application-reference-sheet.tsx +80 -5
  28. package/packages/daemon/src/tui/mirror/runtime/application-shell-catalog.tsx +6 -2
  29. package/packages/daemon/src/tui/mirror/runtime/application-shell-overlays.tsx +6 -5
  30. package/packages/daemon/src/tui/mirror/runtime/application-shell-view.tsx +6 -2
  31. package/packages/daemon/src/tui/mirror/ui/agent-status-marker.ts +7 -5
  32. package/packages/daemon/src/tui/mirror/ui/icons.ts +9 -0
  33. package/packages/daemon/src/tui/mirror/ui/index.ts +4 -0
  34. package/packages/daemon/src/tui/mirror/ui/session-row.tsx +122 -0
  35. package/packages/daemon/src/tui/mirror/ui/working-sessions.tsx +99 -0
  36. package/packages/daemon/src/tui/mirror/workspace/application-action-descriptions.ts +52 -0
  37. package/packages/daemon/src/tui/mirror/workspace/application-command-description.ts +12 -3
  38. package/packages/daemon/src/tui/mirror/workspace/application-shortcuts.ts +19 -7
  39. package/packages/daemon/src/tui/mirror/workspace/palette-list-layout.ts +1 -1
@@ -1,7 +1,9 @@
1
+ import { CHROME_ACTIONS, SIDEBAR_ACTIONS } from "../workspace/application-action-descriptions.ts";
1
2
  import { createAgentStatusMarker } from "../ui/agent-status-marker.ts";
2
3
  import type { TmuxServerDescriptor, TmuxServerScope } from "@tmux-ide/contracts";
3
4
  import { fleetHostColor, summarizeFleetActivity } from "./fleet-presentation.ts";
4
- import { TuiButton } from "../ui/button.tsx";
5
+ import { WorkingSessions } from "../ui/working-sessions.tsx";
6
+ import type { SessionRowModel } from "../ui/session-row.tsx";
5
7
  /* @jsxImportSource @opentui/solid */
6
8
  import {
7
9
  fleetConnectionMessage,
@@ -79,15 +81,7 @@ export interface ApplicationMachineSidebarModel {
79
81
  paneId: string,
80
82
  source: "keyboard" | "mouse",
81
83
  ) => void;
82
- readonly tabs?: Accessor<
83
- readonly {
84
- key: string;
85
- label: string;
86
- hostLabel: string;
87
- active: boolean;
88
- available: boolean;
89
- }[]
90
- >;
84
+ readonly tabs?: Accessor<readonly SessionRowModel[]>;
91
85
  readonly onOpenTab?: (key: string) => void;
92
86
  readonly onCloseTab?: (key: string) => void;
93
87
  readonly onOpenSwitcher?: () => void;
@@ -117,8 +111,8 @@ export function ApplicationMachineSidebar(props: {
117
111
  readonly theme: SemanticThemeSnapshot;
118
112
  readonly agents?: JSX.Element;
119
113
  readonly agentRows?: number;
114
+ readonly onHelp?: (source: "keyboard" | "mouse") => void;
120
115
  }) {
121
- const [showHelp, setShowHelp] = createSignal(false);
122
116
  const [localFocused, setLocalFocused] = createSignal(false);
123
117
  const focused = () => props.model.focused?.() ?? localFocused();
124
118
  const [localCollapsed, setCollapsed] = createSignal<ReadonlySet<string>>(new Set());
@@ -133,16 +127,13 @@ export function ApplicationMachineSidebar(props: {
133
127
  const controlsHeight = () => (props.height >= 8 && controlGroup() ? 4 : 0);
134
128
  const searchHeight = () =>
135
129
  (props.model.onOpenSwitcher ? 1 : 0) + (props.model.onOpenAttention ? 1 : 0);
130
+ const [workingFocused, setWorkingFocused] = createSignal(true);
136
131
  const tabHeight = () =>
137
- Math.min(props.model.tabs?.().length ?? 0, Math.max(0, Math.floor(props.height / 3)));
138
- const visibleTabs = () => {
139
- const tabs = props.model.tabs?.() ?? [];
140
- const active = tabs.findIndex((tab) => tab.active);
141
- const start = Math.max(0, active - tabHeight() + 1);
142
- return tabs.slice(start, start + tabHeight());
143
- };
144
- const helpHeight = () =>
145
- showHelp() ? Math.min(5, Math.max(0, Math.floor(props.height / 3))) : 0;
132
+ (props.model.tabs?.().length ?? 0) > 0 && props.height >= 12
133
+ ? 1 +
134
+ Math.min(props.model.tabs!().length, Math.max(1, Math.floor((props.height / 2 - 1) / 3))) *
135
+ 3
136
+ : 0;
146
137
  const machineHeight = () =>
147
138
  Math.max(
148
139
  0,
@@ -152,7 +143,6 @@ export function ApplicationMachineSidebar(props: {
152
143
  agentHeight() -
153
144
  controlsHeight() -
154
145
  searchHeight() -
155
- helpHeight() -
156
146
  tabHeight(),
157
147
  );
158
148
  const active = (row: Row) =>
@@ -276,6 +266,7 @@ export function ApplicationMachineSidebar(props: {
276
266
  props.model.onCollapse?.(preferenceKey(group), value);
277
267
  };
278
268
  const activate = (row: Row, source: "keyboard" | "mouse") => {
269
+ setWorkingFocused(false);
279
270
  setSelectedKey(row.key);
280
271
  setLocalFocused(true);
281
272
  props.model.onFocus?.();
@@ -315,7 +306,7 @@ export function ApplicationMachineSidebar(props: {
315
306
  const rowHeight = (row: Row) =>
316
307
  (row.agent ? (row.agentHeading ? 3 : 2) : row.serverHeading ? 2 : 1) + sectionGap(row);
317
308
  const revealFocusedRow = () => {
318
- if (!focused() || !scroll) return;
309
+ if (!focused() || (workingFocused() && tabHeight()) || !scroll) return;
319
310
  const y = rows()
320
311
  .slice(0, index())
321
312
  .reduce((sum, row) => sum + rowHeight(row), 0);
@@ -333,6 +324,14 @@ export function ApplicationMachineSidebar(props: {
333
324
  useKeyboardRoute((event) => {
334
325
  if (!focused() || event.eventType !== "press" || event.meta) return false;
335
326
  let key = event.name.toLowerCase();
327
+ if (key === "tab" && tabHeight()) {
328
+ setWorkingFocused((value) => !value);
329
+ event.preventDefault();
330
+ event.stopPropagation();
331
+ return true;
332
+ }
333
+ if (workingFocused() && tabHeight() && !["escape", "?", "question", "/", "a"].includes(key))
334
+ return false;
336
335
  if (event.ctrl && key === "d") key = "halfdown";
337
336
  else if (event.ctrl && key === "u") key = "halfup";
338
337
  else if (!event.ctrl && !event.meta)
@@ -345,7 +344,7 @@ export function ApplicationMachineSidebar(props: {
345
344
  l: "right",
346
345
  g: event.shift ? "end" : "home",
347
346
  "/": "search",
348
- "?": "help",
347
+ [SIDEBAR_ACTIONS.help.key]: "help",
349
348
  question: "help",
350
349
  } as Record<string, string>
351
350
  )[key] ?? key;
@@ -368,9 +367,9 @@ export function ApplicationMachineSidebar(props: {
368
367
  "return",
369
368
  "space",
370
369
  "escape",
371
- "a",
372
- "r",
373
- "d",
370
+ SIDEBAR_ACTIONS.add.key,
371
+ SIDEBAR_ACTIONS.retry.key,
372
+ SIDEBAR_ACTIONS.disconnect.key,
374
373
  "f",
375
374
  ].includes(key)
376
375
  )
@@ -378,7 +377,7 @@ export function ApplicationMachineSidebar(props: {
378
377
  event.preventDefault();
379
378
  event.stopPropagation();
380
379
  if (key === "help") {
381
- setShowHelp(!showHelp());
380
+ props.onHelp?.("keyboard");
382
381
  return true;
383
382
  }
384
383
  if (key === "search") {
@@ -386,15 +385,11 @@ export function ApplicationMachineSidebar(props: {
386
385
  return true;
387
386
  }
388
387
  if (key === "escape") {
389
- if (showHelp()) {
390
- setShowHelp(false);
391
- return true;
392
- }
393
388
  setLocalFocused(false);
394
389
  props.model.onBlur?.();
395
390
  return true;
396
391
  }
397
- if (key === "a") {
392
+ if (key === SIDEBAR_ACTIONS.add.key) {
398
393
  props.model.onAddMachine?.();
399
394
  return true;
400
395
  }
@@ -409,9 +404,9 @@ export function ApplicationMachineSidebar(props: {
409
404
  );
410
405
  return true;
411
406
  }
412
- if (key === "r" || key === "d") {
407
+ if (key === SIDEBAR_ACTIONS.retry.key || key === SIDEBAR_ACTIONS.disconnect.key) {
413
408
  if (row.group.id !== "local") {
414
- if (key === "r") props.model.onRetryMachine?.(row.group.id);
409
+ if (key === SIDEBAR_ACTIONS.retry.key) props.model.onRetryMachine?.(row.group.id);
415
410
  else props.model.onDisconnectMachine?.(row.group.id);
416
411
  }
417
412
  return true;
@@ -460,43 +455,43 @@ export function ApplicationMachineSidebar(props: {
460
455
  overflow="hidden"
461
456
  >
462
457
  <box height={1} flexShrink={0} flexDirection="row" overflow="hidden">
463
- <text fg={props.theme.roles.text.secondary}>{" Machines"}</text>
458
+ <text fg={props.theme.roles.text.secondary}>
459
+ {tabHeight() ? " Workspace" : " Machines"}
460
+ </text>
464
461
  <box flexGrow={1} />
465
462
  <Show when={props.model.tabs?.().length && props.width >= 28}>
466
- <KeyHint theme={props.theme} keys="F9" label="Tabs" quiet />
463
+ <KeyHint
464
+ theme={props.theme}
465
+ keys={CHROME_ACTIONS.tabs.keys}
466
+ label={CHROME_ACTIONS.tabs.label}
467
+ quiet
468
+ />
467
469
  </Show>
468
470
  <KeyHint
469
471
  theme={props.theme}
470
- keys="?"
471
- label={props.width >= 20 ? "Help" : undefined}
472
+ keys={SIDEBAR_ACTIONS.help.keys}
473
+ label={props.width >= 20 ? SIDEBAR_ACTIONS.help.label : undefined}
472
474
  quiet
473
475
  button
474
- onPress={() => setShowHelp(!showHelp())}
476
+ onPress={() => props.onHelp?.("mouse")}
475
477
  />
476
478
  </box>
477
- <For each={visibleTabs()}>
478
- {(tab) => (
479
- <box height={1} flexDirection="row">
480
- <NavigationRow
481
- theme={props.theme}
482
- width={Math.max(1, props.width - 4)}
483
- id={`fleet-tab:${tab.key}`}
484
- label={`${tab.hostLabel} / ${tab.label}`}
485
- marker={tab.active ? "●" : "○"}
486
- detail={tab.available ? "" : "offline"}
487
- focused={false}
488
- onActivate={() => props.model.onOpenTab?.(tab.key)}
489
- />
490
- <TuiButton
491
- theme={props.theme}
492
- label="×"
493
- size="compact"
494
- width={3}
495
- onPress={() => props.model.onCloseTab?.(tab.key)}
496
- />
497
- </box>
498
- )}
499
- </For>
479
+ <Show when={tabHeight() > 0}>
480
+ <WorkingSessions
481
+ theme={props.theme}
482
+ rows={props.model.tabs?.() ?? []}
483
+ width={props.width}
484
+ height={tabHeight()}
485
+ focused={focused() && workingFocused()}
486
+ onFocus={() => {
487
+ setWorkingFocused(true);
488
+ setLocalFocused(true);
489
+ props.model.onFocus?.();
490
+ }}
491
+ onOpen={(key) => props.model.onOpenTab?.(key)}
492
+ onClose={props.model.onCloseTab}
493
+ />
494
+ </Show>
500
495
  <scrollbox
501
496
  ref={(value) => {
502
497
  scroll = value;
@@ -619,7 +614,9 @@ export function ApplicationMachineSidebar(props: {
619
614
  : `${connectionDetail(row.group)}${row.group.agents?.length ? " ?" : ""}`
620
615
  }
621
616
  selected={Boolean((row.session || row.agent) && active(row))}
622
- focused={Boolean(focused() && row.key === selectedKey())}
617
+ focused={Boolean(
618
+ focused() && !(workingFocused() && tabHeight()) && row.key === selectedKey(),
619
+ )}
623
620
  status={
624
621
  (row.agent?.attention ?? activity(row).kind === "attention")
625
622
  ? "blocked"
@@ -657,18 +654,11 @@ export function ApplicationMachineSidebar(props: {
657
654
  </scrollbox>
658
655
  )}
659
656
  </For>
660
- <Show when={helpHeight() > 0}>
661
- <text height={helpHeight()} fg={props.theme.roles.text.secondary}>
662
- {
663
- "j/k ↑↓ move · g/G ends\nPgUp/Dn · Ctrl-U/D half\nh/l ←→ collapse/expand\nEnter open · / search\nf favorite · Esc terminal"
664
- }
665
- </text>
666
- </Show>
667
657
  <Show when={props.model.onOpenSwitcher}>
668
658
  <KeyHint
669
659
  theme={props.theme}
670
- keys="F6"
671
- label="Sessions"
660
+ keys={CHROME_ACTIONS.sessions.keys}
661
+ label="Browse all sessions"
672
662
  width={props.width}
673
663
  quiet
674
664
  button
@@ -678,8 +668,8 @@ export function ApplicationMachineSidebar(props: {
678
668
  <Show when={props.model.onOpenAttention}>
679
669
  <KeyHint
680
670
  theme={props.theme}
681
- keys="F7"
682
- label={`Attention (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`}
671
+ keys={CHROME_ACTIONS.attention.keys}
672
+ label={`${CHROME_ACTIONS.attention.label} (${props.model.groups().reduce((sum, group) => sum + (group.state === "ready" ? (group.agents ?? []).filter((agent) => agent.attention && !agent.disabled).length : 0), 0)})`}
683
673
  width={props.width}
684
674
  quiet
685
675
  button
@@ -696,8 +686,8 @@ export function ApplicationMachineSidebar(props: {
696
686
  </text>
697
687
  <KeyHint
698
688
  theme={props.theme}
699
- keys="R"
700
- label="Retry connection"
689
+ keys={SIDEBAR_ACTIONS.retry.keys}
690
+ label={SIDEBAR_ACTIONS.retry.label}
701
691
  width={props.width}
702
692
  quiet
703
693
  button
@@ -705,8 +695,8 @@ export function ApplicationMachineSidebar(props: {
705
695
  />
706
696
  <KeyHint
707
697
  theme={props.theme}
708
- keys="D"
709
- label="Disconnect"
698
+ keys={SIDEBAR_ACTIONS.disconnect.keys}
699
+ label={SIDEBAR_ACTIONS.disconnect.label}
710
700
  width={props.width}
711
701
  quiet
712
702
  button
@@ -719,8 +709,8 @@ export function ApplicationMachineSidebar(props: {
719
709
  {(add) => (
720
710
  <KeyHint
721
711
  theme={props.theme}
722
- keys="A"
723
- label="Add machine"
712
+ keys={SIDEBAR_ACTIONS.add.keys}
713
+ label={SIDEBAR_ACTIONS.add.label}
724
714
  width={props.width}
725
715
  quiet
726
716
  button
@@ -39,7 +39,7 @@ export function createApplicationPaletteCommandOwner(options: {
39
39
  readonly disabledReason?: (command: ApplicationPaletteCommand) => string | null;
40
40
  readonly targetKey?: () => string;
41
41
  }) {
42
- const [referencePage, setReferencePage] = createSignal<"shortcuts" | "changes">();
42
+ const [referencePage, setReferencePage] = createSignal<"shortcuts" | "changes" | "help">();
43
43
  const [closeArmed, setCloseArmed] = createSignal(false);
44
44
  const [busy, setBusy] = createSignal(false);
45
45
  let armedTarget: string | undefined;
@@ -66,9 +66,11 @@ export function createApplicationPaletteCommandOwner(options: {
66
66
  confirmed = false,
67
67
  ): void => {
68
68
  if (busy()) return;
69
- if (command === "shortcuts" || command === "whats-new") {
69
+ if (command === "help" || command === "shortcuts" || command === "whats-new") {
70
70
  setCloseArmed(false);
71
- setReferencePage(command === "shortcuts" ? "shortcuts" : "changes");
71
+ setReferencePage(
72
+ command === "help" ? "help" : command === "shortcuts" ? "shortcuts" : "changes",
73
+ );
72
74
  return;
73
75
  }
74
76
  if (command === "switch-session") {
@@ -26,6 +26,7 @@ const BASE_COMMANDS: readonly ApplicationPaletteCommand[] = [
26
26
  "close-pane",
27
27
  "zoom-pane",
28
28
  "appearance",
29
+ "help",
29
30
  "shortcuts",
30
31
  "whats-new",
31
32
  "switch-session",
@@ -7,6 +7,7 @@ import { OverlaySearchField } from "../ui/overlay-search-field.tsx";
7
7
  import { overlaySurfaceMetrics } from "../ui/overlay-model.ts";
8
8
  import { useKeyboardRoute, usePasteRoute } from "../ui/keyboard-router.tsx";
9
9
  import { clipTerminal, terminalDisplayWidth } from "../terminal-text.ts";
10
+ import { CHROME_ACTIONS } from "../workspace/application-action-descriptions.ts";
10
11
  import { APPLICATION_SHORTCUTS } from "../workspace/application-shortcuts.ts";
11
12
  import { commandSearchMatch } from "../workspace/application-command-description.ts";
12
13
  import {
@@ -15,6 +16,24 @@ import {
15
16
  } from "./application-pane-rename-input.ts";
16
17
 
17
18
  const releases = [
19
+ {
20
+ version: "2.9.0-beta.41",
21
+ lines: [
22
+ "Working sessions with clear machine/server context and shared status icons.",
23
+ "New-result markers for observed background completions; open to acknowledge.",
24
+ "Tab switches sidebar sections. Browse all sessions with F6.",
25
+ "Using tmux-ide in Commands, with reversible Help and shortcut navigation.",
26
+ ],
27
+ },
28
+ {
29
+ version: "2.9.0-beta.40",
30
+ lines: [
31
+ "Using tmux-ide: a scrollable guide to machines, sessions and agents.",
32
+ "Home filters for All, Working and Needs attention, with a clear Open action.",
33
+ "Consistent shortcut buttons in the sidebar and footer.",
34
+ "Segmented pane headers and dimmed backgrounds for dialogs.",
35
+ ],
36
+ },
18
37
  {
19
38
  version: "2.9.0-beta.30",
20
39
  lines: [
@@ -44,7 +63,7 @@ const releases = [
44
63
  type Row = { kind: "heading" | "text" | "action" | "gap"; label: string; detail?: string };
45
64
 
46
65
  export function ApplicationReferenceSheet(props: {
47
- page: "shortcuts" | "changes";
66
+ page: "shortcuts" | "changes" | "help";
48
67
  width: number;
49
68
  height: number;
50
69
  theme: SemanticThemeSnapshot;
@@ -78,7 +97,55 @@ export function ApplicationReferenceSheet(props: {
78
97
  }
79
98
  if (!result.length) result.push({ kind: "text", label: "No matching shortcuts" });
80
99
  } else {
81
- for (const release of releases) {
100
+ const sections =
101
+ page() === "help"
102
+ ? [
103
+ {
104
+ version: "Find your work",
105
+ lines: [
106
+ "Machines are your local computer and connected SSH hosts. Each session contains terminal windows and panes.",
107
+ "Open a session or agent in the sidebar to jump to its terminal. Home brings agents from your connected machines together.",
108
+ ],
109
+ },
110
+ {
111
+ version: "Move around",
112
+ lines: [
113
+ [
114
+ CHROME_ACTIONS.home,
115
+ CHROME_ACTIONS.terminals,
116
+ CHROME_ACTIONS.sessions,
117
+ CHROME_ACTIONS.attention,
118
+ ]
119
+ .map((action) => `${action.keys} ${action.label}`)
120
+ .join(" · "),
121
+ `${CHROME_ACTIONS.sidebar.keys} shows or hides the sidebar. ${CHROME_ACTIONS.commands.keys} opens Commands for actions, appearance and keyboard shortcuts.`,
122
+ ],
123
+ },
124
+ {
125
+ version: "Use the sidebar",
126
+ lines: [
127
+ "Click a row to open it. With the sidebar focused, use ↑↓ or j/k to move and Enter to open.",
128
+ "←→ or h/l collapse/expand machines. / searches sessions. f toggles a session favorite. Esc returns to the terminal.",
129
+ "PgUp/PgDn scroll a page; Ctrl-U/D scroll half a page. g/G jump to the first or last row.",
130
+ ],
131
+ },
132
+ {
133
+ version: "Follow your agents",
134
+ lines: [
135
+ "On Home, search by name and choose All, Working or Needs attention. Select an agent and press Enter to open its pane.",
136
+ "Unavailable means the connection or observation is missing; it does not mean the agent has stopped.",
137
+ ],
138
+ },
139
+ {
140
+ version: "Connect another machine",
141
+ lines: [
142
+ "Choose Add machine in the sidebar and enter an SSH alias or user@host. Set up SSH key access and host trust first.",
143
+ "Run tmux-ide --headless on the remote machine before connecting.",
144
+ ],
145
+ },
146
+ ]
147
+ : releases;
148
+ for (const release of sections) {
82
149
  if (result.length) result.push({ kind: "gap", label: "" });
83
150
  result.push({ kind: "heading", label: release.version });
84
151
  for (const line of release.lines) {
@@ -115,7 +182,8 @@ export function ApplicationReferenceSheet(props: {
115
182
  const key = event.name.toLowerCase();
116
183
  if (key === "escape") props.onClose();
117
184
  else if (key === "tab") {
118
- setPage(page() === "shortcuts" ? "changes" : "shortcuts");
185
+ const pages = ["help", "shortcuts", "changes"] as const;
186
+ setPage(pages[(pages.indexOf(page()) + (event.shift ? 2 : 1)) % pages.length]!);
119
187
  setOffset(0);
120
188
  } else if (key === "down") move(1);
121
189
  else if (key === "up") move(-1);
@@ -139,8 +207,14 @@ export function ApplicationReferenceSheet(props: {
139
207
  viewportHeight={props.height}
140
208
  width={metrics().width}
141
209
  height={metrics().height}
142
- title={page() === "shortcuts" ? "Keyboard shortcuts" : "What's new"}
143
- footer="Tab switch sheet · ↑↓ scroll · Esc back"
210
+ title={
211
+ page() === "help"
212
+ ? "Using tmux-ide"
213
+ : page() === "shortcuts"
214
+ ? "Keyboard shortcuts"
215
+ : "What's new"
216
+ }
217
+ footer={`Tab ${page() === "help" ? "shortcuts" : page() === "shortcuts" ? "what’s new" : "help"} · ↑↓ scroll · Esc back`}
144
218
  zIndex={100}
145
219
  onDismiss={props.onClose}
146
220
  >
@@ -160,6 +234,7 @@ export function ApplicationReferenceSheet(props: {
160
234
  flexDirection="column"
161
235
  overflow="hidden"
162
236
  onMouseScroll={(event) => {
237
+ if (!event.scroll) return;
163
238
  event.preventDefault();
164
239
  move(event.scroll.direction === "up" ? -3 : 3);
165
240
  }}
@@ -51,8 +51,8 @@ export interface ApplicationCatalogShellProps {
51
51
  readonly paletteSelection?: Accessor<number>;
52
52
  readonly palettePreviewActive?: Accessor<boolean>;
53
53
  readonly onPaletteModalChange?: (open: boolean) => void;
54
- readonly paletteReferencePage?: Accessor<"shortcuts" | "changes" | undefined>;
55
- readonly onPaletteReferenceChange?: (page: "shortcuts" | "changes" | undefined) => void;
54
+ readonly paletteReferencePage?: Accessor<"shortcuts" | "changes" | "help" | undefined>;
55
+ readonly onPaletteReferenceChange?: (page: "shortcuts" | "changes" | "help" | undefined) => void;
56
56
  readonly paletteKeyboardHint?: Accessor<string>;
57
57
  readonly paletteQuery?: Accessor<string>;
58
58
  readonly paletteDisabledReason?: (command: ApplicationPaletteCommand) => string | null;
@@ -418,6 +418,10 @@ export function ApplicationCatalogShell(props: ApplicationCatalogShellProps): JS
418
418
  {(model) => (
419
419
  <ApplicationMachineSidebar
420
420
  model={model()}
421
+ onHelp={(source) => {
422
+ props.onSetPaletteOpen(true, source);
423
+ props.onPaletteReferenceChange?.("help");
424
+ }}
421
425
  width={chrome().sidebar.width}
422
426
  height={chrome().sidebar.height}
423
427
  theme={props.theme}
@@ -106,8 +106,8 @@ export function MinimalPalette(props: {
106
106
  readonly title?: string;
107
107
  readonly onViewport?: (rows: number) => void;
108
108
  readonly onFavorite?: (command: ApplicationPaletteCommand) => void;
109
- readonly referencePage?: "shortcuts" | "changes";
110
- readonly onReferenceChange?: (page: "shortcuts" | "changes" | undefined) => void;
109
+ readonly referencePage?: "shortcuts" | "changes" | "help";
110
+ readonly onReferenceChange?: (page: "shortcuts" | "changes" | "help" | undefined) => void;
111
111
  readonly keyboardHint?: string;
112
112
  readonly previewActive?: boolean;
113
113
  readonly onModalChange?: (open: boolean) => void;
@@ -123,8 +123,8 @@ export function MinimalPalette(props: {
123
123
  }): JSX.Element {
124
124
  const [expanded, setExpanded] = createSignal(false);
125
125
  const [modal, setModal] = createSignal(false);
126
- const [sheet, setSheet] = createSignal<"shortcuts" | "changes">();
127
- const openSheet = (page: "shortcuts" | "changes" | undefined) => {
126
+ const [sheet, setSheet] = createSignal<"shortcuts" | "changes" | "help">();
127
+ const openSheet = (page: "shortcuts" | "changes" | "help" | undefined) => {
128
128
  props.onReferenceChange?.(page);
129
129
  setSheet(page);
130
130
  setModal(!!page);
@@ -344,7 +344,8 @@ export function MinimalPalette(props: {
344
344
  onPress={() => {
345
345
  props.onSelect?.(row().index);
346
346
  const command = row().command;
347
- if (command === "shortcuts" || command === "whats-new")
347
+ if (command === "help") openSheet("help");
348
+ else if (command === "shortcuts" || command === "whats-new")
348
349
  openSheet(PALETTE_REFERENCE_COMMANDS[command].page);
349
350
  else props.onActivate(command);
350
351
  }}
@@ -100,8 +100,8 @@ export interface ApplicationShellViewProps {
100
100
  readonly paletteOpen: Accessor<boolean>;
101
101
  readonly paneRenameDialog?: Accessor<ApplicationPaneRenameDraft | null>;
102
102
  readonly paletteSelection?: Accessor<number>;
103
- readonly paletteReferencePage?: Accessor<"shortcuts" | "changes" | undefined>;
104
- readonly onPaletteReferenceChange?: (page: "shortcuts" | "changes" | undefined) => void;
103
+ readonly paletteReferencePage?: Accessor<"shortcuts" | "changes" | "help" | undefined>;
104
+ readonly onPaletteReferenceChange?: (page: "shortcuts" | "changes" | "help" | undefined) => void;
105
105
  readonly paletteKeyboardHint?: Accessor<string>;
106
106
  readonly palettePreviewActive?: Accessor<boolean>;
107
107
  readonly onPaletteModalChange?: (open: boolean) => void;
@@ -455,6 +455,10 @@ export function ApplicationShellView(props: ApplicationShellViewProps): JSX.Elem
455
455
  {(model) => (
456
456
  <ApplicationMachineSidebar
457
457
  model={model()}
458
+ onHelp={(source) => {
459
+ props.onSetPaletteOpen(true, source);
460
+ props.onPaletteReferenceChange?.("help");
461
+ }}
458
462
  agentRows={
459
463
  model()
460
464
  .groups()
@@ -2,6 +2,8 @@ import { batch, createEffect, createSignal, onCleanup } from "solid-js";
2
2
  import type { AgentActivity } from "@tmux-ide/contracts";
3
3
  import type { SemanticThemeSnapshot } from "../theme.ts";
4
4
 
5
+ import { UI_ICONS } from "./icons.ts";
6
+
5
7
  const FRAMES = ["⠋", "⠙", "⠹", "⠸", "⠼", "⠴", "⠦", "⠧", "⠇", "⠏"];
6
8
  const listeners = new Set<(frame: number) => void>();
7
9
  let timer: ReturnType<typeof setInterval> | undefined;
@@ -46,24 +48,24 @@ export function createAgentStatusMarker(options: {
46
48
  if (animated()) onCleanup(subscribe(setCurrent));
47
49
  });
48
50
  return () => {
49
- if (options.unavailable?.()) return "·";
50
- if (options.attention?.()) return "!";
51
+ if (options.unavailable?.()) return UI_ICONS.unknown;
52
+ if (options.attention?.()) return UI_ICONS.attention;
51
53
  switch (options.status()) {
52
54
  case "running":
53
55
  case "working":
54
56
  return animated() ? FRAMES[current()]! : options.theme().glyphs.active;
55
57
  case "waiting":
56
58
  case "blocked":
57
- return "!";
59
+ return UI_ICONS.attention;
58
60
  case "complete":
59
61
  case "done":
60
62
  return options.theme().glyphs.check;
61
63
  case "failed":
62
- return "×";
64
+ return UI_ICONS.failed;
63
65
  case "idle":
64
66
  return options.theme().glyphs.inactive;
65
67
  default:
66
- return "·";
68
+ return UI_ICONS.unknown;
67
69
  }
68
70
  };
69
71
  }
@@ -0,0 +1,9 @@
1
+ /** Shared semantic glyphs; theme-specific active/idle/check glyphs stay in the theme. */
2
+ export const UI_ICONS = {
3
+ attention: "!",
4
+ failed: "×",
5
+ unknown: "·",
6
+ offline: "○",
7
+ newResult: "•",
8
+ close: "×",
9
+ } as const;
@@ -57,3 +57,7 @@ export {
57
57
  type StatusBarSegmentProps,
58
58
  type StatusSegmentProps,
59
59
  } from "./status-bar.tsx";
60
+
61
+ export { UI_ICONS } from "./icons.ts";
62
+ export { SessionRow, sessionRowStatus, type SessionRowModel } from "./session-row.tsx";
63
+ export { WorkingSessions } from "./working-sessions.tsx";