@brimveyn/aimux 1.4.1 → 1.5.0

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 (44) hide show
  1. package/README.md +142 -176
  2. package/package.json +5 -2
  3. package/src/app-runtime/use-terminal-resize.ts +12 -2
  4. package/src/app.tsx +20 -3
  5. package/src/config.ts +53 -17
  6. package/src/input/keymap/build-handlers.ts +1 -0
  7. package/src/input/keymap/help-entries.ts +44 -0
  8. package/src/input/keymap/keymap-ref.ts +11 -0
  9. package/src/input/keymap/sequence-resolver.ts +35 -2
  10. package/src/input/keymap/trie.ts +1 -0
  11. package/src/input/modes/bridge.ts +1 -1
  12. package/src/input/modes/handlers/shared.ts +0 -16
  13. package/src/input/modes/transitions.ts +4 -4
  14. package/src/input/modes/types.ts +1 -1
  15. package/src/platform/daemon-control.ts +0 -8
  16. package/src/state/reducers/git-panel-state.ts +35 -8
  17. package/src/state/reducers/modal-state.ts +48 -3
  18. package/src/state/selectors.ts +1 -5
  19. package/src/state/session-persistence.ts +1 -20
  20. package/src/state/store.ts +38 -5
  21. package/src/state/types.ts +27 -13
  22. package/src/state/validation.ts +0 -2
  23. package/src/state/workspace-save.ts +6 -2
  24. package/src/ui/components/create-session-modal.tsx +5 -3
  25. package/src/ui/components/git-commit-modal.tsx +1 -3
  26. package/src/ui/components/git-pane-widget.tsx +46 -0
  27. package/src/ui/components/git-panel.tsx +72 -25
  28. package/src/ui/components/help-modal.tsx +156 -42
  29. package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
  30. package/src/ui/components/modal-shell.tsx +15 -3
  31. package/src/ui/components/new-tab-modal.tsx +9 -10
  32. package/src/ui/components/pending-chord-overlay.tsx +2 -1
  33. package/src/ui/components/session-name-modal.tsx +1 -3
  34. package/src/ui/components/session-picker-modal.tsx +1 -3
  35. package/src/ui/components/sidebar.tsx +24 -50
  36. package/src/ui/components/snippet-editor-modal.tsx +1 -3
  37. package/src/ui/components/snippet-picker-modal.tsx +1 -3
  38. package/src/ui/components/status-bar.tsx +0 -4
  39. package/src/ui/components/theme-picker-modal.tsx +6 -3
  40. package/src/ui/components/update-available-modal.tsx +7 -4
  41. package/src/ui/keymap-context.ts +1 -6
  42. package/src/ui/root.tsx +29 -1
  43. package/src/ui/status-bar-model.ts +0 -5
  44. package/src/ui/directory-search.ts +0 -1
package/README.md CHANGED
@@ -1,6 +1,8 @@
1
1
  # aimux
2
2
 
3
- A terminal multiplexer for AI CLIs. Manage multiple AI assistant sessions (Claude, Codex, OpenCode) side by side in a single terminal with tabbed navigation, split panes, persistent sessions, and fully configurable keybindings.
3
+ A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode, and normal
4
+ shell tabs side by side in one TUI with persistent sessions, split panes,
5
+ snippets, themes, and fully configurable keymaps.
4
6
 
5
7
  ![Built with Bun](https://img.shields.io/badge/runtime-Bun-f9f1e1)
6
8
  ![TypeScript](https://img.shields.io/badge/lang-TypeScript-3178c6)
@@ -10,39 +12,16 @@ A terminal multiplexer for AI CLIs. Manage multiple AI assistant sessions (Claud
10
12
 
11
13
  ## Features
12
14
 
13
- - **Multi-tab sessions** Run Claude, Codex, and OpenCode in parallel with instant tab switching
14
- - **Session bar** — Numbered session chips at the top (or bottom) of the screen. Click to switch, drag to reorder, busy spinner for non-focused sessions with live PTY output. Toggle with `<leader>b`; jump with `<leader>1..9`. Position persists via `aimux.config.ts` or `aimux.json`.
15
- - **Split panes** Split vertically (`|`) or horizontally (`-`) to view multiple assistants at once
16
- - **Draggable separators** Resize split panes by dragging with the mouse
17
- - **Click-to-focus** Click any pane or sidebar tab to focus it instantly
18
- - **Full terminal emulation** Powered by xterm.js with mouse tracking, alternate buffer, and scrollback
19
- - **Configurable nvim-style keymaps** — Define keybinds in a typed `aimux.config.ts` with leader keys, multi-key sequences, and a prefix-trie resolver
20
- - **Text selection** Double-click for a word, triple-click for a line, drag for a region. Selections copy to system clipboard automatically
21
- - **Project-scoped sessions** Associate a git repository with each session; all tabs spawn in that directory
22
- - **Directory picker** Fuzzy-search git repos and worktrees from `$HOME` using `fzf`
23
- - **Session persistence** — Workspace state (tabs, titles, layout) saved and restored on restart
24
- - **Git status panel** — Branch + diff summary in the sidebar
25
- - **Seamless daemon updates** — A restartable IPC daemon now reconnects to a long-lived terminal manager, so updates and IPC changes do not kill live tabs
26
- - **Snippets** — Save and reuse prompt snippets across sessions
27
- - **Theme picker** — Switch between 11 built-in themes on the fly
28
- - **Pending-chord indicator** — Bottom-right overlay shows mid-sequence key state (like nvim's `which-key`)
29
- - **Built-in help** — Press `?` to see all keybindings
30
-
31
- ### Session Management
32
-
33
- ![Session management](assets/sessions.gif)
34
-
35
- ### Multi-Tab Workflow
36
-
37
- ![Multi-tab workflow](assets/tabs.gif)
38
-
39
- ### Themes
40
-
41
- ![Themes](assets/themes.gif)
42
-
43
- ### Split Panes
44
-
45
- ![Split panes](assets/splits.gif)
15
+ - multi-session workflow with a dedicated session picker
16
+ - tabs for `claude`, `codex`, `opencode`, and `terminal`
17
+ - split panes with pane focus and resize shortcuts
18
+ - persistent sessions with saved layout and tab state
19
+ - profile-isolated config, catalogs, daemon sockets, and runtime state
20
+ - typed keymap customization through `@brimveyn/aimux-config`
21
+ - snippets catalog and snippet picker
22
+ - configurable git pane (embedded in the sidebar or as a standalone pane) and git mode
23
+ - built-in help generated from the resolved keymap
24
+ - theme picker with 11 built-in themes
46
25
 
47
26
  ## Install
48
27
 
@@ -52,178 +31,165 @@ bun install -g @brimveyn/aimux
52
31
 
53
32
  Requires [Bun](https://bun.sh).
54
33
 
55
- ## Usage
56
-
57
- ```bash
58
- aimux # start the TUI
59
- aimux version # print version
60
- aimux doctor # check setup
61
- aimux update # self-update
62
- aimux restart-daemon # restart IPC daemon only
63
- ```
34
+ ## Quick Start
64
35
 
65
- `aimux update` and `aimux restart-daemon` restart only the IPC daemon. Live PTYs and headless terminal state stay in the long-lived terminal-manager process, so active tabs can be reattached instead of being restarted.
66
-
67
- ## Configuration
68
-
69
- aimux reads `~/.config/aimux/<profile>/aimux.config.ts` at startup. The default installed profile is `default`, while the repository dev scripts use `dev`. Set up the default profile with:
36
+ Create the default profile and install the typed config package into it:
70
37
 
71
38
  ```bash
72
- mkdir -p ~/.config/aimux/default && cd ~/.config/aimux/default
39
+ mkdir -p ~/.config/aimux/default
40
+ cd ~/.config/aimux/default
73
41
  bun init -y
74
42
  bun add -d @brimveyn/aimux-config
75
43
  ```
76
44
 
77
- Then create `~/.config/aimux/default/aimux.config.ts`:
45
+ Create `~/.config/aimux/default/aimux.config.ts`:
78
46
 
79
47
  ```ts
80
- import { defineConfig, actions, themes } from '@brimveyn/aimux-config'
48
+ import { defineConfig, actions } from '@brimveyn/aimux-config'
81
49
 
82
50
  export default defineConfig({
83
- theme: themes.extend('tokyo-night', { accent: '#ff9e64' }),
51
+ sessionBar: {
52
+ position: 'top',
53
+ visible: true,
54
+ },
84
55
 
85
56
  keymaps: (k) =>
86
- k
87
- .leader('<Space>')
88
- .timeout(300)
89
- .mode('navigation', (m) =>
90
- m
91
- .map('j', actions.nextTab)
92
- .map('k', actions.prevTab)
93
- .map('<leader>g', actions.sessionPicker)
94
- .group('<leader>t', 'tabs', (g) =>
95
- g.map('n', actions.newTab).map('r', actions.renameTab).map('x', actions.closeTab)
96
- )
97
- )
98
- .mode('layout', (m) => m.map('|', actions.splitVertical).map('-', actions.splitHorizontal)),
57
+ k.mode('navigation', (m) => m.map('<C-p>', actions.sessionPicker, 'Session picker')),
99
58
  })
100
59
  ```
101
60
 
102
- User bindings override defaults. Use `.unmap(keys)` to remove a default. See [`@brimveyn/aimux-config`](packages/aimux-config/README.md) for the full builder API.
103
-
104
- ### Key notation
105
-
106
- | Notation | Meaning |
107
- | -------------- | -------------------------- |
108
- | `j` | Bare character |
109
- | `J` | Shift+J (uppercase letter) |
110
- | `<C-n>` | Ctrl+N |
111
- | `<M-x>` | Meta/Alt+X |
112
- | `<CR>` `<Esc>` | Return / Escape |
113
- | `<leader>` | Configured leader chord |
114
- | `dd` | Multi-key sequence |
115
- | `<leader>tn` | Leader, then t, then n |
116
-
117
- ## Default Keymaps
118
-
119
- Press `?` in navigation mode for the full, live keybinding list (reflects your config).
120
-
121
- ### Navigation Mode
122
-
123
- | Key | Action |
124
- | --------------------- | -------------------- |
125
- | `j` / `k` | Next / previous tab |
126
- | `Shift+J` / `Shift+K` | Reorder tabs |
127
- | `i` | Enter terminal input |
128
- | `r` | Rename active tab |
129
- | `dd` | Close active tab |
130
- | `Ctrl+N` | New tab |
131
- | `Ctrl+R` | Restart tab |
132
- | `Ctrl+G` | Session picker |
133
- | `Ctrl+B` | Toggle sidebar |
134
- | `Ctrl+H` / `Ctrl+L` | Resize sidebar |
135
- | `Ctrl+S` | Snippet picker |
136
- | `Ctrl+T` | Theme picker |
137
- | `G` | Toggle git panel |
138
- | `<leader>b` | Toggle session bar |
139
- | `<leader>1..9` | Switch to session N |
140
- | `?` | Show help |
141
- | `Ctrl+C` | Quit |
142
-
143
- ### Terminal Input Mode
144
-
145
- Keystrokes pass through to the active tab's PTY. Configured shortcuts:
146
-
147
- | Key | Action |
148
- | ---------- | ------------------- |
149
- | `Ctrl+Z` | Leave to navigation |
150
- | `<leader>` | Enter layout mode |
151
- | `Ctrl+B` | Toggle sidebar |
152
-
153
- ### Layout Mode
154
-
155
- | Key | Action |
156
- | --------------------- | ---------------- |
157
- | `\|` | Split vertical |
158
- | `-` | Split horizontal |
159
- | `h` / `j` / `k` / `l` | Focus pane |
160
- | `Shift+H/J/K/L` | Resize pane |
161
- | `q` | Close pane |
162
- | `Esc` | Back to input |
163
-
164
- ## Architecture
165
-
166
- Runtime split:
167
-
168
- - `aimux` UI connects to the IPC daemon over the app protocol.
169
- - The IPC daemon owns the app-facing socket, protocol negotiation, and reconnect behavior.
170
- - The terminal manager owns PTYs, xterm headless emulators, and live session state.
171
-
172
- This split lets the app or IPC daemon change protocols without dropping live shells.
61
+ Then start the app:
173
62
 
63
+ ```bash
64
+ aimux
174
65
  ```
175
- aimux/
176
- ├── packages/
177
- │ └── aimux-config/ # published as @brimveyn/aimux-config
178
- │ └── src/ # types, builder, actions, themes, defaults
179
- └── src/ # the CLI (published as @brimveyn/aimux)
180
- ├── index.tsx # entry point + CLI dispatcher
181
- ├── app.tsx # main React app + state wiring
182
- ├── config/
183
- │ └── loader.ts # loads aimux.config.ts
184
- ├── ui/ # OpenTUI React components
185
- ├── state/ # reducers + app store
186
- ├── pty/ # PTY and terminal emulation
187
- ├── session-backend/ # local and remote backends
188
- ├── daemon/ # IPC daemon / broker
189
- ├── terminal-manager/ # long-lived PTY/session owner
190
- ├── ipc/ # app and manager protocols
191
- └── input/
192
- ├── modes/ # mode registry + transitions
193
- ├── keymap/ # prefix trie + sequence resolver
194
- └── raw-input-handler.ts
66
+
67
+ On first launch, use the session picker flow to create your first session.
68
+
69
+ For the full setup path, see [`docs/getting-started.md`](docs/getting-started.md).
70
+
71
+ ## Core Concepts
72
+
73
+ ### Profiles
74
+
75
+ Profile-managed config and catalogs live under:
76
+
77
+ ```text
78
+ ~/.config/aimux/<profile>/
195
79
  ```
196
80
 
197
- ## Tech Stack
81
+ The active profile is chosen from:
198
82
 
199
- - [Bun](https://bun.sh) — Runtime and toolchain
200
- - [React](https://react.dev) + [OpenTUI](https://github.com/sst/opentui) — Terminal UI framework
201
- - [xterm.js](https://xtermjs.org) (headless) — Terminal emulation
202
- - [bun-pty](https://github.com/nicolo-ribaudo/bun-pty) — Native PTY spawning
203
- - [Zustand](https://zustand-demo.pmnd.rs/) — State store
83
+ 1. `AIMUX_PROFILE`
84
+ 2. `AIMUX_RUNTIME_PROFILE`
85
+ 3. `default`
204
86
 
205
- ## Development
87
+ Runtime sockets live in a separate runtime directory that depends on the active
88
+ profile. See [`docs/concepts/profiles.md`](docs/concepts/profiles.md).
206
89
 
207
- ```bash
208
- git clone https://github.com/BrimVeyn/aimux && cd aimux
209
- bun install
90
+ ### Config vs Runtime State
91
+
92
+ `aimux` uses multiple files per profile:
93
+
94
+ - `aimux.config.ts` or `aimux.config.js` - typed user config
95
+ - `aimux.json` - app-managed preferences and runtime state
96
+ - `aimux-sessions.json` - session catalog and workspace snapshots
97
+ - `aimux-snippets.json` - snippet catalog
98
+
99
+ See [`docs/concepts/config-and-state.md`](docs/concepts/config-and-state.md).
210
100
 
211
- bun run dev # auto-reload dev mode
212
- bun run start # run from source
213
- bun test # run test suite
214
- bun run check # typecheck
215
- bun run lint # oxlint
101
+ ### Sessions
102
+
103
+ Sessions are named workspaces. A session can have:
104
+
105
+ - a name
106
+ - an optional project directory
107
+ - a persisted workspace snapshot
108
+ - an order in the session bar and session picker
109
+
110
+ See [`docs/guide/sessions.md`](docs/guide/sessions.md).
111
+
112
+ ### Keymaps
113
+
114
+ Keymaps are defined through `@brimveyn/aimux-config` and merged with shipped
115
+ defaults at startup.
116
+
117
+ Important runtime fact:
118
+
119
+ - the shipped leader key is `Ctrl+W`
120
+
121
+ See [`docs/guide/keymaps.md`](docs/guide/keymaps.md).
122
+
123
+ ## Default Everyday Shortcuts
124
+
125
+ - `?` - open help
126
+ - `i` - focus terminal
127
+ - `Ctrl+Z` - leave terminal-input mode
128
+ - `Ctrl+N` - open new-tab modal
129
+ - `Ctrl+G` - open session picker
130
+ - `Ctrl+S` - open snippet picker
131
+ - `Ctrl+T` - open theme picker
132
+ - `Ctrl+B` - toggle sidebar
133
+ - `Ctrl+W b` - toggle session bar
134
+ - `Ctrl+W 1` through `Ctrl+W 9` - switch sessions by index
135
+
136
+ The help modal reflects the resolved keymap, so it includes your overrides.
137
+
138
+ ## CLI
139
+
140
+ ```bash
141
+ aimux
142
+ aimux version
143
+ aimux doctor
144
+ aimux update
145
+ aimux restart-daemon
146
+ aimux restart-terminal-manager
216
147
  ```
217
148
 
218
- By default the app talks to the background IPC daemon. For explicit single-process debugging only, set `AIMUX_LOCAL_BACKEND=1` before starting aimux.
149
+ See [`docs/reference/cli.md`](docs/reference/cli.md) for behavior details.
150
+
151
+ ## Runtime Model
152
+
153
+ `aimux` is split into:
154
+
155
+ - the UI app
156
+ - an IPC daemon
157
+ - a long-lived terminal manager
219
158
 
220
- Profiles live under `~/.config/aimux/<profile>/`. Each profile gets its own config, session catalog, snippet catalog, and matching runtime namespace.
159
+ This split is what allows daemon restarts and some update paths without dropping
160
+ every live PTY immediately.
221
161
 
222
- The repository `bun run dev`, `bun run start`, and `bun run restart-daemon` scripts set `AIMUX_PROFILE=dev`, so source builds use `~/.config/aimux/dev/` and their own IPC daemon / terminal-manager sockets instead of interfering with a globally installed `aimux` instance.
162
+ See [`docs/developer/architecture.md`](docs/developer/architecture.md).
223
163
 
224
- You can override the active profile manually with `AIMUX_PROFILE=<name>` when you need multiple isolated environments on the same machine. `AIMUX_RUNTIME_PROFILE` is still accepted as a fallback alias for runtime compatibility.
164
+ ## Documentation Map
165
+
166
+ - [`docs/getting-started.md`](docs/getting-started.md)
167
+ - [`docs/concepts/config-and-state.md`](docs/concepts/config-and-state.md)
168
+ - [`docs/concepts/profiles.md`](docs/concepts/profiles.md)
169
+ - [`docs/guide/sessions.md`](docs/guide/sessions.md)
170
+ - [`docs/guide/keymaps.md`](docs/guide/keymaps.md)
171
+ - [`docs/guide/themes.md`](docs/guide/themes.md)
172
+ - [`docs/reference/cli.md`](docs/reference/cli.md)
173
+ - [`docs/reference/config-reference.md`](docs/reference/config-reference.md)
174
+ - [`docs/reference/runtime-paths.md`](docs/reference/runtime-paths.md)
175
+ - [`docs/developer/architecture.md`](docs/developer/architecture.md)
176
+ - [`docs/developer/aimux-config-internals.md`](docs/developer/aimux-config-internals.md)
177
+
178
+ ## Development
179
+
180
+ ```bash
181
+ git clone https://github.com/BrimVeyn/aimux && cd aimux
182
+ bun install
183
+
184
+ bun run dev
185
+ bun run start
186
+ bun test
187
+ bun run check
188
+ bun run lint
189
+ ```
225
190
 
226
- This profile move is intentionally breaking: aimux no longer reads legacy flat config or catalog files once profile directories are enabled.
191
+ The repository dev scripts use `AIMUX_PROFILE=dev`, so local development does
192
+ not collide with a globally installed `aimux` instance.
227
193
 
228
194
  ## License
229
195
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.4.1",
3
+ "version": "1.5.0",
4
4
  "description": "A terminal multiplexer for AI CLIs. Run Claude, Codex, OpenCode side-by-side with tabbed navigation, split panes, and persistent sessions.",
5
5
  "keywords": [
6
6
  "ai",
@@ -55,7 +55,9 @@
55
55
  "restart-terminal-manager": "AIMUX_PROFILE=dev bun run src/index.tsx restart-terminal-manager",
56
56
  "lint": "oxlint .",
57
57
  "format": "oxfmt --write .",
58
- "format:check": "oxfmt --check ."
58
+ "format:check": "oxfmt --check .",
59
+ "knip": "knip-bun",
60
+ "bump": "bun run scripts/bump.ts"
59
61
  },
60
62
  "dependencies": {
61
63
  "@brimveyn/aimux-config": "0.3.1",
@@ -71,6 +73,7 @@
71
73
  "@types/node": "^25.5.0",
72
74
  "@types/react": "^19.2.14",
73
75
  "eslint-plugin-perfectionist": "^5.8.0",
76
+ "knip": "^6.4.1",
74
77
  "lefthook": "^2.1.4",
75
78
  "oxfmt": "^0.42.0",
76
79
  "oxlint": "^1.57.0",
@@ -129,6 +129,7 @@ export function useTerminalResize({
129
129
  .map((t) => [t.id, t.scrollIntent])
130
130
  )
131
131
 
132
+ const gitPaneInPaneMode = state.gitPane.mode === 'pane' && state.gitPane.visible
132
133
  const terminalSize = useMemo(() => {
133
134
  const sidebarWidth = state.sidebar.visible ? state.sidebar.width + 1 : 0
134
135
  const sessionBarRows = state.sessionBar.visible ? 1 : 0
@@ -139,16 +140,19 @@ export function useTerminalResize({
139
140
  STATUS_BAR_HEIGHT +
140
141
  TERMINAL_PANE_VERTICAL_CHROME +
141
142
  sessionBarRows
143
+ const gitPaneRaw = gitPaneInPaneMode ? Math.round(state.gitPane.ratio * 80) : 0
144
+ const gitPaneWidth = gitPaneInPaneMode ? Math.max(20, Math.min(80, gitPaneRaw)) : 0
145
+ const gitOnLeft = gitPaneInPaneMode && state.gitPane.position === 'left'
142
146
  const cols = Math.max(
143
147
  MIN_TERMINAL_COLS,
144
- Math.floor(dimensions.width - sidebarWidth - MAIN_AREA_HORIZONTAL_CHROME)
148
+ Math.floor(dimensions.width - sidebarWidth - gitPaneWidth - MAIN_AREA_HORIZONTAL_CHROME)
145
149
  )
146
150
  const rows = Math.max(MIN_TERMINAL_ROWS, Math.floor(dimensions.height - reservedRows))
147
151
 
148
152
  contentOriginRef.current = {
149
153
  cols,
150
154
  rows,
151
- x: sidebarWidth + 1,
155
+ x: sidebarWidth + (gitOnLeft ? gitPaneWidth : 0) + 1,
152
156
  y: 1 + sessionBarTopOffset,
153
157
  }
154
158
 
@@ -161,6 +165,9 @@ export function useTerminalResize({
161
165
  state.sidebar.width,
162
166
  state.sessionBar.visible,
163
167
  state.sessionBar.position,
168
+ gitPaneInPaneMode,
169
+ state.gitPane.position,
170
+ state.gitPane.ratio,
164
171
  ])
165
172
 
166
173
  useLayoutEffect(() => {
@@ -187,6 +194,9 @@ export function useTerminalResize({
187
194
  state.sidebar.width,
188
195
  state.sessionBar.visible,
189
196
  state.sessionBar.position,
197
+ gitPaneInPaneMode,
198
+ state.gitPane.position,
199
+ state.gitPane.ratio,
190
200
  ])
191
201
 
192
202
  useEffect(() => {
package/src/app.tsx CHANGED
@@ -25,6 +25,7 @@ import { useRendererBindings } from './app-runtime/use-renderer-bindings'
25
25
  import { useTerminalResize } from './app-runtime/use-terminal-resize'
26
26
  import { useWorkspaceAutosave } from './app-runtime/use-workspace-autosave'
27
27
  import { loadConfig } from './config'
28
+ import { setActiveKeymap } from './input/keymap/keymap-ref'
28
29
  import { deriveModeId } from './input/modes/bridge'
29
30
  import { registerAllModes } from './input/modes/handlers'
30
31
  import { getHandler, transitionTo } from './input/modes/registry'
@@ -54,7 +55,10 @@ export function App({
54
55
  resolvedConfig: ResolvedConfig
55
56
  }) {
56
57
  const keymapHandlers = useMemo(
57
- () => registerAllModes(resolvedConfig.keymaps),
58
+ () => {
59
+ setActiveKeymap(resolvedConfig.keymaps)
60
+ return registerAllModes(resolvedConfig.keymaps)
61
+ },
58
62
  // Registration has side effects in a global mode registry — run once per app instance.
59
63
  // eslint-disable-next-line react-hooks/exhaustive-deps
60
64
  []
@@ -73,14 +77,27 @@ export function App({
73
77
  const sessionBarVisible = resolvedConfig.sessionBar?.visible ?? json.sessionBarVisible ?? true
74
78
  const sessionBarPosition =
75
79
  resolvedConfig.sessionBar?.position ?? json.sessionBarPosition ?? 'top'
80
+
81
+ // Merge config-file gitPane (persisted prefs) with user's resolved gitPane
82
+ // (programmatic config). User config wins; file provides persisted prior state.
83
+ const userGitPane = resolvedConfig.gitPane
84
+ const gitPaneOverrides = {
85
+ ...json.gitPane,
86
+ ...(userGitPane?.visible !== undefined ? { visible: userGitPane.visible } : {}),
87
+ ...(userGitPane?.mode !== undefined ? { mode: userGitPane.mode } : {}),
88
+ ...(userGitPane?.position !== undefined ? { position: userGitPane.position } : {}),
89
+ ...(userGitPane?.ratio !== undefined ? { ratio: userGitPane.ratio } : {}),
90
+ ...(userGitPane?.path !== undefined ? { path: userGitPane.path } : {}),
91
+ ...(userGitPane?.diffCount !== undefined ? { diffCount: userGitPane.diffCount } : {}),
92
+ }
93
+
76
94
  return createInitialState(
77
95
  json.customCommands,
78
96
  loadSessionCatalog(),
79
97
  loadSnippetCatalog(),
80
98
  true,
81
99
  {
82
- gitPanelRatio: json.gitPanelRatio,
83
- gitPanelVisible: json.gitPanelVisible,
100
+ gitPane: gitPaneOverrides,
84
101
  sessionBarPosition,
85
102
  sessionBarVisible,
86
103
  }
package/src/config.ts CHANGED
@@ -9,18 +9,43 @@ import { THEME_IDS, type ThemeId } from './ui/themes'
9
9
 
10
10
  export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
11
11
 
12
+ export interface PersistedGitPane {
13
+ visible: boolean
14
+ mode: 'embedded' | 'pane'
15
+ position: 'top' | 'bottom' | 'left' | 'right'
16
+ ratio: number
17
+ }
18
+
12
19
  export interface AimuxConfig {
13
20
  version: 2
14
21
  customCommands: Record<string, string>
15
22
  themeId?: ThemeId
16
- gitPanelVisible?: boolean
17
- gitPanelRatio?: number
23
+ gitPane?: PersistedGitPane
18
24
  sessionBarVisible?: boolean
19
25
  sessionBarPosition?: SessionBarPosition
20
26
  workspaceSnapshot?: WorkspaceSnapshotV1
21
27
  skippedUpdateVersion?: string
22
28
  }
23
29
 
30
+ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
31
+ if (typeof value !== 'object' || value === null) return false
32
+ const v = value as Record<string, unknown>
33
+ const modeOk = v.mode === 'embedded' || v.mode === 'pane'
34
+ const positionOk =
35
+ v.position === 'top' ||
36
+ v.position === 'bottom' ||
37
+ v.position === 'left' ||
38
+ v.position === 'right'
39
+ const ratioOk =
40
+ typeof v.ratio === 'number' && Number.isFinite(v.ratio) && v.ratio > 0 && v.ratio < 1
41
+ const visibleOk = typeof v.visible === 'boolean'
42
+ if (!modeOk || !positionOk || !ratioOk || !visibleOk) return false
43
+ // cross-field coherence: embedded => top|bottom; pane => left|right
44
+ if (v.mode === 'embedded' && v.position !== 'top' && v.position !== 'bottom') return false
45
+ if (v.mode === 'pane' && v.position !== 'left' && v.position !== 'right') return false
46
+ return true
47
+ }
48
+
24
49
  const DEFAULT_CONFIG: AimuxConfig = {
25
50
  customCommands: {},
26
51
  version: 2,
@@ -58,6 +83,7 @@ export function loadConfigResult(): ConfigLoadResult {
58
83
  version?: number
59
84
  customCommands?: unknown
60
85
  themeId?: unknown
86
+ gitPane?: unknown
61
87
  gitPanelVisible?: unknown
62
88
  gitPanelRatio?: unknown
63
89
  sessionBarVisible?: unknown
@@ -80,21 +106,32 @@ export function loadConfigResult(): ConfigLoadResult {
80
106
  issues.push('ignored invalid themeId')
81
107
  }
82
108
 
83
- const validGitPanelVisible =
84
- typeof parsed.gitPanelVisible === 'boolean' ? parsed.gitPanelVisible : undefined
85
- if (parsed.gitPanelVisible !== undefined && validGitPanelVisible === undefined) {
86
- issues.push('ignored invalid gitPanelVisible')
109
+ let validGitPane = isPersistedGitPane(parsed.gitPane) ? parsed.gitPane : undefined
110
+ if (parsed.gitPane !== undefined && validGitPane === undefined) {
111
+ issues.push('ignored invalid gitPane')
87
112
  }
88
113
 
89
- const validGitPanelRatio =
90
- typeof parsed.gitPanelRatio === 'number' &&
91
- Number.isFinite(parsed.gitPanelRatio) &&
92
- parsed.gitPanelRatio > 0 &&
93
- parsed.gitPanelRatio < 1
94
- ? parsed.gitPanelRatio
95
- : undefined
96
- if (parsed.gitPanelRatio !== undefined && validGitPanelRatio === undefined) {
97
- issues.push('ignored invalid gitPanelRatio')
114
+ // Legacy migration: previous schema stored gitPanelVisible/gitPanelRatio at
115
+ // top level. If the new `gitPane` field is absent, synthesize it from legacy
116
+ // keys so users don't lose their toggle/ratio on upgrade.
117
+ if (validGitPane === undefined) {
118
+ const legacyVisible =
119
+ typeof parsed.gitPanelVisible === 'boolean' ? parsed.gitPanelVisible : undefined
120
+ const legacyRatio =
121
+ typeof parsed.gitPanelRatio === 'number' &&
122
+ Number.isFinite(parsed.gitPanelRatio) &&
123
+ parsed.gitPanelRatio > 0 &&
124
+ parsed.gitPanelRatio < 1
125
+ ? parsed.gitPanelRatio
126
+ : undefined
127
+ if (legacyVisible !== undefined || legacyRatio !== undefined) {
128
+ validGitPane = {
129
+ mode: 'embedded',
130
+ position: 'bottom',
131
+ ratio: legacyRatio ?? 0.5,
132
+ visible: legacyVisible ?? true,
133
+ }
134
+ }
98
135
  }
99
136
 
100
137
  const validSessionBarVisible =
@@ -133,8 +170,7 @@ export function loadConfigResult(): ConfigLoadResult {
133
170
  return {
134
171
  config: {
135
172
  customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
136
- gitPanelRatio: validGitPanelRatio,
137
- gitPanelVisible: validGitPanelVisible,
173
+ gitPane: validGitPane,
138
174
  sessionBarPosition: validSessionBarPosition,
139
175
  sessionBarVisible: validSessionBarVisible,
140
176
  skippedUpdateVersion: validSkippedUpdateVersion,
@@ -18,6 +18,7 @@ export function buildKeymapHandlers(config: ResolvedKeymapConfig): KeymapModeHan
18
18
  const sequence = parseKeyNotation(binding.keys, leaderChord)
19
19
  trie.insert(sequence, {
20
20
  group: binding.group,
21
+ repeatable: binding.repeatable,
21
22
  result: binding.result,
22
23
  })
23
24
  }
@@ -0,0 +1,44 @@
1
+ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
2
+
3
+ import { describeBindings, type DescribedBinding } from './describe-bindings'
4
+
5
+ export interface HelpEntry extends DescribedBinding {
6
+ mode: ModeId
7
+ modeLabel: string
8
+ }
9
+
10
+ export const HELP_MODE_LABELS: { modeId: ModeId; label: string }[] = [
11
+ { label: 'Navigation', modeId: 'navigation' },
12
+ { label: 'Terminal input', modeId: 'terminal-input' },
13
+ { label: 'Git mode', modeId: 'git-mode' },
14
+ { label: 'Git commit', modeId: 'modal.git-commit' },
15
+ { label: 'New tab', modeId: 'modal.new-tab' },
16
+ { label: 'New tab — command', modeId: 'modal.new-tab.command-edit' },
17
+ { label: 'Session picker', modeId: 'modal.session-picker' },
18
+ { label: 'Session picker — filter', modeId: 'modal.session-picker.filtering' },
19
+ { label: 'Session name', modeId: 'modal.session-name' },
20
+ { label: 'Create session', modeId: 'modal.create-session' },
21
+ { label: 'Rename tab', modeId: 'modal.rename-tab' },
22
+ { label: 'Snippet picker', modeId: 'modal.snippet-picker' },
23
+ { label: 'Snippet picker — filter', modeId: 'modal.snippet-picker.filtering' },
24
+ { label: 'Snippet editor', modeId: 'modal.snippet-editor' },
25
+ { label: 'Theme picker', modeId: 'modal.theme-picker' },
26
+ { label: 'Split picker', modeId: 'modal.split-picker' },
27
+ { label: 'Help', modeId: 'modal.help' },
28
+ { label: 'Help — filter', modeId: 'modal.help.filtering' },
29
+ { label: 'Update available', modeId: 'modal.update-available' },
30
+ ]
31
+
32
+ export function collectHelpEntries(config: ResolvedKeymapConfig): HelpEntry[] {
33
+ const entries: HelpEntry[] = []
34
+ for (const { label, modeId } of HELP_MODE_LABELS) {
35
+ const bindings = describeBindings(config, modeId, {
36
+ dedupeByDescription: true,
37
+ withDescriptionOnly: true,
38
+ })
39
+ for (const binding of bindings) {
40
+ entries.push({ ...binding, mode: modeId, modeLabel: label })
41
+ }
42
+ }
43
+ return entries
44
+ }