@brimveyn/aimux 1.9.9 → 1.10.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 +8 -1
- package/src/app-runtime/click-selection-resolver.ts +90 -56
- package/src/app-runtime/side-effects.ts +7 -1
- package/src/app-runtime/use-backend-runtime.ts +12 -1
- package/src/app-runtime/use-mouse-handlers.ts +299 -6
- package/src/app.tsx +56 -1
- package/src/config.ts +10 -1
- package/src/daemon/daemon.ts +26 -0
- package/src/daemon/session-manager.ts +13 -0
- package/src/daemon/session-registry.ts +8 -0
- package/src/index.tsx +14 -6
- package/src/input/modes/types.ts +1 -0
- package/src/integrations/claude-syntax-overlay.ts +460 -0
- package/src/integrations/claude-theme-sync.ts +118 -0
- package/src/ipc/manager-protocol.ts +14 -1
- package/src/pty/pty-manager.ts +37 -1
- package/src/terminal-manager/manager-client.ts +25 -0
- package/src/terminal-manager/terminal-manager.ts +54 -0
- package/src/ui/components/layout/split-layout.tsx +10 -0
- package/src/ui/components/layout/terminal-pane.tsx +19 -5
- package/src/ui/components/modals/themes/theme-picker-modal.tsx +3 -1
- package/src/ui/root.tsx +13 -1
- package/src/ui/theme-store.ts +18 -0
- package/src/ui/theme.ts +2 -0
- package/src/update.ts +21 -1
|
@@ -12,6 +12,7 @@ import { getTerminalManagerSocketPath } from '../daemon/runtime-paths'
|
|
|
12
12
|
import { logDebug } from '../debug/input-log'
|
|
13
13
|
import {
|
|
14
14
|
encodeManagerMessage,
|
|
15
|
+
MANAGER_PROTOCOL_BROADCAST_GATE_VERSION,
|
|
15
16
|
MANAGER_PROTOCOL_MIN_VERSION,
|
|
16
17
|
MANAGER_PROTOCOL_VERSION,
|
|
17
18
|
type ManagerAttachResult,
|
|
@@ -378,6 +379,30 @@ export class TerminalManagerClient extends EventEmitter<ManagerClientEvents> {
|
|
|
378
379
|
})
|
|
379
380
|
}
|
|
380
381
|
|
|
382
|
+
/**
|
|
383
|
+
* Tell the TM whether to bother snapshotting and broadcasting renders.
|
|
384
|
+
* No-ops on TMs that negotiated a protocol version without the feature
|
|
385
|
+
* (older builds): the worst case is the daemon keeps receiving renders it
|
|
386
|
+
* doesn't strictly need, which matches the pre-fix behaviour.
|
|
387
|
+
*/
|
|
388
|
+
async setBroadcastEnabled(enabled: boolean): Promise<void> {
|
|
389
|
+
if (
|
|
390
|
+
this.selectedProtocolVersion === null ||
|
|
391
|
+
this.selectedProtocolVersion < MANAGER_PROTOCOL_BROADCAST_GATE_VERSION
|
|
392
|
+
) {
|
|
393
|
+
logDebug('managerClient.setBroadcastEnabled.skipped', {
|
|
394
|
+
enabled,
|
|
395
|
+
selectedVersion: this.selectedProtocolVersion,
|
|
396
|
+
})
|
|
397
|
+
return
|
|
398
|
+
}
|
|
399
|
+
await this.sendExpectOk({
|
|
400
|
+
id: crypto.randomUUID(),
|
|
401
|
+
payload: { enabled },
|
|
402
|
+
type: 'setBroadcastEnabled',
|
|
403
|
+
})
|
|
404
|
+
}
|
|
405
|
+
|
|
381
406
|
destroy(): void {
|
|
382
407
|
this.resetConnection('Terminal manager client destroyed')
|
|
383
408
|
}
|
|
@@ -52,6 +52,49 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
52
52
|
const sockets = new Set<Socket>()
|
|
53
53
|
const negotiatedVersions = new Map<Socket, number>()
|
|
54
54
|
|
|
55
|
+
/**
|
|
56
|
+
* Auto-exit when fully idle: no clients connected AND no live PTY sessions.
|
|
57
|
+
* Prevents the "zombie TM" pattern where a stale terminal-manager from an
|
|
58
|
+
* older install keeps consuming CPU after the user has closed everything.
|
|
59
|
+
* The grace window allows brief reconnects (e.g. daemon restart during
|
|
60
|
+
* `aimux update`) without killing the process.
|
|
61
|
+
*
|
|
62
|
+
* Set AIMUX_TM_IDLE_EXIT_MS=0 to disable.
|
|
63
|
+
*/
|
|
64
|
+
const idleExitMs = (() => {
|
|
65
|
+
const raw = process.env.AIMUX_TM_IDLE_EXIT_MS
|
|
66
|
+
if (raw === undefined) return 60_000
|
|
67
|
+
const parsed = Number(raw)
|
|
68
|
+
return Number.isFinite(parsed) && parsed >= 0 ? parsed : 60_000
|
|
69
|
+
})()
|
|
70
|
+
let idleExitTimer: ReturnType<typeof setTimeout> | null = null
|
|
71
|
+
const scheduleIdleExitIfApplicable = (): void => {
|
|
72
|
+
if (idleExitMs === 0) return
|
|
73
|
+
if (idleExitTimer !== null) return
|
|
74
|
+
if (sockets.size > 0) return
|
|
75
|
+
if (sessionManager.hasAnySessions()) return
|
|
76
|
+
logDebug('terminalManager.idleExit.schedule', { idleExitMs })
|
|
77
|
+
idleExitTimer = setTimeout(() => {
|
|
78
|
+
idleExitTimer = null
|
|
79
|
+
if (sockets.size > 0 || sessionManager.hasAnySessions()) {
|
|
80
|
+
logDebug('terminalManager.idleExit.cancelled')
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
logDebug('terminalManager.idleExit.fire')
|
|
84
|
+
sessionManager.disposeAll()
|
|
85
|
+
// Graceful: drop the listening socket so the file is unlinked, matching
|
|
86
|
+
// SIGTERM/SIGINT shutdown. Bail-out timer guards against close() hanging
|
|
87
|
+
// on a bad socket.
|
|
88
|
+
server.close(() => process.exit(0))
|
|
89
|
+
setTimeout(() => process.exit(0), 1_000).unref?.()
|
|
90
|
+
}, idleExitMs)
|
|
91
|
+
idleExitTimer.unref?.()
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// A session can exit naturally (PTY child finishes) while no client is
|
|
95
|
+
// attached — re-evaluate idle state in that case too.
|
|
96
|
+
sessionManager.on('exit', () => scheduleIdleExitIfApplicable())
|
|
97
|
+
|
|
55
98
|
sessionManager.on('render', (sessionId, tabId, viewport, terminalModes) => {
|
|
56
99
|
const event: ManagerEvent = {
|
|
57
100
|
payload: { sessionId, tabId, terminalModes, viewport },
|
|
@@ -233,6 +276,11 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
233
276
|
case 'ping':
|
|
234
277
|
sendOk(socket, message.id)
|
|
235
278
|
break
|
|
279
|
+
case 'setBroadcastEnabled':
|
|
280
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
281
|
+
sessionManager.setBroadcastEnabled(message.payload.enabled)
|
|
282
|
+
sendOk(socket, message.id)
|
|
283
|
+
break
|
|
236
284
|
}
|
|
237
285
|
} catch (error) {
|
|
238
286
|
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
@@ -256,11 +304,13 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
256
304
|
logDebug('terminalManager.client.close')
|
|
257
305
|
sockets.delete(socket)
|
|
258
306
|
negotiatedVersions.delete(socket)
|
|
307
|
+
scheduleIdleExitIfApplicable()
|
|
259
308
|
})
|
|
260
309
|
socket.on('error', () => {
|
|
261
310
|
logDebug('terminalManager.client.error')
|
|
262
311
|
sockets.delete(socket)
|
|
263
312
|
negotiatedVersions.delete(socket)
|
|
313
|
+
scheduleIdleExitIfApplicable()
|
|
264
314
|
})
|
|
265
315
|
})
|
|
266
316
|
|
|
@@ -270,8 +320,12 @@ export async function runTerminalManager(): Promise<void> {
|
|
|
270
320
|
})
|
|
271
321
|
tightenSocketPermissions(socketPath)
|
|
272
322
|
|
|
323
|
+
// First idle eval: if no client connects within idleExitMs of startup, exit.
|
|
324
|
+
scheduleIdleExitIfApplicable()
|
|
325
|
+
|
|
273
326
|
const gracefulShutdown = (signal: string) => {
|
|
274
327
|
logDebug(`terminalManager.${signal}`)
|
|
328
|
+
if (idleExitTimer) clearTimeout(idleExitTimer)
|
|
275
329
|
sessionManager.disposeAll()
|
|
276
330
|
server.close()
|
|
277
331
|
process.exit(0)
|
|
@@ -26,6 +26,8 @@ interface SplitLayoutProps {
|
|
|
26
26
|
onTerminalMouseEvent: (event: OtuiMouseEvent, origin: TerminalContentOrigin) => void
|
|
27
27
|
onTerminalScrollEvent: (event: OtuiMouseEvent) => void
|
|
28
28
|
onTerminalClick?: (event: OtuiMouseEvent, origin: TerminalContentOrigin, tabId?: string) => void
|
|
29
|
+
onTerminalDrag?: (event: OtuiMouseEvent, origin: TerminalContentOrigin, tabId?: string) => boolean
|
|
30
|
+
onTerminalMouseUp?: (event: OtuiMouseEvent) => boolean
|
|
29
31
|
onPaneActivate?: (tabId: string) => void
|
|
30
32
|
onSplitResize?: (tabId: string, ratio: number, axis: SplitDirection) => void
|
|
31
33
|
onSeparatorDragStart?: (info: {
|
|
@@ -54,7 +56,9 @@ export function SplitLayout({
|
|
|
54
56
|
onSeparatorDragStart,
|
|
55
57
|
onSplitResize,
|
|
56
58
|
onTerminalClick,
|
|
59
|
+
onTerminalDrag,
|
|
57
60
|
onTerminalMouseEvent,
|
|
61
|
+
onTerminalMouseUp,
|
|
58
62
|
onTerminalScrollEvent,
|
|
59
63
|
tabs,
|
|
60
64
|
}: SplitLayoutProps) {
|
|
@@ -92,6 +96,8 @@ export function SplitLayout({
|
|
|
92
96
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
93
97
|
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
94
98
|
onTerminalClick={onTerminalClick}
|
|
99
|
+
onTerminalDrag={onTerminalDrag}
|
|
100
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
95
101
|
onPaneActivate={onPaneActivate}
|
|
96
102
|
onSeparatorDrag={onSeparatorDrag}
|
|
97
103
|
onSeparatorDragEnd={onSeparatorDragEnd}
|
|
@@ -123,6 +129,8 @@ export function SplitLayout({
|
|
|
123
129
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
124
130
|
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
125
131
|
onTerminalClick={onTerminalClick}
|
|
132
|
+
onTerminalDrag={onTerminalDrag}
|
|
133
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
126
134
|
onPaneActivate={onPaneActivate}
|
|
127
135
|
onSplitResize={onSplitResize}
|
|
128
136
|
onSeparatorDragStart={onSeparatorDragStart}
|
|
@@ -163,6 +171,8 @@ export function SplitLayout({
|
|
|
163
171
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
164
172
|
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
165
173
|
onTerminalClick={onTerminalClick}
|
|
174
|
+
onTerminalDrag={onTerminalDrag}
|
|
175
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
166
176
|
onPaneActivate={onPaneActivate}
|
|
167
177
|
onSplitResize={onSplitResize}
|
|
168
178
|
onSeparatorDragStart={onSeparatorDragStart}
|
|
@@ -22,6 +22,8 @@ interface TerminalPaneProps {
|
|
|
22
22
|
onTerminalMouseEvent: (event: OtuiMouseEvent, origin: TerminalContentOrigin) => void
|
|
23
23
|
onTerminalScrollEvent: (event: OtuiMouseEvent) => void
|
|
24
24
|
onTerminalClick?: (event: OtuiMouseEvent, origin: TerminalContentOrigin, tabId?: string) => void
|
|
25
|
+
onTerminalDrag?: (event: OtuiMouseEvent, origin: TerminalContentOrigin, tabId?: string) => boolean
|
|
26
|
+
onTerminalMouseUp?: (event: OtuiMouseEvent) => boolean
|
|
25
27
|
onPaneActivate?: (tabId: string) => void
|
|
26
28
|
onSeparatorDrag?: (event: OtuiMouseEvent) => boolean
|
|
27
29
|
onSeparatorDragEnd?: () => void
|
|
@@ -47,9 +49,10 @@ function getTitle(
|
|
|
47
49
|
return `${tab.title} · ${tab.status}`
|
|
48
50
|
}
|
|
49
51
|
|
|
50
|
-
function getBorderColor(isActive: boolean,
|
|
52
|
+
function getBorderColor(isActive: boolean, focusMode: TerminalPaneProps['focusMode']): string {
|
|
51
53
|
const t = getCurrentTheme()
|
|
52
|
-
|
|
54
|
+
if (!isActive) return t.border
|
|
55
|
+
return focusMode === 'terminal-input' ? t.accent : t.primary
|
|
53
56
|
}
|
|
54
57
|
|
|
55
58
|
function renderSpan(span: TerminalSpan, key: string): ReactNode {
|
|
@@ -111,7 +114,9 @@ export function TerminalPane({
|
|
|
111
114
|
onSeparatorDrag,
|
|
112
115
|
onSeparatorDragEnd,
|
|
113
116
|
onTerminalClick,
|
|
117
|
+
onTerminalDrag,
|
|
114
118
|
onTerminalMouseEvent,
|
|
119
|
+
onTerminalMouseUp,
|
|
115
120
|
onTerminalScrollEvent,
|
|
116
121
|
tab,
|
|
117
122
|
tabId,
|
|
@@ -168,11 +173,20 @@ export function TerminalPane({
|
|
|
168
173
|
y: event.y,
|
|
169
174
|
})
|
|
170
175
|
}
|
|
171
|
-
if (event.type === 'drag'
|
|
172
|
-
event
|
|
173
|
-
|
|
176
|
+
if (event.type === 'drag') {
|
|
177
|
+
if (onTerminalDrag?.(event, contentOrigin, tabId)) {
|
|
178
|
+
event.preventDefault()
|
|
179
|
+
return
|
|
180
|
+
}
|
|
181
|
+
if (onSeparatorDrag?.(event)) {
|
|
182
|
+
event.preventDefault()
|
|
183
|
+
return
|
|
184
|
+
}
|
|
174
185
|
}
|
|
175
186
|
if (event.type === 'up') {
|
|
187
|
+
if (onTerminalMouseUp?.(event)) {
|
|
188
|
+
event.preventDefault()
|
|
189
|
+
}
|
|
176
190
|
onSeparatorDragEnd?.()
|
|
177
191
|
}
|
|
178
192
|
if (tabId && onPaneActivate && event.type === 'down') {
|
|
@@ -4,7 +4,7 @@ import type { ThemeId } from '../../../themes'
|
|
|
4
4
|
|
|
5
5
|
import { dispatchGlobal, runSideEffectGlobal } from '../../../../state/dispatch-ref'
|
|
6
6
|
import { filterThemeIds, themeDisplayName } from '../../../filter-themes'
|
|
7
|
-
import { useTheme, useTransparent } from '../../../theme'
|
|
7
|
+
import { useMode, useTheme, useTransparent } from '../../../theme'
|
|
8
8
|
import { uiTokens } from '../../../ui-tokens'
|
|
9
9
|
import { Picker, type PickerItem } from '../shared/picker'
|
|
10
10
|
|
|
@@ -28,6 +28,7 @@ export function ThemePickerModal({
|
|
|
28
28
|
}: ThemePickerModalProps) {
|
|
29
29
|
const t = useTheme()
|
|
30
30
|
const transparent = useTransparent()
|
|
31
|
+
const mode = useMode()
|
|
31
32
|
const filtered = useMemo(() => filterThemeIds(filter), [filter])
|
|
32
33
|
|
|
33
34
|
useLayoutEffect(() => {
|
|
@@ -63,6 +64,7 @@ export function ThemePickerModal({
|
|
|
63
64
|
{filtered.length === 0 ? '' : ` ${effectiveIndex + 1} / ${filtered.length}`}
|
|
64
65
|
</text>
|
|
65
66
|
<text fg={t.textMuted}>{` transparent: ${transparent ? 'on' : 'off'} (ctrl-t)`}</text>
|
|
67
|
+
<text fg={t.textMuted}>{` mode: ${mode} (ctrl-l)`}</text>
|
|
66
68
|
</box>
|
|
67
69
|
}
|
|
68
70
|
items={items}
|
package/src/ui/root.tsx
CHANGED
|
@@ -206,6 +206,8 @@ interface RootViewProps {
|
|
|
206
206
|
onTerminalMouseEvent: (event: MouseEvent, origin: TerminalContentOrigin) => void
|
|
207
207
|
onTerminalScrollEvent: (event: MouseEvent) => void
|
|
208
208
|
onTerminalClick?: (event: MouseEvent, origin: TerminalContentOrigin, tabId?: string) => void
|
|
209
|
+
onTerminalDrag?: (event: MouseEvent, origin: TerminalContentOrigin, tabId?: string) => boolean
|
|
210
|
+
onTerminalMouseUp?: (event: MouseEvent) => boolean
|
|
209
211
|
onPaneActivate?: (tabId: string) => void
|
|
210
212
|
onSplitResize?: (tabId: string, ratio: number, axis: SplitDirection) => void
|
|
211
213
|
onSidebarResizeStart?: (info: { initialWidth: number; screenStart: number }) => void
|
|
@@ -244,7 +246,9 @@ export function RootView({
|
|
|
244
246
|
onSidebarResizeStart,
|
|
245
247
|
onSplitResize,
|
|
246
248
|
onTerminalClick,
|
|
249
|
+
onTerminalDrag,
|
|
247
250
|
onTerminalMouseEvent,
|
|
251
|
+
onTerminalMouseUp,
|
|
248
252
|
onTerminalScrollEvent,
|
|
249
253
|
terminalCols,
|
|
250
254
|
terminalRows,
|
|
@@ -312,7 +316,11 @@ export function RootView({
|
|
|
312
316
|
event.stopPropagation()
|
|
313
317
|
}
|
|
314
318
|
}}
|
|
315
|
-
onMouseUp={() => {
|
|
319
|
+
onMouseUp={(event) => {
|
|
320
|
+
// Catch releases that land outside any TerminalPane (sidebar, gap,
|
|
321
|
+
// status bar, …) so an in-flight multi-click drag — and its
|
|
322
|
+
// auto-scroll interval — is always finalised.
|
|
323
|
+
onTerminalMouseUp?.(event)
|
|
316
324
|
onSeparatorDragEnd?.()
|
|
317
325
|
}}
|
|
318
326
|
>
|
|
@@ -349,6 +357,8 @@ export function RootView({
|
|
|
349
357
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
350
358
|
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
351
359
|
onTerminalClick={onTerminalClick}
|
|
360
|
+
onTerminalDrag={onTerminalDrag}
|
|
361
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
352
362
|
onPaneActivate={onPaneActivate}
|
|
353
363
|
onSplitResize={onSplitResize}
|
|
354
364
|
onSeparatorDragStart={onSeparatorDragStart}
|
|
@@ -373,6 +383,8 @@ export function RootView({
|
|
|
373
383
|
onTerminalMouseEvent={onTerminalMouseEvent}
|
|
374
384
|
onTerminalScrollEvent={onTerminalScrollEvent}
|
|
375
385
|
onTerminalClick={onTerminalClick}
|
|
386
|
+
onTerminalDrag={onTerminalDrag}
|
|
387
|
+
onTerminalMouseUp={onTerminalMouseUp}
|
|
376
388
|
onPaneActivate={onPaneActivate}
|
|
377
389
|
/>
|
|
378
390
|
)}
|
package/src/ui/theme-store.ts
CHANGED
|
@@ -73,6 +73,10 @@ export function useTransparent(): boolean {
|
|
|
73
73
|
return useStore(themeStore, (s) => s.transparent)
|
|
74
74
|
}
|
|
75
75
|
|
|
76
|
+
export function useMode(): ThemeMode {
|
|
77
|
+
return useStore(themeStore, (s) => s.mode)
|
|
78
|
+
}
|
|
79
|
+
|
|
76
80
|
export function getTransparent(): boolean {
|
|
77
81
|
return themeStore.getState().transparent
|
|
78
82
|
}
|
|
@@ -81,3 +85,17 @@ export function setTransparent(value: boolean): void {
|
|
|
81
85
|
if (themeStore.getState().transparent === value) return
|
|
82
86
|
themeStore.setState({ transparent: value })
|
|
83
87
|
}
|
|
88
|
+
|
|
89
|
+
/** Subscribe to theme id/mode changes for non-React side-effects. */
|
|
90
|
+
export function subscribeThemeChanges(
|
|
91
|
+
listener: (resolved: ResolvedTuiTheme, mode: ThemeMode) => void
|
|
92
|
+
): () => void {
|
|
93
|
+
let lastId: ThemeId | null = null
|
|
94
|
+
let lastMode: ThemeMode | null = null
|
|
95
|
+
return themeStore.subscribe((s) => {
|
|
96
|
+
if (s.id === lastId && s.mode === lastMode) return
|
|
97
|
+
lastId = s.id
|
|
98
|
+
lastMode = s.mode
|
|
99
|
+
listener(derive(s.id, s.mode), s.mode)
|
|
100
|
+
})
|
|
101
|
+
}
|
package/src/ui/theme.ts
CHANGED
package/src/update.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { getDaemonSocketPath } from './daemon/runtime-paths'
|
|
2
|
-
import { findIpcDaemonPid } from './platform/daemon-control'
|
|
2
|
+
import { findIpcDaemonPid, findTerminalManagerPid } from './platform/daemon-control'
|
|
3
3
|
import { runRestartDaemon } from './restart-daemon'
|
|
4
4
|
|
|
5
5
|
const REPO = 'BrimVeyn/aimux'
|
|
@@ -59,5 +59,25 @@ export async function runUpdate(): Promise<number> {
|
|
|
59
59
|
await runRestartDaemon()
|
|
60
60
|
}
|
|
61
61
|
|
|
62
|
+
// The terminal-manager is intentionally NOT restarted here: doing so kills
|
|
63
|
+
// live AI sessions. The flip side: the running TM keeps executing the
|
|
64
|
+
// previous version's code path, so any TM-side fix (perf, lifecycle) only
|
|
65
|
+
// takes effect after a manual restart. Surface that explicitly so users
|
|
66
|
+
// aren't silently stuck on stale behaviour.
|
|
67
|
+
const tmPid = await findTerminalManagerPid()
|
|
68
|
+
if (tmPid !== null) {
|
|
69
|
+
process.stdout.write(
|
|
70
|
+
[
|
|
71
|
+
'',
|
|
72
|
+
`Note: terminal-manager (pid ${tmPid}) is still running the previous version.`,
|
|
73
|
+
'TM-side fixes in this update apply to new TMs only — the running one',
|
|
74
|
+
'keeps its old behaviour until restarted.',
|
|
75
|
+
'Run `aimux restart-terminal-manager` when you can afford to lose your',
|
|
76
|
+
'current PTY sessions to upgrade it.',
|
|
77
|
+
'',
|
|
78
|
+
].join('\n')
|
|
79
|
+
)
|
|
80
|
+
}
|
|
81
|
+
|
|
62
82
|
return 0
|
|
63
83
|
}
|