@brimveyn/aimux 1.2.5 → 1.3.1

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 (59) hide show
  1. package/README.md +29 -8
  2. package/package.json +5 -5
  3. package/src/app-runtime/backend-runtime-events.ts +16 -1
  4. package/src/app-runtime/pty-write.ts +7 -4
  5. package/src/app-runtime/side-effects.ts +62 -4
  6. package/src/app-runtime/snippet-actions.ts +3 -2
  7. package/src/app-runtime/use-backend-runtime.ts +7 -2
  8. package/src/app-runtime/use-renderer-bindings.ts +2 -2
  9. package/src/app-runtime/use-terminal-resize.ts +15 -6
  10. package/src/app.tsx +89 -30
  11. package/src/config/loader.ts +2 -2
  12. package/src/config.ts +14 -3
  13. package/src/daemon/daemon.ts +279 -118
  14. package/src/daemon/runtime-paths.ts +34 -2
  15. package/src/daemon/session-manager.ts +20 -5
  16. package/src/daemon/session-registry.ts +18 -11
  17. package/src/index.tsx +19 -4
  18. package/src/input/keymap/describe-bindings.ts +68 -0
  19. package/src/input/keymap/key-format.ts +67 -0
  20. package/src/input/modes/bridge.ts +1 -0
  21. package/src/input/modes/transitions.ts +2 -0
  22. package/src/input/modes/types.ts +2 -0
  23. package/src/ipc/manager-protocol.ts +396 -0
  24. package/src/ipc/protocol.ts +98 -5
  25. package/src/platform/daemon-control.ts +42 -11
  26. package/src/profile-paths.ts +27 -0
  27. package/src/pty/pty-manager.ts +53 -3
  28. package/src/restart-daemon.ts +10 -10
  29. package/src/session-backend/bootstrap.ts +197 -46
  30. package/src/session-backend/local-session-backend.ts +12 -5
  31. package/src/session-backend/remote-session-backend.ts +143 -63
  32. package/src/session-backend/types.ts +4 -2
  33. package/src/state/reducers/modal-state.ts +18 -1
  34. package/src/state/reducers/tab-state.ts +20 -2
  35. package/src/state/session-catalog.ts +5 -4
  36. package/src/state/session-persistence.ts +9 -2
  37. package/src/state/snippet-catalog.ts +4 -4
  38. package/src/state/types.ts +23 -0
  39. package/src/state/validation.ts +8 -0
  40. package/src/terminal-manager/manager-client.ts +384 -0
  41. package/src/terminal-manager/terminal-manager.ts +288 -0
  42. package/src/ui/components/create-session-modal.tsx +3 -5
  43. package/src/ui/components/git-commit-modal.tsx +3 -5
  44. package/src/ui/components/help-modal.tsx +49 -68
  45. package/src/ui/components/list-item.tsx +24 -5
  46. package/src/ui/components/new-tab-modal.tsx +8 -9
  47. package/src/ui/components/pending-chord-overlay.tsx +28 -0
  48. package/src/ui/components/session-name-modal.tsx +3 -5
  49. package/src/ui/components/session-picker-modal.tsx +3 -1
  50. package/src/ui/components/snippet-editor-modal.tsx +3 -1
  51. package/src/ui/components/snippet-picker-modal.tsx +3 -1
  52. package/src/ui/components/status-bar.tsx +6 -2
  53. package/src/ui/components/theme-picker-modal.tsx +3 -6
  54. package/src/ui/components/update-available-modal.tsx +42 -0
  55. package/src/ui/keymap-context.ts +39 -0
  56. package/src/ui/root.tsx +12 -0
  57. package/src/ui/status-bar-model.ts +67 -39
  58. package/src/update/version-check.ts +67 -0
  59. package/src/update.ts +3 -3
@@ -1,5 +1,6 @@
1
1
  import type { DirectoryResult } from '../../state/types'
2
2
 
3
+ import { useModalHelp } from '../keymap-context'
3
4
  import { abbreviatePath } from '../path-format'
4
5
  import { theme } from '../theme'
5
6
  import { uiTokens } from '../ui-tokens'
@@ -50,13 +51,10 @@ export function CreateSessionModal({
50
51
  }: CreateSessionModalProps) {
51
52
  const dirActive = activeField === 'directory'
52
53
  const nameActive = activeField === 'name'
54
+ const help = useModalHelp('modal.create-session')
53
55
 
54
56
  return (
55
- <ModalShell
56
- title="Create session"
57
- help="Tab switch field. Ctrl+n/p nav. Esc cancel."
58
- width={uiTokens.modalWidth.xl}
59
- >
57
+ <ModalShell title="Create session" help={help} width={uiTokens.modalWidth.xl}>
60
58
  <box flexDirection="column">
61
59
  <text fg={dirActive ? theme.text : theme.textMuted}>Search projects</text>
62
60
  <InputField
@@ -1,3 +1,4 @@
1
+ import { useModalHelp } from '../keymap-context'
1
2
  import { theme } from '../theme'
2
3
  import { uiTokens } from '../ui-tokens'
3
4
  import { InputField } from './input-field'
@@ -13,13 +14,10 @@ interface GitCommitModalProps {
13
14
  export function GitCommitModal({ activeField, body, cursorPos, title }: GitCommitModalProps) {
14
15
  const titleActive = activeField === 'title'
15
16
  const bodyActive = activeField === 'body'
17
+ const help = useModalHelp('modal.git-commit')
16
18
 
17
19
  return (
18
- <ModalShell
19
- title="Commit"
20
- help="Tab switch · ←→ move cursor · Enter newline (body) · Ctrl+Enter commit · Esc cancel"
21
- width={uiTokens.modalWidth.xl}
22
- >
20
+ <ModalShell title="Commit" help={help} width={uiTokens.modalWidth.xl}>
23
21
  <box flexDirection="column">
24
22
  <text fg={titleActive ? theme.text : theme.textMuted}>Title</text>
25
23
  <InputField
@@ -1,79 +1,60 @@
1
+ import type { ModeId } from '@brimveyn/aimux-config'
2
+
3
+ import { describeBindings, groupDescribedBindings } from '../../input/keymap/describe-bindings'
4
+ import { useKeymap, useModalHelp } from '../keymap-context'
1
5
  import { theme } from '../theme'
2
6
  import { uiTokens } from '../ui-tokens'
3
7
  import { ModalShell } from './modal-shell'
4
8
 
5
- const SECTIONS = [
6
- {
7
- bindings: [
8
- ['j / k', 'Move between tabs'],
9
- ['Shift+J / K', 'Reorder tabs'],
10
- ['i', 'Focus terminal input'],
11
- ['r', 'Rename tab'],
12
- ['?', 'Show this help'],
13
- ],
14
- title: 'Navigation',
15
- },
16
- {
17
- bindings: [
18
- ['Ctrl+n', 'New tab'],
19
- ['Ctrl+w', 'Close tab'],
20
- ['Ctrl+r', 'Restart tab'],
21
- ['Ctrl+g', 'Session picker'],
22
- ['Ctrl+s', 'Snippet picker'],
23
- ['Ctrl+t', 'Theme picker'],
24
- ],
25
- title: 'Tabs & Sessions',
26
- },
27
- {
28
- bindings: [
29
- ['Ctrl+b', 'Toggle sidebar'],
30
- ['Ctrl+h / l', 'Resize sidebar'],
31
- ['Shift+G', 'Toggle git panel'],
32
- ['Ctrl+j / k', 'Resize git panel'],
33
- ],
34
- title: 'Sidebar',
35
- },
36
- {
37
- bindings: [
38
- ['Ctrl+z', 'Back to navigation'],
39
- ['Ctrl+w', 'Enter layout mode'],
40
- ['Ctrl+b', 'Toggle sidebar'],
41
- ],
42
- title: 'Terminal Input',
43
- },
44
- {
45
- bindings: [
46
- ['|', 'Split vertical'],
47
- ['-', 'Split horizontal'],
48
- ['h / j / k / l', 'Focus pane'],
49
- ['Shift+H/J/K/L', 'Resize pane'],
50
- ['q', 'Close pane'],
51
- ['Esc', 'Cancel'],
52
- ],
53
- title: 'Layout Mode (Ctrl+w)',
54
- },
55
- {
56
- bindings: [['Ctrl+c', 'Quit']],
57
- title: 'General',
58
- },
59
- ] as const
9
+ interface ModeSection {
10
+ modeId: ModeId
11
+ title: string
12
+ }
13
+
14
+ const SECTIONS: ModeSection[] = [
15
+ { modeId: 'navigation', title: 'Navigation' },
16
+ { modeId: 'terminal-input', title: 'Terminal input' },
17
+ { modeId: 'layout', title: 'Layout' },
18
+ { modeId: 'git-mode', title: 'Git mode' },
19
+ ]
20
+
21
+ const KEYS_COLUMN_WIDTH = 22
60
22
 
61
23
  export function HelpModal() {
24
+ const config = useKeymap()
25
+ const help = useModalHelp('modal.help', 1)
26
+
62
27
  return (
63
- <ModalShell title="Keybindings" help="Press Esc to close." width={uiTokens.modalWidth.lg}>
64
- {SECTIONS.map((section) => (
65
- <box key={section.title} flexDirection="column">
66
- <text fg={theme.text}>{section.title}</text>
67
- {section.bindings.map(([key, desc]) => (
68
- <box key={key} flexDirection="row">
69
- <box width={18}>
70
- <text fg={theme.accentAlt}> {key}</text>
28
+ <ModalShell title="Keybindings" help={help} width={uiTokens.modalWidth.lg}>
29
+ {SECTIONS.map((section) => {
30
+ const bindings = describeBindings(config, section.modeId, {
31
+ dedupeByDescription: true,
32
+ withDescriptionOnly: true,
33
+ })
34
+ if (bindings.length === 0) return null
35
+ const groups = groupDescribedBindings(bindings)
36
+ return (
37
+ <box key={section.modeId} flexDirection="column">
38
+ <text fg={theme.text}>{section.title}</text>
39
+ {groups.map((group, groupIdx) => (
40
+ <box
41
+ key={`${section.modeId}-${group.group ?? 'root'}-${groupIdx}`}
42
+ flexDirection="column"
43
+ >
44
+ {group.group ? <text fg={theme.textMuted}> {group.group}</text> : null}
45
+ {group.bindings.map((binding) => (
46
+ <box key={`${binding.keys}-${binding.description}`} flexDirection="row">
47
+ <box width={KEYS_COLUMN_WIDTH}>
48
+ <text fg={theme.accentAlt}> {binding.keysDisplay}</text>
49
+ </box>
50
+ <text fg={theme.textMuted}>{binding.description ?? ''}</text>
51
+ </box>
52
+ ))}
71
53
  </box>
72
- <text fg={theme.textMuted}>{desc}</text>
73
- </box>
74
- ))}
75
- </box>
76
- ))}
54
+ ))}
55
+ </box>
56
+ )
57
+ })}
77
58
  </ModalShell>
78
59
  )
79
60
  }
@@ -3,24 +3,43 @@ import type { ReactNode } from 'react'
3
3
  import { theme } from '../theme'
4
4
  import { Surface } from './surface'
5
5
 
6
+ export type ListItemDirection = 'row' | 'column'
7
+
6
8
  interface ListItemProps {
7
9
  active: boolean
10
+ direction?: ListItemDirection
8
11
  leading?: ReactNode
9
12
  subtitle?: ReactNode
10
13
  title: ReactNode
11
14
  trailing?: ReactNode
12
15
  }
13
16
 
14
- export function ListItem({ active, leading, subtitle, title, trailing }: ListItemProps) {
17
+ export function ListItem({
18
+ active,
19
+ direction = 'column',
20
+ leading,
21
+ subtitle,
22
+ title,
23
+ trailing,
24
+ }: ListItemProps) {
25
+ const isRow = direction === 'row'
15
26
  return (
16
- <Surface tone={active ? 'selected' : 'elevated'} paddingLeft={1} paddingRight={1}>
27
+ <Surface
28
+ tone={active ? 'selected' : 'elevated'}
29
+ paddingLeft={isRow ? 2 : 1}
30
+ paddingRight={isRow ? 2 : 1}
31
+ >
17
32
  <box flexDirection="column">
18
33
  <box flexDirection="row">
19
- <text fg={active ? theme.accent : theme.dim}>{active ? '›' : '·'}</text>
20
- <text> </text>
34
+ {isRow ? null : (
35
+ <>
36
+ <text fg={active ? theme.accent : theme.dim}>{active ? '›' : '·'}</text>
37
+ <text> </text>
38
+ </>
39
+ )}
21
40
  {leading}
22
41
  {leading ? <text> </text> : null}
23
- <box flexGrow={1}>{title}</box>
42
+ <box flexGrow={isRow ? 0 : 1}>{title}</box>
24
43
  {trailing ? <box>{trailing}</box> : null}
25
44
  </box>
26
45
  {subtitle ? <box paddingLeft={2}>{subtitle}</box> : null}
@@ -1,4 +1,5 @@
1
1
  import { getAllAssistantOptions } from '../../pty/command-registry'
2
+ import { useModalHelp } from '../keymap-context'
2
3
  import { theme } from '../theme'
3
4
  import { uiTokens } from '../ui-tokens'
4
5
  import { InputField } from './input-field'
@@ -14,17 +15,15 @@ interface NewTabModalProps {
14
15
  export function NewTabModal({ customCommands, editBuffer, selectedIndex }: NewTabModalProps) {
15
16
  const options = getAllAssistantOptions(customCommands)
16
17
  const selectedOption = options[selectedIndex]
18
+ const editingHelp = useModalHelp('modal.new-tab.command-edit')
19
+ const browsingHelp = useModalHelp('modal.new-tab')
20
+ const help =
21
+ editBuffer !== null
22
+ ? `Editing command for ${selectedOption?.label}. ${editingHelp}`
23
+ : browsingHelp
17
24
 
18
25
  return (
19
- <ModalShell
20
- title="New assistant tab"
21
- help={
22
- editBuffer !== null
23
- ? `Editing command for ${selectedOption?.label}. Enter to confirm, Esc to cancel.`
24
- : 'Use j/k or arrows, Enter to confirm, e to edit command, Esc to cancel.'
25
- }
26
- width={uiTokens.modalWidth.md}
27
- >
26
+ <ModalShell title="New assistant tab" help={help} width={uiTokens.modalWidth.md}>
28
27
  {editBuffer !== null ? (
29
28
  <InputField active value={editBuffer} />
30
29
  ) : (
@@ -0,0 +1,28 @@
1
+ import { parseKeyNotation } from '../../input/keymap/key-chord'
2
+ import { formatChord } from '../../input/keymap/key-format'
3
+ import { useAppStore } from '../../state/app-store'
4
+ import { useKeymap } from '../keymap-context'
5
+ import { theme } from '../theme'
6
+ import { Surface } from './surface'
7
+
8
+ export function PendingChordOverlay() {
9
+ const pendingChords = useAppStore((s) => s.pendingChords)
10
+ const config = useKeymap()
11
+
12
+ if (!pendingChords || pendingChords.length === 0) return null
13
+
14
+ const leaderChord = parseKeyNotation(config.leader)[0]
15
+ const display = pendingChords.map((c) => formatChord(c, leaderChord)).join(' ')
16
+
17
+ return (
18
+ <box position="absolute" bottom={2} right={1}>
19
+ <Surface tone="elevated" paddingLeft={1} paddingRight={1}>
20
+ <box flexDirection="row">
21
+ <text fg={theme.textMuted}>pending: </text>
22
+ <text fg={theme.accent}>{display}</text>
23
+ <text fg={theme.textMuted}> …</text>
24
+ </box>
25
+ </Surface>
26
+ </box>
27
+ )
28
+ }
@@ -1,14 +1,12 @@
1
+ import { useModalHelp } from '../keymap-context'
1
2
  import { uiTokens } from '../ui-tokens'
2
3
  import { InputField } from './input-field'
3
4
  import { ModalShell } from './modal-shell'
4
5
 
5
6
  export function SessionNameModal({ title, value }: { title: string; value: string }) {
7
+ const help = useModalHelp('modal.session-name')
6
8
  return (
7
- <ModalShell
8
- title={title}
9
- help="Type a session name. Enter confirm, Esc cancel."
10
- width={uiTokens.modalWidth.md}
11
- >
9
+ <ModalShell title={title} help={help} width={uiTokens.modalWidth.md}>
12
10
  <InputField active value={value} />
13
11
  </ModalShell>
14
12
  )
@@ -1,6 +1,7 @@
1
1
  import type { SessionRecord } from '../../state/types'
2
2
 
3
3
  import { filterSessions } from '../../state/selectors'
4
+ import { useModalHelp } from '../keymap-context'
4
5
  import { abbreviatePath } from '../path-format'
5
6
  import { theme } from '../theme'
6
7
  import { uiTokens } from '../ui-tokens'
@@ -47,11 +48,12 @@ export function SessionPickerModal({
47
48
  const hasFilter = !!filter
48
49
  const showFilteredEmptyState = filtered.length === 0 && sessions.length > 0
49
50
  const showInitialEmptyState = filtered.length === 0 && sessions.length === 0
51
+ const help = useModalHelp('modal.session-picker')
50
52
 
51
53
  return (
52
54
  <ModalShell
53
55
  title="Sessions"
54
- help="j/k move, Enter resume, n new, r rename, d delete, / filter, Esc cancel."
56
+ help={help}
55
57
  width={uiTokens.modalWidth.lg}
56
58
  footer={<ModalFilterBar filter={filter} />}
57
59
  >
@@ -1,3 +1,4 @@
1
+ import { useModalHelp } from '../keymap-context'
1
2
  import { theme } from '../theme'
2
3
  import { uiTokens } from '../ui-tokens'
3
4
  import { InputField } from './input-field'
@@ -18,11 +19,12 @@ export function SnippetEditorModal({
18
19
  }: SnippetEditorModalProps) {
19
20
  const nameActive = activeField === 'name'
20
21
  const contentActive = activeField === 'content'
22
+ const help = useModalHelp('modal.snippet-editor')
21
23
 
22
24
  return (
23
25
  <ModalShell
24
26
  title={isEditing ? 'Edit snippet' : 'Create snippet'}
25
- help="Tab switch field. Enter on content to save. Esc cancel."
27
+ help={help}
26
28
  width={uiTokens.modalWidth.xl}
27
29
  >
28
30
  <box flexDirection="column">
@@ -1,6 +1,7 @@
1
1
  import type { SnippetRecord } from '../../state/types'
2
2
 
3
3
  import { filterSnippets } from '../../state/selectors'
4
+ import { useModalHelp } from '../keymap-context'
4
5
  import { theme } from '../theme'
5
6
  import { uiTokens } from '../ui-tokens'
6
7
  import { ListItem } from './list-item'
@@ -23,11 +24,12 @@ function truncateContent(content: string): string {
23
24
 
24
25
  export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetPickerModalProps) {
25
26
  const filtered = filterSnippets(snippets, filter)
27
+ const help = useModalHelp('modal.snippet-picker')
26
28
 
27
29
  return (
28
30
  <ModalShell
29
31
  title="Snippets"
30
- help="j/k move, Enter send, n new, e edit, d delete, / filter, Esc cancel."
32
+ help={help}
31
33
  width={uiTokens.modalWidth.xl}
32
34
  footer={<ModalFilterBar filter={filter} />}
33
35
  >
@@ -1,6 +1,8 @@
1
1
  import type { AppState } from '../../state/types'
2
2
 
3
+ import { version as APP_VERSION } from '../../../package.json'
3
4
  import { useAppStore } from '../../state/app-store'
5
+ import { useKeymap } from '../keymap-context'
4
6
  import { getStatusBarModel } from '../status-bar-model'
5
7
  import { theme } from '../theme'
6
8
 
@@ -43,7 +45,8 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
43
45
  export function StatusBar() {
44
46
  const state = useAppStore((s) => s)
45
47
  const activeTab = state.tabs.find((tab) => tab.id === state.activeTabId)
46
- const model = getStatusBarModel(state, activeTab)
48
+ const config = useKeymap()
49
+ const model = getStatusBarModel(state, activeTab, config)
47
50
 
48
51
  return (
49
52
  <box
@@ -60,8 +63,9 @@ export function StatusBar() {
60
63
  <text> </text>
61
64
  <text fg={theme.text}>{model.left}</text>
62
65
  </box>
63
- <box width="100%">
66
+ <box width="100%" flexDirection="row" justifyContent="space-between">
64
67
  <text fg={theme.textMuted}>{model.right}</text>
68
+ <text fg={theme.dim}>v{APP_VERSION}</text>
65
69
  </box>
66
70
  </box>
67
71
  )
@@ -1,3 +1,4 @@
1
+ import { useModalHelp } from '../keymap-context'
1
2
  import { theme } from '../theme'
2
3
  import { THEME_IDS, type ThemeId, THEMES } from '../themes'
3
4
  import { uiTokens } from '../ui-tokens'
@@ -10,13 +11,9 @@ interface ThemePickerModalProps {
10
11
  }
11
12
 
12
13
  export function ThemePickerModal({ currentThemeId, selectedIndex }: ThemePickerModalProps) {
14
+ const help = useModalHelp('modal.theme-picker')
13
15
  return (
14
- <ModalShell
15
- title="Select theme"
16
- help="j/k move, Enter confirm, Esc cancel."
17
- width={uiTokens.modalWidth.md}
18
- listGap={0}
19
- >
16
+ <ModalShell title="Select theme" help={help} width={uiTokens.modalWidth.md} listGap={0}>
20
17
  {THEME_IDS.map((id, index) => {
21
18
  const entry = THEMES[id]
22
19
  const active = index === selectedIndex
@@ -0,0 +1,42 @@
1
+ import { useModalHelp } from '../keymap-context'
2
+ import { theme } from '../theme'
3
+ import { uiTokens } from '../ui-tokens'
4
+ import { ListItem } from './list-item'
5
+ import { ModalShell } from './modal-shell'
6
+
7
+ interface UpdateAvailableModalProps {
8
+ currentVersion: string
9
+ latestVersion: string
10
+ selectedIndex: number
11
+ }
12
+
13
+ const OPTIONS: { label: string }[] = [
14
+ { label: 'Yes, update now' },
15
+ { label: 'No, skip this version' },
16
+ ]
17
+
18
+ export function UpdateAvailableModal({
19
+ currentVersion,
20
+ latestVersion,
21
+ selectedIndex,
22
+ }: UpdateAvailableModalProps) {
23
+ const modalHelp = useModalHelp('modal.update-available')
24
+ const help = `${currentVersion} → ${latestVersion} ${modalHelp}`
25
+ return (
26
+ <ModalShell title="Update available" help={help} width={uiTokens.modalWidth.md} listGap={1}>
27
+ <box flexDirection="row" gap={1} marginTop={1}>
28
+ {OPTIONS.map((option, index) => {
29
+ const active = index === selectedIndex
30
+ return (
31
+ <ListItem
32
+ key={option.label}
33
+ active={active}
34
+ direction="row"
35
+ title={<text fg={active ? theme.text : theme.textMuted}>{option.label}</text>}
36
+ />
37
+ )
38
+ })}
39
+ </box>
40
+ </ModalShell>
41
+ )
42
+ }
@@ -0,0 +1,39 @@
1
+ import {
2
+ getDefaultKeymapConfig,
3
+ type ModeId,
4
+ type ResolvedKeymapConfig,
5
+ } from '@brimveyn/aimux-config'
6
+ import { createContext, useContext, useMemo } from 'react'
7
+
8
+ import { describeBindings } from '../input/keymap/describe-bindings'
9
+
10
+ const FALLBACK_CONFIG = getDefaultKeymapConfig()
11
+
12
+ export const KeymapContext = createContext<ResolvedKeymapConfig>(FALLBACK_CONFIG)
13
+
14
+ export function useKeymap(): ResolvedKeymapConfig {
15
+ return useContext(KeymapContext)
16
+ }
17
+
18
+ const HELP_JOINER = ' · '
19
+ const MAX_HELP_ENTRIES = 5
20
+
21
+ /** Build a single-line help string for a mode (used in modal headers and status bar). */
22
+ export function buildHintText(
23
+ config: ResolvedKeymapConfig,
24
+ modeId: ModeId,
25
+ limit: number = MAX_HELP_ENTRIES
26
+ ): string {
27
+ const bindings = describeBindings(config, modeId, {
28
+ dedupeByDescription: true,
29
+ withDescriptionOnly: true,
30
+ })
31
+ if (bindings.length === 0) return ''
32
+ const trimmed = bindings.slice(0, limit)
33
+ return trimmed.map((b) => `${b.keysDisplay} ${b.description ?? ''}`.trim()).join(HELP_JOINER)
34
+ }
35
+
36
+ export function useModalHelp(modeId: ModeId, limit?: number): string {
37
+ const config = useKeymap()
38
+ return useMemo(() => buildHintText(config, modeId, limit), [config, modeId, limit])
39
+ }
package/src/ui/root.tsx CHANGED
@@ -11,6 +11,7 @@ import { GitCommitModal } from './components/git-commit-modal'
11
11
  import { GitView } from './components/git-view'
12
12
  import { HelpModal } from './components/help-modal'
13
13
  import { NewTabModal } from './components/new-tab-modal'
14
+ import { PendingChordOverlay } from './components/pending-chord-overlay'
14
15
  import { SessionNameModal } from './components/session-name-modal'
15
16
  import { SessionPickerModal } from './components/session-picker-modal'
16
17
  import { Sidebar } from './components/sidebar'
@@ -20,6 +21,7 @@ import { SplitLayout } from './components/split-layout'
20
21
  import { StatusBar } from './components/status-bar'
21
22
  import { TerminalPane } from './components/terminal-pane'
22
23
  import { ThemePickerModal } from './components/theme-picker-modal'
24
+ import { UpdateAvailableModal } from './components/update-available-modal'
23
25
  import { theme } from './theme'
24
26
 
25
27
  function getCreateSessionFields(modal: ModalState) {
@@ -132,6 +134,14 @@ function renderModal(
132
134
  return (
133
135
  <ThemePickerModal selectedIndex={modal.selectedIndex} currentThemeId={options.themeId} />
134
136
  )
137
+ case 'update-available':
138
+ return (
139
+ <UpdateAvailableModal
140
+ selectedIndex={modal.selectedIndex}
141
+ currentVersion={modal.currentVersion}
142
+ latestVersion={modal.latestVersion}
143
+ />
144
+ )
135
145
  case 'help':
136
146
  return <HelpModal />
137
147
  case 'git-commit': {
@@ -216,6 +226,7 @@ export function RootView({
216
226
  <box flexDirection="column" width="100%" height="100%" backgroundColor={theme.background}>
217
227
  <GitView />
218
228
  <StatusBar />
229
+ <PendingChordOverlay />
219
230
  {renderModal(modal, {
220
231
  createSessionFields,
221
232
  currentSessionId,
@@ -280,6 +291,7 @@ export function RootView({
280
291
  )}
281
292
  </box>
282
293
  <StatusBar />
294
+ <PendingChordOverlay />
283
295
  {renderModal(modal, {
284
296
  createSessionFields,
285
297
  currentSessionId,