@brimveyn/aimux 1.9.0 → 1.9.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 (67) hide show
  1. package/package.json +2 -2
  2. package/src/app-runtime/side-effects.ts +6 -1
  3. package/src/app.tsx +20 -16
  4. package/src/input/keymap/describe-bindings.ts +63 -2
  5. package/src/pty/terminal-snapshot.ts +7 -5
  6. package/src/state/reducers/git-mode-state.ts +58 -0
  7. package/src/state/types.ts +11 -6
  8. package/src/ui/breaking-update-screen.tsx +2 -2
  9. package/src/ui/components/git/diff-renderer/build-rows.ts +467 -0
  10. package/src/ui/components/{diff-renderer → git/diff-renderer}/fold-strip.tsx +1 -1
  11. package/src/ui/components/git/diff-renderer/highlight.ts +102 -0
  12. package/src/ui/components/{diff-renderer → git/diff-renderer}/pierre-diff.tsx +23 -15
  13. package/src/ui/components/git/diff-renderer/prepare-diff.ts +62 -0
  14. package/src/ui/components/{diff-renderer → git/diff-renderer}/split-view.tsx +122 -47
  15. package/src/ui/components/{diff-renderer → git/diff-renderer}/stacked-view.tsx +106 -35
  16. package/src/ui/components/{diff-renderer → git/diff-renderer}/use-diff-prefetch.ts +8 -17
  17. package/src/ui/components/git/diff-renderer/use-diff-preparation.ts +240 -0
  18. package/src/ui/components/git/diff-renderer/use-segment-virtualization.ts +95 -0
  19. package/src/ui/components/{git-panel.tsx → git/git-panel.tsx} +9 -5
  20. package/src/ui/components/{git-view.tsx → git/git-view.tsx} +70 -37
  21. package/src/ui/components/{git-pane-context-menu.ts → git/pane/git-pane-context-menu.ts} +2 -2
  22. package/src/ui/components/{git-pane-widget.tsx → git/pane/git-pane-widget.tsx} +4 -4
  23. package/src/ui/components/{session-bar.tsx → layout/session-bar.tsx} +36 -13
  24. package/src/ui/components/{sidebar-group-metadata.ts → layout/sidebar/sidebar-group-metadata.ts} +2 -2
  25. package/src/ui/components/{sidebar.tsx → layout/sidebar/sidebar.tsx} +6 -6
  26. package/src/ui/components/{tab-item.tsx → layout/sidebar/tab-item.tsx} +5 -5
  27. package/src/ui/components/{use-sidebar-branch.ts → layout/sidebar/use-sidebar-branch.ts} +1 -1
  28. package/src/ui/components/{split-layout.tsx → layout/split-layout.tsx} +5 -5
  29. package/src/ui/components/{status-bar.tsx → layout/status-bar.tsx} +7 -7
  30. package/src/ui/components/{terminal-pane.tsx → layout/terminal-pane.tsx} +8 -8
  31. package/src/ui/components/{ai-usage-modal.tsx → modals/app/ai-usage-modal.tsx} +10 -6
  32. package/src/ui/components/{help-modal.tsx → modals/app/help-modal.tsx} +6 -6
  33. package/src/ui/components/{update-available-modal.tsx → modals/app/update-available-modal.tsx} +4 -4
  34. package/src/ui/components/{git-commit-modal.tsx → modals/git/git-commit-modal.tsx} +8 -8
  35. package/src/ui/components/modals/sessions/create-session-modal.tsx +79 -0
  36. package/src/ui/components/modals/sessions/session-name-modal.tsx +10 -0
  37. package/src/ui/components/{session-picker-modal.tsx → modals/sessions/session-picker-modal.tsx} +8 -8
  38. package/src/ui/components/modals/shared/form.tsx +164 -0
  39. package/src/ui/components/{modal-keybinds-overlay.tsx → modals/shared/modal-keybinds-overlay.tsx} +7 -7
  40. package/src/ui/components/{modal-shell.tsx → modals/shared/modal-shell.tsx} +1 -1
  41. package/src/ui/components/{picker.tsx → modals/shared/picker.tsx} +3 -3
  42. package/src/ui/components/modals/snippets/snippet-editor-modal.tsx +30 -0
  43. package/src/ui/components/{snippet-picker-modal.tsx → modals/snippets/snippet-picker-modal.tsx} +6 -6
  44. package/src/ui/components/{new-tab-modal.tsx → modals/tabs/new-tab-modal.tsx} +17 -20
  45. package/src/ui/components/{theme-picker-modal.tsx → modals/themes/theme-picker-modal.tsx} +6 -6
  46. package/src/ui/components/{ai-usage-indicator.tsx → overlays/ai-usage/ai-usage-indicator.tsx} +3 -3
  47. package/src/ui/components/{context-menu-box.tsx → overlays/context-menu/context-menu-box.tsx} +1 -1
  48. package/src/ui/components/{context-menu-overlay.tsx → overlays/context-menu/context-menu-overlay.tsx} +3 -3
  49. package/src/ui/components/{pending-chord-overlay.tsx → overlays/pending-chord-overlay.tsx} +6 -6
  50. package/src/ui/components/{bare-input.tsx → primitives/bare-input.tsx} +2 -2
  51. package/src/ui/components/{input-field.tsx → primitives/input-field.tsx} +2 -2
  52. package/src/ui/components/{list-item.tsx → primitives/list-item.tsx} +1 -1
  53. package/src/ui/components/{surface.tsx → primitives/surface.tsx} +1 -1
  54. package/src/ui/root.tsx +24 -22
  55. package/src/ui/theme-store.ts +30 -26
  56. package/src/ui/themes.ts +1 -0
  57. package/src/ui/components/create-session-modal.tsx +0 -96
  58. package/src/ui/components/diff-renderer/build-rows.ts +0 -349
  59. package/src/ui/components/diff-renderer/highlight.ts +0 -44
  60. package/src/ui/components/diff-renderer/prepare-diff.ts +0 -65
  61. package/src/ui/components/diff-renderer/use-diff-preparation.ts +0 -106
  62. package/src/ui/components/session-name-modal.tsx +0 -11
  63. package/src/ui/components/snippet-editor-modal.tsx +0 -40
  64. /package/src/ui/components/{diff-renderer → git/diff-renderer}/filetype.ts +0 -0
  65. /package/src/ui/components/{diff-renderer → git/diff-renderer}/index.ts +0 -0
  66. /package/src/ui/components/{sidebar-scroll.ts → layout/sidebar/sidebar-scroll.ts} +0 -0
  67. /package/src/ui/components/{use-sidebar-auto-scroll.ts → layout/sidebar/use-sidebar-auto-scroll.ts} +0 -0
@@ -1,6 +1,6 @@
1
1
  import { useEffect, useState } from 'react'
2
2
 
3
- import { getCurrentBranch } from '../git-branch'
3
+ import { getCurrentBranch } from '../../../git-branch'
4
4
 
5
5
  const BRANCH_POLL_INTERVAL_MS = 5_000
6
6
 
@@ -1,17 +1,17 @@
1
1
  import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
 
3
- import type { TerminalContentOrigin } from '../../input/raw-input-handler'
4
- import type { FocusMode, TabSession } from '../../state/types'
3
+ import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
4
+ import type { FocusMode, TabSession } from '../../../state/types'
5
5
 
6
- import { logInputDebug } from '../../debug/input-log'
6
+ import { logInputDebug } from '../../../debug/input-log'
7
7
  import {
8
8
  computePaneRects,
9
9
  type LayoutNode,
10
10
  PANE_BORDER,
11
11
  type PaneRect,
12
12
  type SplitDirection,
13
- } from '../../state/layout-tree'
14
- import { useTokens } from '../theme'
13
+ } from '../../../state/layout-tree'
14
+ import { useTokens } from '../../theme'
15
15
  import { TerminalPane } from './terminal-pane'
16
16
 
17
17
  const PANE_CHROME = PANE_BORDER
@@ -1,11 +1,11 @@
1
- import type { AppState } from '../../state/types'
1
+ import type { AppState } from '../../../state/types'
2
2
 
3
- import { version as APP_VERSION } from '../../../package.json'
4
- import { useAppStore } from '../../state/app-store'
5
- import { useKeymap } from '../keymap-context'
6
- import { getStatusBarModel } from '../status-bar-model'
7
- import { getCurrentTokens, useBg, useTokens } from '../theme'
8
- import { AIUsageIndicator } from './ai-usage-indicator'
3
+ import { version as APP_VERSION } from '../../../../package.json'
4
+ import { useAppStore } from '../../../state/app-store'
5
+ import { useKeymap } from '../../keymap-context'
6
+ import { getStatusBarModel } from '../../status-bar-model'
7
+ import { getCurrentTokens, useBg, useTokens } from '../../theme'
8
+ import { AIUsageIndicator } from '../overlays/ai-usage/ai-usage-indicator'
9
9
 
10
10
  function getModeColor(focusMode: AppState['focusMode']): string {
11
11
  const t = getCurrentTokens()
@@ -2,19 +2,19 @@ import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
 
3
3
  import { memo, type ReactNode } from 'react'
4
4
 
5
- import type { TerminalContentOrigin } from '../../input/raw-input-handler'
6
- import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
5
+ import type { TerminalContentOrigin } from '../../../input/raw-input-handler'
6
+ import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../../state/types'
7
7
 
8
- import { logInputDebug } from '../../debug/input-log'
9
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
10
- import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
11
- import { getCurrentTokens, useBg, useTokens } from '../theme'
12
- import { ContextMenuBox } from './context-menu-box'
8
+ import { logInputDebug } from '../../../debug/input-log'
9
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../state/dispatch-ref'
10
+ import { type ContextMenuItem, openContextMenu } from '../../context-menu/controller'
11
+ import { getCurrentTokens, useBg, useTokens } from '../../theme'
12
+ import { ContextMenuBox } from '../overlays/context-menu/context-menu-box'
13
13
 
14
14
  interface TerminalPaneProps {
15
15
  tab?: TabSession
16
16
  tabId?: string
17
- focusMode: import('../../state/types').FocusMode
17
+ focusMode: import('../../../state/types').FocusMode
18
18
  isActive?: boolean
19
19
  contentOrigin: TerminalContentOrigin
20
20
  mouseForwardingEnabled: boolean
@@ -1,12 +1,16 @@
1
1
  import type { AIUsageTool } from '@brimveyn/aimux-config'
2
2
  import type { ReactNode } from 'react'
3
3
 
4
- import type { UsagePaceStage, UsageSnapshot, UsageWindow } from '../../services/ai-usage/types'
5
-
6
- import { useAIUsageStore } from '../../state/ai-usage-store'
7
- import { useTokens } from '../theme'
8
- import { uiTokens } from '../ui-tokens'
9
- import { ModalShell } from './modal-shell'
4
+ import type {
5
+ UsagePaceStage,
6
+ UsageSnapshot,
7
+ UsageWindow,
8
+ } from '../../../../services/ai-usage/types'
9
+
10
+ import { useAIUsageStore } from '../../../../state/ai-usage-store'
11
+ import { useTokens } from '../../../theme'
12
+ import { uiTokens } from '../../../ui-tokens'
13
+ import { ModalShell } from '../shared/modal-shell'
10
14
 
11
15
  const TOOL_TITLE: Record<AIUsageTool, string> = {
12
16
  claude: 'Claude',
@@ -2,12 +2,12 @@ import type { ModeId } from '@brimveyn/aimux-config'
2
2
 
3
3
  import { useLayoutEffect, useMemo } from 'react'
4
4
 
5
- import { collectHelpEntries, HELP_MODE_LABELS } from '../../input/keymap/help-entries'
6
- import { dispatchGlobal } from '../../state/dispatch-ref'
7
- import { useKeymap } from '../keymap-context'
8
- import { useTokens } from '../theme'
9
- import { uiTokens } from '../ui-tokens'
10
- import { Picker, type PickerItem } from './picker'
5
+ import { collectHelpEntries, HELP_MODE_LABELS } from '../../../../input/keymap/help-entries'
6
+ import { dispatchGlobal } from '../../../../state/dispatch-ref'
7
+ import { useKeymap } from '../../../keymap-context'
8
+ import { useTokens } from '../../../theme'
9
+ import { uiTokens } from '../../../ui-tokens'
10
+ import { Picker, type PickerItem } from '../shared/picker'
11
11
 
12
12
  interface HelpModalProps {
13
13
  filter: string | null
@@ -1,7 +1,7 @@
1
- import { useTokens } from '../theme'
2
- import { uiTokens } from '../ui-tokens'
3
- import { ListItem } from './list-item'
4
- import { ModalShell } from './modal-shell'
1
+ import { useTokens } from '../../../theme'
2
+ import { uiTokens } from '../../../ui-tokens'
3
+ import { ListItem } from '../../primitives/list-item'
4
+ import { ModalShell } from '../shared/modal-shell'
5
5
 
6
6
  interface UpdateAvailableModalProps {
7
7
  currentVersion: string
@@ -1,14 +1,14 @@
1
1
  import { isAutoCommitEnabled } from '@brimveyn/aimux-config'
2
2
  import { useEffect, useState } from 'react'
3
3
 
4
- import type { ModeId } from '../../input/modes/types'
5
-
6
- import { useAppStore } from '../../state/app-store'
7
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
8
- import { useTokens } from '../theme'
9
- import { uiTokens } from '../ui-tokens'
10
- import { InputField } from './input-field'
11
- import { ModalShell } from './modal-shell'
4
+ import type { ModeId } from '../../../../input/modes/types'
5
+
6
+ import { useAppStore } from '../../../../state/app-store'
7
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
8
+ import { useTokens } from '../../../theme'
9
+ import { uiTokens } from '../../../ui-tokens'
10
+ import { InputField } from '../../primitives/input-field'
11
+ import { ModalShell } from '../shared/modal-shell'
12
12
 
13
13
  interface GitCommitModalProps {
14
14
  activeField: 'title' | 'body'
@@ -0,0 +1,79 @@
1
+ import type { DirectoryResult } from '../../../../state/types'
2
+
3
+ import { abbreviatePath } from '../../../path-format'
4
+ import { getCurrentTokens, useTokens } from '../../../theme'
5
+ import { uiTokens } from '../../../ui-tokens'
6
+ import { AutoComplete, Form, type FormOptionItem, TextField } from '../shared/form'
7
+
8
+ const VISIBLE_ROWS = 8
9
+
10
+ function getDirectoryResultIcon(result: DirectoryResult): string {
11
+ if (result.type === 'worktree') return '\u{e728}'
12
+ if (result.type === 'workspace') return '\u{f07c}'
13
+ return '\u{e702}'
14
+ }
15
+
16
+ function getDirectoryResultColor(result: DirectoryResult): string {
17
+ const t = getCurrentTokens()
18
+ if (result.type === 'worktree') return t.palette.warning
19
+ if (result.type === 'workspace') return t.accent
20
+ return t.palette.primary
21
+ }
22
+
23
+ interface CreateSessionModalProps {
24
+ activeField: 'directory' | 'name'
25
+ directoryQuery: string
26
+ sessionName: string
27
+ results: DirectoryResult[]
28
+ selectedIndex: number
29
+ pendingProjectPath: string | null
30
+ }
31
+
32
+ export function CreateSessionModal({
33
+ activeField,
34
+ directoryQuery,
35
+ pendingProjectPath,
36
+ results,
37
+ selectedIndex,
38
+ sessionName,
39
+ }: CreateSessionModalProps) {
40
+ const t = useTokens()
41
+ const dirActive = activeField === 'directory'
42
+ const nameActive = activeField === 'name'
43
+
44
+ const items: FormOptionItem[] = results.map((result) => {
45
+ return {
46
+ key: result.path,
47
+ leading: <text fg={getDirectoryResultColor(result)}>{getDirectoryResultIcon(result)}</text>,
48
+ title: (active) => (
49
+ <text fg={active ? t.palette.ink : t.muted}>{abbreviatePath(result.path)}</text>
50
+ ),
51
+ }
52
+ })
53
+
54
+ return (
55
+ <Form
56
+ title="Create workspace"
57
+ keybindsModeId="modal.create-session"
58
+ width={uiTokens.modalWidth.xl}
59
+ >
60
+ <AutoComplete
61
+ active={dirActive}
62
+ label="Search projects"
63
+ placeholder="Type a project name..."
64
+ value={directoryQuery}
65
+ displayValue={pendingProjectPath ? abbreviatePath(pendingProjectPath) : directoryQuery}
66
+ items={items}
67
+ selectedIndex={selectedIndex}
68
+ maxVisibleRows={VISIBLE_ROWS}
69
+ emptyState={
70
+ <text fg={t.muted}>
71
+ {directoryQuery.length > 0 ? 'No matches' : 'Type a project name to search...'}
72
+ </text>
73
+ }
74
+ />
75
+
76
+ <TextField active={nameActive} label="Workspace name" value={sessionName} />
77
+ </Form>
78
+ )
79
+ }
@@ -0,0 +1,10 @@
1
+ import { uiTokens } from '../../../ui-tokens'
2
+ import { Form, TextField } from '../shared/form'
3
+
4
+ export function SessionNameModal({ title, value }: { title: string; value: string }) {
5
+ return (
6
+ <Form title={title} keybindsModeId="modal.session-name" width={uiTokens.modalWidth.md}>
7
+ <TextField active value={value} />
8
+ </Form>
9
+ )
10
+ }
@@ -1,12 +1,12 @@
1
- import type { SessionRecord } from '../../state/types'
1
+ import type { SessionRecord } from '../../../../state/types'
2
2
 
3
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
- import { filterSessions } from '../../state/selectors'
5
- import { abbreviatePath } from '../path-format'
6
- import { orderSessionsForDisplay } from '../session-ordering'
7
- import { useTokens } from '../theme'
8
- import { uiTokens } from '../ui-tokens'
9
- import { Picker, type PickerItem } from './picker'
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
+ import { filterSessions } from '../../../../state/selectors'
5
+ import { abbreviatePath } from '../../../path-format'
6
+ import { orderSessionsForDisplay } from '../../../session-ordering'
7
+ import { useTokens } from '../../../theme'
8
+ import { uiTokens } from '../../../ui-tokens'
9
+ import { Picker, type PickerItem } from '../shared/picker'
10
10
 
11
11
  interface SessionPickerModalProps {
12
12
  sessions: SessionRecord[]
@@ -0,0 +1,164 @@
1
+ import type { ModeId } from '@brimveyn/aimux-config'
2
+
3
+ import { type ReactNode } from 'react'
4
+
5
+ import { useTokens } from '../../../theme'
6
+ import { InputField } from '../../primitives/input-field'
7
+ import { ListItem } from '../../primitives/list-item'
8
+ import { ModalShell } from './modal-shell'
9
+
10
+ interface FormProps {
11
+ children: ReactNode
12
+ footer?: ReactNode
13
+ keybindsModeId?: ModeId
14
+ listGap?: number
15
+ subtitle?: string
16
+ title: string
17
+ width: number | `${number}%`
18
+ }
19
+
20
+ interface FieldLabelProps {
21
+ active?: boolean
22
+ children?: ReactNode
23
+ description?: ReactNode
24
+ }
25
+
26
+ interface TextFieldProps {
27
+ active: boolean
28
+ cursorPos?: number
29
+ description?: ReactNode
30
+ label?: ReactNode
31
+ placeholder?: string
32
+ value: string
33
+ }
34
+
35
+ type FormOptionContent = ReactNode | ((active: boolean) => ReactNode)
36
+
37
+ export interface FormOptionItem {
38
+ key: string
39
+ leading?: FormOptionContent
40
+ subtitle?: FormOptionContent
41
+ title: FormOptionContent
42
+ trailing?: FormOptionContent
43
+ onClick?: () => void
44
+ }
45
+
46
+ interface AutoCompleteProps {
47
+ active: boolean
48
+ cursorPos?: number
49
+ displayValue?: string
50
+ emptyState?: ReactNode
51
+ items: FormOptionItem[]
52
+ label: ReactNode
53
+ maxVisibleRows?: number
54
+ onHover?: (index: number) => void
55
+ placeholder?: string
56
+ selectedIndex: number
57
+ value: string
58
+ }
59
+
60
+ export function Form({
61
+ children,
62
+ footer,
63
+ keybindsModeId,
64
+ listGap,
65
+ subtitle,
66
+ title,
67
+ width,
68
+ }: FormProps) {
69
+ return (
70
+ <ModalShell
71
+ title={title}
72
+ subtitle={subtitle}
73
+ width={width}
74
+ keybindsModeId={keybindsModeId}
75
+ listGap={listGap}
76
+ footer={footer}
77
+ >
78
+ {children}
79
+ </ModalShell>
80
+ )
81
+ }
82
+
83
+ export function FieldLabel({ active = false, children, description }: FieldLabelProps) {
84
+ const t = useTokens()
85
+ return (
86
+ <box flexDirection="column">
87
+ {children ? <text fg={active ? t.palette.ink : t.muted}>{children}</text> : null}
88
+ {description ? <text fg={t.muted}>{description}</text> : null}
89
+ </box>
90
+ )
91
+ }
92
+
93
+ export function TextField({
94
+ active,
95
+ cursorPos,
96
+ description,
97
+ label,
98
+ placeholder,
99
+ value,
100
+ }: TextFieldProps) {
101
+ return (
102
+ <box flexDirection="column">
103
+ {label || description ? (
104
+ <FieldLabel active={active} description={description}>
105
+ {label}
106
+ </FieldLabel>
107
+ ) : null}
108
+ <InputField active={active} value={value} cursorPos={cursorPos} placeholder={placeholder} />
109
+ </box>
110
+ )
111
+ }
112
+
113
+ function resolveItemContent(content: FormOptionContent | undefined, active: boolean): ReactNode {
114
+ return typeof content === 'function' ? content(active) : content
115
+ }
116
+
117
+ export function AutoComplete({
118
+ active,
119
+ cursorPos,
120
+ displayValue,
121
+ emptyState,
122
+ items,
123
+ label,
124
+ maxVisibleRows = 8,
125
+ onHover,
126
+ placeholder,
127
+ selectedIndex,
128
+ value,
129
+ }: AutoCompleteProps) {
130
+ const scrollOffset = Math.max(0, selectedIndex - maxVisibleRows + 1)
131
+ const visibleItems = items.slice(scrollOffset, scrollOffset + maxVisibleRows)
132
+
133
+ return (
134
+ <box flexDirection="column" gap={1}>
135
+ <TextField
136
+ active={active}
137
+ label={label}
138
+ value={active ? value : (displayValue ?? value)}
139
+ cursorPos={cursorPos}
140
+ placeholder={placeholder}
141
+ />
142
+ <box flexDirection="column" height={maxVisibleRows}>
143
+ {items.length === 0
144
+ ? (emptyState ?? null)
145
+ : visibleItems.map((item, index) => {
146
+ const optionIndex = scrollOffset + index
147
+ const optionActive = active && optionIndex === selectedIndex
148
+ return (
149
+ <ListItem
150
+ key={item.key}
151
+ active={optionActive}
152
+ leading={resolveItemContent(item.leading, optionActive)}
153
+ title={resolveItemContent(item.title, optionActive)}
154
+ subtitle={resolveItemContent(item.subtitle, optionActive)}
155
+ trailing={resolveItemContent(item.trailing, optionActive)}
156
+ onHover={onHover ? () => onHover(optionIndex) : undefined}
157
+ onClick={item.onClick}
158
+ />
159
+ )
160
+ })}
161
+ </box>
162
+ </box>
163
+ )
164
+ }
@@ -1,9 +1,9 @@
1
1
  import type { ModeId } from '@brimveyn/aimux-config'
2
2
 
3
- import { describeBindings } from '../../input/keymap/describe-bindings'
4
- import { useKeymap } from '../keymap-context'
5
- import { useTokens } from '../theme'
6
- import { Surface } from './surface'
3
+ import { describeBindings } from '../../../../input/keymap/describe-bindings'
4
+ import { useKeymap } from '../../../keymap-context'
5
+ import { useTokens } from '../../../theme'
6
+ import { Surface } from '../../primitives/surface'
7
7
 
8
8
  const KEYS_COLUMN_WIDTH = 12
9
9
 
@@ -16,7 +16,7 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
16
16
  const t = useTokens()
17
17
  const config = useKeymap()
18
18
  const bindings = describeBindings(config, modeId, {
19
- dedupeByDescription: true,
19
+ mergeAlternativesByDescription: true,
20
20
  withDescriptionOnly: true,
21
21
  })
22
22
 
@@ -24,10 +24,10 @@ export function ModalKeybindsOverlay({ limit, modeId }: ModalKeybindsOverlayProp
24
24
  const entries = typeof limit === 'number' ? bindings.slice(0, limit) : bindings
25
25
 
26
26
  return (
27
- <box position="absolute" bottom={3} right={5}>
27
+ <box position="absolute" bottom={0} right={0}>
28
28
  <Surface tone="elevated" paddingLeft={2} paddingRight={2} paddingTop={1} paddingBottom={1}>
29
29
  {entries.map((binding) => (
30
- <box key={`${binding.keys}-${binding.description}`} flexDirection="row">
30
+ <box key={binding.description ?? binding.keys} flexDirection="row">
31
31
  <box width={KEYS_COLUMN_WIDTH}>
32
32
  <text fg={t.accent}>{binding.keysDisplay}</text>
33
33
  </box>
@@ -3,7 +3,7 @@ import type { ModeId } from '@brimveyn/aimux-config'
3
3
  import { type BoxRenderable, type OptimizedBuffer, RGBA } from '@opentui/core'
4
4
  import { type ReactNode } from 'react'
5
5
 
6
- import { useTokens, useTransparent } from '../theme'
6
+ import { useTokens, useTransparent } from '../../../theme'
7
7
  import { ModalKeybindsOverlay } from './modal-keybinds-overlay'
8
8
 
9
9
  interface ModalShellProps {
@@ -4,9 +4,9 @@ import type { ScrollBoxRenderable } from '@opentui/core'
4
4
  import { useTerminalDimensions } from '@opentui/react'
5
5
  import { type ReactNode, useLayoutEffect, useRef } from 'react'
6
6
 
7
- import { useTokens } from '../theme'
8
- import { BareInput } from './bare-input'
9
- import { ListItem } from './list-item'
7
+ import { useTokens } from '../../../theme'
8
+ import { BareInput } from '../../primitives/bare-input'
9
+ import { ListItem } from '../../primitives/list-item'
10
10
  import { ModalShell } from './modal-shell'
11
11
 
12
12
  export interface PickerItem {
@@ -0,0 +1,30 @@
1
+ import { uiTokens } from '../../../ui-tokens'
2
+ import { Form, TextField } from '../shared/form'
3
+
4
+ interface SnippetEditorModalProps {
5
+ activeField: 'name' | 'content'
6
+ snippetName: string
7
+ snippetContent: string
8
+ isEditing: boolean
9
+ }
10
+
11
+ export function SnippetEditorModal({
12
+ activeField,
13
+ isEditing,
14
+ snippetContent,
15
+ snippetName,
16
+ }: SnippetEditorModalProps) {
17
+ const nameActive = activeField === 'name'
18
+ const contentActive = activeField === 'content'
19
+
20
+ return (
21
+ <Form
22
+ title={isEditing ? 'Edit snippet' : 'Create snippet'}
23
+ keybindsModeId="modal.snippet-editor"
24
+ width={uiTokens.modalWidth.xl}
25
+ >
26
+ <TextField active={nameActive} label="Name" value={snippetName} />
27
+ <TextField active={contentActive} label="Content" value={snippetContent} />
28
+ </Form>
29
+ )
30
+ }
@@ -1,10 +1,10 @@
1
- import type { SnippetRecord } from '../../state/types'
1
+ import type { SnippetRecord } from '../../../../state/types'
2
2
 
3
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
- import { filterSnippets } from '../../state/selectors'
5
- import { useTokens } from '../theme'
6
- import { uiTokens } from '../ui-tokens'
7
- import { Picker, type PickerItem } from './picker'
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
+ import { filterSnippets } from '../../../../state/selectors'
5
+ import { useTokens } from '../../../theme'
6
+ import { uiTokens } from '../../../ui-tokens'
7
+ import { Picker, type PickerItem } from '../shared/picker'
8
8
 
9
9
  interface SnippetPickerModalProps {
10
10
  snippets: SnippetRecord[]
@@ -1,13 +1,12 @@
1
- import type { AssistantId } from '../../state/types'
1
+ import type { AssistantId } from '../../../../state/types'
2
2
 
3
- import { getAllAssistantOptions, getAssistantOption } from '../../pty/command-registry'
4
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
5
- import { filterAssistants } from '../../state/selectors'
6
- import { useTokens } from '../theme'
7
- import { uiTokens } from '../ui-tokens'
8
- import { InputField } from './input-field'
9
- import { ModalShell } from './modal-shell'
10
- import { Picker, type PickerItem } from './picker'
3
+ import { getAllAssistantOptions, getAssistantOption } from '../../../../pty/command-registry'
4
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
5
+ import { filterAssistants } from '../../../../state/selectors'
6
+ import { useTokens } from '../../../theme'
7
+ import { uiTokens } from '../../../ui-tokens'
8
+ import { Form, TextField } from '../shared/form'
9
+ import { Picker, type PickerItem } from '../shared/picker'
11
10
 
12
11
  interface NewTabModalProps {
13
12
  selectedIndex: number
@@ -33,21 +32,19 @@ export function NewTabModal({
33
32
  getAllAssistantOptions(customCommands).find((o) => o.id === editingCommand) ??
34
33
  getAssistantOption(0)
35
34
  return (
36
- <ModalShell
35
+ <Form
37
36
  title={`Edit command — ${option.label}`}
38
37
  keybindsModeId="modal.new-tab.editing-command"
39
38
  width={uiTokens.modalWidth.md}
40
39
  >
41
- <box flexDirection="column">
42
- <text fg={t.muted}>Custom command (blank to reset to default: {option.command})</text>
43
- <InputField
44
- active
45
- value={editBuffer}
46
- cursorPos={cursorPos}
47
- placeholder={option.command}
48
- />
49
- </box>
50
- </ModalShell>
40
+ <TextField
41
+ active
42
+ description={<>blank to reset to default: {option.command}</>}
43
+ value={editBuffer}
44
+ cursorPos={cursorPos}
45
+ placeholder={option.command}
46
+ />
47
+ </Form>
51
48
  )
52
49
  }
53
50
 
@@ -1,11 +1,11 @@
1
1
  import { useLayoutEffect, useMemo } from 'react'
2
2
 
3
- import { dispatchGlobal, runSideEffectGlobal } from '../../state/dispatch-ref'
4
- import { filterThemeIds } from '../filter-themes'
5
- import { useTokens, useTransparent } from '../theme'
6
- import { type ThemeId, THEMES } from '../themes'
7
- import { uiTokens } from '../ui-tokens'
8
- import { Picker, type PickerItem } from './picker'
3
+ import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
4
+ import { filterThemeIds } from '../../../filter-themes'
5
+ import { useTokens, useTransparent } from '../../../theme'
6
+ import { type ThemeId, THEMES } from '../../../themes'
7
+ import { uiTokens } from '../../../ui-tokens'
8
+ import { Picker, type PickerItem } from '../shared/picker'
9
9
 
10
10
  interface ThemePickerModalProps {
11
11
  currentThemeId: ThemeId
@@ -1,8 +1,8 @@
1
1
  import type { AIUsageTool } from '@brimveyn/aimux-config'
2
2
 
3
- import { useAIUsageStore } from '../../state/ai-usage-store'
4
- import { dispatchGlobal } from '../../state/dispatch-ref'
5
- import { useBg, useTokens } from '../theme'
3
+ import { useAIUsageStore } from '../../../../state/ai-usage-store'
4
+ import { dispatchGlobal } from '../../../../state/dispatch-ref'
5
+ import { useBg, useTokens } from '../../../theme'
6
6
 
7
7
  const TOOL_ICON: Record<AIUsageTool, string> = {
8
8
  claude: 'CC',
@@ -1,7 +1,7 @@
1
1
  import type { BoxRenderable, MouseEvent as OtuiMouseEvent } from '@opentui/core'
2
2
  import type { BoxProps } from '@opentui/react'
3
3
 
4
- import { type ContextMenuItem, openContextMenu } from '../context-menu/controller'
4
+ import { type ContextMenuItem, openContextMenu } from '../../../context-menu/controller'
5
5
 
6
6
  interface ContextMenuBoxProps extends BoxProps {
7
7
  rightClickMenu?: ContextMenuItem[]