@brimveyn/aimux 1.4.1 → 1.5.4

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 (46) hide show
  1. package/README.md +142 -176
  2. package/package.json +6 -3
  3. package/src/app-runtime/use-renderer-bindings.ts +1 -1
  4. package/src/app-runtime/use-terminal-resize.ts +12 -2
  5. package/src/app.tsx +20 -3
  6. package/src/config.ts +53 -17
  7. package/src/index.tsx +2 -2
  8. package/src/input/keymap/build-handlers.ts +1 -0
  9. package/src/input/keymap/help-entries.ts +44 -0
  10. package/src/input/keymap/keymap-ref.ts +11 -0
  11. package/src/input/keymap/sequence-resolver.ts +35 -2
  12. package/src/input/keymap/trie.ts +1 -0
  13. package/src/input/modes/bridge.ts +1 -1
  14. package/src/input/modes/handlers/shared.ts +0 -16
  15. package/src/input/modes/transitions.ts +4 -4
  16. package/src/input/modes/types.ts +1 -1
  17. package/src/platform/daemon-control.ts +0 -8
  18. package/src/state/reducers/git-panel-state.ts +35 -8
  19. package/src/state/reducers/modal-state.ts +48 -3
  20. package/src/state/selectors.ts +1 -5
  21. package/src/state/session-persistence.ts +1 -20
  22. package/src/state/store.ts +38 -5
  23. package/src/state/types.ts +27 -13
  24. package/src/state/validation.ts +0 -2
  25. package/src/state/workspace-save.ts +6 -2
  26. package/src/ui/components/create-session-modal.tsx +5 -3
  27. package/src/ui/components/git-commit-modal.tsx +1 -3
  28. package/src/ui/components/git-pane-widget.tsx +46 -0
  29. package/src/ui/components/git-panel.tsx +72 -25
  30. package/src/ui/components/help-modal.tsx +156 -42
  31. package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
  32. package/src/ui/components/modal-shell.tsx +15 -3
  33. package/src/ui/components/new-tab-modal.tsx +9 -10
  34. package/src/ui/components/pending-chord-overlay.tsx +2 -1
  35. package/src/ui/components/session-name-modal.tsx +1 -3
  36. package/src/ui/components/session-picker-modal.tsx +1 -3
  37. package/src/ui/components/sidebar.tsx +24 -50
  38. package/src/ui/components/snippet-editor-modal.tsx +1 -3
  39. package/src/ui/components/snippet-picker-modal.tsx +1 -3
  40. package/src/ui/components/status-bar.tsx +0 -4
  41. package/src/ui/components/theme-picker-modal.tsx +6 -3
  42. package/src/ui/components/update-available-modal.tsx +7 -4
  43. package/src/ui/keymap-context.ts +1 -6
  44. package/src/ui/root.tsx +29 -1
  45. package/src/ui/status-bar-model.ts +0 -5
  46. package/src/ui/directory-search.ts +0 -1
@@ -2,12 +2,9 @@ import type { ScrollBoxRenderable } from '@opentui/core'
2
2
 
3
3
  import { memo, useMemo, useRef } from 'react'
4
4
 
5
- import type { GitPanelState } from '../../state/types'
6
-
7
- import { useGitPanelPolling } from '../../git/git-poller'
8
5
  import { useAppStore } from '../../state/app-store'
9
6
  import { theme } from '../theme'
10
- import { GitPanel } from './git-panel'
7
+ import { GitPaneWidget } from './git-pane-widget'
11
8
  import { buildTabGroupInfo } from './sidebar-group-metadata'
12
9
  import { TabItem } from './tab-item'
13
10
  import { useSidebarAutoScroll } from './use-sidebar-auto-scroll'
@@ -143,50 +140,29 @@ const TabsBody = memo(function TabsBody({ onTabActivate }: TabsBodyProps) {
143
140
  )
144
141
  })
145
142
 
146
- const GitBody = memo(function GitBody() {
147
- const gitPanel = useAppStore((s) => s.gitPanel)
148
- const currentSessionId = useAppStore((s) => s.currentSessionId)
149
- const sessions = useAppStore((s) => s.sessions)
150
- const sidebarVisible = useAppStore((s) => s.sidebar.visible)
151
- const gitPanelVisible = useAppStore((s) => s.sidebar.gitPanelVisible)
152
- const currentSession = currentSessionId
153
- ? sessions.find((s) => s.id === currentSessionId)
154
- : undefined
155
- const projectPath = currentSession?.projectPath
156
-
157
- useGitPanelPolling({
158
- enabled: sidebarVisible && gitPanelVisible,
159
- projectPath,
160
- })
161
-
162
- const lastGoodRef = useRef<GitPanelState | null>(null)
163
- const prevProjectPathRef = useRef(projectPath)
164
- if (prevProjectPathRef.current !== projectPath) {
165
- prevProjectPathRef.current = projectPath
166
- lastGoodRef.current = null
167
- }
168
- const isGood = gitPanel.error === null && gitPanel.branch !== null
169
- if (isGood) {
170
- lastGoodRef.current = gitPanel
171
- }
172
- const display = lastGoodRef.current ?? gitPanel
173
-
174
- return <GitPanel gitPanel={display} projectPath={projectPath} />
175
- })
176
-
177
143
  export function Sidebar({ onTabActivate }: SidebarProps) {
178
144
  const sidebarVisible = useAppStore((s) => s.sidebar.visible)
179
145
  const sidebarWidth = useAppStore((s) => s.sidebar.width)
180
- const gitPanelVisible = useAppStore((s) => s.sidebar.gitPanelVisible)
181
- const gitPanelRatio = useAppStore((s) => s.sidebar.gitPanelRatio)
146
+ const gitPane = useAppStore((s) => s.gitPane)
182
147
 
183
148
  if (!sidebarVisible) {
184
149
  return null
185
150
  }
186
151
 
152
+ const gitEmbedded = gitPane.mode === 'embedded' && gitPane.visible
153
+ const gitOnTop = gitEmbedded && gitPane.position === 'top'
154
+ const gitOnBottom = gitEmbedded && gitPane.position === 'bottom'
155
+
187
156
  // flex-grow scaled by 100 (integer preferred); tabs gets (1-ratio), git gets ratio.
188
- const tabsGrow = Math.max(1, Math.round((1 - gitPanelRatio) * 100))
189
- const gitGrow = Math.max(1, Math.round(gitPanelRatio * 100))
157
+ const tabsGrow = gitEmbedded ? Math.max(1, Math.round((1 - gitPane.ratio) * 100)) : 1
158
+ const gitGrow = gitEmbedded ? Math.max(1, Math.round(gitPane.ratio * 100)) : 0
159
+
160
+ const separator = <text fg={theme.dim}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
161
+ const gitBody = gitEmbedded ? (
162
+ <box flexDirection="column" flexGrow={gitGrow} flexShrink={1} flexBasis={0} overflow="hidden">
163
+ <GitPaneWidget pollingEnabled={gitPane.visible} />
164
+ </box>
165
+ ) : null
190
166
 
191
167
  return (
192
168
  <box
@@ -197,6 +173,12 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
197
173
  gap={0}
198
174
  >
199
175
  <SidebarTop />
176
+ {gitOnTop ? (
177
+ <>
178
+ {gitBody}
179
+ {separator}
180
+ </>
181
+ ) : null}
200
182
  <box
201
183
  flexDirection="column"
202
184
  flexGrow={tabsGrow}
@@ -206,18 +188,10 @@ export function Sidebar({ onTabActivate }: SidebarProps) {
206
188
  >
207
189
  <TabsBody onTabActivate={onTabActivate} />
208
190
  </box>
209
- {gitPanelVisible ? (
191
+ {gitOnBottom ? (
210
192
  <>
211
- <text fg={theme.dim}>{'·'.repeat(Math.max(0, sidebarWidth - 2))}</text>
212
- <box
213
- flexDirection="column"
214
- flexGrow={gitGrow}
215
- flexShrink={1}
216
- flexBasis={0}
217
- overflow="hidden"
218
- >
219
- <GitBody />
220
- </box>
193
+ {separator}
194
+ {gitBody}
221
195
  </>
222
196
  ) : null}
223
197
  </box>
@@ -1,4 +1,3 @@
1
- import { useModalHelp } from '../keymap-context'
2
1
  import { theme } from '../theme'
3
2
  import { uiTokens } from '../ui-tokens'
4
3
  import { InputField } from './input-field'
@@ -19,12 +18,11 @@ export function SnippetEditorModal({
19
18
  }: SnippetEditorModalProps) {
20
19
  const nameActive = activeField === 'name'
21
20
  const contentActive = activeField === 'content'
22
- const help = useModalHelp('modal.snippet-editor')
23
21
 
24
22
  return (
25
23
  <ModalShell
26
24
  title={isEditing ? 'Edit snippet' : 'Create snippet'}
27
- help={help}
25
+ keybindsModeId="modal.snippet-editor"
28
26
  width={uiTokens.modalWidth.xl}
29
27
  >
30
28
  <box flexDirection="column">
@@ -1,7 +1,6 @@
1
1
  import type { SnippetRecord } from '../../state/types'
2
2
 
3
3
  import { filterSnippets } from '../../state/selectors'
4
- import { useModalHelp } from '../keymap-context'
5
4
  import { theme } from '../theme'
6
5
  import { uiTokens } from '../ui-tokens'
7
6
  import { ListItem } from './list-item'
@@ -24,12 +23,11 @@ function truncateContent(content: string): string {
24
23
 
25
24
  export function SnippetPickerModal({ filter, selectedIndex, snippets }: SnippetPickerModalProps) {
26
25
  const filtered = filterSnippets(snippets, filter)
27
- const help = useModalHelp('modal.snippet-picker')
28
26
 
29
27
  return (
30
28
  <ModalShell
31
29
  title="Snippets"
32
- help={help}
30
+ keybindsModeId="modal.snippet-picker"
33
31
  width={uiTokens.modalWidth.xl}
34
32
  footer={<ModalFilterBar filter={filter} />}
35
33
  >
@@ -10,8 +10,6 @@ function getModeColor(focusMode: AppState['focusMode']): string {
10
10
  switch (focusMode) {
11
11
  case 'terminal-input':
12
12
  return theme.accent
13
- case 'layout':
14
- return theme.warning
15
13
  case 'modal':
16
14
  return theme.warning
17
15
  case 'command-edit':
@@ -28,8 +26,6 @@ function getModeLabel(focusMode: AppState['focusMode']): string {
28
26
  switch (focusMode) {
29
27
  case 'terminal-input':
30
28
  return 'input'
31
- case 'layout':
32
- return 'layout'
33
29
  case 'modal':
34
30
  return 'modal'
35
31
  case 'command-edit':
@@ -1,4 +1,3 @@
1
- import { useModalHelp } from '../keymap-context'
2
1
  import { theme } from '../theme'
3
2
  import { THEME_IDS, type ThemeId, THEMES } from '../themes'
4
3
  import { uiTokens } from '../ui-tokens'
@@ -11,9 +10,13 @@ interface ThemePickerModalProps {
11
10
  }
12
11
 
13
12
  export function ThemePickerModal({ currentThemeId, selectedIndex }: ThemePickerModalProps) {
14
- const help = useModalHelp('modal.theme-picker')
15
13
  return (
16
- <ModalShell title="Select theme" help={help} width={uiTokens.modalWidth.md} listGap={0}>
14
+ <ModalShell
15
+ title="Select theme"
16
+ keybindsModeId="modal.theme-picker"
17
+ width={uiTokens.modalWidth.md}
18
+ listGap={0}
19
+ >
17
20
  {THEME_IDS.map((id, index) => {
18
21
  const entry = THEMES[id]
19
22
  const active = index === selectedIndex
@@ -1,4 +1,3 @@
1
- import { useModalHelp } from '../keymap-context'
2
1
  import { theme } from '../theme'
3
2
  import { uiTokens } from '../ui-tokens'
4
3
  import { ListItem } from './list-item'
@@ -20,10 +19,14 @@ export function UpdateAvailableModal({
20
19
  latestVersion,
21
20
  selectedIndex,
22
21
  }: UpdateAvailableModalProps) {
23
- const modalHelp = useModalHelp('modal.update-available')
24
- const help = `${currentVersion} → ${latestVersion} ${modalHelp}`
25
22
  return (
26
- <ModalShell title="Update available" help={help} width={uiTokens.modalWidth.md} listGap={1}>
23
+ <ModalShell
24
+ title="Update available"
25
+ subtitle={`${currentVersion} → ${latestVersion}`}
26
+ keybindsModeId="modal.update-available"
27
+ width={uiTokens.modalWidth.md}
28
+ listGap={1}
29
+ >
27
30
  <box flexDirection="row" gap={1} marginTop={1}>
28
31
  {OPTIONS.map((option, index) => {
29
32
  const active = index === selectedIndex
@@ -3,7 +3,7 @@ import {
3
3
  type ModeId,
4
4
  type ResolvedKeymapConfig,
5
5
  } from '@brimveyn/aimux-config'
6
- import { createContext, useContext, useMemo } from 'react'
6
+ import { createContext, useContext } from 'react'
7
7
 
8
8
  import { describeBindings } from '../input/keymap/describe-bindings'
9
9
 
@@ -32,8 +32,3 @@ export function buildHintText(
32
32
  const trimmed = bindings.slice(0, limit)
33
33
  return trimmed.map((b) => `${b.keysDisplay} ${b.description ?? ''}`.trim()).join(HELP_JOINER)
34
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
@@ -8,6 +8,7 @@ import { useAppStore } from '../state/app-store'
8
8
  import { getTreeForTab, PANE_BORDER, type SplitDirection } from '../state/layout-tree'
9
9
  import { CreateSessionModal } from './components/create-session-modal'
10
10
  import { GitCommitModal } from './components/git-commit-modal'
11
+ import { GitPaneWidget } from './components/git-pane-widget'
11
12
  import { GitView } from './components/git-view'
12
13
  import { HelpModal } from './components/help-modal'
13
14
  import { NewTabModal } from './components/new-tab-modal'
@@ -144,7 +145,7 @@ function renderModal(
144
145
  />
145
146
  )
146
147
  case 'help':
147
- return <HelpModal />
148
+ return <HelpModal filter={modal.editBuffer} selectedIndex={modal.selectedIndex} />
148
149
  case 'git-commit': {
149
150
  const titleText =
150
151
  modal.activeField === 'title' ? (modal.editBuffer ?? '') : modal.contentBuffer
@@ -215,6 +216,10 @@ export function RootView({
215
216
  const sessions = useAppStore((s) => s.sessions)
216
217
  const currentSessionId = useAppStore((s) => s.currentSessionId)
217
218
  const sessionBarPosition = useAppStore((s) => s.sessionBar.position)
219
+ const gitPaneMode = useAppStore((s) => s.gitPane.mode)
220
+ const gitPaneVisible = useAppStore((s) => s.gitPane.visible)
221
+ const gitPanePosition = useAppStore((s) => s.gitPane.position)
222
+ const gitPaneRatio = useAppStore((s) => s.gitPane.ratio)
218
223
 
219
224
  const activeTab = tabs.find((tab) => tab.id === activeTabId)
220
225
  const activeTree = activeTabId ? getTreeForTab(layoutTrees, tabGroupMap, activeTabId) : null
@@ -248,6 +253,9 @@ export function RootView({
248
253
  {sessionBarPosition === 'top' && <SessionBar />}
249
254
  <box flexDirection="row" gap={0} padding={0} flexGrow={1}>
250
255
  <Sidebar onTabActivate={onPaneActivate} />
256
+ {gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'left' ? (
257
+ <GitPaneInPaneMode ratio={gitPaneRatio} />
258
+ ) : null}
251
259
  {activeTree && activeTree.type === 'split' ? (
252
260
  <SplitLayout
253
261
  node={activeTree}
@@ -292,6 +300,9 @@ export function RootView({
292
300
  onPaneActivate={onPaneActivate}
293
301
  />
294
302
  )}
303
+ {gitPaneMode === 'pane' && gitPaneVisible && gitPanePosition === 'right' ? (
304
+ <GitPaneInPaneMode ratio={gitPaneRatio} />
305
+ ) : null}
295
306
  </box>
296
307
  {sessionBarPosition === 'bottom' && <SessionBar />}
297
308
  <StatusBar />
@@ -309,3 +320,20 @@ export function RootView({
309
320
  </box>
310
321
  )
311
322
  }
323
+
324
+ function GitPaneInPaneMode({ ratio }: { ratio: number }) {
325
+ // Ratio maps to a fixed column count (20..80), mirroring the reservation in
326
+ // use-terminal-resize so the terminal-content area stays in sync.
327
+ const width = Math.max(20, Math.min(80, Math.round(ratio * 80)))
328
+ return (
329
+ <box
330
+ flexDirection="column"
331
+ width={width}
332
+ flexShrink={0}
333
+ backgroundColor={theme.panel}
334
+ overflow="hidden"
335
+ >
336
+ <GitPaneWidget pollingEnabled />
337
+ </box>
338
+ )
339
+ }
@@ -62,11 +62,6 @@ export function getStatusBarModel(
62
62
  right: modalMode ? hintForMode(config, modalMode) : '',
63
63
  }
64
64
  }
65
- case 'layout':
66
- return {
67
- left: `${getActiveTabLabel(activeTab)} ${sessionIcon} ${sessionLabel}`,
68
- right: hintForMode(config, 'layout'),
69
- }
70
65
  case 'git':
71
66
  return {
72
67
  left: `${sessionIcon} ${sessionLabel}`,
@@ -1 +0,0 @@
1
- export { searchProjectDirectories as searchDirectories } from '../platform/project-search'