@brimveyn/aimux 1.2.5 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (59) hide show
  1. package/README.md +29 -8
  2. package/package.json +5 -5
  3. package/src/app-runtime/backend-runtime-events.ts +16 -1
  4. package/src/app-runtime/pty-write.ts +7 -4
  5. package/src/app-runtime/side-effects.ts +62 -4
  6. package/src/app-runtime/snippet-actions.ts +3 -2
  7. package/src/app-runtime/use-backend-runtime.ts +7 -2
  8. package/src/app-runtime/use-renderer-bindings.ts +2 -2
  9. package/src/app-runtime/use-terminal-resize.ts +15 -6
  10. package/src/app.tsx +89 -30
  11. package/src/config/loader.ts +2 -2
  12. package/src/config.ts +14 -3
  13. package/src/daemon/daemon.ts +279 -118
  14. package/src/daemon/runtime-paths.ts +34 -2
  15. package/src/daemon/session-manager.ts +20 -5
  16. package/src/daemon/session-registry.ts +18 -11
  17. package/src/index.tsx +19 -4
  18. package/src/input/keymap/describe-bindings.ts +68 -0
  19. package/src/input/keymap/key-format.ts +67 -0
  20. package/src/input/modes/bridge.ts +1 -0
  21. package/src/input/modes/transitions.ts +2 -0
  22. package/src/input/modes/types.ts +2 -0
  23. package/src/ipc/manager-protocol.ts +396 -0
  24. package/src/ipc/protocol.ts +98 -5
  25. package/src/platform/daemon-control.ts +42 -11
  26. package/src/profile-paths.ts +27 -0
  27. package/src/pty/pty-manager.ts +53 -3
  28. package/src/restart-daemon.ts +10 -10
  29. package/src/session-backend/bootstrap.ts +197 -46
  30. package/src/session-backend/local-session-backend.ts +12 -5
  31. package/src/session-backend/remote-session-backend.ts +143 -63
  32. package/src/session-backend/types.ts +4 -2
  33. package/src/state/reducers/modal-state.ts +18 -1
  34. package/src/state/reducers/tab-state.ts +20 -2
  35. package/src/state/session-catalog.ts +5 -4
  36. package/src/state/session-persistence.ts +9 -2
  37. package/src/state/snippet-catalog.ts +4 -4
  38. package/src/state/types.ts +23 -0
  39. package/src/state/validation.ts +8 -0
  40. package/src/terminal-manager/manager-client.ts +384 -0
  41. package/src/terminal-manager/terminal-manager.ts +288 -0
  42. package/src/ui/components/create-session-modal.tsx +3 -5
  43. package/src/ui/components/git-commit-modal.tsx +3 -5
  44. package/src/ui/components/help-modal.tsx +49 -68
  45. package/src/ui/components/list-item.tsx +24 -5
  46. package/src/ui/components/new-tab-modal.tsx +8 -9
  47. package/src/ui/components/pending-chord-overlay.tsx +28 -0
  48. package/src/ui/components/session-name-modal.tsx +3 -5
  49. package/src/ui/components/session-picker-modal.tsx +3 -1
  50. package/src/ui/components/snippet-editor-modal.tsx +3 -1
  51. package/src/ui/components/snippet-picker-modal.tsx +3 -1
  52. package/src/ui/components/status-bar.tsx +6 -2
  53. package/src/ui/components/theme-picker-modal.tsx +3 -6
  54. package/src/ui/components/update-available-modal.tsx +42 -0
  55. package/src/ui/keymap-context.ts +39 -0
  56. package/src/ui/root.tsx +12 -0
  57. package/src/ui/status-bar-model.ts +67 -39
  58. package/src/update/version-check.ts +67 -0
  59. package/src/update.ts +3 -3
@@ -1,12 +1,5 @@
1
1
  import { EventEmitter } from 'node:events'
2
2
 
3
- import type {
4
- TabSession,
5
- TerminalModeState,
6
- TerminalSnapshot,
7
- WorkspaceSnapshotV1,
8
- } from '../state/types'
9
-
10
3
  import { logDebug } from '../debug/input-log'
11
4
  import { PtyManager } from '../pty/pty-manager'
12
5
  import {
@@ -15,6 +8,14 @@ import {
15
8
  restoreTabsFromWorkspace,
16
9
  } from '../state/session-persistence'
17
10
  import { createDefaultTerminalModes } from '../state/terminal-modes'
11
+ import {
12
+ DEFAULT_SCROLL_INTENT,
13
+ type ScrollIntent,
14
+ type TabSession,
15
+ type TerminalModeState,
16
+ type TerminalSnapshot,
17
+ type WorkspaceSnapshotV1,
18
+ } from '../state/types'
18
19
 
19
20
  type SessionRegistryEvents = {
20
21
  render: [tabId: string, viewport: TerminalSnapshot, terminalModes: TerminalModeState]
@@ -138,6 +139,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
138
139
  buffer: '',
139
140
  command: [options.command, ...(options.args ?? [])].join(' '),
140
141
  id: options.tabId,
142
+ scrollIntent: DEFAULT_SCROLL_INTENT,
141
143
  status: 'starting',
142
144
  terminalModes: createDefaultTerminalModes(),
143
145
  title: options.title,
@@ -149,6 +151,7 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
149
151
  existing.exitCode = undefined
150
152
  existing.viewport = undefined
151
153
  existing.terminalModes = createDefaultTerminalModes()
154
+ existing.scrollIntent = DEFAULT_SCROLL_INTENT
152
155
  existing.assistant = options.assistant
153
156
  existing.title = options.title
154
157
  existing.command = [options.command, ...(options.args ?? [])].join(' ')
@@ -162,12 +165,12 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
162
165
  this.ptyManager.write(tabId, data)
163
166
  }
164
167
 
165
- resizeAll(cols: number, rows: number): void {
166
- this.ptyManager.resizeAll(cols, rows)
168
+ resizeAll(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
169
+ this.ptyManager.resizeAll(cols, rows, intents)
167
170
  }
168
171
 
169
- resizeTab(tabId: string, cols: number, rows: number): void {
170
- this.ptyManager.resizeSession(tabId, cols, rows)
172
+ resizeTab(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
173
+ this.ptyManager.resizeSession(tabId, cols, rows, intent)
171
174
  }
172
175
 
173
176
  scrollViewport(tabId: string, deltaLines: number): void {
@@ -178,6 +181,10 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
178
181
  this.ptyManager.scrollViewportToBottom(tabId)
179
182
  }
180
183
 
184
+ reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
185
+ this.ptyManager.reapplyScrollIntent(tabId, intent)
186
+ }
187
+
181
188
  setActiveTab(tabId: string | null): void {
182
189
  if (tabId === null) {
183
190
  this.activeTabId = null
package/src/index.tsx CHANGED
@@ -3,14 +3,18 @@ import { createCliRenderer } from '@opentui/core'
3
3
  import { createRoot } from '@opentui/react'
4
4
 
5
5
  import { App } from './app'
6
+ import { loadUserConfig } from './config/loader'
6
7
  import { runDaemon } from './daemon/daemon'
8
+ import { getRuntimeProfile } from './daemon/runtime-paths'
7
9
  import { logDebug } from './debug/input-log'
8
10
  import { runDoctor } from './doctor'
9
11
  import { runRestartDaemon } from './restart-daemon'
10
12
  import { createSessionBackend } from './session-backend/bootstrap'
13
+ import { runTerminalManager } from './terminal-manager/terminal-manager'
11
14
  import { runUpdate } from './update'
12
15
 
13
16
  const command = process.argv[2]
17
+ const runtimeProfile = getRuntimeProfile()
14
18
 
15
19
  if (command === '--version' || command === '-v' || command === 'version') {
16
20
  const { version } = await import('../package.json')
@@ -31,13 +35,18 @@ if (command === 'update') {
31
35
  }
32
36
 
33
37
  if (command === 'daemon') {
34
- logDebug('index.daemonMode')
38
+ logDebug('index.daemonMode', { runtimeProfile })
35
39
  await runDaemon()
36
40
  }
37
41
 
42
+ if (command === 'terminal-manager') {
43
+ logDebug('index.terminalManagerMode', { runtimeProfile })
44
+ await runTerminalManager()
45
+ }
46
+
38
47
  if (command === '--help' || command === '-h') {
39
48
  process.stdout.write(
40
- 'aimux -- terminal multiplexer for AI CLIs\n\nUsage:\n aimux Start aimux\n aimux update Update to latest version\n aimux doctor Diagnose setup issues\n aimux restart-daemon Restart background daemon\n\n'
49
+ 'aimux -- terminal multiplexer for AI CLIs\n\nUsage:\n aimux Start aimux\n aimux update Update to latest version\n aimux doctor Diagnose setup issues\n aimux restart-daemon Restart IPC daemon\n\n'
41
50
  )
42
51
  process.exit(0)
43
52
  }
@@ -51,6 +60,12 @@ const renderer = await createCliRenderer({
51
60
  })
52
61
 
53
62
  const backend = await createSessionBackend()
54
- logDebug('index.backendReady', { backend: backend.constructor.name })
63
+ logDebug('index.backendReady', { backend: backend.constructor.name, runtimeProfile })
64
+
65
+ const resolvedConfig = await loadUserConfig()
66
+ logDebug('index.userConfigLoaded', {
67
+ leader: resolvedConfig.keymaps.leader,
68
+ modeCount: resolvedConfig.keymaps.modes.size,
69
+ })
55
70
 
56
- createRoot(renderer).render(<App backend={backend} />)
71
+ createRoot(renderer).render(<App backend={backend} resolvedConfig={resolvedConfig} />)
@@ -0,0 +1,68 @@
1
+ import type { ModeId, ResolvedKeymapConfig } from '@brimveyn/aimux-config'
2
+
3
+ import { parseKeyNotation } from './key-chord'
4
+ import { formatNotationForDisplay } from './key-format'
5
+
6
+ export interface DescribedBinding {
7
+ keys: string
8
+ keysDisplay: string
9
+ description?: string
10
+ group?: string
11
+ }
12
+
13
+ export interface DescribedBindingGroup {
14
+ group?: string
15
+ bindings: DescribedBinding[]
16
+ }
17
+
18
+ interface DescribeOptions {
19
+ withDescriptionOnly?: boolean
20
+ dedupeByDescription?: boolean
21
+ }
22
+
23
+ export function describeBindings(
24
+ config: ResolvedKeymapConfig,
25
+ modeId: ModeId,
26
+ options: DescribeOptions = {}
27
+ ): DescribedBinding[] {
28
+ const mode = config.modes.get(modeId)
29
+ if (!mode) return []
30
+
31
+ const leaderChord = parseKeyNotation(config.leader)[0]
32
+ const seenDescriptions = new Set<string>()
33
+ const result: DescribedBinding[] = []
34
+
35
+ for (const binding of mode.bindings) {
36
+ if (options.withDescriptionOnly && !binding.description) continue
37
+ if (options.dedupeByDescription && binding.description) {
38
+ if (seenDescriptions.has(binding.description)) continue
39
+ seenDescriptions.add(binding.description)
40
+ }
41
+ result.push({
42
+ description: binding.description,
43
+ group: binding.group,
44
+ keys: binding.keys,
45
+ keysDisplay: formatNotationForDisplay(binding.keys, leaderChord),
46
+ })
47
+ }
48
+
49
+ return result
50
+ }
51
+
52
+ export function groupDescribedBindings(bindings: DescribedBinding[]): DescribedBindingGroup[] {
53
+ const groups: DescribedBindingGroup[] = []
54
+ const indexByLabel = new Map<string | undefined, number>()
55
+
56
+ for (const binding of bindings) {
57
+ const label = binding.group
58
+ const existing = indexByLabel.get(label)
59
+ if (existing === undefined) {
60
+ indexByLabel.set(label, groups.length)
61
+ groups.push({ bindings: [binding], group: label })
62
+ } else {
63
+ groups[existing]?.bindings.push(binding)
64
+ }
65
+ }
66
+
67
+ return groups
68
+ }
@@ -0,0 +1,67 @@
1
+ import { type KeyChord, parseKeyNotation } from './key-chord'
2
+
3
+ const SPECIAL_DISPLAY: Record<string, string> = {
4
+ backspace: 'Backspace',
5
+ down: '↓',
6
+ escape: 'Esc',
7
+ left: '←',
8
+ return: 'Enter',
9
+ right: '→',
10
+ space: 'Space',
11
+ tab: 'Tab',
12
+ up: '↑',
13
+ }
14
+
15
+ function formatModifierKey(baseName: string): string {
16
+ const special = SPECIAL_DISPLAY[baseName]
17
+ if (special) return special
18
+ if (baseName.length === 1) return baseName.toUpperCase()
19
+ return baseName.charAt(0).toUpperCase() + baseName.slice(1)
20
+ }
21
+
22
+ /** Convert a single canonical KeyChord back to a human-readable label. */
23
+ export function formatChord(chord: KeyChord, leader?: KeyChord): string {
24
+ if (leader && chord === leader) return '<leader>'
25
+
26
+ let ctrl = false
27
+ let meta = false
28
+ let rest = chord
29
+
30
+ while (true) {
31
+ if (rest.startsWith('C-')) {
32
+ ctrl = true
33
+ rest = rest.slice(2)
34
+ continue
35
+ }
36
+ if (rest.startsWith('M-')) {
37
+ meta = true
38
+ rest = rest.slice(2)
39
+ continue
40
+ }
41
+ break
42
+ }
43
+
44
+ if (!ctrl && !meta) {
45
+ if (rest.length === 1 && /^[A-Z]$/.test(rest)) {
46
+ return `Shift+${rest}`
47
+ }
48
+ return SPECIAL_DISPLAY[rest] ?? rest
49
+ }
50
+
51
+ const parts: string[] = []
52
+ if (ctrl) parts.push('Ctrl')
53
+ if (meta) parts.push('Alt')
54
+ parts.push(formatModifierKey(rest))
55
+ return parts.join('+')
56
+ }
57
+
58
+ /** Convert a raw key notation string (e.g. `<C-n>`, `dd`) to a human label. */
59
+ export function formatNotationForDisplay(notation: string, leader?: KeyChord): string {
60
+ let chords: KeyChord[]
61
+ try {
62
+ chords = parseKeyNotation(notation, leader)
63
+ } catch {
64
+ return notation
65
+ }
66
+ return chords.map((c) => formatChord(c, leader)).join(' ')
67
+ }
@@ -28,6 +28,7 @@ const MODAL_MODE_IDS: Partial<Record<SupportedModalType, ModeId>> = {
28
28
  'snippet-picker': 'modal.snippet-picker',
29
29
  'split-picker': 'modal.split-picker',
30
30
  'theme-picker': 'modal.theme-picker',
31
+ 'update-available': 'modal.update-available',
31
32
  }
32
33
 
33
34
  export function deriveModeId(state: AppState): ModeId {
@@ -22,6 +22,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
22
22
  'modal.snippet-picker.filtering': ['modal.snippet-picker'],
23
23
  'modal.split-picker': ['navigation'],
24
24
  'modal.theme-picker': ['navigation'],
25
+ 'modal.update-available': ['navigation'],
25
26
  'navigation': [
26
27
  'terminal-input',
27
28
  'modal.new-tab',
@@ -30,6 +31,7 @@ const TRANSITIONS: Record<ModeId, readonly ModeId[]> = {
30
31
  'modal.snippet-picker',
31
32
  'modal.theme-picker',
32
33
  'modal.rename-tab',
34
+ 'modal.update-available',
33
35
  'git-mode',
34
36
  ],
35
37
  'terminal-input': ['navigation', 'layout'],
@@ -21,6 +21,7 @@ export type ModeId =
21
21
  | 'modal.help'
22
22
  | 'modal.split-picker'
23
23
  | 'modal.git-commit'
24
+ | 'modal.update-available'
24
25
 
25
26
  export type SideEffect =
26
27
  | { type: 'quit'; state: AppState }
@@ -51,6 +52,7 @@ export type SideEffect =
51
52
  | { type: 'git-rm'; path: string }
52
53
  | { type: 'git-commit'; title: string; body: string }
53
54
  | { type: 'git-push' }
55
+ | { type: 'confirm-update-selection' }
54
56
 
55
57
  export interface KeyResult {
56
58
  actions: AppAction[]