@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/config.ts
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { existsSync, mkdirSync, readFileSync, writeFileSync } from 'node:fs'
|
|
2
|
-
import { dirname, join } from 'node:path'
|
|
3
2
|
|
|
4
3
|
import type { WorkspaceSnapshotV1 } from './state/types'
|
|
5
4
|
|
|
6
5
|
import { logDebug } from './debug/input-log'
|
|
6
|
+
import { getProfileConfigDir } from './profile-paths'
|
|
7
7
|
import { isWorkspaceSnapshotV1 } from './state/validation'
|
|
8
8
|
import { THEME_IDS, type ThemeId } from './ui/themes'
|
|
9
9
|
|
|
10
|
-
export const CONFIG_PATH =
|
|
10
|
+
export const CONFIG_PATH = `${getProfileConfigDir()}/aimux.json`
|
|
11
11
|
|
|
12
12
|
export interface AimuxConfig {
|
|
13
13
|
version: 2
|
|
@@ -16,6 +16,7 @@ export interface AimuxConfig {
|
|
|
16
16
|
gitPanelVisible?: boolean
|
|
17
17
|
gitPanelRatio?: number
|
|
18
18
|
workspaceSnapshot?: WorkspaceSnapshotV1
|
|
19
|
+
skippedUpdateVersion?: string
|
|
19
20
|
}
|
|
20
21
|
|
|
21
22
|
const DEFAULT_CONFIG: AimuxConfig = {
|
|
@@ -58,6 +59,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
58
59
|
gitPanelVisible?: unknown
|
|
59
60
|
gitPanelRatio?: unknown
|
|
60
61
|
workspaceSnapshot?: unknown
|
|
62
|
+
skippedUpdateVersion?: unknown
|
|
61
63
|
}
|
|
62
64
|
|
|
63
65
|
const issues: string[] = []
|
|
@@ -98,6 +100,14 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
98
100
|
issues.push('ignored invalid workspaceSnapshot')
|
|
99
101
|
}
|
|
100
102
|
|
|
103
|
+
const validSkippedUpdateVersion =
|
|
104
|
+
typeof parsed.skippedUpdateVersion === 'string' && parsed.skippedUpdateVersion.length > 0
|
|
105
|
+
? parsed.skippedUpdateVersion
|
|
106
|
+
: undefined
|
|
107
|
+
if (parsed.skippedUpdateVersion !== undefined && validSkippedUpdateVersion === undefined) {
|
|
108
|
+
issues.push('ignored invalid skippedUpdateVersion')
|
|
109
|
+
}
|
|
110
|
+
|
|
101
111
|
if (issues.length > 0) {
|
|
102
112
|
logDebug('config.load.validationIssue', { issues, path: CONFIG_PATH })
|
|
103
113
|
}
|
|
@@ -107,6 +117,7 @@ export function loadConfigResult(): ConfigLoadResult {
|
|
|
107
117
|
customCommands: isCustomCommandsRecord(parsed.customCommands) ? parsed.customCommands : {},
|
|
108
118
|
gitPanelRatio: validGitPanelRatio,
|
|
109
119
|
gitPanelVisible: validGitPanelVisible,
|
|
120
|
+
skippedUpdateVersion: validSkippedUpdateVersion,
|
|
110
121
|
themeId: isThemeId(parsed.themeId) ? parsed.themeId : undefined,
|
|
111
122
|
version: 2,
|
|
112
123
|
workspaceSnapshot: isWorkspaceSnapshotV1(parsed.workspaceSnapshot)
|
|
@@ -133,7 +144,7 @@ export function loadConfig(): AimuxConfig {
|
|
|
133
144
|
|
|
134
145
|
export function saveConfig(config: AimuxConfig): boolean {
|
|
135
146
|
try {
|
|
136
|
-
mkdirSync(
|
|
147
|
+
mkdirSync(getProfileConfigDir(), { recursive: true })
|
|
137
148
|
writeFileSync(CONFIG_PATH, `${JSON.stringify(config, null, 2)}\n`)
|
|
138
149
|
return true
|
|
139
150
|
} catch (error) {
|
package/src/daemon/daemon.ts
CHANGED
|
@@ -1,22 +1,28 @@
|
|
|
1
|
-
import { createServer, type Socket } from 'node:net'
|
|
1
|
+
import { connect, createServer, type Socket } from 'node:net'
|
|
2
2
|
|
|
3
3
|
import { logDebug } from '../debug/input-log'
|
|
4
4
|
import {
|
|
5
5
|
type ClientRequest,
|
|
6
6
|
encodeMessage,
|
|
7
|
+
getProcessVersion,
|
|
8
|
+
IPC_PROTOCOL_MIN_VERSION,
|
|
7
9
|
IPC_PROTOCOL_VERSION,
|
|
8
10
|
MessageDecoder,
|
|
11
|
+
negotiateProtocolVersion,
|
|
9
12
|
parseClientRequest,
|
|
10
13
|
type ServerEvent,
|
|
11
14
|
type ServerResponse,
|
|
12
15
|
} from '../ipc/protocol'
|
|
13
|
-
import {
|
|
16
|
+
import { findSocketProcessPid, spawnDetachedTerminalManager } from '../platform/daemon-control'
|
|
17
|
+
import { TerminalManagerClient } from '../terminal-manager/manager-client'
|
|
14
18
|
import {
|
|
15
|
-
|
|
19
|
+
getIpcDaemonSocketPath,
|
|
20
|
+
getSocketSecurityIssue,
|
|
21
|
+
getTerminalManagerSocketPath,
|
|
16
22
|
removeDaemonSocketIfExists,
|
|
17
|
-
|
|
23
|
+
removeTerminalManagerSocketIfExists,
|
|
24
|
+
tightenSocketPermissions,
|
|
18
25
|
} from './runtime-paths'
|
|
19
|
-
import { SessionManager } from './session-manager'
|
|
20
26
|
|
|
21
27
|
function send(socket: Socket, message: ServerResponse | ServerEvent): void {
|
|
22
28
|
socket.write(encodeMessage(message))
|
|
@@ -34,25 +40,88 @@ function requireSession(socket: Socket, attachedSessions: Map<Socket, string>):
|
|
|
34
40
|
return sessionId
|
|
35
41
|
}
|
|
36
42
|
|
|
43
|
+
function requireNegotiatedVersion(socket: Socket, versions: Map<Socket, number>): number {
|
|
44
|
+
const version = versions.get(socket)
|
|
45
|
+
if (version === undefined) {
|
|
46
|
+
throw new Error('Protocol handshake required before attach')
|
|
47
|
+
}
|
|
48
|
+
return version
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
async function canConnectToSocket(socketPath: string): Promise<boolean> {
|
|
52
|
+
const securityIssue = getSocketSecurityIssue(socketPath)
|
|
53
|
+
if (securityIssue) {
|
|
54
|
+
logDebug('daemon.socketUnhealthy', { issue: securityIssue, socketPath })
|
|
55
|
+
return false
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return new Promise<boolean>((resolve) => {
|
|
59
|
+
const socket = connect(socketPath)
|
|
60
|
+
const finish = (result: boolean) => {
|
|
61
|
+
socket.removeAllListeners()
|
|
62
|
+
socket.destroy()
|
|
63
|
+
resolve(result)
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
socket.once('connect', () => finish(true))
|
|
67
|
+
socket.once('error', () => finish(false))
|
|
68
|
+
})
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
async function ensureTerminalManagerReady(manager: TerminalManagerClient): Promise<void> {
|
|
72
|
+
try {
|
|
73
|
+
logDebug('daemon.ensureTerminalManager.connectExisting.start', {
|
|
74
|
+
socketPath: getTerminalManagerSocketPath(),
|
|
75
|
+
})
|
|
76
|
+
await manager.connect()
|
|
77
|
+
logDebug('daemon.ensureTerminalManager.connectExisting.success')
|
|
78
|
+
return
|
|
79
|
+
} catch (error) {
|
|
80
|
+
logDebug('daemon.ensureTerminalManager.connectExisting.failed', {
|
|
81
|
+
error: error instanceof Error ? error.message : String(error),
|
|
82
|
+
})
|
|
83
|
+
const socketPath = getTerminalManagerSocketPath()
|
|
84
|
+
if (!(await canConnectToSocket(socketPath))) {
|
|
85
|
+
removeTerminalManagerSocketIfExists()
|
|
86
|
+
logDebug('daemon.ensureTerminalManager.spawn.start', { socketPath })
|
|
87
|
+
const ok = await spawnDetachedTerminalManager()
|
|
88
|
+
if (!ok) {
|
|
89
|
+
throw new Error('Failed to start terminal manager')
|
|
90
|
+
}
|
|
91
|
+
logDebug('daemon.ensureTerminalManager.spawn.success', { socketPath })
|
|
92
|
+
}
|
|
93
|
+
await manager.connect()
|
|
94
|
+
logDebug('daemon.ensureTerminalManager.connectAfterSpawn.success')
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
37
98
|
export async function runDaemon(): Promise<void> {
|
|
38
|
-
const socketPath =
|
|
99
|
+
const socketPath = getIpcDaemonSocketPath()
|
|
39
100
|
logDebug('daemon.start', { pid: process.pid, socketPath })
|
|
40
101
|
|
|
41
|
-
const existingPid = await
|
|
102
|
+
const existingPid = await findSocketProcessPid(socketPath)
|
|
42
103
|
if (existingPid !== null && existingPid !== process.pid) {
|
|
43
104
|
logDebug('daemon.alreadyRunning', { existingPid })
|
|
44
105
|
process.stderr.write(`aimux daemon already running (pid ${existingPid})\n`)
|
|
45
106
|
process.exit(1)
|
|
46
107
|
}
|
|
47
108
|
|
|
48
|
-
logDebug('daemon.removeStaleSocket', { socketPath })
|
|
49
109
|
removeDaemonSocketIfExists()
|
|
50
110
|
|
|
51
|
-
const
|
|
111
|
+
const manager = new TerminalManagerClient()
|
|
112
|
+
await ensureTerminalManagerReady(manager)
|
|
113
|
+
|
|
52
114
|
const sockets = new Set<Socket>()
|
|
53
115
|
const attachedSessions = new Map<Socket, string>()
|
|
116
|
+
const negotiatedVersions = new Map<Socket, number>()
|
|
54
117
|
|
|
55
|
-
|
|
118
|
+
manager.on('render', (sessionId, tabId, viewport, terminalModes) => {
|
|
119
|
+
logDebug('daemon.manager.render', {
|
|
120
|
+
attachedSocketCount: sockets.size,
|
|
121
|
+
sessionId,
|
|
122
|
+
tabId,
|
|
123
|
+
viewportLines: viewport.lines.length,
|
|
124
|
+
})
|
|
56
125
|
const event: ServerEvent = { payload: { tabId, terminalModes, viewport }, type: 'tabRender' }
|
|
57
126
|
for (const socket of sockets) {
|
|
58
127
|
if (attachedSessions.get(socket) === sessionId) {
|
|
@@ -60,7 +129,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
60
129
|
}
|
|
61
130
|
}
|
|
62
131
|
})
|
|
63
|
-
|
|
132
|
+
manager.on('exit', (sessionId, tabId, exitCode) => {
|
|
133
|
+
logDebug('daemon.manager.exit', { exitCode, sessionId, tabId })
|
|
64
134
|
const event: ServerEvent = { payload: { exitCode, tabId }, type: 'tabExit' }
|
|
65
135
|
for (const socket of sockets) {
|
|
66
136
|
if (attachedSessions.get(socket) === sessionId) {
|
|
@@ -68,7 +138,8 @@ export async function runDaemon(): Promise<void> {
|
|
|
68
138
|
}
|
|
69
139
|
}
|
|
70
140
|
})
|
|
71
|
-
|
|
141
|
+
manager.on('error', (sessionId, tabId, message) => {
|
|
142
|
+
logDebug('daemon.manager.error', { message, sessionId, tabId })
|
|
72
143
|
const event: ServerEvent = { payload: { message, tabId }, type: 'tabError' }
|
|
73
144
|
for (const socket of sockets) {
|
|
74
145
|
if (attachedSessions.get(socket) === sessionId) {
|
|
@@ -83,117 +154,212 @@ export async function runDaemon(): Promise<void> {
|
|
|
83
154
|
const decoder = new MessageDecoder<ClientRequest>(parseClientRequest)
|
|
84
155
|
|
|
85
156
|
socket.on('data', (chunk) => {
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
message.payload.
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
157
|
+
void (async () => {
|
|
158
|
+
try {
|
|
159
|
+
for (const message of decoder.push(chunk)) {
|
|
160
|
+
try {
|
|
161
|
+
switch (message.type) {
|
|
162
|
+
case 'hello': {
|
|
163
|
+
logDebug('daemon.request.hello', {
|
|
164
|
+
maxVersion: message.payload.maxVersion,
|
|
165
|
+
minVersion: message.payload.minVersion,
|
|
166
|
+
})
|
|
167
|
+
const selectedVersion = negotiateProtocolVersion(
|
|
168
|
+
message.payload.minVersion,
|
|
169
|
+
message.payload.maxVersion,
|
|
170
|
+
IPC_PROTOCOL_MIN_VERSION,
|
|
171
|
+
IPC_PROTOCOL_VERSION
|
|
172
|
+
)
|
|
173
|
+
if (selectedVersion === null) {
|
|
174
|
+
send(socket, {
|
|
175
|
+
id: message.id,
|
|
176
|
+
payload: { message: 'No compatible app protocol version' },
|
|
177
|
+
type: 'error',
|
|
178
|
+
})
|
|
179
|
+
break
|
|
180
|
+
}
|
|
181
|
+
negotiatedVersions.set(socket, selectedVersion)
|
|
182
|
+
logDebug('daemon.request.hello.success', { selectedVersion })
|
|
183
|
+
send(socket, {
|
|
184
|
+
id: message.id,
|
|
185
|
+
payload: {
|
|
186
|
+
maxVersion: IPC_PROTOCOL_VERSION,
|
|
187
|
+
minVersion: IPC_PROTOCOL_MIN_VERSION,
|
|
188
|
+
processVersion: getProcessVersion(),
|
|
189
|
+
selectedVersion,
|
|
190
|
+
},
|
|
191
|
+
type: 'helloResult',
|
|
192
|
+
})
|
|
193
|
+
break
|
|
194
|
+
}
|
|
195
|
+
case 'attach': {
|
|
196
|
+
logDebug('daemon.request.attach.start', {
|
|
197
|
+
cols: message.payload.cols,
|
|
198
|
+
rows: message.payload.rows,
|
|
199
|
+
sessionId: message.payload.sessionId,
|
|
200
|
+
snapshotTabs: message.payload.workspaceSnapshot?.tabs.length ?? 0,
|
|
201
|
+
})
|
|
202
|
+
const negotiatedVersion = requireNegotiatedVersion(socket, negotiatedVersions)
|
|
203
|
+
if (message.payload.protocolVersion !== negotiatedVersion) {
|
|
204
|
+
throw new Error(
|
|
205
|
+
`Protocol mismatch: client v${message.payload.protocolVersion}, daemon v${negotiatedVersion}`
|
|
206
|
+
)
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
attachedSessions.set(socket, message.payload.sessionId)
|
|
210
|
+
const attachResult = await manager.attachSession({
|
|
211
|
+
cols: message.payload.cols,
|
|
212
|
+
rows: message.payload.rows,
|
|
213
|
+
sessionId: message.payload.sessionId,
|
|
214
|
+
workspaceSnapshot: message.payload.workspaceSnapshot,
|
|
215
|
+
})
|
|
216
|
+
send(socket, {
|
|
217
|
+
id: message.id,
|
|
218
|
+
payload: {
|
|
219
|
+
activeTabId: attachResult.activeTabId,
|
|
220
|
+
protocolVersion: negotiatedVersion,
|
|
221
|
+
tabs: attachResult.tabs,
|
|
222
|
+
},
|
|
223
|
+
type: 'attachResult',
|
|
224
|
+
})
|
|
225
|
+
logDebug('daemon.request.attach.success', {
|
|
226
|
+
activeTabId: attachResult.activeTabId,
|
|
227
|
+
sessionId: message.payload.sessionId,
|
|
228
|
+
tabs: attachResult.tabs.length,
|
|
229
|
+
})
|
|
230
|
+
break
|
|
231
|
+
}
|
|
232
|
+
case 'createTab': {
|
|
233
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
234
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
235
|
+
logDebug('daemon.request.createTab.start', {
|
|
236
|
+
command: message.payload.command,
|
|
237
|
+
sessionId,
|
|
238
|
+
tabId: message.payload.tabId,
|
|
239
|
+
title: message.payload.title,
|
|
240
|
+
})
|
|
241
|
+
await manager.createTab({ ...message.payload, sessionId })
|
|
242
|
+
sendOk(socket, message.id)
|
|
243
|
+
logDebug('daemon.request.createTab.success', {
|
|
244
|
+
sessionId,
|
|
245
|
+
tabId: message.payload.tabId,
|
|
246
|
+
})
|
|
247
|
+
break
|
|
248
|
+
}
|
|
249
|
+
case 'write': {
|
|
250
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
251
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
252
|
+
await manager.write(sessionId, message.payload.tabId, message.payload.data)
|
|
253
|
+
sendOk(socket, message.id)
|
|
254
|
+
break
|
|
255
|
+
}
|
|
256
|
+
case 'resizeClient': {
|
|
257
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
258
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
259
|
+
await manager.resize(
|
|
260
|
+
sessionId,
|
|
261
|
+
message.payload.cols,
|
|
262
|
+
message.payload.rows,
|
|
263
|
+
message.payload.intents
|
|
264
|
+
)
|
|
265
|
+
sendOk(socket, message.id)
|
|
266
|
+
break
|
|
267
|
+
}
|
|
268
|
+
case 'resizeTab': {
|
|
269
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
270
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
271
|
+
await manager.resizeTab(
|
|
272
|
+
sessionId,
|
|
273
|
+
message.payload.tabId,
|
|
274
|
+
message.payload.cols,
|
|
275
|
+
message.payload.rows,
|
|
276
|
+
message.payload.intent
|
|
277
|
+
)
|
|
278
|
+
sendOk(socket, message.id)
|
|
279
|
+
break
|
|
166
280
|
}
|
|
167
|
-
|
|
168
|
-
|
|
281
|
+
case 'scroll': {
|
|
282
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
283
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
284
|
+
await manager.scroll(sessionId, message.payload.tabId, message.payload.deltaLines)
|
|
285
|
+
sendOk(socket, message.id)
|
|
286
|
+
break
|
|
287
|
+
}
|
|
288
|
+
case 'scrollToBottom': {
|
|
289
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
290
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
291
|
+
await manager.scrollToBottom(sessionId, message.payload.tabId)
|
|
292
|
+
sendOk(socket, message.id)
|
|
293
|
+
break
|
|
294
|
+
}
|
|
295
|
+
case 'reapplyScrollIntent': {
|
|
296
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
297
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
298
|
+
await manager.reapplyScrollIntent(
|
|
299
|
+
sessionId,
|
|
300
|
+
message.payload.tabId,
|
|
301
|
+
message.payload.intent
|
|
302
|
+
)
|
|
303
|
+
sendOk(socket, message.id)
|
|
304
|
+
break
|
|
305
|
+
}
|
|
306
|
+
case 'setActiveTab': {
|
|
307
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
308
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
309
|
+
await manager.setActiveTab(sessionId, message.payload.tabId)
|
|
310
|
+
sendOk(socket, message.id)
|
|
311
|
+
break
|
|
312
|
+
}
|
|
313
|
+
case 'closeTab': {
|
|
314
|
+
const sessionId = requireSession(socket, attachedSessions)
|
|
315
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
316
|
+
await manager.closeTab(sessionId, message.payload.tabId)
|
|
317
|
+
sendOk(socket, message.id)
|
|
318
|
+
break
|
|
319
|
+
}
|
|
320
|
+
case 'disposeAll': {
|
|
321
|
+
const sessionId = attachedSessions.get(socket)
|
|
322
|
+
requireNegotiatedVersion(socket, negotiatedVersions)
|
|
323
|
+
if (sessionId) {
|
|
324
|
+
await manager.disposeSession(sessionId)
|
|
325
|
+
}
|
|
326
|
+
sendOk(socket, message.id)
|
|
327
|
+
break
|
|
328
|
+
}
|
|
329
|
+
case 'ping':
|
|
330
|
+
sendOk(socket, message.id)
|
|
331
|
+
break
|
|
169
332
|
}
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
333
|
+
} catch (error) {
|
|
334
|
+
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
335
|
+
logDebug('daemon.request.error', {
|
|
336
|
+
error: errorMessage,
|
|
337
|
+
requestId: message.id,
|
|
338
|
+
type: message.type,
|
|
339
|
+
})
|
|
340
|
+
send(socket, { id: message.id, payload: { message: errorMessage }, type: 'error' })
|
|
173
341
|
}
|
|
174
|
-
} catch (error) {
|
|
175
|
-
const errorMessage = error instanceof Error ? error.message : String(error)
|
|
176
|
-
logDebug('daemon.request.error', { error: errorMessage, requestId: message.id })
|
|
177
|
-
send(socket, { id: message.id, payload: { message: errorMessage }, type: 'error' })
|
|
178
342
|
}
|
|
343
|
+
} catch (error) {
|
|
344
|
+
const message = error instanceof Error ? error.message : String(error)
|
|
345
|
+
logDebug('daemon.decoder.error', { error: message })
|
|
346
|
+
decoder.reset()
|
|
347
|
+
send(socket, { id: crypto.randomUUID(), payload: { message }, type: 'error' })
|
|
179
348
|
}
|
|
180
|
-
}
|
|
181
|
-
const message = error instanceof Error ? error.message : String(error)
|
|
182
|
-
logDebug('daemon.request.error', { error: message })
|
|
183
|
-
decoder.reset()
|
|
184
|
-
send(socket, { id: crypto.randomUUID(), payload: { message }, type: 'error' })
|
|
185
|
-
}
|
|
349
|
+
})()
|
|
186
350
|
})
|
|
187
351
|
|
|
188
352
|
socket.on('close', () => {
|
|
189
|
-
logDebug('daemon.client.close')
|
|
353
|
+
logDebug('daemon.client.close', { sessionId: attachedSessions.get(socket) ?? null })
|
|
190
354
|
sockets.delete(socket)
|
|
191
355
|
attachedSessions.delete(socket)
|
|
356
|
+
negotiatedVersions.delete(socket)
|
|
192
357
|
})
|
|
193
358
|
socket.on('error', () => {
|
|
194
|
-
logDebug('daemon.client.error')
|
|
359
|
+
logDebug('daemon.client.error', { sessionId: attachedSessions.get(socket) ?? null })
|
|
195
360
|
sockets.delete(socket)
|
|
196
361
|
attachedSessions.delete(socket)
|
|
362
|
+
negotiatedVersions.delete(socket)
|
|
197
363
|
})
|
|
198
364
|
})
|
|
199
365
|
|
|
@@ -201,12 +367,11 @@ export async function runDaemon(): Promise<void> {
|
|
|
201
367
|
server.once('error', reject)
|
|
202
368
|
server.listen(socketPath, () => resolve())
|
|
203
369
|
})
|
|
204
|
-
|
|
205
|
-
logDebug('daemon.listening', { socketPath })
|
|
370
|
+
tightenSocketPermissions(socketPath)
|
|
206
371
|
|
|
207
372
|
const gracefulShutdown = (signal: string) => {
|
|
208
373
|
logDebug(`daemon.${signal}`)
|
|
209
|
-
|
|
374
|
+
manager.destroy()
|
|
210
375
|
server.close()
|
|
211
376
|
process.exit(0)
|
|
212
377
|
}
|
|
@@ -216,18 +381,14 @@ export async function runDaemon(): Promise<void> {
|
|
|
216
381
|
|
|
217
382
|
process.on('uncaughtException', (error) => {
|
|
218
383
|
logDebug('daemon.uncaughtException', { error: error.message, stack: error.stack })
|
|
219
|
-
|
|
220
|
-
server.close()
|
|
221
|
-
process.exit(1)
|
|
384
|
+
gracefulShutdown('uncaughtException')
|
|
222
385
|
})
|
|
223
386
|
|
|
224
387
|
process.on('unhandledRejection', (reason) => {
|
|
225
388
|
const message = reason instanceof Error ? reason.message : String(reason)
|
|
226
389
|
const stack = reason instanceof Error ? reason.stack : undefined
|
|
227
390
|
logDebug('daemon.unhandledRejection', { error: message, stack })
|
|
228
|
-
|
|
229
|
-
server.close()
|
|
230
|
-
process.exit(1)
|
|
391
|
+
gracefulShutdown('unhandledRejection')
|
|
231
392
|
})
|
|
232
393
|
|
|
233
394
|
await new Promise<void>(() => {
|
|
@@ -1,12 +1,18 @@
|
|
|
1
1
|
import { chmodSync, constants, existsSync, lstatSync, mkdirSync, unlinkSync } from 'node:fs'
|
|
2
2
|
import { dirname, join } from 'node:path'
|
|
3
3
|
|
|
4
|
+
import { getProfileName } from '../profile-paths'
|
|
5
|
+
|
|
6
|
+
export function getRuntimeProfile(): string {
|
|
7
|
+
return getProfileName()
|
|
8
|
+
}
|
|
9
|
+
|
|
4
10
|
function getRuntimeBaseDir(): string {
|
|
5
11
|
if (process.env.XDG_RUNTIME_DIR) {
|
|
6
|
-
return join(process.env.XDG_RUNTIME_DIR,
|
|
12
|
+
return join(process.env.XDG_RUNTIME_DIR, `aimux-${getRuntimeProfile()}`)
|
|
7
13
|
}
|
|
8
14
|
|
|
9
|
-
return join(process.env.HOME ?? '.', '.local', 'state',
|
|
15
|
+
return join(process.env.HOME ?? '.', '.local', 'state', `aimux-${getRuntimeProfile()}`)
|
|
10
16
|
}
|
|
11
17
|
|
|
12
18
|
export function ensureRuntimeDir(): string {
|
|
@@ -24,6 +30,14 @@ export function getDaemonSocketPath(): string {
|
|
|
24
30
|
return join(ensureRuntimeDir(), 'daemon.sock')
|
|
25
31
|
}
|
|
26
32
|
|
|
33
|
+
export function getIpcDaemonSocketPath(): string {
|
|
34
|
+
return getDaemonSocketPath()
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export function getTerminalManagerSocketPath(): string {
|
|
38
|
+
return join(ensureRuntimeDir(), 'terminal-manager.sock')
|
|
39
|
+
}
|
|
40
|
+
|
|
27
41
|
export function ensureParentDir(filePath: string): void {
|
|
28
42
|
mkdirSync(dirname(filePath), { recursive: true })
|
|
29
43
|
}
|
|
@@ -36,6 +50,10 @@ export function tightenDaemonSocketPermissions(socketPath: string): void {
|
|
|
36
50
|
}
|
|
37
51
|
}
|
|
38
52
|
|
|
53
|
+
export function tightenSocketPermissions(socketPath: string): void {
|
|
54
|
+
tightenDaemonSocketPermissions(socketPath)
|
|
55
|
+
}
|
|
56
|
+
|
|
39
57
|
export function getDaemonSocketSecurityIssue(socketPath: string): string | null {
|
|
40
58
|
if (!existsSync(socketPath)) {
|
|
41
59
|
return 'socket missing'
|
|
@@ -61,9 +79,23 @@ export function getDaemonSocketSecurityIssue(socketPath: string): string | null
|
|
|
61
79
|
}
|
|
62
80
|
}
|
|
63
81
|
|
|
82
|
+
export function getSocketSecurityIssue(socketPath: string): string | null {
|
|
83
|
+
return getDaemonSocketSecurityIssue(socketPath)
|
|
84
|
+
}
|
|
85
|
+
|
|
64
86
|
export function removeDaemonSocketIfExists(): void {
|
|
65
87
|
const socketPath = getDaemonSocketPath()
|
|
66
88
|
if (existsSync(socketPath)) {
|
|
67
89
|
unlinkSync(socketPath)
|
|
68
90
|
}
|
|
69
91
|
}
|
|
92
|
+
|
|
93
|
+
export function removeSocketIfExists(socketPath: string): void {
|
|
94
|
+
if (existsSync(socketPath)) {
|
|
95
|
+
unlinkSync(socketPath)
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export function removeTerminalManagerSocketIfExists(): void {
|
|
100
|
+
removeSocketIfExists(getTerminalManagerSocketPath())
|
|
101
|
+
}
|
|
@@ -1,6 +1,11 @@
|
|
|
1
1
|
import { EventEmitter } from 'node:events'
|
|
2
2
|
|
|
3
|
-
import type {
|
|
3
|
+
import type {
|
|
4
|
+
ScrollIntent,
|
|
5
|
+
TerminalModeState,
|
|
6
|
+
TerminalSnapshot,
|
|
7
|
+
WorkspaceSnapshotV1,
|
|
8
|
+
} from '../state/types'
|
|
4
9
|
|
|
5
10
|
import { logDebug } from '../debug/input-log'
|
|
6
11
|
import { SessionRegistry } from './session-registry'
|
|
@@ -62,12 +67,18 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
62
67
|
this.getOrCreateRegistry(sessionId).write(tabId, data)
|
|
63
68
|
}
|
|
64
69
|
|
|
65
|
-
resize(sessionId: string, cols: number, rows: number): void {
|
|
66
|
-
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows)
|
|
70
|
+
resize(sessionId: string, cols: number, rows: number, intents?: Map<string, ScrollIntent>): void {
|
|
71
|
+
this.getOrCreateRegistry(sessionId).resizeAll(cols, rows, intents)
|
|
67
72
|
}
|
|
68
73
|
|
|
69
|
-
resizeTab(
|
|
70
|
-
|
|
74
|
+
resizeTab(
|
|
75
|
+
sessionId: string,
|
|
76
|
+
tabId: string,
|
|
77
|
+
cols: number,
|
|
78
|
+
rows: number,
|
|
79
|
+
intent?: ScrollIntent
|
|
80
|
+
): void {
|
|
81
|
+
this.getOrCreateRegistry(sessionId).resizeTab(tabId, cols, rows, intent)
|
|
71
82
|
}
|
|
72
83
|
|
|
73
84
|
scroll(sessionId: string, tabId: string, deltaLines: number): void {
|
|
@@ -78,6 +89,10 @@ export class SessionManager extends EventEmitter<SessionManagerEvents> {
|
|
|
78
89
|
this.getOrCreateRegistry(sessionId).scrollViewportToBottom(tabId)
|
|
79
90
|
}
|
|
80
91
|
|
|
92
|
+
reapplyScrollIntent(sessionId: string, tabId: string, intent: ScrollIntent): void {
|
|
93
|
+
this.getOrCreateRegistry(sessionId).reapplyScrollIntent(tabId, intent)
|
|
94
|
+
}
|
|
95
|
+
|
|
81
96
|
setActiveTab(sessionId: string, tabId: string | null): void {
|
|
82
97
|
this.getOrCreateRegistry(sessionId).setActiveTab(tabId)
|
|
83
98
|
}
|