@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.
- package/README.md +29 -8
- package/package.json +5 -5
- package/src/app-runtime/backend-runtime-events.ts +16 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +62 -4
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/use-backend-runtime.ts +7 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -2
- package/src/app-runtime/use-terminal-resize.ts +15 -6
- package/src/app.tsx +89 -30
- package/src/config/loader.ts +2 -2
- package/src/config.ts +14 -3
- package/src/daemon/daemon.ts +279 -118
- package/src/daemon/runtime-paths.ts +34 -2
- package/src/daemon/session-manager.ts +20 -5
- package/src/daemon/session-registry.ts +18 -11
- package/src/index.tsx +19 -4
- package/src/input/keymap/describe-bindings.ts +68 -0
- package/src/input/keymap/key-format.ts +67 -0
- package/src/input/modes/bridge.ts +1 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +2 -0
- package/src/ipc/manager-protocol.ts +396 -0
- package/src/ipc/protocol.ts +98 -5
- package/src/platform/daemon-control.ts +42 -11
- package/src/profile-paths.ts +27 -0
- package/src/pty/pty-manager.ts +53 -3
- package/src/restart-daemon.ts +10 -10
- package/src/session-backend/bootstrap.ts +197 -46
- package/src/session-backend/local-session-backend.ts +12 -5
- package/src/session-backend/remote-session-backend.ts +143 -63
- package/src/session-backend/types.ts +4 -2
- package/src/state/reducers/modal-state.ts +18 -1
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/session-catalog.ts +5 -4
- package/src/state/session-persistence.ts +9 -2
- package/src/state/snippet-catalog.ts +4 -4
- package/src/state/types.ts +23 -0
- package/src/state/validation.ts +8 -0
- package/src/terminal-manager/manager-client.ts +384 -0
- package/src/terminal-manager/terminal-manager.ts +288 -0
- package/src/ui/components/create-session-modal.tsx +3 -5
- package/src/ui/components/git-commit-modal.tsx +3 -5
- package/src/ui/components/help-modal.tsx +49 -68
- package/src/ui/components/list-item.tsx +24 -5
- package/src/ui/components/new-tab-modal.tsx +8 -9
- package/src/ui/components/pending-chord-overlay.tsx +28 -0
- package/src/ui/components/session-name-modal.tsx +3 -5
- package/src/ui/components/session-picker-modal.tsx +3 -1
- package/src/ui/components/snippet-editor-modal.tsx +3 -1
- package/src/ui/components/snippet-picker-modal.tsx +3 -1
- package/src/ui/components/status-bar.tsx +6 -2
- package/src/ui/components/theme-picker-modal.tsx +3 -6
- package/src/ui/components/update-available-modal.tsx +42 -0
- package/src/ui/keymap-context.ts +39 -0
- package/src/ui/root.tsx +12 -0
- package/src/ui/status-bar-model.ts +67 -39
- package/src/update/version-check.ts +67 -0
- 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
|
|
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'],
|
package/src/input/modes/types.ts
CHANGED
|
@@ -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[]
|