@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.
- package/README.md +142 -176
- package/package.json +5 -2
- package/src/app-runtime/use-terminal-resize.ts +12 -2
- package/src/app.tsx +20 -3
- package/src/config.ts +53 -17
- package/src/input/keymap/build-handlers.ts +1 -0
- package/src/input/keymap/help-entries.ts +44 -0
- package/src/input/keymap/keymap-ref.ts +11 -0
- package/src/input/keymap/sequence-resolver.ts +35 -2
- package/src/input/keymap/trie.ts +1 -0
- package/src/input/modes/bridge.ts +1 -1
- package/src/input/modes/handlers/shared.ts +0 -16
- package/src/input/modes/transitions.ts +4 -4
- package/src/input/modes/types.ts +1 -1
- package/src/platform/daemon-control.ts +0 -8
- package/src/state/reducers/git-panel-state.ts +35 -8
- package/src/state/reducers/modal-state.ts +48 -3
- package/src/state/selectors.ts +1 -5
- package/src/state/session-persistence.ts +1 -20
- package/src/state/store.ts +38 -5
- package/src/state/types.ts +27 -13
- package/src/state/validation.ts +0 -2
- package/src/state/workspace-save.ts +6 -2
- package/src/ui/components/create-session-modal.tsx +5 -3
- package/src/ui/components/git-commit-modal.tsx +1 -3
- package/src/ui/components/git-pane-widget.tsx +46 -0
- package/src/ui/components/git-panel.tsx +72 -25
- package/src/ui/components/help-modal.tsx +156 -42
- package/src/ui/components/modal-keybinds-overlay.tsx +39 -0
- package/src/ui/components/modal-shell.tsx +15 -3
- package/src/ui/components/new-tab-modal.tsx +9 -10
- package/src/ui/components/pending-chord-overlay.tsx +2 -1
- package/src/ui/components/session-name-modal.tsx +1 -3
- package/src/ui/components/session-picker-modal.tsx +1 -3
- package/src/ui/components/sidebar.tsx +24 -50
- package/src/ui/components/snippet-editor-modal.tsx +1 -3
- package/src/ui/components/snippet-picker-modal.tsx +1 -3
- package/src/ui/components/status-bar.tsx +0 -4
- package/src/ui/components/theme-picker-modal.tsx +6 -3
- package/src/ui/components/update-available-modal.tsx +7 -4
- package/src/ui/keymap-context.ts +1 -6
- package/src/ui/root.tsx +29 -1
- package/src/ui/status-bar-model.ts +0 -5
- 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.
|
|
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
|

|
|
6
8
|

|
|
@@ -10,39 +12,16 @@ A terminal multiplexer for AI CLIs. Manage multiple AI assistant sessions (Claud
|
|
|
10
12
|
|
|
11
13
|
## Features
|
|
12
14
|
|
|
13
|
-
-
|
|
14
|
-
-
|
|
15
|
-
-
|
|
16
|
-
-
|
|
17
|
-
-
|
|
18
|
-
-
|
|
19
|
-
-
|
|
20
|
-
-
|
|
21
|
-
-
|
|
22
|
-
-
|
|
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
|
-

|
|
34
|
-
|
|
35
|
-
### Multi-Tab Workflow
|
|
36
|
-
|
|
37
|
-

|
|
38
|
-
|
|
39
|
-
### Themes
|
|
40
|
-
|
|
41
|
-

|
|
42
|
-
|
|
43
|
-
### Split Panes
|
|
44
|
-
|
|
45
|
-

|
|
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
|
-
##
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
45
|
+
Create `~/.config/aimux/default/aimux.config.ts`:
|
|
78
46
|
|
|
79
47
|
```ts
|
|
80
|
-
import { defineConfig, actions
|
|
48
|
+
import { defineConfig, actions } from '@brimveyn/aimux-config'
|
|
81
49
|
|
|
82
50
|
export default defineConfig({
|
|
83
|
-
|
|
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
|
-
|
|
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
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
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
|
-
|
|
81
|
+
The active profile is chosen from:
|
|
198
82
|
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
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
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
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
|
-
|
|
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
|
-
|
|
159
|
+
This split is what allows daemon restarts and some update paths without dropping
|
|
160
|
+
every live PTY immediately.
|
|
221
161
|
|
|
222
|
-
|
|
162
|
+
See [`docs/developer/architecture.md`](docs/developer/architecture.md).
|
|
223
163
|
|
|
224
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
-
() =>
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
? parsed.
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
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
|
-
|
|
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
|
+
}
|