@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
|
@@ -0,0 +1,396 @@
|
|
|
1
|
+
import type {
|
|
2
|
+
ScrollIntent,
|
|
3
|
+
TabSession,
|
|
4
|
+
TerminalModeState,
|
|
5
|
+
TerminalSnapshot,
|
|
6
|
+
WorkspaceSnapshotV1,
|
|
7
|
+
} from '../state/types'
|
|
8
|
+
|
|
9
|
+
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
10
|
+
import {
|
|
11
|
+
getProcessVersion,
|
|
12
|
+
IpcProtocolError,
|
|
13
|
+
MessageDecoder,
|
|
14
|
+
negotiateProtocolVersion,
|
|
15
|
+
} from './protocol'
|
|
16
|
+
|
|
17
|
+
export const MANAGER_PROTOCOL_MIN_VERSION = 1
|
|
18
|
+
export const MANAGER_PROTOCOL_VERSION = 1
|
|
19
|
+
|
|
20
|
+
export interface ManagerHelloRequest {
|
|
21
|
+
minVersion: number
|
|
22
|
+
maxVersion: number
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface ManagerHelloResult {
|
|
26
|
+
minVersion: number
|
|
27
|
+
maxVersion: number
|
|
28
|
+
processVersion: string
|
|
29
|
+
selectedVersion: number
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ManagerAttachRequest {
|
|
33
|
+
protocolVersion: number
|
|
34
|
+
sessionId: string
|
|
35
|
+
cols: number
|
|
36
|
+
rows: number
|
|
37
|
+
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface ManagerAttachResult {
|
|
41
|
+
protocolVersion: number
|
|
42
|
+
tabs: TabSession[]
|
|
43
|
+
activeTabId: string | null
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export type ManagerRequest =
|
|
47
|
+
| { id: string; type: 'hello'; payload: ManagerHelloRequest }
|
|
48
|
+
| { id: string; type: 'attachSession'; payload: ManagerAttachRequest }
|
|
49
|
+
| {
|
|
50
|
+
id: string
|
|
51
|
+
type: 'createTab'
|
|
52
|
+
payload: {
|
|
53
|
+
sessionId: string
|
|
54
|
+
tabId: string
|
|
55
|
+
assistant: TabSession['assistant']
|
|
56
|
+
title: string
|
|
57
|
+
command: string
|
|
58
|
+
args?: string[]
|
|
59
|
+
cols: number
|
|
60
|
+
rows: number
|
|
61
|
+
cwd?: string
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
| { id: string; type: 'write'; payload: { sessionId: string; tabId: string; data: string } }
|
|
65
|
+
| {
|
|
66
|
+
id: string
|
|
67
|
+
type: 'resizeClient'
|
|
68
|
+
payload: {
|
|
69
|
+
sessionId: string
|
|
70
|
+
cols: number
|
|
71
|
+
rows: number
|
|
72
|
+
intents?: Record<string, ScrollIntent>
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
| {
|
|
76
|
+
id: string
|
|
77
|
+
type: 'resizeTab'
|
|
78
|
+
payload: {
|
|
79
|
+
sessionId: string
|
|
80
|
+
tabId: string
|
|
81
|
+
cols: number
|
|
82
|
+
rows: number
|
|
83
|
+
intent?: ScrollIntent
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
| {
|
|
87
|
+
id: string
|
|
88
|
+
type: 'scrollToBottom'
|
|
89
|
+
payload: { sessionId: string; tabId: string }
|
|
90
|
+
}
|
|
91
|
+
| {
|
|
92
|
+
id: string
|
|
93
|
+
type: 'scroll'
|
|
94
|
+
payload: { sessionId: string; tabId: string; deltaLines: number }
|
|
95
|
+
}
|
|
96
|
+
| {
|
|
97
|
+
id: string
|
|
98
|
+
type: 'reapplyScrollIntent'
|
|
99
|
+
payload: { sessionId: string; tabId: string; intent: ScrollIntent }
|
|
100
|
+
}
|
|
101
|
+
| { id: string; type: 'setActiveTab'; payload: { sessionId: string; tabId: string | null } }
|
|
102
|
+
| { id: string; type: 'closeTab'; payload: { sessionId: string; tabId: string } }
|
|
103
|
+
| { id: string; type: 'disposeSession'; payload: { sessionId: string } }
|
|
104
|
+
| { id: string; type: 'ping'; payload: Record<string, never> }
|
|
105
|
+
|
|
106
|
+
export type ManagerResponse =
|
|
107
|
+
| { id: string; type: 'helloResult'; payload: ManagerHelloResult }
|
|
108
|
+
| { id: string; type: 'ok'; payload: Record<string, never> }
|
|
109
|
+
| { id: string; type: 'attachResult'; payload: ManagerAttachResult }
|
|
110
|
+
| { id: string; type: 'error'; payload: { message: string } }
|
|
111
|
+
|
|
112
|
+
export type ManagerEvent =
|
|
113
|
+
| {
|
|
114
|
+
type: 'tabRender'
|
|
115
|
+
payload: {
|
|
116
|
+
sessionId: string
|
|
117
|
+
tabId: string
|
|
118
|
+
viewport: TerminalSnapshot
|
|
119
|
+
terminalModes: TerminalModeState
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
| { type: 'tabExit'; payload: { sessionId: string; tabId: string; exitCode: number } }
|
|
123
|
+
| { type: 'tabError'; payload: { sessionId: string; tabId: string; message: string } }
|
|
124
|
+
|
|
125
|
+
export type ManagerMessage = ManagerRequest | ManagerResponse | ManagerEvent
|
|
126
|
+
|
|
127
|
+
function isObjectRecord(value: unknown): value is Record<string, unknown> {
|
|
128
|
+
return typeof value === 'object' && value !== null
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function isString(value: unknown): value is string {
|
|
132
|
+
return typeof value === 'string'
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function isNullableString(value: unknown): value is string | null {
|
|
136
|
+
return value === null || isString(value)
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
function isFiniteNumber(value: unknown): value is number {
|
|
140
|
+
return typeof value === 'number' && Number.isFinite(value)
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
function isStringArray(value: unknown): value is string[] {
|
|
144
|
+
return Array.isArray(value) && value.every(isString)
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
function isTerminalSpan(value: unknown): boolean {
|
|
148
|
+
return (
|
|
149
|
+
isObjectRecord(value) &&
|
|
150
|
+
isString(value.text) &&
|
|
151
|
+
(value.fg === undefined || isString(value.fg)) &&
|
|
152
|
+
(value.bg === undefined || isString(value.bg)) &&
|
|
153
|
+
(value.bold === undefined || typeof value.bold === 'boolean') &&
|
|
154
|
+
(value.italic === undefined || typeof value.italic === 'boolean') &&
|
|
155
|
+
(value.underline === undefined || typeof value.underline === 'boolean') &&
|
|
156
|
+
(value.cursor === undefined || typeof value.cursor === 'boolean')
|
|
157
|
+
)
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
161
|
+
return (
|
|
162
|
+
isObjectRecord(value) &&
|
|
163
|
+
Array.isArray(value.lines) &&
|
|
164
|
+
value.lines.every(
|
|
165
|
+
(line) =>
|
|
166
|
+
isObjectRecord(line) && Array.isArray(line.spans) && line.spans.every(isTerminalSpan)
|
|
167
|
+
) &&
|
|
168
|
+
isFiniteNumber(value.viewportY) &&
|
|
169
|
+
isFiniteNumber(value.baseY) &&
|
|
170
|
+
typeof value.cursorVisible === 'boolean'
|
|
171
|
+
)
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
175
|
+
if (!isObjectRecord(value)) return false
|
|
176
|
+
if (value.kind === 'bottom') return true
|
|
177
|
+
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
178
|
+
return false
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
182
|
+
if (!isObjectRecord(value)) return false
|
|
183
|
+
return Object.values(value).every(isScrollIntent)
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
187
|
+
return (
|
|
188
|
+
isObjectRecord(value) &&
|
|
189
|
+
(value.mouseTrackingMode === 'none' ||
|
|
190
|
+
value.mouseTrackingMode === 'x10' ||
|
|
191
|
+
value.mouseTrackingMode === 'vt200' ||
|
|
192
|
+
value.mouseTrackingMode === 'drag' ||
|
|
193
|
+
value.mouseTrackingMode === 'any') &&
|
|
194
|
+
typeof value.sendFocusMode === 'boolean' &&
|
|
195
|
+
typeof value.alternateScrollMode === 'boolean' &&
|
|
196
|
+
typeof value.isAlternateBuffer === 'boolean' &&
|
|
197
|
+
typeof value.bracketedPasteMode === 'boolean'
|
|
198
|
+
)
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
function isAttachResult(value: unknown): value is ManagerAttachResult {
|
|
202
|
+
return (
|
|
203
|
+
isObjectRecord(value) &&
|
|
204
|
+
isFiniteNumber(value.protocolVersion) &&
|
|
205
|
+
Array.isArray(value.tabs) &&
|
|
206
|
+
value.tabs.every((tab) => isObjectRecord(tab) && isString(tab.id)) &&
|
|
207
|
+
isNullableString(value.activeTabId)
|
|
208
|
+
)
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
function isHelloResult(value: unknown): value is ManagerHelloResult {
|
|
212
|
+
return (
|
|
213
|
+
isObjectRecord(value) &&
|
|
214
|
+
isFiniteNumber(value.minVersion) &&
|
|
215
|
+
isFiniteNumber(value.maxVersion) &&
|
|
216
|
+
isFiniteNumber(value.selectedVersion) &&
|
|
217
|
+
isString(value.processVersion)
|
|
218
|
+
)
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
function assert(condition: boolean, message: string): asserts condition {
|
|
222
|
+
if (!condition) {
|
|
223
|
+
throw new IpcProtocolError(message)
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
export function selectManagerProtocolVersion(payload: ManagerHelloRequest): number | null {
|
|
228
|
+
return negotiateProtocolVersion(
|
|
229
|
+
payload.minVersion,
|
|
230
|
+
payload.maxVersion,
|
|
231
|
+
MANAGER_PROTOCOL_MIN_VERSION,
|
|
232
|
+
MANAGER_PROTOCOL_VERSION
|
|
233
|
+
)
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
export function createManagerHelloResult(selectedVersion: number): ManagerHelloResult {
|
|
237
|
+
return {
|
|
238
|
+
maxVersion: MANAGER_PROTOCOL_VERSION,
|
|
239
|
+
minVersion: MANAGER_PROTOCOL_MIN_VERSION,
|
|
240
|
+
processVersion: getProcessVersion(),
|
|
241
|
+
selectedVersion,
|
|
242
|
+
}
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export function parseManagerRequest(value: unknown): ManagerRequest {
|
|
246
|
+
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
247
|
+
assert(isString(value.id), 'IPC request id must be a string')
|
|
248
|
+
assert(isString(value.type), 'IPC request type must be a string')
|
|
249
|
+
assert(isObjectRecord(value.payload), 'IPC request payload must be an object')
|
|
250
|
+
|
|
251
|
+
switch (value.type) {
|
|
252
|
+
case 'hello':
|
|
253
|
+
assert(isFiniteNumber(value.payload.minVersion), 'hello.minVersion must be a number')
|
|
254
|
+
assert(isFiniteNumber(value.payload.maxVersion), 'hello.maxVersion must be a number')
|
|
255
|
+
return value as ManagerRequest
|
|
256
|
+
case 'attachSession':
|
|
257
|
+
assert(
|
|
258
|
+
isFiniteNumber(value.payload.protocolVersion),
|
|
259
|
+
'attachSession.protocolVersion must be a number'
|
|
260
|
+
)
|
|
261
|
+
assert(isString(value.payload.sessionId), 'attachSession.sessionId must be a string')
|
|
262
|
+
assert(isFiniteNumber(value.payload.cols), 'attachSession.cols must be a number')
|
|
263
|
+
assert(isFiniteNumber(value.payload.rows), 'attachSession.rows must be a number')
|
|
264
|
+
assert(
|
|
265
|
+
value.payload.workspaceSnapshot === undefined ||
|
|
266
|
+
isWorkspaceSnapshotV1(value.payload.workspaceSnapshot),
|
|
267
|
+
'attachSession.workspaceSnapshot must be a valid workspace snapshot'
|
|
268
|
+
)
|
|
269
|
+
return value as ManagerRequest
|
|
270
|
+
case 'createTab':
|
|
271
|
+
assert(isString(value.payload.sessionId), 'createTab.sessionId must be a string')
|
|
272
|
+
assert(isString(value.payload.tabId), 'createTab.tabId must be a string')
|
|
273
|
+
assert(
|
|
274
|
+
isString(value.payload.assistant) && value.payload.assistant.length > 0,
|
|
275
|
+
'createTab.assistant must be a non-empty string'
|
|
276
|
+
)
|
|
277
|
+
assert(isString(value.payload.title), 'createTab.title must be a string')
|
|
278
|
+
assert(isString(value.payload.command), 'createTab.command must be a string')
|
|
279
|
+
assert(
|
|
280
|
+
value.payload.args === undefined || isStringArray(value.payload.args),
|
|
281
|
+
'createTab.args must be a string array'
|
|
282
|
+
)
|
|
283
|
+
assert(isFiniteNumber(value.payload.cols), 'createTab.cols must be a number')
|
|
284
|
+
assert(isFiniteNumber(value.payload.rows), 'createTab.rows must be a number')
|
|
285
|
+
assert(
|
|
286
|
+
value.payload.cwd === undefined || isString(value.payload.cwd),
|
|
287
|
+
'createTab.cwd must be a string'
|
|
288
|
+
)
|
|
289
|
+
return value as ManagerRequest
|
|
290
|
+
case 'write':
|
|
291
|
+
assert(isString(value.payload.sessionId), 'write.sessionId must be a string')
|
|
292
|
+
assert(isString(value.payload.tabId), 'write.tabId must be a string')
|
|
293
|
+
assert(isString(value.payload.data), 'write.data must be a string')
|
|
294
|
+
return value as ManagerRequest
|
|
295
|
+
case 'resizeClient':
|
|
296
|
+
assert(isString(value.payload.sessionId), 'resizeClient.sessionId must be a string')
|
|
297
|
+
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
298
|
+
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
299
|
+
assert(
|
|
300
|
+
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
301
|
+
'resizeClient.intents must be a scroll-intent record'
|
|
302
|
+
)
|
|
303
|
+
return value as ManagerRequest
|
|
304
|
+
case 'resizeTab':
|
|
305
|
+
assert(isString(value.payload.sessionId), 'resizeTab.sessionId must be a string')
|
|
306
|
+
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
307
|
+
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
308
|
+
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
309
|
+
assert(
|
|
310
|
+
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
311
|
+
'resizeTab.intent must be a scroll intent'
|
|
312
|
+
)
|
|
313
|
+
return value as ManagerRequest
|
|
314
|
+
case 'scroll':
|
|
315
|
+
assert(isString(value.payload.sessionId), 'scroll.sessionId must be a string')
|
|
316
|
+
assert(isString(value.payload.tabId), 'scroll.tabId must be a string')
|
|
317
|
+
assert(isFiniteNumber(value.payload.deltaLines), 'scroll.deltaLines must be a number')
|
|
318
|
+
return value as ManagerRequest
|
|
319
|
+
case 'scrollToBottom':
|
|
320
|
+
assert(isString(value.payload.sessionId), 'scrollToBottom.sessionId must be a string')
|
|
321
|
+
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
322
|
+
return value as ManagerRequest
|
|
323
|
+
case 'reapplyScrollIntent':
|
|
324
|
+
assert(isString(value.payload.sessionId), 'reapplyScrollIntent.sessionId must be a string')
|
|
325
|
+
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
326
|
+
assert(
|
|
327
|
+
isScrollIntent(value.payload.intent),
|
|
328
|
+
'reapplyScrollIntent.intent must be a scroll intent'
|
|
329
|
+
)
|
|
330
|
+
return value as ManagerRequest
|
|
331
|
+
case 'setActiveTab':
|
|
332
|
+
assert(isString(value.payload.sessionId), 'setActiveTab.sessionId must be a string')
|
|
333
|
+
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
|
334
|
+
return value as ManagerRequest
|
|
335
|
+
case 'closeTab':
|
|
336
|
+
assert(isString(value.payload.sessionId), 'closeTab.sessionId must be a string')
|
|
337
|
+
assert(isString(value.payload.tabId), 'closeTab.tabId must be a string')
|
|
338
|
+
return value as ManagerRequest
|
|
339
|
+
case 'disposeSession':
|
|
340
|
+
assert(isString(value.payload.sessionId), 'disposeSession.sessionId must be a string')
|
|
341
|
+
return value as ManagerRequest
|
|
342
|
+
case 'ping':
|
|
343
|
+
return value as ManagerRequest
|
|
344
|
+
default:
|
|
345
|
+
throw new IpcProtocolError(`Unknown IPC request type: ${String(value.type)}`)
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
|
|
349
|
+
export function parseManagerMessage(value: unknown): ManagerResponse | ManagerEvent {
|
|
350
|
+
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
351
|
+
assert(isString(value.type), 'IPC response type must be a string')
|
|
352
|
+
assert(isObjectRecord(value.payload), 'IPC response payload must be an object')
|
|
353
|
+
|
|
354
|
+
switch (value.type) {
|
|
355
|
+
case 'helloResult':
|
|
356
|
+
assert(isString(value.id), 'helloResult.id must be a string')
|
|
357
|
+
assert(isHelloResult(value.payload), 'helloResult.payload is invalid')
|
|
358
|
+
return value as ManagerResponse
|
|
359
|
+
case 'ok':
|
|
360
|
+
assert(isString(value.id), 'ok.id must be a string')
|
|
361
|
+
return value as ManagerResponse
|
|
362
|
+
case 'attachResult':
|
|
363
|
+
assert(isString(value.id), 'attachResult.id must be a string')
|
|
364
|
+
assert(isAttachResult(value.payload), 'attachResult.payload is invalid')
|
|
365
|
+
return value as ManagerResponse
|
|
366
|
+
case 'error':
|
|
367
|
+
assert(isString(value.id), 'error.id must be a string')
|
|
368
|
+
assert(isString(value.payload.message), 'error.message must be a string')
|
|
369
|
+
return value as ManagerResponse
|
|
370
|
+
case 'tabRender':
|
|
371
|
+
assert(isString(value.payload.sessionId), 'tabRender.sessionId must be a string')
|
|
372
|
+
assert(isString(value.payload.tabId), 'tabRender.tabId must be a string')
|
|
373
|
+
assert(isTerminalSnapshot(value.payload.viewport), 'tabRender.viewport is invalid')
|
|
374
|
+
assert(isTerminalModeState(value.payload.terminalModes), 'tabRender.terminalModes is invalid')
|
|
375
|
+
return value as ManagerEvent
|
|
376
|
+
case 'tabExit':
|
|
377
|
+
assert(isString(value.payload.sessionId), 'tabExit.sessionId must be a string')
|
|
378
|
+
assert(isString(value.payload.tabId), 'tabExit.tabId must be a string')
|
|
379
|
+
assert(isFiniteNumber(value.payload.exitCode), 'tabExit.exitCode must be a number')
|
|
380
|
+
return value as ManagerEvent
|
|
381
|
+
case 'tabError':
|
|
382
|
+
assert(isString(value.payload.sessionId), 'tabError.sessionId must be a string')
|
|
383
|
+
assert(isString(value.payload.tabId), 'tabError.tabId must be a string')
|
|
384
|
+
assert(isString(value.payload.message), 'tabError.message must be a string')
|
|
385
|
+
return value as ManagerEvent
|
|
386
|
+
default:
|
|
387
|
+
throw new IpcProtocolError(`Unknown IPC response type: ${String(value.type)}`)
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
export function encodeManagerMessage(message: ManagerMessage): Buffer {
|
|
392
|
+
const payload = JSON.stringify(message)
|
|
393
|
+
return Buffer.from(`${Buffer.byteLength(payload, 'utf8')}\n${payload}`, 'utf8')
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
export { MessageDecoder }
|
package/src/ipc/protocol.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type {
|
|
2
|
+
ScrollIntent,
|
|
2
3
|
TabSession,
|
|
3
4
|
TerminalModeState,
|
|
4
5
|
TerminalSnapshot,
|
|
@@ -7,7 +8,20 @@ import type {
|
|
|
7
8
|
|
|
8
9
|
import { isWorkspaceSnapshotV1 } from '../state/validation'
|
|
9
10
|
|
|
10
|
-
export const
|
|
11
|
+
export const IPC_PROTOCOL_MIN_VERSION = 2
|
|
12
|
+
export const IPC_PROTOCOL_VERSION = 2
|
|
13
|
+
|
|
14
|
+
export interface ProtocolHelloRequest {
|
|
15
|
+
minVersion: number
|
|
16
|
+
maxVersion: number
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export interface ProtocolHelloResult {
|
|
20
|
+
minVersion: number
|
|
21
|
+
maxVersion: number
|
|
22
|
+
processVersion: string
|
|
23
|
+
selectedVersion: number
|
|
24
|
+
}
|
|
11
25
|
|
|
12
26
|
export interface AttachRequest {
|
|
13
27
|
protocolVersion: number
|
|
@@ -24,6 +38,7 @@ export interface AttachResult {
|
|
|
24
38
|
}
|
|
25
39
|
|
|
26
40
|
export type ClientRequest =
|
|
41
|
+
| { id: string; type: 'hello'; payload: ProtocolHelloRequest }
|
|
27
42
|
| { id: string; type: 'attach'; payload: AttachRequest }
|
|
28
43
|
| {
|
|
29
44
|
id: string
|
|
@@ -40,16 +55,30 @@ export type ClientRequest =
|
|
|
40
55
|
}
|
|
41
56
|
}
|
|
42
57
|
| { id: string; type: 'write'; payload: { tabId: string; data: string } }
|
|
43
|
-
| {
|
|
44
|
-
|
|
58
|
+
| {
|
|
59
|
+
id: string
|
|
60
|
+
type: 'resizeClient'
|
|
61
|
+
payload: { cols: number; rows: number; intents?: Record<string, ScrollIntent> }
|
|
62
|
+
}
|
|
63
|
+
| {
|
|
64
|
+
id: string
|
|
65
|
+
type: 'resizeTab'
|
|
66
|
+
payload: { tabId: string; cols: number; rows: number; intent?: ScrollIntent }
|
|
67
|
+
}
|
|
45
68
|
| { id: string; type: 'scrollToBottom'; payload: { tabId: string } }
|
|
46
69
|
| { id: string; type: 'scroll'; payload: { tabId: string; deltaLines: number } }
|
|
70
|
+
| {
|
|
71
|
+
id: string
|
|
72
|
+
type: 'reapplyScrollIntent'
|
|
73
|
+
payload: { tabId: string; intent: ScrollIntent }
|
|
74
|
+
}
|
|
47
75
|
| { id: string; type: 'setActiveTab'; payload: { tabId: string | null } }
|
|
48
76
|
| { id: string; type: 'closeTab'; payload: { tabId: string } }
|
|
49
77
|
| { id: string; type: 'disposeAll'; payload: Record<string, never> }
|
|
50
78
|
| { id: string; type: 'ping'; payload: Record<string, never> }
|
|
51
79
|
|
|
52
80
|
export type ServerResponse =
|
|
81
|
+
| { id: string; type: 'helloResult'; payload: ProtocolHelloResult }
|
|
53
82
|
| { id: string; type: 'ok'; payload: Record<string, never> }
|
|
54
83
|
| { id: string; type: 'attachResult'; payload: AttachResult }
|
|
55
84
|
| { id: string; type: 'error'; payload: { message: string } }
|
|
@@ -128,6 +157,18 @@ function isTerminalSnapshot(value: unknown): value is TerminalSnapshot {
|
|
|
128
157
|
)
|
|
129
158
|
}
|
|
130
159
|
|
|
160
|
+
function isScrollIntent(value: unknown): value is ScrollIntent {
|
|
161
|
+
if (!isObjectRecord(value)) return false
|
|
162
|
+
if (value.kind === 'bottom') return true
|
|
163
|
+
if (value.kind === 'anchor') return isFiniteNumber(value.absoluteLine)
|
|
164
|
+
return false
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function isScrollIntentRecord(value: unknown): value is Record<string, ScrollIntent> {
|
|
168
|
+
if (!isObjectRecord(value)) return false
|
|
169
|
+
return Object.values(value).every(isScrollIntent)
|
|
170
|
+
}
|
|
171
|
+
|
|
131
172
|
function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
132
173
|
return (
|
|
133
174
|
isObjectRecord(value) &&
|
|
@@ -143,6 +184,16 @@ function isTerminalModeState(value: unknown): value is TerminalModeState {
|
|
|
143
184
|
)
|
|
144
185
|
}
|
|
145
186
|
|
|
187
|
+
function isProtocolHelloResult(value: unknown): value is ProtocolHelloResult {
|
|
188
|
+
return (
|
|
189
|
+
isObjectRecord(value) &&
|
|
190
|
+
isFiniteNumber(value.minVersion) &&
|
|
191
|
+
isFiniteNumber(value.maxVersion) &&
|
|
192
|
+
isFiniteNumber(value.selectedVersion) &&
|
|
193
|
+
isString(value.processVersion)
|
|
194
|
+
)
|
|
195
|
+
}
|
|
196
|
+
|
|
146
197
|
function isAttachResult(value: unknown): value is AttachResult {
|
|
147
198
|
return (
|
|
148
199
|
isObjectRecord(value) &&
|
|
@@ -178,6 +229,25 @@ function assert(condition: boolean, message: string): asserts condition {
|
|
|
178
229
|
}
|
|
179
230
|
}
|
|
180
231
|
|
|
232
|
+
export function negotiateProtocolVersion(
|
|
233
|
+
clientMinVersion: number,
|
|
234
|
+
clientMaxVersion: number,
|
|
235
|
+
serverMinVersion: number,
|
|
236
|
+
serverMaxVersion: number
|
|
237
|
+
): number | null {
|
|
238
|
+
const minVersion = Math.max(clientMinVersion, serverMinVersion)
|
|
239
|
+
const maxVersion = Math.min(clientMaxVersion, serverMaxVersion)
|
|
240
|
+
if (minVersion > maxVersion) {
|
|
241
|
+
return null
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
return maxVersion
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export function getProcessVersion(): string {
|
|
248
|
+
return Bun.version
|
|
249
|
+
}
|
|
250
|
+
|
|
181
251
|
export function parseClientRequest(value: unknown): ClientRequest {
|
|
182
252
|
assert(isObjectRecord(value), 'IPC message must be an object')
|
|
183
253
|
assert(isString(value.id), 'IPC request id must be a string')
|
|
@@ -185,10 +255,14 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
185
255
|
assert(isObjectRecord(value.payload), 'IPC request payload must be an object')
|
|
186
256
|
|
|
187
257
|
switch (value.type) {
|
|
258
|
+
case 'hello':
|
|
259
|
+
assert(isFiniteNumber(value.payload.minVersion), 'hello.minVersion must be a number')
|
|
260
|
+
assert(isFiniteNumber(value.payload.maxVersion), 'hello.maxVersion must be a number')
|
|
261
|
+
return value as ClientRequest
|
|
188
262
|
case 'attach':
|
|
189
263
|
assert(
|
|
190
|
-
value.payload.protocolVersion
|
|
191
|
-
|
|
264
|
+
isFiniteNumber(value.payload.protocolVersion),
|
|
265
|
+
'attach.protocolVersion must be a number'
|
|
192
266
|
)
|
|
193
267
|
assert(isString(value.payload.sessionId), 'attach.sessionId must be a string')
|
|
194
268
|
assert(isFiniteNumber(value.payload.cols), 'attach.cols must be a number')
|
|
@@ -225,11 +299,19 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
225
299
|
case 'resizeClient':
|
|
226
300
|
assert(isFiniteNumber(value.payload.cols), 'resizeClient.cols must be a number')
|
|
227
301
|
assert(isFiniteNumber(value.payload.rows), 'resizeClient.rows must be a number')
|
|
302
|
+
assert(
|
|
303
|
+
value.payload.intents === undefined || isScrollIntentRecord(value.payload.intents),
|
|
304
|
+
'resizeClient.intents must be a scroll-intent record'
|
|
305
|
+
)
|
|
228
306
|
return value as ClientRequest
|
|
229
307
|
case 'resizeTab':
|
|
230
308
|
assert(isString(value.payload.tabId), 'resizeTab.tabId must be a string')
|
|
231
309
|
assert(isFiniteNumber(value.payload.cols), 'resizeTab.cols must be a number')
|
|
232
310
|
assert(isFiniteNumber(value.payload.rows), 'resizeTab.rows must be a number')
|
|
311
|
+
assert(
|
|
312
|
+
value.payload.intent === undefined || isScrollIntent(value.payload.intent),
|
|
313
|
+
'resizeTab.intent must be a scroll intent'
|
|
314
|
+
)
|
|
233
315
|
return value as ClientRequest
|
|
234
316
|
case 'scroll':
|
|
235
317
|
assert(isString(value.payload.tabId), 'scroll.tabId must be a string')
|
|
@@ -238,6 +320,13 @@ export function parseClientRequest(value: unknown): ClientRequest {
|
|
|
238
320
|
case 'scrollToBottom':
|
|
239
321
|
assert(isString(value.payload.tabId), 'scrollToBottom.tabId must be a string')
|
|
240
322
|
return value as ClientRequest
|
|
323
|
+
case 'reapplyScrollIntent':
|
|
324
|
+
assert(isString(value.payload.tabId), 'reapplyScrollIntent.tabId must be a string')
|
|
325
|
+
assert(
|
|
326
|
+
isScrollIntent(value.payload.intent),
|
|
327
|
+
'reapplyScrollIntent.intent must be a scroll intent'
|
|
328
|
+
)
|
|
329
|
+
return value as ClientRequest
|
|
241
330
|
case 'setActiveTab':
|
|
242
331
|
assert(isNullableString(value.payload.tabId), 'setActiveTab.tabId must be a string or null')
|
|
243
332
|
return value as ClientRequest
|
|
@@ -258,6 +347,10 @@ export function parseServerMessage(value: unknown): ServerResponse | ServerEvent
|
|
|
258
347
|
assert(isObjectRecord(value.payload), 'IPC response payload must be an object')
|
|
259
348
|
|
|
260
349
|
switch (value.type) {
|
|
350
|
+
case 'helloResult':
|
|
351
|
+
assert(isString(value.id), 'helloResult.id must be a string')
|
|
352
|
+
assert(isProtocolHelloResult(value.payload), 'helloResult.payload is invalid')
|
|
353
|
+
return value as ServerResponse
|
|
261
354
|
case 'ok':
|
|
262
355
|
assert(isString(value.id), 'ok.id must be a string')
|
|
263
356
|
return value as ServerResponse
|
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { existsSync } from 'node:fs'
|
|
2
2
|
import { resolve } from 'node:path'
|
|
3
3
|
|
|
4
|
-
import {
|
|
4
|
+
import { getIpcDaemonSocketPath, getTerminalManagerSocketPath } from '../daemon/runtime-paths'
|
|
5
5
|
import { logDebug } from '../debug/input-log'
|
|
6
6
|
|
|
7
7
|
const ENTRY_POINT = resolve(import.meta.dir, '..', 'index.tsx')
|
|
8
8
|
|
|
9
|
-
export async function
|
|
9
|
+
export async function findSocketProcessPid(socketPath: string): Promise<number | null> {
|
|
10
10
|
try {
|
|
11
11
|
const proc = Bun.spawn(['lsof', '-t', socketPath], { stderr: 'ignore', stdout: 'pipe' })
|
|
12
12
|
const text = await new Response(proc.stdout).text()
|
|
@@ -21,7 +21,19 @@ export async function findDaemonPid(socketPath: string): Promise<number | null>
|
|
|
21
21
|
}
|
|
22
22
|
}
|
|
23
23
|
|
|
24
|
-
export async function
|
|
24
|
+
export async function findDaemonPid(socketPath: string): Promise<number | null> {
|
|
25
|
+
return findSocketProcessPid(socketPath)
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export async function findIpcDaemonPid(): Promise<number | null> {
|
|
29
|
+
return findSocketProcessPid(getIpcDaemonSocketPath())
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export async function findTerminalManagerPid(): Promise<number | null> {
|
|
33
|
+
return findSocketProcessPid(getTerminalManagerSocketPath())
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export async function killProcess(pid: number): Promise<void> {
|
|
25
37
|
process.kill(pid, 'SIGTERM')
|
|
26
38
|
|
|
27
39
|
const deadline = Date.now() + 3_000
|
|
@@ -41,16 +53,12 @@ export async function killDaemon(pid: number): Promise<void> {
|
|
|
41
53
|
}
|
|
42
54
|
}
|
|
43
55
|
|
|
44
|
-
export async function
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
stderr: 'ignore',
|
|
48
|
-
stdin: 'ignore',
|
|
49
|
-
stdout: 'ignore',
|
|
50
|
-
}).unref()
|
|
56
|
+
export async function killDaemon(pid: number): Promise<void> {
|
|
57
|
+
await killProcess(pid)
|
|
58
|
+
}
|
|
51
59
|
|
|
60
|
+
async function waitForSocket(socketPath: string): Promise<boolean> {
|
|
52
61
|
const deadline = Date.now() + 2_000
|
|
53
|
-
const socketPath = getDaemonSocketPath()
|
|
54
62
|
while (Date.now() < deadline) {
|
|
55
63
|
if (existsSync(socketPath)) {
|
|
56
64
|
return true
|
|
@@ -60,3 +68,26 @@ export async function spawnDetachedDaemon(): Promise<boolean> {
|
|
|
60
68
|
|
|
61
69
|
return false
|
|
62
70
|
}
|
|
71
|
+
|
|
72
|
+
async function spawnDetachedProcess(command: 'daemon' | 'terminal-manager', socketPath: string) {
|
|
73
|
+
Bun.spawn([process.execPath, 'run', ENTRY_POINT, command], {
|
|
74
|
+
detached: true,
|
|
75
|
+
stderr: 'ignore',
|
|
76
|
+
stdin: 'ignore',
|
|
77
|
+
stdout: 'ignore',
|
|
78
|
+
}).unref()
|
|
79
|
+
|
|
80
|
+
return waitForSocket(socketPath)
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
export async function spawnDetachedDaemon(): Promise<boolean> {
|
|
84
|
+
return spawnDetachedProcess('daemon', getIpcDaemonSocketPath())
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export async function spawnDetachedIpcDaemon(): Promise<boolean> {
|
|
88
|
+
return spawnDetachedDaemon()
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export async function spawnDetachedTerminalManager(): Promise<boolean> {
|
|
92
|
+
return spawnDetachedProcess('terminal-manager', getTerminalManagerSocketPath())
|
|
93
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { join } from 'node:path'
|
|
2
|
+
|
|
3
|
+
export const DEFAULT_PROFILE = 'default'
|
|
4
|
+
|
|
5
|
+
function sanitizeProfile(profile: string): string {
|
|
6
|
+
const trimmed = profile.trim().toLowerCase()
|
|
7
|
+
const normalized = trimmed.replace(/[^a-z0-9._-]+/g, '-')
|
|
8
|
+
const collapsed = normalized.replace(/-+/g, '-').replace(/^-|-$/g, '')
|
|
9
|
+
return collapsed || DEFAULT_PROFILE
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export function getProfileName(): string {
|
|
13
|
+
const configured = process.env.AIMUX_PROFILE ?? process.env.AIMUX_RUNTIME_PROFILE
|
|
14
|
+
if (!configured) {
|
|
15
|
+
return DEFAULT_PROFILE
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
return sanitizeProfile(configured)
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function getConfigProfilesRootDir(): string {
|
|
22
|
+
return join(process.env.HOME ?? '~', '.config', 'aimux')
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function getProfileConfigDir(): string {
|
|
26
|
+
return join(getConfigProfilesRootDir(), getProfileName())
|
|
27
|
+
}
|