@brimveyn/aimux 1.5.4 → 1.6.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 (65) hide show
  1. package/README.md +14 -2
  2. package/package.json +5 -3
  3. package/src/app-runtime/side-effects.ts +41 -6
  4. package/src/app.tsx +17 -5
  5. package/src/config.ts +29 -4
  6. package/src/diff-parser/clean-last-newline.ts +5 -0
  7. package/src/diff-parser/constants.ts +13 -0
  8. package/src/diff-parser/index.ts +12 -0
  9. package/src/diff-parser/parse-line-type.ts +29 -0
  10. package/src/diff-parser/parse-patch-files.ts +369 -0
  11. package/src/diff-parser/types.ts +75 -0
  12. package/src/input/modes/bridge.ts +8 -0
  13. package/src/input/modes/transitions.ts +2 -1
  14. package/src/input/modes/types.ts +4 -1
  15. package/src/pty/terminal-snapshot.ts +4 -4
  16. package/src/state/git-tree.ts +220 -0
  17. package/src/state/reducers/git-mode-state.ts +232 -35
  18. package/src/state/reducers/git-panel-state.ts +29 -3
  19. package/src/state/reducers/modal-state.ts +43 -10
  20. package/src/state/store.ts +5 -1
  21. package/src/state/types.ts +41 -5
  22. package/src/state/workspace-save.ts +2 -0
  23. package/src/ui/components/create-session-modal.tsx +23 -7
  24. package/src/ui/components/diff-renderer/build-rows.ts +349 -0
  25. package/src/ui/components/diff-renderer/filetype.ts +29 -0
  26. package/src/ui/components/diff-renderer/fold-strip.tsx +84 -0
  27. package/src/ui/components/diff-renderer/highlight.ts +48 -0
  28. package/src/ui/components/diff-renderer/index.ts +1 -0
  29. package/src/ui/components/diff-renderer/pierre-diff.tsx +170 -0
  30. package/src/ui/components/diff-renderer/split-view.tsx +207 -0
  31. package/src/ui/components/diff-renderer/stacked-view.tsx +166 -0
  32. package/src/ui/components/git-commit-modal.tsx +14 -2
  33. package/src/ui/components/git-pane-widget.tsx +5 -0
  34. package/src/ui/components/git-panel.tsx +176 -79
  35. package/src/ui/components/git-view.tsx +89 -87
  36. package/src/ui/components/help-modal.tsx +43 -11
  37. package/src/ui/components/input-field.tsx +2 -2
  38. package/src/ui/components/list-item.tsx +9 -1
  39. package/src/ui/components/modal-filter-bar.tsx +1 -1
  40. package/src/ui/components/modal-keybinds-overlay.tsx +2 -2
  41. package/src/ui/components/modal-shell.tsx +3 -3
  42. package/src/ui/components/new-tab-modal.tsx +15 -3
  43. package/src/ui/components/pending-chord-overlay.tsx +3 -3
  44. package/src/ui/components/session-bar.tsx +10 -5
  45. package/src/ui/components/session-picker-modal.tsx +26 -9
  46. package/src/ui/components/sidebar.tsx +22 -10
  47. package/src/ui/components/snippet-editor-modal.tsx +16 -2
  48. package/src/ui/components/snippet-picker-modal.tsx +11 -3
  49. package/src/ui/components/split-layout.tsx +1 -1
  50. package/src/ui/components/status-bar.tsx +12 -9
  51. package/src/ui/components/surface.tsx +5 -5
  52. package/src/ui/components/tab-item.tsx +22 -16
  53. package/src/ui/components/terminal-pane.tsx +25 -15
  54. package/src/ui/components/theme-picker-modal.tsx +111 -16
  55. package/src/ui/components/update-available-modal.tsx +11 -1
  56. package/src/ui/filter-themes.ts +10 -0
  57. package/src/ui/house-themes.ts +313 -0
  58. package/src/ui/keymap-context.ts +10 -2
  59. package/src/ui/root.tsx +26 -6
  60. package/src/ui/shiki.ts +49 -0
  61. package/src/ui/status-bar-model.ts +19 -1
  62. package/src/ui/theme.ts +22 -10
  63. package/src/ui/themes.generated.ts +59794 -0
  64. package/src/ui/themes.ts +68 -274
  65. package/src/ui/syntax.ts +0 -102
package/README.md CHANGED
@@ -19,9 +19,11 @@ snippets, themes, and fully configurable keymaps.
19
19
  - profile-isolated config, catalogs, daemon sockets, and runtime state
20
20
  - typed keymap customization through `@brimveyn/aimux-config`
21
21
  - snippets catalog and snippet picker
22
- - configurable git pane (embedded in the sidebar or as a standalone pane) and git mode
22
+ - configurable git pane (embedded in the sidebar or as a standalone pane) and a
23
+ dedicated [git mode](docs/guide/git-mode.md) for review / stage / commit /
24
+ push with a split or stacked diff view and shiki-powered highlighting
23
25
  - built-in help generated from the resolved keymap
24
- - theme picker with 11 built-in themes
26
+ - theme picker with 67 built-in themes (shiki catalog + aimux house themes) and a `/` filter
25
27
 
26
28
  ## Install
27
29
 
@@ -130,6 +132,7 @@ See [`docs/guide/keymaps.md`](docs/guide/keymaps.md).
130
132
  - `Ctrl+S` - open snippet picker
131
133
  - `Ctrl+T` - open theme picker
132
134
  - `Ctrl+B` - toggle sidebar
135
+ - `Ctrl+D` - enter git mode
133
136
  - `Ctrl+W b` - toggle session bar
134
137
  - `Ctrl+W 1` through `Ctrl+W 9` - switch sessions by index
135
138
 
@@ -172,6 +175,7 @@ See [`docs/developer/architecture.md`](docs/developer/architecture.md).
172
175
  - [`docs/reference/cli.md`](docs/reference/cli.md)
173
176
  - [`docs/reference/config-reference.md`](docs/reference/config-reference.md)
174
177
  - [`docs/reference/runtime-paths.md`](docs/reference/runtime-paths.md)
178
+ - [`docs/guide/git-mode.md`](docs/guide/git-mode.md)
175
179
  - [`docs/developer/architecture.md`](docs/developer/architecture.md)
176
180
  - [`docs/developer/aimux-config-internals.md`](docs/developer/aimux-config-internals.md)
177
181
 
@@ -191,6 +195,14 @@ bun run lint
191
195
  The repository dev scripts use `AIMUX_PROFILE=dev`, so local development does
192
196
  not collide with a globally installed `aimux` instance.
193
197
 
198
+ ## References
199
+
200
+ - [Pierre — diffs.com](https://diffs.com) — inspiration for the git-mode diff
201
+ review experience: the split / stacked layout, progressive context folding,
202
+ and focusing on the first change rather than the top of the file.
203
+ - [shiki](https://shiki.style) — syntax highlighting for the git diff view
204
+ and the source of the bundled theme catalog.
205
+
194
206
  ## License
195
207
 
196
208
  MIT © BrimVeyn
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.5.4",
3
+ "version": "1.6.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",
@@ -57,15 +57,17 @@
57
57
  "format": "oxfmt --write .",
58
58
  "format:check": "oxfmt --check .",
59
59
  "knip": "knip-bun",
60
- "bump": "bun run scripts/bump.ts"
60
+ "bump": "bun run scripts/bump.ts",
61
+ "generate-themes": "bun run scripts/generate-themes.ts"
61
62
  },
62
63
  "dependencies": {
63
- "@brimveyn/aimux-config": "0.3.5",
64
+ "@brimveyn/aimux-config": "0.4.0",
64
65
  "@opentui/core": "^0.1.90",
65
66
  "@opentui/react": "^0.1.90",
66
67
  "@xterm/headless": "^6.0.0",
67
68
  "bun-pty": "^0.4.8",
68
69
  "react": "^19.2.4",
70
+ "shiki": "^4.0.2",
69
71
  "zustand": "^5.0.12"
70
72
  },
71
73
  "devDependencies": {
@@ -34,9 +34,10 @@ import {
34
34
  type TabSession,
35
35
  } from '../state/types'
36
36
  import { saveCurrentWorkspace } from '../state/workspace-save'
37
+ import { filterThemeIds } from '../ui/filter-themes'
37
38
  import { scrollGitDiff } from '../ui/git-view-controls'
38
39
  import { applyTheme } from '../ui/theme'
39
- import { THEME_IDS, type ThemeId } from '../ui/themes'
40
+ import { type ThemeId } from '../ui/themes'
40
41
  import {
41
42
  handleCreateSessionEffect,
42
43
  handleDeleteSessionEffect,
@@ -167,16 +168,18 @@ function applyThemeEffect(
167
168
  ctx: SideEffectContext
168
169
  ): void {
169
170
  const { state } = ctx
171
+ const filter = state.modal.type === 'theme-picker' ? state.modal.editBuffer : null
172
+ const ids = filterThemeIds(filter)
170
173
 
171
174
  switch (effect.action) {
172
175
  case 'open':
173
- applyTheme(THEME_IDS[0] ?? 'aimux')
176
+ applyTheme(ctx.themeId)
174
177
  return
175
178
  case 'restore':
176
179
  applyTheme(ctx.themeId)
177
180
  return
178
181
  case 'confirm': {
179
- const selectedId = THEME_IDS[state.modal.selectedIndex]
182
+ const selectedId = ids[state.modal.selectedIndex]
180
183
  if (selectedId) {
181
184
  applyTheme(selectedId)
182
185
  ctx.setThemeId(selectedId)
@@ -185,9 +188,9 @@ function applyThemeEffect(
185
188
  return
186
189
  }
187
190
  case 'preview': {
188
- const count = THEME_IDS.length
189
- const nextIndex = (state.modal.selectedIndex + effect.delta + count) % count
190
- const previewId = THEME_IDS[nextIndex]
191
+ if (ids.length === 0) return
192
+ const nextIndex = (state.modal.selectedIndex + effect.delta + ids.length) % ids.length
193
+ const previewId = ids[nextIndex]
191
194
  if (previewId) {
192
195
  applyTheme(previewId)
193
196
  }
@@ -457,6 +460,38 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
457
460
  scrollGitDiff(effect.delta)
458
461
  return
459
462
  }
463
+ case 'persist-git-diff-mode-ratio': {
464
+ const config = loadConfig()
465
+ const persistedGitPane = config.gitPane
466
+ saveConfig({
467
+ ...config,
468
+ gitPane: {
469
+ diffModeRatio: effect.ratio,
470
+ fileListMode: persistedGitPane?.fileListMode,
471
+ mode: persistedGitPane?.mode ?? 'embedded',
472
+ position: persistedGitPane?.position ?? 'bottom',
473
+ ratio: persistedGitPane?.ratio ?? 0.5,
474
+ visible: persistedGitPane?.visible ?? true,
475
+ },
476
+ })
477
+ return
478
+ }
479
+ case 'persist-git-file-list-mode': {
480
+ const config = loadConfig()
481
+ const persistedGitPane = config.gitPane
482
+ saveConfig({
483
+ ...config,
484
+ gitPane: {
485
+ diffModeRatio: persistedGitPane?.diffModeRatio,
486
+ fileListMode: effect.mode,
487
+ mode: persistedGitPane?.mode ?? 'embedded',
488
+ position: persistedGitPane?.position ?? 'bottom',
489
+ ratio: persistedGitPane?.ratio ?? 0.5,
490
+ visible: persistedGitPane?.visible ?? true,
491
+ },
492
+ })
493
+ return
494
+ }
460
495
  case 'git-stage': {
461
496
  void enqueueGitOp(() => runGitAction(ctx, ['add', '--', effect.path], effect.path))
462
497
  return
package/src/app.tsx CHANGED
@@ -15,7 +15,6 @@ import type { KeyChord } from './input/keymap/key-chord'
15
15
  import type { TrieBinding } from './input/keymap/trie'
16
16
  import type { KeyResult, ModeContext, ModeId } from './input/modes/types'
17
17
  import type { SessionBackend } from './session-backend/types'
18
- import type { ThemeId } from './ui/themes'
19
18
 
20
19
  import { executeSideEffect, type SideEffectContext } from './app-runtime/side-effects'
21
20
  import { useBackendRuntime } from './app-runtime/use-backend-runtime'
@@ -39,6 +38,7 @@ import { appReducer, createInitialState } from './state/store'
39
38
  import { KeymapContext } from './ui/keymap-context'
40
39
  import { RootView } from './ui/root'
41
40
  import { applyTheme } from './ui/theme'
41
+ import { isKnownThemeId, registerUserThemes, type ThemeId } from './ui/themes'
42
42
  import {
43
43
  fetchLatestNpmVersion,
44
44
  getCurrentPackageVersion,
@@ -66,11 +66,16 @@ export function App({
66
66
  const renderer = useRenderer()
67
67
  const dimensions = useTerminalDimensions()
68
68
  const [themeId, setThemeId] = useState<ThemeId>(() => {
69
+ registerUserThemes(resolvedConfig.themes)
69
70
  const config = loadConfig()
70
- if (config.themeId) {
71
- applyTheme(config.themeId)
72
- }
73
- return config.themeId ?? 'aimux'
71
+ const persisted = config.themeId && isKnownThemeId(config.themeId) ? config.themeId : undefined
72
+ const fromConfig =
73
+ resolvedConfig.theme && isKnownThemeId(resolvedConfig.theme)
74
+ ? resolvedConfig.theme
75
+ : undefined
76
+ const initial = persisted ?? fromConfig ?? 'aimux'
77
+ applyTheme(initial)
78
+ return initial
74
79
  })
75
80
  const [state, dispatch] = useReducer(appReducer, undefined, () => {
76
81
  const json = loadConfig()
@@ -81,12 +86,19 @@ export function App({
81
86
  // Merge config-file gitPane (persisted prefs) with user's resolved gitPane
82
87
  // (programmatic config). User config wins; file provides persisted prior state.
83
88
  const userGitPane = resolvedConfig.gitPane
89
+ const fileListMode = userGitPane?.fileListMode ?? json.gitPane?.fileListMode ?? 'tree'
90
+ const diffModeRatio = userGitPane?.diffModeRatio ?? json.gitPane?.diffModeRatio ?? 0.35
84
91
  const gitPaneOverrides = {
85
92
  ...json.gitPane,
93
+ diffModeRatio,
94
+ fileListMode,
86
95
  ...(userGitPane?.visible !== undefined ? { visible: userGitPane.visible } : {}),
87
96
  ...(userGitPane?.mode !== undefined ? { mode: userGitPane.mode } : {}),
88
97
  ...(userGitPane?.position !== undefined ? { position: userGitPane.position } : {}),
89
98
  ...(userGitPane?.ratio !== undefined ? { ratio: userGitPane.ratio } : {}),
99
+ ...(userGitPane?.diffModeRatio !== undefined
100
+ ? { diffModeRatio: userGitPane.diffModeRatio }
101
+ : {}),
90
102
  ...(userGitPane?.path !== undefined ? { path: userGitPane.path } : {}),
91
103
  ...(userGitPane?.diffCount !== undefined ? { diffCount: userGitPane.diffCount } : {}),
92
104
  }
package/src/config.ts CHANGED
@@ -1,15 +1,30 @@
1
1
  import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
2
2
 
3
- import type { SessionBarPosition, WorkspaceSnapshotV1 } from './state/types'
3
+ import type { GitFileListMode, SessionBarPosition, WorkspaceSnapshotV1 } from './state/types'
4
4
 
5
5
  import { logDebug } from './debug/input-log'
6
6
  import { getProfileConfigDir } from './profile-paths'
7
7
  import { isWorkspaceSnapshotV1 } from './state/validation'
8
8
  import { THEME_IDS, type ThemeId } from './ui/themes'
9
9
 
10
+ const LEGACY_THEME_ALIASES: Record<string, ThemeId> = {
11
+ 'everforest': 'everforest-dark',
12
+ 'gruvbox-dark': 'gruvbox-dark-hard',
13
+ 'kanagawa': 'kanagawa-wave',
14
+ 'one-dark': 'one-dark-pro',
15
+ }
16
+
17
+ function migrateThemeId(value: unknown): ThemeId | undefined {
18
+ if (typeof value !== 'string') return undefined
19
+ if (THEME_IDS.includes(value as ThemeId)) return value as ThemeId
20
+ return LEGACY_THEME_ALIASES[value]
21
+ }
22
+
10
23
  export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
11
24
 
12
25
  export interface PersistedGitPane {
26
+ diffModeRatio?: number
27
+ fileListMode?: GitFileListMode
13
28
  visible: boolean
14
29
  mode: 'embedded' | 'pane'
15
30
  position: 'top' | 'bottom' | 'left' | 'right'
@@ -38,8 +53,18 @@ function isPersistedGitPane(value: unknown): value is PersistedGitPane {
38
53
  v.position === 'right'
39
54
  const ratioOk =
40
55
  typeof v.ratio === 'number' && Number.isFinite(v.ratio) && v.ratio > 0 && v.ratio < 1
56
+ const diffModeRatioOk =
57
+ v.diffModeRatio === undefined ||
58
+ (typeof v.diffModeRatio === 'number' &&
59
+ Number.isFinite(v.diffModeRatio) &&
60
+ v.diffModeRatio > 0 &&
61
+ v.diffModeRatio < 1)
41
62
  const visibleOk = typeof v.visible === 'boolean'
42
- if (!modeOk || !positionOk || !ratioOk || !visibleOk) return false
63
+ const fileListModeOk =
64
+ v.fileListMode === undefined || v.fileListMode === 'tree' || v.fileListMode === 'flat'
65
+ if (!modeOk || !positionOk || !ratioOk || !diffModeRatioOk || !visibleOk || !fileListModeOk) {
66
+ return false
67
+ }
43
68
  // cross-field coherence: embedded => top|bottom; pane => left|right
44
69
  if (v.mode === 'embedded' && v.position !== 'top' && v.position !== 'bottom') return false
45
70
  if (v.mode === 'pane' && v.position !== 'left' && v.position !== 'right') return false
@@ -58,7 +83,7 @@ export interface ConfigLoadResult {
58
83
  }
59
84
 
60
85
  function isThemeId(value: unknown): value is ThemeId {
61
- return typeof value === 'string' && THEME_IDS.includes(value as ThemeId)
86
+ return migrateThemeId(value) !== undefined
62
87
  }
63
88
 
64
89
  function isCustomCommandsRecord(value: unknown): value is Record<string, string> {
@@ -174,7 +199,7 @@ export function loadConfigResult(): ConfigLoadResult {
174
199
  sessionBarPosition: validSessionBarPosition,
175
200
  sessionBarVisible: validSessionBarVisible,
176
201
  skippedUpdateVersion: validSkippedUpdateVersion,
177
- themeId: isThemeId(parsed.themeId) ? parsed.themeId : undefined,
202
+ themeId: migrateThemeId(parsed.themeId),
178
203
  version: 2,
179
204
  workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
180
205
  ? parsed.workspaceSnapshot
@@ -0,0 +1,5 @@
1
+ // Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
2
+
3
+ export function cleanLastNewline(contents: string): string {
4
+ return contents.replace(/\n$|\r\n$/, '')
5
+ }
@@ -0,0 +1,13 @@
1
+ // Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
2
+
3
+ export const COMMIT_METADATA_SPLIT = /(?=^From [a-f0-9]+ .+$)/m
4
+ export const GIT_DIFF_FILE_BREAK_REGEX = /(?=^diff --git)/gm
5
+ export const UNIFIED_DIFF_FILE_BREAK_REGEX = /(?=^---\s+\S)/gm
6
+ export const FILE_CONTEXT_BLOB = /(?=^@@ )/gm
7
+ export const HUNK_HEADER = /^@@ -(\d+)(?:,(\d+))? \+(\d+)(?:,(\d+))? @@(?: (.*))?/m
8
+ export const SPLIT_WITH_NEWLINES = /(?<=\n)/
9
+ export const FILENAME_HEADER_REGEX = /^(---|\+\+\+)\s+([^\t\r\n]+)/
10
+ export const FILENAME_HEADER_REGEX_GIT = /^(---|\+\+\+)\s+[ab]\/([^\t\r\n]+)/
11
+ export const ALTERNATE_FILE_NAMES_GIT =
12
+ /^diff --git (?:"a\/(.+?)"|a\/(.+?)) (?:"b\/(.+?)"|b\/(.+?))$/
13
+ export const INDEX_LINE_METADATA = /^index ([0-9a-f]+)\.\.([0-9a-f]+)(?: (\d+))?$/i
@@ -0,0 +1,12 @@
1
+ export { parsePatchFiles, processFile, processPatch } from './parse-patch-files'
2
+ export type {
3
+ ChangeContent,
4
+ ChangeTypes,
5
+ ContextContent,
6
+ FileContents,
7
+ FileDiffMetadata,
8
+ Hunk,
9
+ HunkContent,
10
+ HunkLineType,
11
+ ParsedPatch,
12
+ } from './types'
@@ -0,0 +1,29 @@
1
+ // Vendored from @pierre/diffs v1.1.15 — parser only. See AIMUX-14.
2
+ /* eslint-disable no-console, no-nested-ternary */
3
+
4
+ import type { HunkLineType } from './types'
5
+
6
+ export interface ParsedLine {
7
+ line: string
8
+ type: HunkLineType
9
+ }
10
+
11
+ export function parseLineType(line: string): ParsedLine | undefined {
12
+ const firstChar = line[0]
13
+ if (firstChar !== '+' && firstChar !== '-' && firstChar !== ' ' && firstChar !== '\\') {
14
+ console.error(`parseLineType: Invalid firstChar: "${firstChar}", full line: "${line}"`)
15
+ return
16
+ }
17
+ const processedLine = line.substring(1)
18
+ return {
19
+ line: processedLine === '' ? '\n' : processedLine,
20
+ type:
21
+ firstChar === ' '
22
+ ? 'context'
23
+ : firstChar === '\\'
24
+ ? 'metadata'
25
+ : firstChar === '+'
26
+ ? 'addition'
27
+ : 'deletion',
28
+ }
29
+ }