@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.
- package/README.md +29 -8
- package/package.json +5 -5
- package/src/app-runtime/backend-runtime-events.ts +16 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +62 -4
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/use-backend-runtime.ts +7 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -2
- package/src/app-runtime/use-terminal-resize.ts +15 -6
- package/src/app.tsx +89 -30
- package/src/config/loader.ts +2 -2
- package/src/config.ts +14 -3
- package/src/daemon/daemon.ts +279 -118
- package/src/daemon/runtime-paths.ts +34 -2
- package/src/daemon/session-manager.ts +20 -5
- package/src/daemon/session-registry.ts +18 -11
- package/src/index.tsx +19 -4
- package/src/input/keymap/describe-bindings.ts +68 -0
- package/src/input/keymap/key-format.ts +67 -0
- package/src/input/modes/bridge.ts +1 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +2 -0
- package/src/ipc/manager-protocol.ts +396 -0
- package/src/ipc/protocol.ts +98 -5
- package/src/platform/daemon-control.ts +42 -11
- package/src/profile-paths.ts +27 -0
- package/src/pty/pty-manager.ts +53 -3
- package/src/restart-daemon.ts +10 -10
- package/src/session-backend/bootstrap.ts +197 -46
- package/src/session-backend/local-session-backend.ts +12 -5
- package/src/session-backend/remote-session-backend.ts +143 -63
- package/src/session-backend/types.ts +4 -2
- package/src/state/reducers/modal-state.ts +18 -1
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/session-catalog.ts +5 -4
- package/src/state/session-persistence.ts +9 -2
- package/src/state/snippet-catalog.ts +4 -4
- package/src/state/types.ts +23 -0
- package/src/state/validation.ts +8 -0
- package/src/terminal-manager/manager-client.ts +384 -0
- package/src/terminal-manager/terminal-manager.ts +288 -0
- package/src/ui/components/create-session-modal.tsx +3 -5
- package/src/ui/components/git-commit-modal.tsx +3 -5
- package/src/ui/components/help-modal.tsx +49 -68
- package/src/ui/components/list-item.tsx +24 -5
- package/src/ui/components/new-tab-modal.tsx +8 -9
- package/src/ui/components/pending-chord-overlay.tsx +28 -0
- package/src/ui/components/session-name-modal.tsx +3 -5
- package/src/ui/components/session-picker-modal.tsx +3 -1
- package/src/ui/components/snippet-editor-modal.tsx +3 -1
- package/src/ui/components/snippet-picker-modal.tsx +3 -1
- package/src/ui/components/status-bar.tsx +6 -2
- package/src/ui/components/theme-picker-modal.tsx +3 -6
- package/src/ui/components/update-available-modal.tsx +42 -0
- package/src/ui/keymap-context.ts +39 -0
- package/src/ui/root.tsx +12 -0
- package/src/ui/status-bar-model.ts +67 -39
- package/src/update/version-check.ts +67 -0
- 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
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
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=
|
|
64
|
-
{SECTIONS.map((section) =>
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
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
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
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({
|
|
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
|
|
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
|
-
|
|
20
|
-
|
|
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=
|
|
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=
|
|
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=
|
|
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
|
|
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,
|