@brimveyn/aimux 1.2.5 → 1.3.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +29 -8
- package/package.json +5 -5
- package/src/app-runtime/backend-runtime-events.ts +16 -1
- package/src/app-runtime/pty-write.ts +7 -4
- package/src/app-runtime/side-effects.ts +62 -4
- package/src/app-runtime/snippet-actions.ts +3 -2
- package/src/app-runtime/use-backend-runtime.ts +7 -2
- package/src/app-runtime/use-renderer-bindings.ts +2 -2
- package/src/app-runtime/use-terminal-resize.ts +15 -6
- package/src/app.tsx +89 -30
- package/src/config/loader.ts +2 -2
- package/src/config.ts +14 -3
- package/src/daemon/daemon.ts +279 -118
- package/src/daemon/runtime-paths.ts +34 -2
- package/src/daemon/session-manager.ts +20 -5
- package/src/daemon/session-registry.ts +18 -11
- package/src/index.tsx +19 -4
- package/src/input/keymap/describe-bindings.ts +68 -0
- package/src/input/keymap/key-format.ts +67 -0
- package/src/input/modes/bridge.ts +1 -0
- package/src/input/modes/transitions.ts +2 -0
- package/src/input/modes/types.ts +2 -0
- package/src/ipc/manager-protocol.ts +396 -0
- package/src/ipc/protocol.ts +98 -5
- package/src/platform/daemon-control.ts +42 -11
- package/src/profile-paths.ts +27 -0
- package/src/pty/pty-manager.ts +53 -3
- package/src/restart-daemon.ts +10 -10
- package/src/session-backend/bootstrap.ts +197 -46
- package/src/session-backend/local-session-backend.ts +12 -5
- package/src/session-backend/remote-session-backend.ts +143 -63
- package/src/session-backend/types.ts +4 -2
- package/src/state/reducers/modal-state.ts +18 -1
- package/src/state/reducers/tab-state.ts +20 -2
- package/src/state/session-catalog.ts +5 -4
- package/src/state/session-persistence.ts +9 -2
- package/src/state/snippet-catalog.ts +4 -4
- package/src/state/types.ts +23 -0
- package/src/state/validation.ts +8 -0
- package/src/terminal-manager/manager-client.ts +384 -0
- package/src/terminal-manager/terminal-manager.ts +288 -0
- package/src/ui/components/create-session-modal.tsx +3 -5
- package/src/ui/components/git-commit-modal.tsx +3 -5
- package/src/ui/components/help-modal.tsx +49 -68
- package/src/ui/components/list-item.tsx +24 -5
- package/src/ui/components/new-tab-modal.tsx +8 -9
- package/src/ui/components/pending-chord-overlay.tsx +28 -0
- package/src/ui/components/session-name-modal.tsx +3 -5
- package/src/ui/components/session-picker-modal.tsx +3 -1
- package/src/ui/components/snippet-editor-modal.tsx +3 -1
- package/src/ui/components/snippet-picker-modal.tsx +3 -1
- package/src/ui/components/status-bar.tsx +6 -2
- package/src/ui/components/theme-picker-modal.tsx +3 -6
- package/src/ui/components/update-available-modal.tsx +42 -0
- package/src/ui/keymap-context.ts +39 -0
- package/src/ui/root.tsx +12 -0
- package/src/ui/status-bar-model.ts +67 -39
- package/src/update/version-check.ts +67 -0
- package/src/update.ts +3 -3
package/src/pty/pty-manager.ts
CHANGED
|
@@ -2,8 +2,9 @@ import { Terminal as XTerm } from '@xterm/headless'
|
|
|
2
2
|
import { type IPty, spawn } from 'bun-pty'
|
|
3
3
|
import { EventEmitter } from 'node:events'
|
|
4
4
|
|
|
5
|
-
import type { TerminalModeState, TerminalSnapshot } from '../state/types'
|
|
5
|
+
import type { ScrollIntent, TerminalModeState, TerminalSnapshot } from '../state/types'
|
|
6
6
|
|
|
7
|
+
import { logDebug } from '../debug/input-log'
|
|
7
8
|
import { areTerminalSnapshotsEqual, snapshotTerminal } from './terminal-snapshot'
|
|
8
9
|
|
|
9
10
|
type PtyManagerEvents = {
|
|
@@ -99,6 +100,12 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
99
100
|
|
|
100
101
|
session.lastSnapshot = nextSnapshot
|
|
101
102
|
session.lastTerminalModes = nextTerminalModes
|
|
103
|
+
logDebug('ptyManager.render', {
|
|
104
|
+
isAlternateBuffer: nextTerminalModes.isAlternateBuffer,
|
|
105
|
+
lines: nextSnapshot.lines.length,
|
|
106
|
+
tabId: session.tabId,
|
|
107
|
+
viewportY: nextSnapshot.viewportY,
|
|
108
|
+
})
|
|
102
109
|
this.emit('render', session.tabId, nextSnapshot, nextTerminalModes)
|
|
103
110
|
}
|
|
104
111
|
|
|
@@ -111,6 +118,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
111
118
|
this.sessions.delete(session.tabId)
|
|
112
119
|
this.emitRenderIfChanged(session)
|
|
113
120
|
session.emulator.dispose()
|
|
121
|
+
logDebug('ptyManager.finalize', { exitCode, tabId: session.tabId })
|
|
114
122
|
this.emit('exit', session.tabId, exitCode)
|
|
115
123
|
}
|
|
116
124
|
|
|
@@ -123,6 +131,14 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
123
131
|
cwd?: string
|
|
124
132
|
}): void {
|
|
125
133
|
this.disposeSession(options.tabId)
|
|
134
|
+
logDebug('ptyManager.create.start', {
|
|
135
|
+
args: options.args ?? [],
|
|
136
|
+
cols: options.cols,
|
|
137
|
+
command: options.command,
|
|
138
|
+
cwd: options.cwd ?? process.cwd(),
|
|
139
|
+
rows: options.rows,
|
|
140
|
+
tabId: options.tabId,
|
|
141
|
+
})
|
|
126
142
|
|
|
127
143
|
try {
|
|
128
144
|
const emulator = new XTerm({
|
|
@@ -157,6 +173,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
157
173
|
}
|
|
158
174
|
|
|
159
175
|
pty.onData((data) => {
|
|
176
|
+
logDebug('ptyManager.data', {
|
|
177
|
+
byteLength: Buffer.byteLength(data, 'utf8'),
|
|
178
|
+
pendingWrites: session.pendingWrites,
|
|
179
|
+
tabId: options.tabId,
|
|
180
|
+
})
|
|
160
181
|
const trackedModes = trackPrivateModes(
|
|
161
182
|
session.alternateScrollMode,
|
|
162
183
|
session.cursorVisible,
|
|
@@ -178,6 +199,7 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
178
199
|
})
|
|
179
200
|
|
|
180
201
|
pty.onExit(({ exitCode }) => {
|
|
202
|
+
logDebug('ptyManager.exit', { exitCode, tabId: options.tabId })
|
|
181
203
|
const current = this.sessions.get(options.tabId)
|
|
182
204
|
if (!current || current.pty !== pty) {
|
|
183
205
|
return
|
|
@@ -192,9 +214,11 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
192
214
|
})
|
|
193
215
|
|
|
194
216
|
this.sessions.set(options.tabId, session)
|
|
217
|
+
logDebug('ptyManager.create.success', { tabId: options.tabId })
|
|
195
218
|
this.emitRenderIfChanged(session)
|
|
196
219
|
} catch (error) {
|
|
197
220
|
const message = error instanceof Error ? error.message : String(error)
|
|
221
|
+
logDebug('ptyManager.create.error', { error: message, tabId: options.tabId })
|
|
198
222
|
this.emit('error', options.tabId, `Failed to start session: ${message}`)
|
|
199
223
|
}
|
|
200
224
|
}
|
|
@@ -223,18 +247,34 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
223
247
|
this.emitRenderIfChanged(session)
|
|
224
248
|
}
|
|
225
249
|
|
|
226
|
-
|
|
250
|
+
private applyScrollIntent(session: SessionHandle, intent: ScrollIntent | undefined): void {
|
|
251
|
+
if (!intent || intent.kind === 'bottom') {
|
|
252
|
+
session.emulator.scrollToBottom()
|
|
253
|
+
return
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
const baseY = session.emulator.buffer.active.baseY
|
|
257
|
+
if (intent.absoluteLine >= baseY) {
|
|
258
|
+
session.emulator.scrollToBottom()
|
|
259
|
+
return
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
session.emulator.scrollToLine(Math.max(0, intent.absoluteLine))
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
resizeAll(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
|
|
227
266
|
const safeCols = Math.max(20, cols)
|
|
228
267
|
const safeRows = Math.max(8, rows)
|
|
229
268
|
|
|
230
269
|
for (const session of this.sessions.values()) {
|
|
231
270
|
session.pty.resize(safeCols, safeRows)
|
|
232
271
|
session.emulator.resize(safeCols, safeRows)
|
|
272
|
+
this.applyScrollIntent(session, intents?.get(session.tabId))
|
|
233
273
|
this.emitRenderIfChanged(session)
|
|
234
274
|
}
|
|
235
275
|
}
|
|
236
276
|
|
|
237
|
-
resizeSession(tabId: string, cols: number, rows: number): void {
|
|
277
|
+
resizeSession(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
|
|
238
278
|
const session = this.sessions.get(tabId)
|
|
239
279
|
if (!session) {
|
|
240
280
|
return
|
|
@@ -243,6 +283,16 @@ export class PtyManager extends EventEmitter<PtyManagerEvents> {
|
|
|
243
283
|
const safeRows = Math.max(8, rows)
|
|
244
284
|
session.pty.resize(safeCols, safeRows)
|
|
245
285
|
session.emulator.resize(safeCols, safeRows)
|
|
286
|
+
this.applyScrollIntent(session, intent)
|
|
287
|
+
this.emitRenderIfChanged(session)
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
291
|
+
const session = this.sessions.get(tabId)
|
|
292
|
+
if (!session) {
|
|
293
|
+
return
|
|
294
|
+
}
|
|
295
|
+
this.applyScrollIntent(session, intent)
|
|
246
296
|
this.emitRenderIfChanged(session)
|
|
247
297
|
}
|
|
248
298
|
|
package/src/restart-daemon.ts
CHANGED
|
@@ -1,28 +1,28 @@
|
|
|
1
1
|
import { getDaemonSocketPath, removeDaemonSocketIfExists } from './daemon/runtime-paths'
|
|
2
|
-
import {
|
|
2
|
+
import { findIpcDaemonPid, killProcess, spawnDetachedIpcDaemon } from './platform/daemon-control'
|
|
3
3
|
|
|
4
4
|
export async function runRestartDaemon(): Promise<number> {
|
|
5
5
|
const socketPath = getDaemonSocketPath()
|
|
6
|
-
const pid = await
|
|
6
|
+
const pid = await findIpcDaemonPid()
|
|
7
7
|
|
|
8
8
|
if (pid !== null) {
|
|
9
|
-
process.stdout.write(`Stopping daemon (pid ${pid})...\n`)
|
|
10
|
-
await
|
|
11
|
-
process.stdout.write('
|
|
9
|
+
process.stdout.write(`Stopping IPC daemon (pid ${pid})...\n`)
|
|
10
|
+
await killProcess(pid)
|
|
11
|
+
process.stdout.write('IPC daemon stopped.\n')
|
|
12
12
|
} else {
|
|
13
|
-
process.stdout.write('No running daemon found.\n')
|
|
13
|
+
process.stdout.write('No running IPC daemon found.\n')
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
removeDaemonSocketIfExists()
|
|
17
17
|
|
|
18
|
-
process.stdout.write('Starting daemon...\n')
|
|
19
|
-
const ok = await
|
|
18
|
+
process.stdout.write('Starting IPC daemon...\n')
|
|
19
|
+
const ok = await spawnDetachedIpcDaemon()
|
|
20
20
|
|
|
21
21
|
if (ok) {
|
|
22
|
-
process.stdout.write(
|
|
22
|
+
process.stdout.write(`IPC daemon started on ${socketPath}.\n`)
|
|
23
23
|
return 0
|
|
24
24
|
}
|
|
25
25
|
|
|
26
|
-
process.stderr.write('Failed to start daemon.\n')
|
|
26
|
+
process.stderr.write('Failed to start IPC daemon.\n')
|
|
27
27
|
return 1
|
|
28
28
|
}
|
|
@@ -3,32 +3,45 @@ import { connect } from 'node:net'
|
|
|
3
3
|
import type { SessionBackend } from './types'
|
|
4
4
|
|
|
5
5
|
import {
|
|
6
|
-
|
|
7
|
-
|
|
6
|
+
getIpcDaemonSocketPath,
|
|
7
|
+
getSocketSecurityIssue,
|
|
8
8
|
removeDaemonSocketIfExists,
|
|
9
9
|
} from '../daemon/runtime-paths'
|
|
10
10
|
import { logDebug } from '../debug/input-log'
|
|
11
|
-
import {
|
|
12
|
-
|
|
11
|
+
import {
|
|
12
|
+
encodeMessage,
|
|
13
|
+
IPC_PROTOCOL_MIN_VERSION,
|
|
14
|
+
IPC_PROTOCOL_VERSION,
|
|
15
|
+
MessageDecoder,
|
|
16
|
+
parseServerMessage,
|
|
17
|
+
} from '../ipc/protocol'
|
|
18
|
+
import { findIpcDaemonPid, killProcess, spawnDetachedIpcDaemon } from '../platform/daemon-control'
|
|
13
19
|
import { LocalSessionBackend } from './local-session-backend'
|
|
14
20
|
import { RemoteSessionBackend } from './remote-session-backend'
|
|
15
21
|
|
|
22
|
+
interface DaemonHandshakeProbeResult {
|
|
23
|
+
compatible: boolean
|
|
24
|
+
error?: string
|
|
25
|
+
processVersion?: string
|
|
26
|
+
selectedVersion?: number
|
|
27
|
+
}
|
|
28
|
+
|
|
16
29
|
async function spawnDaemon(): Promise<void> {
|
|
17
30
|
logDebug('backend.spawnDaemon.start', {
|
|
18
31
|
execPath: process.execPath,
|
|
19
|
-
socketPath:
|
|
32
|
+
socketPath: getIpcDaemonSocketPath(),
|
|
20
33
|
})
|
|
21
|
-
const ok = await
|
|
34
|
+
const ok = await spawnDetachedIpcDaemon()
|
|
22
35
|
if (ok) {
|
|
23
|
-
logDebug('backend.spawnDaemon.ready', { socketPath:
|
|
36
|
+
logDebug('backend.spawnDaemon.ready', { socketPath: getIpcDaemonSocketPath() })
|
|
24
37
|
return
|
|
25
38
|
}
|
|
26
39
|
|
|
27
|
-
|
|
40
|
+
throw new Error(`IPC daemon unavailable at ${getIpcDaemonSocketPath()}`)
|
|
28
41
|
}
|
|
29
42
|
|
|
30
43
|
async function canConnectToDaemon(socketPath: string): Promise<boolean> {
|
|
31
|
-
const securityIssue =
|
|
44
|
+
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
32
45
|
if (securityIssue) {
|
|
33
46
|
logDebug('backend.healthcheck.socketIssue', { issue: securityIssue, socketPath })
|
|
34
47
|
return false
|
|
@@ -54,54 +67,192 @@ async function canConnectToDaemon(socketPath: string): Promise<boolean> {
|
|
|
54
67
|
})
|
|
55
68
|
}
|
|
56
69
|
|
|
70
|
+
export async function probeDaemonProtocolCompatibility(
|
|
71
|
+
socketPath: string
|
|
72
|
+
): Promise<DaemonHandshakeProbeResult> {
|
|
73
|
+
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
74
|
+
if (securityIssue) {
|
|
75
|
+
return { compatible: false, error: securityIssue }
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
return await new Promise<DaemonHandshakeProbeResult>((resolve) => {
|
|
79
|
+
const socket = connect(socketPath)
|
|
80
|
+
const decoder = new MessageDecoder(parseServerMessage)
|
|
81
|
+
const helloRequestId = crypto.randomUUID()
|
|
82
|
+
const attachRequestId = crypto.randomUUID()
|
|
83
|
+
const disposeRequestId = crypto.randomUUID()
|
|
84
|
+
const probeSessionId = `probe-${crypto.randomUUID()}`
|
|
85
|
+
let daemonProcessVersion: string | undefined
|
|
86
|
+
let settled = false
|
|
87
|
+
const timer = setTimeout(() => {
|
|
88
|
+
finish({ compatible: false, error: 'handshake timed out' })
|
|
89
|
+
}, 2_000)
|
|
90
|
+
|
|
91
|
+
const finish = (result: DaemonHandshakeProbeResult) => {
|
|
92
|
+
if (settled) {
|
|
93
|
+
return
|
|
94
|
+
}
|
|
95
|
+
settled = true
|
|
96
|
+
clearTimeout(timer)
|
|
97
|
+
socket.removeAllListeners()
|
|
98
|
+
socket.destroy()
|
|
99
|
+
resolve(result)
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
socket.once('connect', () => {
|
|
103
|
+
socket.write(
|
|
104
|
+
encodeMessage({
|
|
105
|
+
id: helloRequestId,
|
|
106
|
+
payload: {
|
|
107
|
+
maxVersion: IPC_PROTOCOL_VERSION,
|
|
108
|
+
minVersion: IPC_PROTOCOL_MIN_VERSION,
|
|
109
|
+
},
|
|
110
|
+
type: 'hello',
|
|
111
|
+
})
|
|
112
|
+
)
|
|
113
|
+
})
|
|
114
|
+
socket.once('error', (error: NodeJS.ErrnoException) => {
|
|
115
|
+
finish({ compatible: false, error: error.message })
|
|
116
|
+
})
|
|
117
|
+
socket.on('data', (chunk) => {
|
|
118
|
+
try {
|
|
119
|
+
for (const message of decoder.push(chunk)) {
|
|
120
|
+
if (!('id' in message)) {
|
|
121
|
+
continue
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
if (message.id === helloRequestId) {
|
|
125
|
+
if (message.type !== 'helloResult') {
|
|
126
|
+
finish({
|
|
127
|
+
compatible: false,
|
|
128
|
+
error:
|
|
129
|
+
message.type === 'error' ? message.payload.message : `unexpected ${message.type}`,
|
|
130
|
+
})
|
|
131
|
+
return
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
daemonProcessVersion = message.payload.processVersion
|
|
135
|
+
|
|
136
|
+
socket.write(
|
|
137
|
+
encodeMessage({
|
|
138
|
+
id: attachRequestId,
|
|
139
|
+
payload: {
|
|
140
|
+
cols: 80,
|
|
141
|
+
protocolVersion: message.payload.selectedVersion,
|
|
142
|
+
rows: 24,
|
|
143
|
+
sessionId: probeSessionId,
|
|
144
|
+
},
|
|
145
|
+
type: 'attach',
|
|
146
|
+
})
|
|
147
|
+
)
|
|
148
|
+
return
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (message.id === attachRequestId) {
|
|
152
|
+
if (message.type !== 'attachResult') {
|
|
153
|
+
finish({
|
|
154
|
+
compatible: false,
|
|
155
|
+
error:
|
|
156
|
+
message.type === 'error' ? message.payload.message : `unexpected ${message.type}`,
|
|
157
|
+
})
|
|
158
|
+
return
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const compatible =
|
|
162
|
+
message.payload.protocolVersion >= IPC_PROTOCOL_MIN_VERSION &&
|
|
163
|
+
message.payload.protocolVersion <= IPC_PROTOCOL_VERSION
|
|
164
|
+
|
|
165
|
+
socket.write(
|
|
166
|
+
encodeMessage({
|
|
167
|
+
id: disposeRequestId,
|
|
168
|
+
payload: {},
|
|
169
|
+
type: 'disposeAll',
|
|
170
|
+
})
|
|
171
|
+
)
|
|
172
|
+
finish({
|
|
173
|
+
compatible,
|
|
174
|
+
error: compatible
|
|
175
|
+
? undefined
|
|
176
|
+
: `attach returned protocol v${message.payload.protocolVersion}`,
|
|
177
|
+
processVersion: daemonProcessVersion,
|
|
178
|
+
selectedVersion: message.payload.protocolVersion,
|
|
179
|
+
})
|
|
180
|
+
return
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
if (message.id === disposeRequestId) {
|
|
184
|
+
continue
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} catch (error) {
|
|
188
|
+
finish({
|
|
189
|
+
compatible: false,
|
|
190
|
+
error: error instanceof Error ? error.message : String(error),
|
|
191
|
+
})
|
|
192
|
+
}
|
|
193
|
+
})
|
|
194
|
+
})
|
|
195
|
+
}
|
|
196
|
+
|
|
57
197
|
async function restartDaemon(socketPath: string): Promise<void> {
|
|
58
|
-
const pid = await
|
|
198
|
+
const pid = await findIpcDaemonPid()
|
|
59
199
|
if (pid !== null) {
|
|
60
|
-
logDebug('backend.restartDaemon.killing', { pid })
|
|
61
|
-
await
|
|
200
|
+
logDebug('backend.restartDaemon.killing', { pid, socketPath })
|
|
201
|
+
await killProcess(pid)
|
|
62
202
|
}
|
|
63
203
|
removeDaemonSocketIfExists()
|
|
64
204
|
await spawnDaemon()
|
|
65
205
|
}
|
|
66
206
|
|
|
67
207
|
export async function createSessionBackend(): Promise<SessionBackend> {
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
if (!initialReachable) {
|
|
74
|
-
removeDaemonSocketIfExists()
|
|
75
|
-
await spawnDaemon()
|
|
76
|
-
}
|
|
208
|
+
if (process.env.AIMUX_LOCAL_BACKEND === '1') {
|
|
209
|
+
logDebug('backend.create.localExplicit')
|
|
210
|
+
return new LocalSessionBackend()
|
|
211
|
+
}
|
|
77
212
|
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
}
|
|
213
|
+
const socketPath = getIpcDaemonSocketPath()
|
|
214
|
+
const initialReachable = await canConnectToDaemon(socketPath)
|
|
215
|
+
logDebug('backend.create.start', { initialReachable, socketPath })
|
|
82
216
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
await restartDaemon(getDaemonSocketPath())
|
|
93
|
-
logDebug('backend.create.remoteAfterRestart')
|
|
94
|
-
return new RemoteSessionBackend()
|
|
95
|
-
} catch (retryError) {
|
|
96
|
-
logDebug('backend.create.retryFailed', {
|
|
97
|
-
error: retryError instanceof Error ? retryError.message : String(retryError),
|
|
98
|
-
})
|
|
99
|
-
}
|
|
100
|
-
}
|
|
217
|
+
if (!initialReachable) {
|
|
218
|
+
removeDaemonSocketIfExists()
|
|
219
|
+
await spawnDaemon()
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
const reachable = await canConnectToDaemon(socketPath)
|
|
223
|
+
if (!reachable) {
|
|
224
|
+
throw new Error(`IPC daemon unavailable at ${socketPath}`)
|
|
225
|
+
}
|
|
101
226
|
|
|
102
|
-
|
|
103
|
-
|
|
227
|
+
const handshake = await probeDaemonProtocolCompatibility(socketPath)
|
|
228
|
+
logDebug('backend.create.handshake', {
|
|
229
|
+
compatible: handshake.compatible,
|
|
230
|
+
error: handshake.error ?? null,
|
|
231
|
+
processVersion: handshake.processVersion ?? null,
|
|
232
|
+
selectedVersion: handshake.selectedVersion ?? null,
|
|
233
|
+
socketPath,
|
|
234
|
+
})
|
|
235
|
+
if (!handshake.compatible) {
|
|
236
|
+
logDebug('backend.create.restartForHandshake', {
|
|
237
|
+
error: handshake.error ?? 'incompatible daemon handshake',
|
|
238
|
+
socketPath,
|
|
104
239
|
})
|
|
105
|
-
|
|
240
|
+
await restartDaemon(socketPath)
|
|
241
|
+
const retriedHandshake = await probeDaemonProtocolCompatibility(socketPath)
|
|
242
|
+
logDebug('backend.create.handshakeAfterRestart', {
|
|
243
|
+
compatible: retriedHandshake.compatible,
|
|
244
|
+
error: retriedHandshake.error ?? null,
|
|
245
|
+
processVersion: retriedHandshake.processVersion ?? null,
|
|
246
|
+
selectedVersion: retriedHandshake.selectedVersion ?? null,
|
|
247
|
+
socketPath,
|
|
248
|
+
})
|
|
249
|
+
if (!retriedHandshake.compatible) {
|
|
250
|
+
throw new Error(
|
|
251
|
+
`IPC daemon handshake failed after restart: ${retriedHandshake.error ?? 'incompatible protocol'}`
|
|
252
|
+
)
|
|
253
|
+
}
|
|
106
254
|
}
|
|
255
|
+
|
|
256
|
+
logDebug('backend.create.remote', { socketPath })
|
|
257
|
+
return new RemoteSessionBackend()
|
|
107
258
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
-
import type { AssistantId, WorkspaceSnapshotV1 } from '../state/types'
|
|
3
|
+
import type { AssistantId, ScrollIntent, WorkspaceSnapshotV1 } from '../state/types'
|
|
4
4
|
import type { SessionBackend, SessionBackendEvents } from './types'
|
|
5
5
|
|
|
6
6
|
import { SessionManager } from '../daemon/session-manager'
|
|
@@ -114,6 +114,13 @@ export class LocalSessionBackend
|
|
|
114
114
|
this.sessionManager.scrollToBottom(this.currentSessionId, tabId)
|
|
115
115
|
}
|
|
116
116
|
|
|
117
|
+
reapplyScrollIntent(tabId: string, intent: ScrollIntent): void {
|
|
118
|
+
if (!this.currentSessionId) {
|
|
119
|
+
return
|
|
120
|
+
}
|
|
121
|
+
this.sessionManager.reapplyScrollIntent(this.currentSessionId, tabId, intent)
|
|
122
|
+
}
|
|
123
|
+
|
|
117
124
|
setActiveTab(tabId: string | null): void {
|
|
118
125
|
if (!this.currentSessionId) {
|
|
119
126
|
return
|
|
@@ -122,18 +129,18 @@ export class LocalSessionBackend
|
|
|
122
129
|
this.sessionManager.setActiveTab(this.currentSessionId, tabId)
|
|
123
130
|
}
|
|
124
131
|
|
|
125
|
-
resizeAll(cols: number, rows: number): void {
|
|
132
|
+
resizeAll(cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
|
|
126
133
|
if (!this.currentSessionId) {
|
|
127
134
|
return
|
|
128
135
|
}
|
|
129
|
-
this.sessionManager.resize(this.currentSessionId, cols, rows)
|
|
136
|
+
this.sessionManager.resize(this.currentSessionId, cols, rows, intents)
|
|
130
137
|
}
|
|
131
138
|
|
|
132
|
-
resizeTab(tabId: string, cols: number, rows: number): void {
|
|
139
|
+
resizeTab(tabId: string, cols: number, rows: number, intent?: ScrollIntent): void {
|
|
133
140
|
if (!this.currentSessionId) {
|
|
134
141
|
return
|
|
135
142
|
}
|
|
136
|
-
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows)
|
|
143
|
+
this.sessionManager.resizeTab(this.currentSessionId, tabId, cols, rows, intent)
|
|
137
144
|
}
|
|
138
145
|
|
|
139
146
|
disposeSession(tabId: string): void {
|