@brimveyn/aimux 1.4.0 → 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/package.json +3 -2
- package/src/app-runtime/use-terminal-resize.ts +100 -30
- package/src/daemon/session-manager.ts +11 -4
- package/src/daemon/session-registry.ts +15 -4
- package/src/index.tsx +6 -1
- package/src/pty/pty-manager.ts +127 -11
- package/src/restart-terminal-manager.ts +44 -0
- package/src/session-backend/local-session-backend.ts +15 -4
- package/src/session-backend/remote-session-backend.ts +13 -2
- package/src/session-backend/types.ts +13 -2
- package/src/ui/components/terminal-pane.tsx +17 -12
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@brimveyn/aimux",
|
|
3
|
-
"version": "1.4.
|
|
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.3.
|
|
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",
|
|
@@ -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,
|
|
@@ -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/pty/pty-manager.ts
CHANGED
|
@@ -79,8 +79,98 @@ function getTerminalModes(emulator: XTerm, alternateScrollMode: boolean): Termin
|
|
|
79
79
|
}
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
function envInt(name: string, fallback: number): number {
|
|
83
|
+
const raw = process.env[name]
|
|
84
|
+
if (raw === undefined) return fallback
|
|
85
|
+
const parsed = Number(raw)
|
|
86
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : fallback
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const RENDER_COALESCE_MS = 16
|
|
90
|
+
const DATA_DEBOUNCE_MS = envInt('AIMUX_RENDER_DEBOUNCE_MS', 32)
|
|
91
|
+
const BURST_MAX_MS = envInt('AIMUX_RENDER_BURST_MS', 500)
|
|
92
|
+
|
|
82
93
|
export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
83
94
|
private sessions = new Map<string, SessionHandle>()
|
|
95
|
+
private pendingFlushes = new Map<string, ReturnType<typeof setTimeout>>()
|
|
96
|
+
private pendingBurstCaps = new Map<string, ReturnType<typeof setTimeout>>()
|
|
97
|
+
|
|
98
|
+
private clearTimers(tabId: string): void {
|
|
99
|
+
const flush = this.pendingFlushes.get(tabId)
|
|
100
|
+
if (flush) {
|
|
101
|
+
clearTimeout(flush)
|
|
102
|
+
this.pendingFlushes.delete(tabId)
|
|
103
|
+
}
|
|
104
|
+
const burst = this.pendingBurstCaps.get(tabId)
|
|
105
|
+
if (burst) {
|
|
106
|
+
clearTimeout(burst)
|
|
107
|
+
this.pendingBurstCaps.delete(tabId)
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
private scheduleRender(session: SessionHandle): void {
|
|
112
|
+
if (this.pendingFlushes.has(session.tabId)) {
|
|
113
|
+
return
|
|
114
|
+
}
|
|
115
|
+
const timer = setTimeout(() => {
|
|
116
|
+
this.pendingFlushes.delete(session.tabId)
|
|
117
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
118
|
+
return
|
|
119
|
+
}
|
|
120
|
+
const burst = this.pendingBurstCaps.get(session.tabId)
|
|
121
|
+
if (burst) {
|
|
122
|
+
clearTimeout(burst)
|
|
123
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
124
|
+
}
|
|
125
|
+
this.emitRenderIfChanged(session)
|
|
126
|
+
}, RENDER_COALESCE_MS)
|
|
127
|
+
this.pendingFlushes.set(session.tabId, timer)
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
private scheduleDataRender(session: SessionHandle): void {
|
|
131
|
+
const existingFlush = this.pendingFlushes.get(session.tabId)
|
|
132
|
+
if (existingFlush) {
|
|
133
|
+
clearTimeout(existingFlush)
|
|
134
|
+
}
|
|
135
|
+
const flushTimer = setTimeout(() => {
|
|
136
|
+
this.pendingFlushes.delete(session.tabId)
|
|
137
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
138
|
+
return
|
|
139
|
+
}
|
|
140
|
+
if (session.pendingWrites > 0) {
|
|
141
|
+
this.scheduleDataRender(session)
|
|
142
|
+
return
|
|
143
|
+
}
|
|
144
|
+
const burst = this.pendingBurstCaps.get(session.tabId)
|
|
145
|
+
if (burst) {
|
|
146
|
+
clearTimeout(burst)
|
|
147
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
148
|
+
}
|
|
149
|
+
this.emitRenderIfChanged(session)
|
|
150
|
+
}, DATA_DEBOUNCE_MS)
|
|
151
|
+
this.pendingFlushes.set(session.tabId, flushTimer)
|
|
152
|
+
|
|
153
|
+
if (!this.pendingBurstCaps.has(session.tabId)) {
|
|
154
|
+
const burstTimer = setTimeout(() => {
|
|
155
|
+
this.pendingBurstCaps.delete(session.tabId)
|
|
156
|
+
if (this.sessions.get(session.tabId) !== session) {
|
|
157
|
+
return
|
|
158
|
+
}
|
|
159
|
+
const flush = this.pendingFlushes.get(session.tabId)
|
|
160
|
+
if (flush) {
|
|
161
|
+
clearTimeout(flush)
|
|
162
|
+
this.pendingFlushes.delete(session.tabId)
|
|
163
|
+
}
|
|
164
|
+
this.emitRenderIfChanged(session)
|
|
165
|
+
}, BURST_MAX_MS)
|
|
166
|
+
this.pendingBurstCaps.set(session.tabId, burstTimer)
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
private flushRenderNow(session: SessionHandle): void {
|
|
171
|
+
this.clearTimers(session.tabId)
|
|
172
|
+
this.emitRenderIfChanged(session)
|
|
173
|
+
}
|
|
84
174
|
|
|
85
175
|
private emitRenderIfChanged(session: SessionHandle): void {
|
|
86
176
|
const nextSnapshot = snapshotTerminal(session.emulator, session.cursorVisible)
|
|
@@ -116,7 +206,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
116
206
|
}
|
|
117
207
|
|
|
118
208
|
this.sessions.delete(session.tabId)
|
|
119
|
-
this.
|
|
209
|
+
this.flushRenderNow(session)
|
|
120
210
|
session.emulator.dispose()
|
|
121
211
|
logDebug('ptyManager.finalize', { exitCode, tabId: session.tabId })
|
|
122
212
|
this.emit('exit', session.tabId, exitCode)
|
|
@@ -188,9 +278,10 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
188
278
|
session.cursorVisible = trackedModes.cursorVisible
|
|
189
279
|
session.pendingModeSequence = trackedModes.pendingSequence
|
|
190
280
|
session.pendingWrites += 1
|
|
281
|
+
this.scheduleDataRender(session)
|
|
191
282
|
emulator.write(data, () => {
|
|
192
283
|
session.pendingWrites -= 1
|
|
193
|
-
this.
|
|
284
|
+
this.scheduleDataRender(session)
|
|
194
285
|
|
|
195
286
|
if (session.pendingWrites === 0 && session.pendingExitCode !== null) {
|
|
196
287
|
this.finalizeSession(session, session.pendingExitCode)
|
|
@@ -215,7 +306,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
215
306
|
|
|
216
307
|
this.sessions.set(options.tabId, session)
|
|
217
308
|
logDebug('ptyManager.create.success', { tabId: options.tabId })
|
|
218
|
-
this.
|
|
309
|
+
this.scheduleRender(session)
|
|
219
310
|
} catch (error) {
|
|
220
311
|
const message = error instanceof Error ? error.message : String(error)
|
|
221
312
|
logDebug('ptyManager.create.error', { error: message, tabId: options.tabId })
|
|
@@ -224,7 +315,9 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
224
315
|
}
|
|
225
316
|
|
|
226
317
|
write(tabId: string, input: string): void {
|
|
227
|
-
this.sessions.get(tabId)
|
|
318
|
+
const session = this.sessions.get(tabId)
|
|
319
|
+
if (!session) return
|
|
320
|
+
session.pty.write(input)
|
|
228
321
|
}
|
|
229
322
|
|
|
230
323
|
scrollViewport(tabId: string, deltaLines: number): void {
|
|
@@ -234,7 +327,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
234
327
|
}
|
|
235
328
|
|
|
236
329
|
session.emulator.scrollLines(deltaLines)
|
|
237
|
-
this.
|
|
330
|
+
this.scheduleRender(session)
|
|
238
331
|
}
|
|
239
332
|
|
|
240
333
|
scrollViewportToBottom(tabId: string): void {
|
|
@@ -244,7 +337,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
244
337
|
}
|
|
245
338
|
|
|
246
339
|
session.emulator.scrollToBottom()
|
|
247
|
-
this.
|
|
340
|
+
this.scheduleRender(session)
|
|
248
341
|
}
|
|
249
342
|
|
|
250
343
|
private applyScrollIntent(session: SessionHandle, intent: ScrollIntent | undefined): void {
|
|
@@ -262,7 +355,12 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
262
355
|
session.emulator.scrollToLine(Math.max(0, intent.absoluteLine))
|
|
263
356
|
}
|
|
264
357
|
|
|
265
|
-
resizeAll(
|
|
358
|
+
resizeAll(
|
|
359
|
+
cols: number,
|
|
360
|
+
rows: number,
|
|
361
|
+
intents?: Map<string, ScrollIntent>,
|
|
362
|
+
options?: { sync?: boolean }
|
|
363
|
+
): void {
|
|
266
364
|
const safeCols = Math.max(20, cols)
|
|
267
365
|
const safeRows = Math.max(8, rows)
|
|
268
366
|
|
|
@@ -270,11 +368,21 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
270
368
|
session.pty.resize(safeCols, safeRows)
|
|
271
369
|
session.emulator.resize(safeCols, safeRows)
|
|
272
370
|
this.applyScrollIntent(session, intents?.get(session.tabId))
|
|
273
|
-
|
|
371
|
+
if (options?.sync) {
|
|
372
|
+
this.flushRenderNow(session)
|
|
373
|
+
} else {
|
|
374
|
+
this.scheduleRender(session)
|
|
375
|
+
}
|
|
274
376
|
}
|
|
275
377
|
}
|
|
276
378
|
|
|
277
|
-
resizeSession(
|
|
379
|
+
resizeSession(
|
|
380
|
+
tabId: string,
|
|
381
|
+
cols: number,
|
|
382
|
+
rows: number,
|
|
383
|
+
intent?: ScrollIntent,
|
|
384
|
+
options?: { sync?: boolean }
|
|
385
|
+
): void {
|
|
278
386
|
const session = this.sessions.get(tabId)
|
|
279
387
|
if (!session) {
|
|
280
388
|
return
|
|
@@ -284,7 +392,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
284
392
|
session.pty.resize(safeCols, safeRows)
|
|
285
393
|
session.emulator.resize(safeCols, safeRows)
|
|
286
394
|
this.applyScrollIntent(session, intent)
|
|
287
|
-
|
|
395
|
+
if (options?.sync) {
|
|
396
|
+
this.flushRenderNow(session)
|
|
397
|
+
} else {
|
|
398
|
+
this.scheduleRender(session)
|
|
399
|
+
}
|
|
288
400
|
}
|
|
289
401
|
|
|
290
402
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
@@ -293,7 +405,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
293
405
|
return
|
|
294
406
|
}
|
|
295
407
|
this.applyScrollIntent(session, intent)
|
|
296
|
-
this.
|
|
408
|
+
this.scheduleRender(session)
|
|
297
409
|
}
|
|
298
410
|
|
|
299
411
|
disposeSession(tabId: string): void {
|
|
@@ -302,12 +414,16 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
302
414
|
return
|
|
303
415
|
}
|
|
304
416
|
|
|
417
|
+
this.clearTimers(tabId)
|
|
305
418
|
this.sessions.delete(tabId)
|
|
306
419
|
session.pty.kill()
|
|
307
420
|
session.emulator.dispose()
|
|
308
421
|
}
|
|
309
422
|
|
|
310
423
|
disposeAll(): void {
|
|
424
|
+
for (const tabId of this.sessions.keys()) {
|
|
425
|
+
this.clearTimers(tabId)
|
|
426
|
+
}
|
|
311
427
|
for (const session of this.sessions.values()) {
|
|
312
428
|
session.pty.kill()
|
|
313
429
|
session.emulator.dispose()
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
import {
|
|
2
|
+
getTerminalManagerSocketPath,
|
|
3
|
+
removeTerminalManagerSocketIfExists,
|
|
4
|
+
} from './daemon/runtime-paths'
|
|
5
|
+
import {
|
|
6
|
+
findIpcDaemonPid,
|
|
7
|
+
findTerminalManagerPid,
|
|
8
|
+
killProcess,
|
|
9
|
+
spawnDetachedTerminalManager,
|
|
10
|
+
} from './platform/daemon-control'
|
|
11
|
+
|
|
12
|
+
export async function runRestartTerminalManager(): Promise<number> {
|
|
13
|
+
const socketPath = getTerminalManagerSocketPath()
|
|
14
|
+
|
|
15
|
+
const daemonPid = await findIpcDaemonPid()
|
|
16
|
+
if (daemonPid !== null) {
|
|
17
|
+
process.stdout.write(`Stopping IPC daemon (pid ${daemonPid})...\n`)
|
|
18
|
+
await killProcess(daemonPid)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const managerPid = await findTerminalManagerPid()
|
|
22
|
+
if (managerPid !== null) {
|
|
23
|
+
process.stdout.write(
|
|
24
|
+
`WARNING: killing terminal-manager (pid ${managerPid}) will kill live sessions.\n`
|
|
25
|
+
)
|
|
26
|
+
await killProcess(managerPid)
|
|
27
|
+
process.stdout.write('Terminal-manager stopped.\n')
|
|
28
|
+
} else {
|
|
29
|
+
process.stdout.write('No running terminal-manager found.\n')
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
removeTerminalManagerSocketIfExists()
|
|
33
|
+
|
|
34
|
+
process.stdout.write('Starting terminal-manager...\n')
|
|
35
|
+
const ok = await spawnDetachedTerminalManager()
|
|
36
|
+
|
|
37
|
+
if (ok) {
|
|
38
|
+
process.stdout.write(`Terminal-manager started on ${socketPath}.\n`)
|
|
39
|
+
return 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
process.stderr.write('Failed to start terminal-manager.\n')
|
|
43
|
+
return 1
|
|
44
|
+
}
|
|
@@ -151,18 +151,29 @@ export class LocalSessionBackend
|
|
|
151
151
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
152
152
|
}
|
|
153
153
|
|
|
154
|
-
resizeAll(
|
|
154
|
+
resizeAll(
|
|
155
|
+
cols: number,
|
|
156
|
+
rows: number,
|
|
157
|
+
intents?: Map<string, ScrollIntent>,
|
|
158
|
+
options?: { sync?: boolean }
|
|
159
|
+
): void {
|
|
155
160
|
if (!this.currentSessionId) {
|
|
156
161
|
return
|
|
157
162
|
}
|
|
158
|
-
this.sessionManager.resize(this.currentSessionId, cols, rows, intents)
|
|
163
|
+
this.sessionManager.resize(this.currentSessionId, cols, rows, intents, options)
|
|
159
164
|
}
|
|
160
165
|
|
|
161
|
-
resizeTab(
|
|
166
|
+
resizeTab(
|
|
167
|
+
tabId: string,
|
|
168
|
+
cols: number,
|
|
169
|
+
rows: number,
|
|
170
|
+
intent?: ScrollIntent,
|
|
171
|
+
options?: { sync?: boolean }
|
|
172
|
+
): void {
|
|
162
173
|
if (!this.currentSessionId) {
|
|
163
174
|
return
|
|
164
175
|
}
|
|
165
|
-
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent)
|
|
176
|
+
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent, options)
|
|
166
177
|
}
|
|
167
178
|
|
|
168
179
|
disposeSession(tabId: string): void {
|
|
@@ -398,7 +398,12 @@ export class RemoteSessionBackend
|
|
|
398
398
|
}).catch((error) => this.reportCommandError('setActiveTab', error))
|
|
399
399
|
}
|
|
400
400
|
|
|
401
|
-
resizeAll(
|
|
401
|
+
resizeAll(
|
|
402
|
+
cols: number,
|
|
403
|
+
rows: number,
|
|
404
|
+
intents?: Map<string, ScrollIntent>,
|
|
405
|
+
_options?: { sync?: boolean }
|
|
406
|
+
): void {
|
|
402
407
|
if (!this.attached) {
|
|
403
408
|
logDebug('backend.remote.skipResizeBeforeAttach', { cols, rows })
|
|
404
409
|
return
|
|
@@ -412,7 +417,13 @@ export class RemoteSessionBackend
|
|
|
412
417
|
}).catch((error) => this.reportCommandError('resizeClient', error))
|
|
413
418
|
}
|
|
414
419
|
|
|
415
|
-
resizeTab(
|
|
420
|
+
resizeTab(
|
|
421
|
+
tabId: string,
|
|
422
|
+
cols: number,
|
|
423
|
+
rows: number,
|
|
424
|
+
intent?: ScrollIntent,
|
|
425
|
+
_options?: { sync?: boolean }
|
|
426
|
+
): void {
|
|
416
427
|
if (!this.attached) {
|
|
417
428
|
return
|
|
418
429
|
}
|
|
@@ -42,8 +42,19 @@ export interface SessionBackend extends EventEmitter<SessionBackendEvents> {
|
|
|
42
42
|
scrollViewportToBottom(tabId: string): void
|
|
43
43
|
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void
|
|
44
44
|
setActiveTab(tabId: string | null): void
|
|
45
|
-
resizeAll(
|
|
46
|
-
|
|
45
|
+
resizeAll(
|
|
46
|
+
cols: number,
|
|
47
|
+
rows: number,
|
|
48
|
+
intents?: Map<string, ScrollIntent>,
|
|
49
|
+
options?: { sync?: boolean }
|
|
50
|
+
): void
|
|
51
|
+
resizeTab(
|
|
52
|
+
tabId: string,
|
|
53
|
+
cols: number,
|
|
54
|
+
rows: number,
|
|
55
|
+
intent?: ScrollIntent,
|
|
56
|
+
options?: { sync?: boolean }
|
|
57
|
+
): void
|
|
47
58
|
disposeSession(tabId: string): void
|
|
48
59
|
disposeAll(): void
|
|
49
60
|
destroy(keepSessions?: boolean): Promise<void> | void
|
|
@@ -1,8 +1,9 @@
|
|
|
1
1
|
import type { MouseEvent as OtuiMouseEvent } from '@opentui/core'
|
|
2
|
-
|
|
2
|
+
|
|
3
|
+
import { memo, type ReactNode } from 'react'
|
|
3
4
|
|
|
4
5
|
import type { TerminalContentOrigin } from '../../input/raw-input-handler'
|
|
5
|
-
import type { TabSession, TerminalSpan } from '../../state/types'
|
|
6
|
+
import type { TabSession, TerminalSnapshot, TerminalSpan } from '../../state/types'
|
|
6
7
|
|
|
7
8
|
import { logInputDebug } from '../../debug/input-log'
|
|
8
9
|
import { theme } from '../theme'
|
|
@@ -77,9 +78,17 @@ function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
|
77
78
|
)
|
|
78
79
|
}
|
|
79
80
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
81
|
+
interface TerminalViewportProps {
|
|
82
|
+
viewport: TerminalSnapshot | undefined
|
|
83
|
+
buffer: string
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const TerminalViewport = memo(function TerminalViewport({
|
|
87
|
+
buffer,
|
|
88
|
+
viewport,
|
|
89
|
+
}: TerminalViewportProps) {
|
|
90
|
+
if (viewport && viewport.lines.length > 0) {
|
|
91
|
+
const lines = viewport.lines
|
|
83
92
|
return (
|
|
84
93
|
<text fg={theme.text}>
|
|
85
94
|
{lines.map((line, lineIndex) => (
|
|
@@ -92,12 +101,8 @@ function renderViewport(tab: TabSession): ReactNode {
|
|
|
92
101
|
)
|
|
93
102
|
}
|
|
94
103
|
|
|
95
|
-
return
|
|
96
|
-
|
|
97
|
-
{tab.buffer.length > 0 ? tab.buffer : 'Waiting for session output...'}
|
|
98
|
-
</text>
|
|
99
|
-
)
|
|
100
|
-
}
|
|
104
|
+
return <text fg={theme.text}>{buffer.length > 0 ? buffer : 'Waiting for session output...'}</text>
|
|
105
|
+
})
|
|
101
106
|
|
|
102
107
|
export function TerminalPane({
|
|
103
108
|
contentOrigin,
|
|
@@ -199,7 +204,7 @@ export function TerminalPane({
|
|
|
199
204
|
onMouseDrag={forwardMouseEvent}
|
|
200
205
|
onMouseScroll={forwardScrollEvent}
|
|
201
206
|
>
|
|
202
|
-
{
|
|
207
|
+
<TerminalViewport viewport={tab.viewport} buffer={tab.buffer} />
|
|
203
208
|
</box>
|
|
204
209
|
)}
|
|
205
210
|
</box>
|