@brimveyn/aimux 1.3.0 → 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/package.json +2 -2
- package/src/app-runtime/backend-runtime-events.ts +7 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +54 -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.ts +11 -0
- package/src/daemon/daemon.ts +19 -2
- package/src/daemon/session-manager.ts +20 -5
- package/src/daemon/session-registry.ts +18 -11
- package/src/index.tsx +8 -1
- 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 +51 -2
- package/src/ipc/protocol.ts +43 -2
- package/src/pty/pty-manager.ts +29 -3
- package/src/session-backend/local-session-backend.ts +12 -5
- package/src/session-backend/remote-session-backend.ts +18 -5
- 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-persistence.ts +9 -2
- package/src/state/types.ts +23 -0
- package/src/state/validation.ts +8 -0
- package/src/terminal-manager/manager-client.ts +29 -5
- package/src/terminal-manager/terminal-manager.ts +19 -3
- 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/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.3.
|
|
3
|
+
"version": "1.3.1",
|
|
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,7 +57,7 @@
|
|
|
57
57
|
"format:check": "oxfmt --check ."
|
|
58
58
|
},
|
|
59
59
|
"dependencies": {
|
|
60
|
-
"@brimveyn/aimux-config": "0.
|
|
60
|
+
"@brimveyn/aimux-config": "0.2.6",
|
|
61
61
|
"@opentui/core": "^0.1.90",
|
|
62
62
|
"@opentui/react": "^0.1.90",
|
|
63
63
|
"@xterm/headless": "^6.0.0",
|
|
@@ -51,7 +51,13 @@ export function bindBackendRuntimeEvents({
|
|
|
51
51
|
viewportY: viewport.viewportY,
|
|
52
52
|
})
|
|
53
53
|
|
|
54
|
-
dispatch({
|
|
54
|
+
dispatch({
|
|
55
|
+
source: resizingRef.current ? 'resize' : 'data',
|
|
56
|
+
tabId,
|
|
57
|
+
terminalModes,
|
|
58
|
+
type: 'replace-tab-viewport',
|
|
59
|
+
viewport,
|
|
60
|
+
})
|
|
55
61
|
if (timeouts.isStartupGraceActive(tabId) || resizingRef.current) {
|
|
56
62
|
return
|
|
57
63
|
}
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { SessionBackend } from '../session-backend/types'
|
|
2
|
-
import type { TabSession } from '../state/types'
|
|
2
|
+
import type { AppAction, TabSession } from '../state/types'
|
|
3
3
|
|
|
4
4
|
import { buildPtyPastePayload } from '../input/paste'
|
|
5
5
|
|
|
@@ -12,10 +12,12 @@ export function writeToTab(
|
|
|
12
12
|
backend: SessionBackend,
|
|
13
13
|
tabId: string,
|
|
14
14
|
tab: TabSession | undefined,
|
|
15
|
-
input: string
|
|
15
|
+
input: string,
|
|
16
|
+
dispatch?: (action: AppAction) => void
|
|
16
17
|
): void {
|
|
17
18
|
if (tab && shouldScrollViewportToBottom(tab)) {
|
|
18
19
|
backend.scrollViewportToBottom(tabId)
|
|
20
|
+
dispatch?.({ intent: { kind: 'bottom' }, tabId, type: 'set-scroll-intent' })
|
|
19
21
|
}
|
|
20
22
|
|
|
21
23
|
backend.write(tabId, input)
|
|
@@ -25,8 +27,9 @@ export function writePasteToTab(
|
|
|
25
27
|
backend: SessionBackend,
|
|
26
28
|
tabId: string,
|
|
27
29
|
tab: TabSession | undefined,
|
|
28
|
-
text: string
|
|
30
|
+
text: string,
|
|
31
|
+
dispatch?: (action: AppAction) => void
|
|
29
32
|
): void {
|
|
30
33
|
const payload = buildPtyPastePayload(text, tab?.terminalModes.bracketedPasteMode ?? false)
|
|
31
|
-
writeToTab(backend, tabId, tab, payload)
|
|
34
|
+
writeToTab(backend, tabId, tab, payload, dispatch)
|
|
32
35
|
}
|
|
@@ -2,7 +2,6 @@ import { $ } from 'bun'
|
|
|
2
2
|
|
|
3
3
|
import type { SideEffect } from '../input/modes/types'
|
|
4
4
|
import type { SessionBackend } from '../session-backend/types'
|
|
5
|
-
import type { AppAction, AppState, AssistantId, TabSession } from '../state/types'
|
|
6
5
|
|
|
7
6
|
import { loadConfig, saveConfig } from '../config'
|
|
8
7
|
import { logInputDebug } from '../debug/input-log'
|
|
@@ -27,6 +26,13 @@ import {
|
|
|
27
26
|
} from '../state/layout-tree'
|
|
28
27
|
import { filterSessions, filterSnippets } from '../state/selectors'
|
|
29
28
|
import { createDefaultTerminalModes } from '../state/terminal-modes'
|
|
29
|
+
import {
|
|
30
|
+
type AppAction,
|
|
31
|
+
type AppState,
|
|
32
|
+
type AssistantId,
|
|
33
|
+
DEFAULT_SCROLL_INTENT,
|
|
34
|
+
type TabSession,
|
|
35
|
+
} from '../state/types'
|
|
30
36
|
import { saveCurrentWorkspace } from '../state/workspace-save'
|
|
31
37
|
import { scrollGitDiff } from '../ui/git-view-controls'
|
|
32
38
|
import { applyTheme } from '../ui/theme'
|
|
@@ -124,14 +130,14 @@ function pasteSnippetToActiveGroup(ctx: SideEffectContext): void {
|
|
|
124
130
|
const groupId = getGroupIdForTab(state.tabGroupMap, state.activeTabId)
|
|
125
131
|
const groupTree = groupId ? state.layoutTrees[groupId] : null
|
|
126
132
|
if (!groupTree) {
|
|
127
|
-
pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet)
|
|
133
|
+
pasteSnippetToTab(backend, state.activeTabId, activeTab, snippet, ctx.dispatch)
|
|
128
134
|
return
|
|
129
135
|
}
|
|
130
136
|
|
|
131
137
|
for (const tabId of allLeafIds(groupTree)) {
|
|
132
138
|
const tab = state.tabs.find((entry) => entry.id === tabId)
|
|
133
139
|
if (tab) {
|
|
134
|
-
pasteSnippetToTab(backend, tabId, tab, snippet)
|
|
140
|
+
pasteSnippetToTab(backend, tabId, tab, snippet, ctx.dispatch)
|
|
135
141
|
}
|
|
136
142
|
}
|
|
137
143
|
}
|
|
@@ -229,6 +235,7 @@ export function createTabSession(
|
|
|
229
235
|
buffer: '',
|
|
230
236
|
command: customCommand ?? option.command,
|
|
231
237
|
id: createTabId(),
|
|
238
|
+
scrollIntent: DEFAULT_SCROLL_INTENT,
|
|
232
239
|
status: 'starting',
|
|
233
240
|
terminalModes: createDefaultTerminalModes(),
|
|
234
241
|
title: option.label,
|
|
@@ -393,7 +400,13 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
393
400
|
)
|
|
394
401
|
return
|
|
395
402
|
case 'paste-selected-snippet': {
|
|
396
|
-
pasteSnippetToTab(
|
|
403
|
+
pasteSnippetToTab(
|
|
404
|
+
backend,
|
|
405
|
+
state.activeTabId,
|
|
406
|
+
ctx.activeTab,
|
|
407
|
+
getSelectedSnippet(state),
|
|
408
|
+
dispatch
|
|
409
|
+
)
|
|
397
410
|
return
|
|
398
411
|
}
|
|
399
412
|
case 'paste-snippet-to-group': {
|
|
@@ -471,11 +484,48 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
471
484
|
void enqueueGitOp(() => runGitPush(ctx))
|
|
472
485
|
return
|
|
473
486
|
}
|
|
487
|
+
case 'confirm-update-selection': {
|
|
488
|
+
handleConfirmUpdateSelection(ctx)
|
|
489
|
+
return
|
|
490
|
+
}
|
|
474
491
|
default:
|
|
475
492
|
effect satisfies never
|
|
476
493
|
}
|
|
477
494
|
}
|
|
478
495
|
|
|
496
|
+
function handleConfirmUpdateSelection(ctx: SideEffectContext): void {
|
|
497
|
+
const { state } = ctx
|
|
498
|
+
if (state.modal.type !== 'update-available') {
|
|
499
|
+
return
|
|
500
|
+
}
|
|
501
|
+
const latest = state.modal.latestVersion
|
|
502
|
+
if (state.modal.selectedIndex === 0) {
|
|
503
|
+
runUpdateFromTui(ctx, latest)
|
|
504
|
+
return
|
|
505
|
+
}
|
|
506
|
+
saveConfig({ ...loadConfig(), skippedUpdateVersion: latest })
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
function runUpdateFromTui(ctx: SideEffectContext, latestVersion: string): void {
|
|
510
|
+
saveCurrentWorkspace(ctx.state)
|
|
511
|
+
void ctx.backend.destroy(true)
|
|
512
|
+
ctx.renderer.destroy()
|
|
513
|
+
process.stdout.write(`\nUpdating aimux to ${latestVersion}...\n`)
|
|
514
|
+
const proc = Bun.spawn(['bun', 'update', '-g', '@brimveyn/aimux', '@brimveyn/aimux-config'], {
|
|
515
|
+
stderr: 'inherit',
|
|
516
|
+
stdin: 'inherit',
|
|
517
|
+
stdout: 'inherit',
|
|
518
|
+
})
|
|
519
|
+
void proc.exited.then((code) => {
|
|
520
|
+
if (code === 0) {
|
|
521
|
+
process.stdout.write(`\nUpdated. Run \`aimux\` to start the new version.\n`)
|
|
522
|
+
} else {
|
|
523
|
+
process.stderr.write(`\nUpdate failed (exit code ${code}).\n`)
|
|
524
|
+
}
|
|
525
|
+
process.exit(code ?? 1)
|
|
526
|
+
})
|
|
527
|
+
}
|
|
528
|
+
|
|
479
529
|
async function runGitAction(
|
|
480
530
|
ctx: SideEffectContext,
|
|
481
531
|
args: string[],
|
|
@@ -57,13 +57,14 @@ export function pasteSnippetToTab(
|
|
|
57
57
|
backend: SessionBackend,
|
|
58
58
|
activeTabId: string | null,
|
|
59
59
|
activeTab: TabSession | undefined,
|
|
60
|
-
snippet: SnippetRecord | undefined
|
|
60
|
+
snippet: SnippetRecord | undefined,
|
|
61
|
+
dispatch?: (action: AppAction) => void
|
|
61
62
|
): void {
|
|
62
63
|
if (!snippet || !activeTabId || !activeTab) {
|
|
63
64
|
return
|
|
64
65
|
}
|
|
65
66
|
|
|
66
|
-
writePasteToTab(backend, activeTabId, activeTab, snippet.content)
|
|
67
|
+
writePasteToTab(backend, activeTabId, activeTab, snippet.content, dispatch)
|
|
67
68
|
}
|
|
68
69
|
|
|
69
70
|
export function handleDeleteSnippetEffect(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { type MutableRefObject, useEffect, useRef } from 'react'
|
|
2
2
|
|
|
3
3
|
import type { SessionBackend } from '../session-backend/types'
|
|
4
|
-
import type { AppAction, LayoutState } from '../state/types'
|
|
4
|
+
import type { AppAction, LayoutState, ScrollIntent } from '../state/types'
|
|
5
5
|
|
|
6
6
|
import { attachCurrentSession } from './backend-attach-runtime'
|
|
7
7
|
import { bindBackendRuntimeEvents } from './backend-runtime-events'
|
|
@@ -11,6 +11,7 @@ interface BackendRuntimeOptions {
|
|
|
11
11
|
backend: SessionBackend
|
|
12
12
|
dispatch: (action: AppAction) => void
|
|
13
13
|
activeTabId: string | null
|
|
14
|
+
activeTabScrollIntentRef: MutableRefObject<ScrollIntent | null>
|
|
14
15
|
currentSessionId: string | null
|
|
15
16
|
layoutRef: MutableRefObject<LayoutState>
|
|
16
17
|
resizingRef: MutableRefObject<boolean>
|
|
@@ -25,6 +26,7 @@ export interface TabRuntimeControls {
|
|
|
25
26
|
|
|
26
27
|
export function useBackendRuntime({
|
|
27
28
|
activeTabId,
|
|
29
|
+
activeTabScrollIntentRef,
|
|
28
30
|
backend,
|
|
29
31
|
currentSessionId,
|
|
30
32
|
currentSessionWorkspaceSnapshot,
|
|
@@ -65,7 +67,10 @@ export function useBackendRuntime({
|
|
|
65
67
|
}
|
|
66
68
|
|
|
67
69
|
backend.setActiveTab(activeTabId)
|
|
68
|
-
|
|
70
|
+
if (activeTabId && activeTabScrollIntentRef.current) {
|
|
71
|
+
backend.reapplyScrollIntent(activeTabId, activeTabScrollIntentRef.current)
|
|
72
|
+
}
|
|
73
|
+
}, [activeTabId, activeTabScrollIntentRef, backend, currentSessionId])
|
|
69
74
|
|
|
70
75
|
useEffect(() => {
|
|
71
76
|
return bindBackendRuntimeEvents({
|
|
@@ -60,7 +60,7 @@ export function useRendererBindings({
|
|
|
60
60
|
activeTabRef.current?.terminalModes.bracketedPasteMode ?? false,
|
|
61
61
|
getFocusMode: () => focusModeRef.current,
|
|
62
62
|
handleTerminalShortcut,
|
|
63
|
-
writeToPty: (tabId, data) => writeToTab(backend, tabId, activeTabRef.current, data),
|
|
63
|
+
writeToPty: (tabId, data) => writeToTab(backend, tabId, activeTabRef.current, data, dispatch),
|
|
64
64
|
})
|
|
65
65
|
|
|
66
66
|
const handlePasteEvent = (event: { bytes: Uint8Array; defaultPrevented?: boolean }) => {
|
|
@@ -96,7 +96,7 @@ export function useRendererBindings({
|
|
|
96
96
|
return
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
-
writePasteToTab(backend, tabId, tab, payload)
|
|
99
|
+
writePasteToTab(backend, tabId, tab, payload, dispatch)
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
const handleSelection = (selection: OtuiSelection) => {
|
|
@@ -2,7 +2,7 @@ import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
|
|
2
2
|
|
|
3
3
|
import type { TerminalContentOrigin } from '../input/raw-input-handler'
|
|
4
4
|
import type { SessionBackend } from '../session-backend/types'
|
|
5
|
-
import type { AppAction, AppState } from '../state/types'
|
|
5
|
+
import type { AppAction, AppState, ScrollIntent } from '../state/types'
|
|
6
6
|
|
|
7
7
|
import {
|
|
8
8
|
createTerminalBounds,
|
|
@@ -27,20 +27,21 @@ function resizeSplitTabs(
|
|
|
27
27
|
layoutTrees: AppState['layoutTrees'],
|
|
28
28
|
tabIds: string[],
|
|
29
29
|
cols: number,
|
|
30
|
-
rows: number
|
|
30
|
+
rows: number,
|
|
31
|
+
intents: Map<string, ScrollIntent>
|
|
31
32
|
): void {
|
|
32
33
|
const bounds = getTerminalBounds(cols, rows)
|
|
33
34
|
const resizedTabIds = new Set<string>()
|
|
34
35
|
|
|
35
36
|
forEachSplitPaneRect(Object.values(layoutTrees), bounds, (tabId, rect) => {
|
|
36
37
|
const size = toTerminalContentSize(rect)
|
|
37
|
-
backend.resizeTab(tabId, size.cols, size.rows)
|
|
38
|
+
backend.resizeTab(tabId, size.cols, size.rows, intents.get(tabId))
|
|
38
39
|
resizedTabIds.add(tabId)
|
|
39
40
|
})
|
|
40
41
|
|
|
41
42
|
for (const id of tabIds) {
|
|
42
43
|
if (!resizedTabIds.has(id)) {
|
|
43
|
-
backend.resizeTab(id, cols, rows)
|
|
44
|
+
backend.resizeTab(id, cols, rows, intents.get(id))
|
|
44
45
|
}
|
|
45
46
|
}
|
|
46
47
|
}
|
|
@@ -64,6 +65,7 @@ export function useTerminalResize({
|
|
|
64
65
|
}: UseTerminalResizeOptions) {
|
|
65
66
|
const resizingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
66
67
|
const tabIdsRef = useRef<string[]>([])
|
|
68
|
+
const intentsRef = useRef<Map<string, ScrollIntent>>(new Map())
|
|
67
69
|
|
|
68
70
|
const currentTabIds = state.tabs.map((t) => t.id)
|
|
69
71
|
const tabIdsChanged =
|
|
@@ -74,6 +76,12 @@ export function useTerminalResize({
|
|
|
74
76
|
}
|
|
75
77
|
const stableTabIds = tabIdsRef.current
|
|
76
78
|
|
|
79
|
+
intentsRef.current = new Map(
|
|
80
|
+
state.tabs
|
|
81
|
+
.filter((t): t is typeof t & { scrollIntent: ScrollIntent } => t.scrollIntent !== undefined)
|
|
82
|
+
.map((t) => [t.id, t.scrollIntent])
|
|
83
|
+
)
|
|
84
|
+
|
|
77
85
|
const terminalSize = useMemo(() => {
|
|
78
86
|
const sidebarWidth = state.sidebar.visible ? state.sidebar.width + 1 : 0
|
|
79
87
|
const reservedRows =
|
|
@@ -118,10 +126,11 @@ export function useTerminalResize({
|
|
|
118
126
|
state.layoutTrees,
|
|
119
127
|
stableTabIds,
|
|
120
128
|
terminalSize.cols,
|
|
121
|
-
terminalSize.rows
|
|
129
|
+
terminalSize.rows,
|
|
130
|
+
intentsRef.current
|
|
122
131
|
)
|
|
123
132
|
} else {
|
|
124
|
-
backend.resizeAll(terminalSize.cols, terminalSize.rows)
|
|
133
|
+
backend.resizeAll(terminalSize.cols, terminalSize.rows, intentsRef.current)
|
|
125
134
|
}
|
|
126
135
|
resizingTimerRef.current = setTimeout(() => {
|
|
127
136
|
resizingRef.current = false
|
package/src/app.tsx
CHANGED
|
@@ -1,6 +1,15 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import type { ResolvedConfig } from '@brimveyn/aimux-config'
|
|
2
|
+
|
|
2
3
|
import { useKeyboard, useRenderer, useTerminalDimensions } from '@opentui/react'
|
|
3
|
-
import {
|
|
4
|
+
import {
|
|
5
|
+
useCallback,
|
|
6
|
+
useEffect,
|
|
7
|
+
useLayoutEffect,
|
|
8
|
+
useMemo,
|
|
9
|
+
useReducer,
|
|
10
|
+
useRef,
|
|
11
|
+
useState,
|
|
12
|
+
} from 'react'
|
|
4
13
|
|
|
5
14
|
import type { KeyChord } from './input/keymap/key-chord'
|
|
6
15
|
import type { TrieBinding } from './input/keymap/trie'
|
|
@@ -20,19 +29,36 @@ import { deriveModeId } from './input/modes/bridge'
|
|
|
20
29
|
import { registerAllModes } from './input/modes/handlers'
|
|
21
30
|
import { getHandler, transitionTo } from './input/modes/registry'
|
|
22
31
|
import { type TerminalContentOrigin } from './input/raw-input-handler'
|
|
32
|
+
import { getProfileName } from './profile-paths'
|
|
23
33
|
import { appStore } from './state/app-store'
|
|
24
34
|
import { setActiveDispatch } from './state/dispatch-ref'
|
|
25
35
|
import { loadSessionCatalog } from './state/session-catalog'
|
|
26
36
|
import { loadSnippetCatalog } from './state/snippet-catalog'
|
|
27
37
|
import { appReducer, createInitialState } from './state/store'
|
|
38
|
+
import { KeymapContext } from './ui/keymap-context'
|
|
28
39
|
import { RootView } from './ui/root'
|
|
29
40
|
import { applyTheme } from './ui/theme'
|
|
30
|
-
|
|
31
|
-
|
|
41
|
+
import {
|
|
42
|
+
fetchLatestNpmVersion,
|
|
43
|
+
getCurrentPackageVersion,
|
|
44
|
+
isNewerVersion,
|
|
45
|
+
} from './update/version-check'
|
|
32
46
|
|
|
33
47
|
const WORKSPACE_SAVE_DEBOUNCE_MS = 250
|
|
34
48
|
|
|
35
|
-
export function App({
|
|
49
|
+
export function App({
|
|
50
|
+
backend,
|
|
51
|
+
resolvedConfig,
|
|
52
|
+
}: {
|
|
53
|
+
backend: SessionBackend
|
|
54
|
+
resolvedConfig: ResolvedConfig
|
|
55
|
+
}) {
|
|
56
|
+
const keymapHandlers = useMemo(
|
|
57
|
+
() => registerAllModes(resolvedConfig.keymaps),
|
|
58
|
+
// Registration has side effects in a global mode registry — run once per app instance.
|
|
59
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
60
|
+
[]
|
|
61
|
+
)
|
|
36
62
|
const renderer = useRenderer()
|
|
37
63
|
const dimensions = useTerminalDimensions()
|
|
38
64
|
const [themeId, setThemeId] = useState<ThemeId>(() => {
|
|
@@ -59,6 +85,31 @@ export function App({ backend }: { backend: SessionBackend }) {
|
|
|
59
85
|
return () => setActiveDispatch(null)
|
|
60
86
|
}, [dispatch])
|
|
61
87
|
|
|
88
|
+
useEffect(() => {
|
|
89
|
+
if (process.env.AIMUX_DISABLE_UPDATE_CHECK === '1') return
|
|
90
|
+
if (getProfileName() === 'dev') return
|
|
91
|
+
|
|
92
|
+
let cancelled = false
|
|
93
|
+
void (async () => {
|
|
94
|
+
const [current, latest] = await Promise.all([
|
|
95
|
+
getCurrentPackageVersion(),
|
|
96
|
+
fetchLatestNpmVersion('@brimveyn/aimux'),
|
|
97
|
+
])
|
|
98
|
+
if (cancelled || !latest) return
|
|
99
|
+
if (!isNewerVersion(latest, current)) return
|
|
100
|
+
if (loadConfig().skippedUpdateVersion === latest) return
|
|
101
|
+
dispatch({
|
|
102
|
+
currentVersion: current,
|
|
103
|
+
latestVersion: latest,
|
|
104
|
+
type: 'open-update-available-modal',
|
|
105
|
+
})
|
|
106
|
+
})()
|
|
107
|
+
|
|
108
|
+
return () => {
|
|
109
|
+
cancelled = true
|
|
110
|
+
}
|
|
111
|
+
}, [])
|
|
112
|
+
|
|
62
113
|
const resizingRef = useRef(false)
|
|
63
114
|
const layoutRef = useRef(state.layout)
|
|
64
115
|
layoutRef.current = state.layout
|
|
@@ -81,6 +132,8 @@ export function App({ backend }: { backend: SessionBackend }) {
|
|
|
81
132
|
activeTabIdRef.current = state.activeTabId
|
|
82
133
|
const activeTabRef = useRef(activeTab)
|
|
83
134
|
activeTabRef.current = activeTab
|
|
135
|
+
const activeTabScrollIntentRef = useRef(activeTab?.scrollIntent ?? null)
|
|
136
|
+
activeTabScrollIntentRef.current = activeTab?.scrollIntent ?? null
|
|
84
137
|
|
|
85
138
|
const stateRef = useRef(state)
|
|
86
139
|
stateRef.current = state
|
|
@@ -90,6 +143,7 @@ export function App({ backend }: { backend: SessionBackend }) {
|
|
|
90
143
|
|
|
91
144
|
const { clearIdleTimer, clearStartupGrace, startStartupGrace } = useBackendRuntime({
|
|
92
145
|
activeTabId: state.activeTabId,
|
|
146
|
+
activeTabScrollIntentRef,
|
|
93
147
|
backend,
|
|
94
148
|
currentSessionId: state.currentSessionId,
|
|
95
149
|
currentSessionWorkspaceSnapshot,
|
|
@@ -132,15 +186,18 @@ export function App({ backend }: { backend: SessionBackend }) {
|
|
|
132
186
|
// Allows handleTerminalShortcut (a stable callback) to reach the latest closure.
|
|
133
187
|
const processKeyResultRef = useRef<(result: KeyResult, modeId: ModeId) => void>(() => {})
|
|
134
188
|
|
|
135
|
-
const handleTerminalShortcut = useCallback(
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
189
|
+
const handleTerminalShortcut = useCallback(
|
|
190
|
+
(chord: KeyChord): boolean => {
|
|
191
|
+
const terminalHandler = keymapHandlers.find((h) => h.id === 'terminal-input')
|
|
192
|
+
if (!terminalHandler) return false
|
|
193
|
+
const ctx: ModeContext = { state: stateRef.current }
|
|
194
|
+
const result = terminalHandler.handleChord(chord, ctx)
|
|
195
|
+
if (!result) return false
|
|
196
|
+
processKeyResultRef.current(result, 'terminal-input')
|
|
197
|
+
return true
|
|
198
|
+
},
|
|
199
|
+
[keymapHandlers]
|
|
200
|
+
)
|
|
144
201
|
|
|
145
202
|
useRendererBindings({
|
|
146
203
|
activeTabId: state.activeTabId,
|
|
@@ -233,21 +290,23 @@ export function App({ backend }: { backend: SessionBackend }) {
|
|
|
233
290
|
})
|
|
234
291
|
|
|
235
292
|
return (
|
|
236
|
-
<
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
293
|
+
<KeymapContext.Provider value={resolvedConfig.keymaps}>
|
|
294
|
+
<RootView
|
|
295
|
+
themeId={themeId}
|
|
296
|
+
contentOrigin={contentOriginRef.current}
|
|
297
|
+
mouseForwardingEnabled={activeMouseForwardingEnabled}
|
|
298
|
+
localScrollbackEnabled={activeLocalScrollbackEnabled}
|
|
299
|
+
onTerminalMouseEvent={handleTerminalMouseEvent}
|
|
300
|
+
onTerminalScrollEvent={handleTerminalScrollEvent}
|
|
301
|
+
onTerminalClick={handleTerminalClick}
|
|
302
|
+
onPaneActivate={handlePaneActivate}
|
|
303
|
+
onSplitResize={handleSplitResize}
|
|
304
|
+
onSeparatorDragStart={handleSeparatorDragStart}
|
|
305
|
+
onSeparatorDrag={handleSeparatorDrag}
|
|
306
|
+
onSeparatorDragEnd={handleSeparatorDragEnd}
|
|
307
|
+
terminalCols={terminalSize.cols}
|
|
308
|
+
terminalRows={terminalSize.rows}
|
|
309
|
+
/>
|
|
310
|
+
</KeymapContext.Provider>
|
|
252
311
|
)
|
|
253
312
|
}
|
package/src/config.ts
CHANGED
|
@@ -16,6 +16,7 @@ export interface AimuxConfig {
|
|
|
16
16
|
gitPanelVisible?: boolean
|
|
17
17
|
gitPanelRatio?: number
|
|
18
18
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
19
|
+
skippedUpdateVersion?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const DEFAULT_CONFIG: AimuxConfig = {
|
|
@@ -58,6 +59,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
58
59
|
gitPanelVisible?: unknown
|
|
59
60
|
gitPanelRatio?: unknown
|
|
60
61
|
workspaceSnapshot?: unknown
|
|
62
|
+
skippedUpdateVersion?: unknown
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const issues: string[] = []
|
|
@@ -98,6 +100,14 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
98
100
|
issues.push('ignored invalid workspaceSnapshot')
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const validSkippedUpdateVersion =
|
|
104
|
+
typeof parsed.skippedUpdateVersion === 'string' && parsed.skippedUpdateVersion.length > 0
|
|
105
|
+
? parsed.skippedUpdateVersion
|
|
106
|
+
: undefined
|
|
107
|
+
if (parsed.skippedUpdateVersion !== undefined && validSkippedUpdateVersion === undefined) {
|
|
108
|
+
issues.push('ignored invalid skippedUpdateVersion')
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
if (issues.length > 0) {
|
|
102
112
|
logDebug('config.load.validationIssue', { issues, path: CONFIG_PATH })
|
|
103
113
|
}
|
|
@@ -107,6 +117,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
107
117
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
108
118
|
gitPanelRatio: validGitPanelRatio,
|
|
109
119
|
gitPanelVisible: validGitPanelVisible,
|
|
120
|
+
skippedUpdateVersion: validSkippedUpdateVersion,
|
|
110
121
|
themeId: isThemeId(parsed.themeId) ? parsed.themeId : undefined,
|
|
111
122
|
version: 2,
|
|
112
123
|
workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -256,7 +256,12 @@ export async function runDaemon(): Promise<void> {
|
|
|
256
256
|
case 'resizeClient': {
|
|
257
257
|
const sessionId = requireSession(socket, attachedSessions)
|
|
258
258
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
259
|
-
await manager.resize(
|
|
259
|
+
await manager.resize(
|
|
260
|
+
sessionId,
|
|
261
|
+
message.payload.cols,
|
|
262
|
+
message.payload.rows,
|
|
263
|
+
message.payload.intents
|
|
264
|
+
)
|
|
260
265
|
sendOk(socket, message.id)
|
|
261
266
|
break
|
|
262
267
|
}
|
|
@@ -267,7 +272,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
267
272
|
sessionId,
|
|
268
273
|
message.payload.tabId,
|
|
269
274
|
message.payload.cols,
|
|
270
|
-
message.payload.rows
|
|
275
|
+
message.payload.rows,
|
|
276
|
+
message.payload.intent
|
|
271
277
|
)
|
|
272
278
|
sendOk(socket, message.id)
|
|
273
279
|
break
|
|
@@ -286,6 +292,17 @@ export async function runDaemon(): Promise<void> {
|
|
|
286
292
|
sendOk(socket, message.id)
|
|
287
293
|
break
|
|
288
294
|
}
|
|
295
|
+
case 'reapplyScrollIntent': {
|
|
296
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
297
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
298
|
+
await manager.reapplyScrollIntent(
|
|
299
|
+
sessionId,
|
|
300
|
+
message.payload.tabId,
|
|
301
|
+
message.payload.intent
|
|
302
|
+
)
|
|
303
|
+
sendOk(socket, message.id)
|
|
304
|
+
break
|
|
305
|
+
}
|
|
289
306
|
case 'setActiveTab': {
|
|
290
307
|
const sessionId = requireSession(socket, attachedSessions)
|
|
291
308
|
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ScrollIntent,
|
|
5
|
+
TerminalModeState,
|
|
6
|
+
TerminalSnapshot,
|
|
7
|
+
WorkspaceSnapshotV1,
|
|
8
|
+
} from '../state/types'
|
|
4
9
|
|
|
5
10
|
import { logDebug } from '../debug/input-log'
|
|
6
11
|
import { SessionRegistry } from './session-registry'
|
|
@@ -62,12 +67,18 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
62
67
|
this.getOrCreateRegistry(sessionId).write(tabId, data)
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
resize(sessionId: string, cols: number, rows: number): void {
|
|
66
|
-
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows)
|
|
70
|
+
resize(sessionId: string, cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
|
|
71
|
+
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents)
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
resizeTab(
|
|
70
|
-
|
|
74
|
+
resizeTab(
|
|
75
|
+
sessionId: string,
|
|
76
|
+
tabId: string,
|
|
77
|
+
cols: number,
|
|
78
|
+
rows: number,
|
|
79
|
+
intent?: ScrollIntent
|
|
80
|
+
): void {
|
|
81
|
+
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, intent)
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
scroll(sessionId: string, tabId: string, deltaLines: number): void {
|
|
@@ -78,6 +89,10 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
78
89
|
this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): void {
|
|
93
|
+
this.getOrCreateRegistry(sessionId).reapplyScrollIntent(tabId, intent)
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
setActiveTab(sessionId: string, tabId: string | null): void {
|
|
82
97
|
this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
|
|
83
98
|
}
|