@brimveyn/aimux 1.3.1 → 1.4.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 +3 -0
- package/package.json +3 -2
- package/src/app-runtime/backend-runtime-events.ts +6 -0
- package/src/app-runtime/side-effects.ts +18 -0
- package/src/app-runtime/use-terminal-resize.ts +100 -30
- package/src/app.tsx +24 -7
- package/src/config.ts +21 -1
- package/src/daemon/session-manager.ts +11 -4
- package/src/daemon/session-registry.ts +15 -4
- package/src/index.tsx +6 -1
- package/src/input/modes/types.ts +1 -0
- package/src/pty/pty-manager.ts +127 -11
- package/src/restart-terminal-manager.ts +44 -0
- package/src/session-backend/local-session-backend.ts +42 -4
- package/src/session-backend/remote-session-backend.ts +13 -2
- package/src/session-backend/types.ts +14 -2
- package/src/state/dispatch-ref.ts +11 -0
- package/src/state/reducers/session-state.ts +28 -0
- package/src/state/reducers/ui-state.ts +8 -0
- package/src/state/session-catalog.ts +22 -1
- package/src/state/store.ts +8 -1
- package/src/state/types.ts +14 -0
- package/src/state/validation.ts +2 -0
- package/src/state/workspace-save.ts +2 -0
- package/src/ui/components/session-bar.tsx +208 -0
- package/src/ui/components/tab-item.tsx +3 -15
- package/src/ui/components/terminal-pane.tsx +17 -12
- package/src/ui/hooks/use-busy-spinner.ts +18 -0
- package/src/ui/root.tsx +4 -0
- package/src/ui/session-ordering.ts +34 -0
package/README.md
CHANGED
|
@@ -11,6 +11,7 @@ A terminal multiplexer for AI CLIs. Manage multiple AI assistant sessions (Claud
|
|
|
11
11
|
## Features
|
|
12
12
|
|
|
13
13
|
- **Multi-tab sessions** — Run Claude, Codex, and OpenCode in parallel with instant tab switching
|
|
14
|
+
- **Session bar** — Numbered session chips at the top (or bottom) of the screen. Click to switch, drag to reorder, busy spinner for non-focused sessions with live PTY output. Toggle with `<leader>b`; jump with `<leader>1..9`. Position persists via `aimux.config.ts` or `aimux.json`.
|
|
14
15
|
- **Split panes** — Split vertically (`|`) or horizontally (`-`) to view multiple assistants at once
|
|
15
16
|
- **Draggable separators** — Resize split panes by dragging with the mouse
|
|
16
17
|
- **Click-to-focus** — Click any pane or sidebar tab to focus it instantly
|
|
@@ -134,6 +135,8 @@ Press `?` in navigation mode for the full, live keybinding list (reflects your c
|
|
|
134
135
|
| `Ctrl+S` | Snippet picker |
|
|
135
136
|
| `Ctrl+T` | Theme picker |
|
|
136
137
|
| `G` | Toggle git panel |
|
|
138
|
+
| `<leader>b` | Toggle session bar |
|
|
139
|
+
| `<leader>1..9` | Switch to session N |
|
|
137
140
|
| `?` | Show help |
|
|
138
141
|
| `Ctrl+C` | Quit |
|
|
139
142
|
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.4.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",
|
|
@@ -52,12 +52,13 @@
|
|
|
52
52
|
"demo:splits": "vhs assets/splits.tape",
|
|
53
53
|
"demo:themes": "vhs assets/themes.tape",
|
|
54
54
|
"restart-daemon": "AIMUX_PROFILE=dev bun run src/index.tsx restart-daemon",
|
|
55
|
+
"restart-terminal-manager": "AIMUX_PROFILE=dev bun run src/index.tsx restart-terminal-manager",
|
|
55
56
|
"lint": "oxlint .",
|
|
56
57
|
"format": "oxfmt --write .",
|
|
57
58
|
"format:check": "oxfmt --check ."
|
|
58
59
|
},
|
|
59
60
|
"dependencies": {
|
|
60
|
-
"@brimveyn/aimux-config": "0.
|
|
61
|
+
"@brimveyn/aimux-config": "0.3.1",
|
|
61
62
|
"@opentui/core": "^0.1.90",
|
|
62
63
|
"@opentui/react": "^0.1.90",
|
|
63
64
|
"@xterm/headless": "^6.0.0",
|
|
@@ -79,15 +79,21 @@ export function bindBackendRuntimeEvents({
|
|
|
79
79
|
dispatch({ message, tabId, type: 'set-tab-error' })
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
const handleSessionActivity = (sessionId: string, busy: boolean) => {
|
|
83
|
+
dispatch({ busy, sessionId, type: 'set-session-busy' })
|
|
84
|
+
}
|
|
85
|
+
|
|
82
86
|
backend.on('render', handleRender)
|
|
83
87
|
backend.on('exit', handleExit)
|
|
84
88
|
backend.on('error', handleError)
|
|
89
|
+
backend.on('sessionActivity', handleSessionActivity)
|
|
85
90
|
|
|
86
91
|
return () => {
|
|
87
92
|
timeouts.clearAllTimers()
|
|
88
93
|
backend.off('render', handleRender)
|
|
89
94
|
backend.off('exit', handleExit)
|
|
90
95
|
backend.off('error', handleError)
|
|
96
|
+
backend.off('sessionActivity', handleSessionActivity)
|
|
91
97
|
void backend.destroy(true)
|
|
92
98
|
}
|
|
93
99
|
}
|
|
@@ -488,11 +488,29 @@ export function executeSideEffect(effect: SideEffect, ctx: SideEffectContext): v
|
|
|
488
488
|
handleConfirmUpdateSelection(ctx)
|
|
489
489
|
return
|
|
490
490
|
}
|
|
491
|
+
case 'switch-session-by-index': {
|
|
492
|
+
handleSwitchSessionByIndex(ctx, effect.index)
|
|
493
|
+
return
|
|
494
|
+
}
|
|
491
495
|
default:
|
|
492
496
|
effect satisfies never
|
|
493
497
|
}
|
|
494
498
|
}
|
|
495
499
|
|
|
500
|
+
function handleSwitchSessionByIndex(ctx: SideEffectContext, index: number): void {
|
|
501
|
+
const { backend, dispatch, state } = ctx
|
|
502
|
+
const ordered = state.sessions
|
|
503
|
+
.slice()
|
|
504
|
+
.sort((a, b) => (a.order ?? Number.MAX_SAFE_INTEGER) - (b.order ?? Number.MAX_SAFE_INTEGER))
|
|
505
|
+
const target = ordered[index - 1]
|
|
506
|
+
if (!target) {
|
|
507
|
+
logInputDebug('app.sessionBar.switchOutOfRange', { index, total: ordered.length })
|
|
508
|
+
return
|
|
509
|
+
}
|
|
510
|
+
if (target.id === state.currentSessionId) return
|
|
511
|
+
handleSwitchSessionEffect(state, backend, dispatch, target)
|
|
512
|
+
}
|
|
513
|
+
|
|
496
514
|
function handleConfirmUpdateSelection(ctx: SideEffectContext): void {
|
|
497
515
|
const { state } = ctx
|
|
498
516
|
if (state.modal.type !== 'update-available') {
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type MutableRefObject, useEffect, useMemo, useRef } from 'react'
|
|
1
|
+
import { type MutableRefObject, useEffect, useLayoutEffect, 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'
|
|
@@ -28,20 +28,21 @@ function resizeSplitTabs(
|
|
|
28
28
|
tabIds: string[],
|
|
29
29
|
cols: number,
|
|
30
30
|
rows: number,
|
|
31
|
-
intents: Map<string, ScrollIntent
|
|
31
|
+
intents: Map<string, ScrollIntent>,
|
|
32
|
+
options?: { sync?: boolean }
|
|
32
33
|
): void {
|
|
33
34
|
const bounds = getTerminalBounds(cols, rows)
|
|
34
35
|
const resizedTabIds = new Set<string>()
|
|
35
36
|
|
|
36
37
|
forEachSplitPaneRect(Object.values(layoutTrees), bounds, (tabId, rect) => {
|
|
37
38
|
const size = toTerminalContentSize(rect)
|
|
38
|
-
backend.resizeTab(tabId, size.cols, size.rows, intents.get(tabId))
|
|
39
|
+
backend.resizeTab(tabId, size.cols, size.rows, intents.get(tabId), options)
|
|
39
40
|
resizedTabIds.add(tabId)
|
|
40
41
|
})
|
|
41
42
|
|
|
42
43
|
for (const id of tabIds) {
|
|
43
44
|
if (!resizedTabIds.has(id)) {
|
|
44
|
-
backend.resizeTab(id, cols, rows, intents.get(id))
|
|
45
|
+
backend.resizeTab(id, cols, rows, intents.get(id), options)
|
|
45
46
|
}
|
|
46
47
|
}
|
|
47
48
|
}
|
|
@@ -55,6 +56,50 @@ interface UseTerminalResizeOptions {
|
|
|
55
56
|
resizingRef: MutableRefObject<boolean>
|
|
56
57
|
}
|
|
57
58
|
|
|
59
|
+
interface RunResizeCascadeArgs {
|
|
60
|
+
backend: SessionBackend
|
|
61
|
+
dispatch: (action: AppAction) => void
|
|
62
|
+
resizingRef: MutableRefObject<boolean>
|
|
63
|
+
resizingTimerRef: MutableRefObject<ReturnType<typeof setTimeout> | null>
|
|
64
|
+
cols: number
|
|
65
|
+
rows: number
|
|
66
|
+
layoutTrees: AppState['layoutTrees']
|
|
67
|
+
stableTabIds: string[]
|
|
68
|
+
intents: Map<string, ScrollIntent>
|
|
69
|
+
sync: boolean
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function runResizeCascade({
|
|
73
|
+
backend,
|
|
74
|
+
cols,
|
|
75
|
+
dispatch,
|
|
76
|
+
intents,
|
|
77
|
+
layoutTrees,
|
|
78
|
+
resizingRef,
|
|
79
|
+
resizingTimerRef,
|
|
80
|
+
rows,
|
|
81
|
+
stableTabIds,
|
|
82
|
+
sync,
|
|
83
|
+
}: RunResizeCascadeArgs): void {
|
|
84
|
+
dispatch({ cols, rows, type: 'set-terminal-size' })
|
|
85
|
+
resizingRef.current = true
|
|
86
|
+
if (resizingTimerRef.current) {
|
|
87
|
+
clearTimeout(resizingTimerRef.current)
|
|
88
|
+
}
|
|
89
|
+
const trees = Object.values(layoutTrees)
|
|
90
|
+
const hasSplits = trees.some((t) => t.type === 'split')
|
|
91
|
+
const options = sync ? { sync: true } : undefined
|
|
92
|
+
if (hasSplits) {
|
|
93
|
+
resizeSplitTabs(backend, layoutTrees, stableTabIds, cols, rows, intents, options)
|
|
94
|
+
} else {
|
|
95
|
+
backend.resizeAll(cols, rows, intents, options)
|
|
96
|
+
}
|
|
97
|
+
resizingTimerRef.current = setTimeout(() => {
|
|
98
|
+
resizingRef.current = false
|
|
99
|
+
resizingTimerRef.current = null
|
|
100
|
+
}, RESIZE_ACTIVITY_SETTLE_MS)
|
|
101
|
+
}
|
|
102
|
+
|
|
58
103
|
export function useTerminalResize({
|
|
59
104
|
backend,
|
|
60
105
|
contentOriginRef,
|
|
@@ -66,6 +111,8 @@ export function useTerminalResize({
|
|
|
66
111
|
const resizingTimerRef = useRef<ReturnType<typeof setTimeout> | null>(null)
|
|
67
112
|
const tabIdsRef = useRef<string[]>([])
|
|
68
113
|
const intentsRef = useRef<Map<string, ScrollIntent>>(new Map())
|
|
114
|
+
const handledBySyncRef = useRef(false)
|
|
115
|
+
const sidebarMountedRef = useRef(false)
|
|
69
116
|
|
|
70
117
|
const currentTabIds = state.tabs.map((t) => t.id)
|
|
71
118
|
const tabIdsChanged =
|
|
@@ -84,8 +131,14 @@ export function useTerminalResize({
|
|
|
84
131
|
|
|
85
132
|
const terminalSize = useMemo(() => {
|
|
86
133
|
const sidebarWidth = state.sidebar.visible ? state.sidebar.width + 1 : 0
|
|
134
|
+
const sessionBarRows = state.sessionBar.visible ? 1 : 0
|
|
135
|
+
const sessionBarTopOffset =
|
|
136
|
+
state.sessionBar.visible && state.sessionBar.position === 'top' ? 1 : 0
|
|
87
137
|
const reservedRows =
|
|
88
|
-
MAIN_AREA_VERTICAL_PADDING +
|
|
138
|
+
MAIN_AREA_VERTICAL_PADDING +
|
|
139
|
+
STATUS_BAR_HEIGHT +
|
|
140
|
+
TERMINAL_PANE_VERTICAL_CHROME +
|
|
141
|
+
sessionBarRows
|
|
89
142
|
const cols = Math.max(
|
|
90
143
|
MIN_TERMINAL_COLS,
|
|
91
144
|
Math.floor(dimensions.width - sidebarWidth - MAIN_AREA_HORIZONTAL_CHROME)
|
|
@@ -96,7 +149,7 @@ export function useTerminalResize({
|
|
|
96
149
|
cols,
|
|
97
150
|
rows,
|
|
98
151
|
x: sidebarWidth + 1,
|
|
99
|
-
y: 1,
|
|
152
|
+
y: 1 + sessionBarTopOffset,
|
|
100
153
|
}
|
|
101
154
|
|
|
102
155
|
return { cols, rows }
|
|
@@ -106,36 +159,53 @@ export function useTerminalResize({
|
|
|
106
159
|
dimensions.width,
|
|
107
160
|
state.sidebar.visible,
|
|
108
161
|
state.sidebar.width,
|
|
162
|
+
state.sessionBar.visible,
|
|
163
|
+
state.sessionBar.position,
|
|
109
164
|
])
|
|
110
165
|
|
|
111
|
-
|
|
112
|
-
|
|
166
|
+
useLayoutEffect(() => {
|
|
167
|
+
if (!sidebarMountedRef.current) {
|
|
168
|
+
sidebarMountedRef.current = true
|
|
169
|
+
return
|
|
170
|
+
}
|
|
171
|
+
runResizeCascade({
|
|
172
|
+
backend,
|
|
113
173
|
cols: terminalSize.cols,
|
|
174
|
+
dispatch,
|
|
175
|
+
intents: intentsRef.current,
|
|
176
|
+
layoutTrees: state.layoutTrees,
|
|
177
|
+
resizingRef,
|
|
178
|
+
resizingTimerRef,
|
|
114
179
|
rows: terminalSize.rows,
|
|
115
|
-
|
|
180
|
+
stableTabIds,
|
|
181
|
+
sync: true,
|
|
116
182
|
})
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
intentsRef.current
|
|
131
|
-
)
|
|
132
|
-
} else {
|
|
133
|
-
backend.resizeAll(terminalSize.cols, terminalSize.rows, intentsRef.current)
|
|
183
|
+
handledBySyncRef.current = true
|
|
184
|
+
// eslint-disable-next-line react-hooks/exhaustive-deps
|
|
185
|
+
}, [
|
|
186
|
+
state.sidebar.visible,
|
|
187
|
+
state.sidebar.width,
|
|
188
|
+
state.sessionBar.visible,
|
|
189
|
+
state.sessionBar.position,
|
|
190
|
+
])
|
|
191
|
+
|
|
192
|
+
useEffect(() => {
|
|
193
|
+
if (handledBySyncRef.current) {
|
|
194
|
+
handledBySyncRef.current = false
|
|
195
|
+
return
|
|
134
196
|
}
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
197
|
+
runResizeCascade({
|
|
198
|
+
backend,
|
|
199
|
+
cols: terminalSize.cols,
|
|
200
|
+
dispatch,
|
|
201
|
+
intents: intentsRef.current,
|
|
202
|
+
layoutTrees: state.layoutTrees,
|
|
203
|
+
resizingRef,
|
|
204
|
+
resizingTimerRef,
|
|
205
|
+
rows: terminalSize.rows,
|
|
206
|
+
stableTabIds,
|
|
207
|
+
sync: false,
|
|
208
|
+
})
|
|
139
209
|
}, [
|
|
140
210
|
backend,
|
|
141
211
|
dispatch,
|
package/src/app.tsx
CHANGED
|
@@ -31,7 +31,7 @@ import { getHandler, transitionTo } from './input/modes/registry'
|
|
|
31
31
|
import { type TerminalContentOrigin } from './input/raw-input-handler'
|
|
32
32
|
import { getProfileName } from './profile-paths'
|
|
33
33
|
import { appStore } from './state/app-store'
|
|
34
|
-
import { setActiveDispatch } from './state/dispatch-ref'
|
|
34
|
+
import { setActiveDispatch, setActiveSideEffectRunner } from './state/dispatch-ref'
|
|
35
35
|
import { loadSessionCatalog } from './state/session-catalog'
|
|
36
36
|
import { loadSnippetCatalog } from './state/snippet-catalog'
|
|
37
37
|
import { appReducer, createInitialState } from './state/store'
|
|
@@ -69,11 +69,22 @@ export function App({
|
|
|
69
69
|
return config.themeId ?? 'aimux'
|
|
70
70
|
})
|
|
71
71
|
const [state, dispatch] = useReducer(appReducer, undefined, () => {
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
72
|
+
const json = loadConfig()
|
|
73
|
+
const sessionBarVisible = resolvedConfig.sessionBar?.visible ?? json.sessionBarVisible ?? true
|
|
74
|
+
const sessionBarPosition =
|
|
75
|
+
resolvedConfig.sessionBar?.position ?? json.sessionBarPosition ?? 'top'
|
|
76
|
+
return createInitialState(
|
|
77
|
+
json.customCommands,
|
|
78
|
+
loadSessionCatalog(),
|
|
79
|
+
loadSnippetCatalog(),
|
|
80
|
+
true,
|
|
81
|
+
{
|
|
82
|
+
gitPanelRatio: json.gitPanelRatio,
|
|
83
|
+
gitPanelVisible: json.gitPanelVisible,
|
|
84
|
+
sessionBarPosition,
|
|
85
|
+
sessionBarVisible,
|
|
86
|
+
}
|
|
87
|
+
)
|
|
77
88
|
})
|
|
78
89
|
|
|
79
90
|
useLayoutEffect(() => {
|
|
@@ -82,7 +93,10 @@ export function App({
|
|
|
82
93
|
|
|
83
94
|
useLayoutEffect(() => {
|
|
84
95
|
setActiveDispatch(dispatch)
|
|
85
|
-
return () =>
|
|
96
|
+
return () => {
|
|
97
|
+
setActiveDispatch(null)
|
|
98
|
+
setActiveSideEffectRunner(null)
|
|
99
|
+
}
|
|
86
100
|
}, [dispatch])
|
|
87
101
|
|
|
88
102
|
useEffect(() => {
|
|
@@ -251,6 +265,9 @@ export function App({
|
|
|
251
265
|
|
|
252
266
|
// Keep the ref pointing at the latest closure so stable callbacks can invoke it
|
|
253
267
|
processKeyResultRef.current = processKeyResult
|
|
268
|
+
// Expose the side-effect runner so non-keyboard call sites (mouse, IPC) can
|
|
269
|
+
// invoke the same effect pipeline with the current context.
|
|
270
|
+
setActiveSideEffectRunner((effect) => executeSideEffect(effect, sideEffectCtx))
|
|
254
271
|
|
|
255
272
|
useLayoutEffect(() => {
|
|
256
273
|
for (const handler of keymapHandlers) {
|
package/src/config.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
2
|
|
|
3
|
-
import type { WorkspaceSnapshotV1 } from './state/types'
|
|
3
|
+
import type { SessionBarPosition, WorkspaceSnapshotV1 } from './state/types'
|
|
4
4
|
|
|
5
5
|
import { logDebug } from './debug/input-log'
|
|
6
6
|
import { getProfileConfigDir } from './profile-paths'
|
|
@@ -15,6 +15,8 @@ export interface AimuxConfig {
|
|
|
15
15
|
themeId?: ThemeId
|
|
16
16
|
gitPanelVisible?: boolean
|
|
17
17
|
gitPanelRatio?: number
|
|
18
|
+
sessionBarVisible?: boolean
|
|
19
|
+
sessionBarPosition?: SessionBarPosition
|
|
18
20
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
19
21
|
skippedUpdateVersion?: string
|
|
20
22
|
}
|
|
@@ -58,6 +60,8 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
58
60
|
themeId?: unknown
|
|
59
61
|
gitPanelVisible?: unknown
|
|
60
62
|
gitPanelRatio?: unknown
|
|
63
|
+
sessionBarVisible?: unknown
|
|
64
|
+
sessionBarPosition?: unknown
|
|
61
65
|
workspaceSnapshot?: unknown
|
|
62
66
|
skippedUpdateVersion?: unknown
|
|
63
67
|
}
|
|
@@ -93,6 +97,20 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
93
97
|
issues.push('ignored invalid gitPanelRatio')
|
|
94
98
|
}
|
|
95
99
|
|
|
100
|
+
const validSessionBarVisible =
|
|
101
|
+
typeof parsed.sessionBarVisible === 'boolean' ? parsed.sessionBarVisible : undefined
|
|
102
|
+
if (parsed.sessionBarVisible !== undefined && validSessionBarVisible === undefined) {
|
|
103
|
+
issues.push('ignored invalid sessionBarVisible')
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
const validSessionBarPosition =
|
|
107
|
+
parsed.sessionBarPosition === 'top' || parsed.sessionBarPosition === 'bottom'
|
|
108
|
+
? parsed.sessionBarPosition
|
|
109
|
+
: undefined
|
|
110
|
+
if (parsed.sessionBarPosition !== undefined && validSessionBarPosition === undefined) {
|
|
111
|
+
issues.push('ignored invalid sessionBarPosition')
|
|
112
|
+
}
|
|
113
|
+
|
|
96
114
|
if (
|
|
97
115
|
parsed.workspaceSnapshot !== undefined &&
|
|
98
116
|
!isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
|
|
@@ -117,6 +135,8 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
117
135
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
118
136
|
gitPanelRatio: validGitPanelRatio,
|
|
119
137
|
gitPanelVisible: validGitPanelVisible,
|
|
138
|
+
sessionBarPosition: validSessionBarPosition,
|
|
139
|
+
sessionBarVisible: validSessionBarVisible,
|
|
120
140
|
skippedUpdateVersion: validSkippedUpdateVersion,
|
|
121
141
|
themeId: isThemeId(parsed.themeId) ? parsed.themeId : undefined,
|
|
122
142
|
version: 2,
|
|
@@ -67,8 +67,14 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
67
67
|
this.getOrCreateRegistry(sessionId).write(tabId, data)
|
|
68
68
|
}
|
|
69
69
|
|
|
70
|
-
resize(
|
|
71
|
-
|
|
70
|
+
resize(
|
|
71
|
+
sessionId: string,
|
|
72
|
+
cols: number,
|
|
73
|
+
rows: number,
|
|
74
|
+
intents?: Map<string, ScrollIntent>,
|
|
75
|
+
options?: { sync?: boolean }
|
|
76
|
+
): void {
|
|
77
|
+
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents, options)
|
|
72
78
|
}
|
|
73
79
|
|
|
74
80
|
resizeTab(
|
|
@@ -76,9 +82,10 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
76
82
|
tabId: string,
|
|
77
83
|
cols: number,
|
|
78
84
|
rows: number,
|
|
79
|
-
intent?: ScrollIntent
|
|
85
|
+
intent?: ScrollIntent,
|
|
86
|
+
options?: { sync?: boolean }
|
|
80
87
|
): void {
|
|
81
|
-
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, intent)
|
|
88
|
+
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, intent, options)
|
|
82
89
|
}
|
|
83
90
|
|
|
84
91
|
scroll(sessionId: string, tabId: string, deltaLines: number): void {
|
|
@@ -165,12 +165,23 @@ export class SessionRegistry extends EventEmitter<SessionRegistryEvents> {
|
|
|
165
165
|
this.ptyManager.write(tabId, data)
|
|
166
166
|
}
|
|
167
167
|
|
|
168
|
-
resizeAll(
|
|
169
|
-
|
|
168
|
+
resizeAll(
|
|
169
|
+
cols: number,
|
|
170
|
+
rows: number,
|
|
171
|
+
intents?: Map<string, ScrollIntent>,
|
|
172
|
+
options?: { sync?: boolean }
|
|
173
|
+
): void {
|
|
174
|
+
this.ptyManager.resizeAll(cols, rows, intents, options)
|
|
170
175
|
}
|
|
171
176
|
|
|
172
|
-
resizeTab(
|
|
173
|
-
|
|
177
|
+
resizeTab(
|
|
178
|
+
tabId: string,
|
|
179
|
+
cols: number,
|
|
180
|
+
rows: number,
|
|
181
|
+
intent?: ScrollIntent,
|
|
182
|
+
options?: { sync?: boolean }
|
|
183
|
+
): void {
|
|
184
|
+
this.ptyManager.resizeSession(tabId, cols, rows, intent, options)
|
|
174
185
|
}
|
|
175
186
|
|
|
176
187
|
scrollViewport(tabId: string, deltaLines: number): void {
|
package/src/index.tsx
CHANGED
|
@@ -9,6 +9,7 @@ import { getRuntimeProfile } from './daemon/runtime-paths'
|
|
|
9
9
|
import { logDebug } from './debug/input-log'
|
|
10
10
|
import { runDoctor } from './doctor'
|
|
11
11
|
import { runRestartDaemon } from './restart-daemon'
|
|
12
|
+
import { runRestartTerminalManager } from './restart-terminal-manager'
|
|
12
13
|
import { createSessionBackend } from './session-backend/bootstrap'
|
|
13
14
|
import { runTerminalManager } from './terminal-manager/terminal-manager'
|
|
14
15
|
import { runUpdate } from './update'
|
|
@@ -30,6 +31,10 @@ if (command === 'restart-daemon') {
|
|
|
30
31
|
process.exit(await runRestartDaemon())
|
|
31
32
|
}
|
|
32
33
|
|
|
34
|
+
if (command === 'restart-terminal-manager') {
|
|
35
|
+
process.exit(await runRestartTerminalManager())
|
|
36
|
+
}
|
|
37
|
+
|
|
33
38
|
if (command === 'update') {
|
|
34
39
|
process.exit(await runUpdate())
|
|
35
40
|
}
|
|
@@ -46,7 +51,7 @@ if (command === 'terminal-manager') {
|
|
|
46
51
|
|
|
47
52
|
if (command === '--help' || command === '-h') {
|
|
48
53
|
process.stdout.write(
|
|
49
|
-
'aimux -- terminal multiplexer for AI CLIs\n\nUsage:\n aimux
|
|
54
|
+
'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 aimux restart-terminal-manager Restart terminal-manager (kills live sessions)\n\n'
|
|
50
55
|
)
|
|
51
56
|
process.exit(0)
|
|
52
57
|
}
|
package/src/input/modes/types.ts
CHANGED
|
@@ -53,6 +53,7 @@ export type SideEffect =
|
|
|
53
53
|
| { type: 'git-commit'; title: string; body: string }
|
|
54
54
|
| { type: 'git-push' }
|
|
55
55
|
| { type: 'confirm-update-selection' }
|
|
56
|
+
| { type: 'switch-session-by-index'; index: number }
|
|
56
57
|
|
|
57
58
|
export interface KeyResult {
|
|
58
59
|
actions: AppAction[]
|