@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@brimveyn/aimux",
3
- "version": "1.4.0",
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.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",
@@ -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 + STATUS_BAR_HEIGHT + TERMINAL_PANE_VERTICAL_CHROME
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
- useEffect(() => {
112
- dispatch({
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
- type: 'set-terminal-size',
180
+ stableTabIds,
181
+ sync: true,
116
182
  })
117
- resizingRef.current = true
118
- if (resizingTimerRef.current) {
119
- clearTimeout(resizingTimerRef.current)
120
- }
121
- const trees = Object.values(state.layoutTrees)
122
- const hasSplits = trees.some((t) => t.type === 'split')
123
- if (hasSplits) {
124
- resizeSplitTabs(
125
- backend,
126
- state.layoutTrees,
127
- stableTabIds,
128
- terminalSize.cols,
129
- terminalSize.rows,
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
- resizingTimerRef.current = setTimeout(() => {
136
- resizingRef.current = false
137
- resizingTimerRef.current = null
138
- }, RESIZE_ACTIVITY_SETTLE_MS)
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(sessionId: string, cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
71
- this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents)
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(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
169
- this.ptyManager.resizeAll(cols, rows, intents)
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(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
173
- this.ptyManager.resizeSession(tabId, cols, rows, intent)
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 Start aimux\n aimux update Update to latest version\n aimux doctor Diagnose setup issues\n aimux restart-daemon Restart IPC daemon\n\n'
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
  }
@@ -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.emitRenderIfChanged(session)
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.emitRenderIfChanged(session)
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.emitRenderIfChanged(session)
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)?.pty.write(input)
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.emitRenderIfChanged(session)
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.emitRenderIfChanged(session)
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(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
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
- this.emitRenderIfChanged(session)
371
+ if (options?.sync) {
372
+ this.flushRenderNow(session)
373
+ } else {
374
+ this.scheduleRender(session)
375
+ }
274
376
  }
275
377
  }
276
378
 
277
- resizeSession(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
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
- this.emitRenderIfChanged(session)
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.emitRenderIfChanged(session)
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(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
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(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
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(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
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(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
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(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void
46
- resizeTab(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void
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
- import type { ReactNode } from 'react'
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
- function renderViewport(tab: TabSession): ReactNode {
81
- if (tab.viewport && tab.viewport.lines.length > 0) {
82
- const lines = tab.viewport.lines
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
- <text fg={theme.text}>
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
- {renderViewport(tab)}
207
+ <TerminalViewport viewport={tab.viewport} buffer={tab.buffer} />
203
208
  </box>
204
209
  )}
205
210
  </box>