@jacexh/claude-web-console 0.2.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 +102 -0
- package/bin/cli.js +64 -0
- package/claude-web-console.service +26 -0
- package/client/dist/assets/_baseUniq-Cnoz3HXL.js +1 -0
- package/client/dist/assets/arc-Z_2trbN8.js +1 -0
- package/client/dist/assets/architectureDiagram-Q4EWVU46-BkJc1Y5A.js +36 -0
- package/client/dist/assets/blockDiagram-DXYQGD6D-DajeZur_.js +132 -0
- package/client/dist/assets/c4Diagram-AHTNJAMY-DPrSiSIe.js +10 -0
- package/client/dist/assets/channel-xmJgF8AT.js +1 -0
- package/client/dist/assets/chunk-4BX2VUAB-Ap8VOuEx.js +1 -0
- package/client/dist/assets/chunk-4TB4RGXK-7WUmxI0T.js +206 -0
- package/client/dist/assets/chunk-55IACEB6-gyKf_szG.js +1 -0
- package/client/dist/assets/chunk-EDXVE4YY-Dqsdf8RE.js +1 -0
- package/client/dist/assets/chunk-FMBD7UC4-CFwejjYg.js +15 -0
- package/client/dist/assets/chunk-OYMX7WX6-DjhMJFGF.js +231 -0
- package/client/dist/assets/chunk-QZHKN3VN-BZl3lFpB.js +1 -0
- package/client/dist/assets/chunk-YZCP3GAM-Bgp9VGoJ.js +1 -0
- package/client/dist/assets/classDiagram-6PBFFD2Q-CUsL9OeO.js +1 -0
- package/client/dist/assets/classDiagram-v2-HSJHXN6E-CUsL9OeO.js +1 -0
- package/client/dist/assets/clone-YPoNuUWg.js +1 -0
- package/client/dist/assets/cose-bilkent-S5V4N54A-BhMgNxzB.js +1 -0
- package/client/dist/assets/cytoscape.esm-BQaXIfA_.js +331 -0
- package/client/dist/assets/dagre-KV5264BT-Im4xvruI.js +4 -0
- package/client/dist/assets/defaultLocale-DX6XiGOO.js +1 -0
- package/client/dist/assets/diagram-5BDNPKRD-C_LUH1t_.js +10 -0
- package/client/dist/assets/diagram-G4DWMVQ6-ovGPIIZa.js +24 -0
- package/client/dist/assets/diagram-MMDJMWI5-Cc_S6QYf.js +43 -0
- package/client/dist/assets/diagram-TYMM5635-DNtJ4zRJ.js +24 -0
- package/client/dist/assets/erDiagram-SMLLAGMA-mjVqYF51.js +85 -0
- package/client/dist/assets/flowDiagram-DWJPFMVM-gfeU3vbm.js +162 -0
- package/client/dist/assets/ganttDiagram-T4ZO3ILL-DHzKAwPu.js +292 -0
- package/client/dist/assets/gitGraphDiagram-UUTBAWPF-NmVr9v2T.js +106 -0
- package/client/dist/assets/graph-DWZBkQul.js +1 -0
- package/client/dist/assets/index-Bs3edPh7.js +329 -0
- package/client/dist/assets/index-BvGI0U64.css +1 -0
- package/client/dist/assets/infoDiagram-42DDH7IO-uZQs2FWg.js +2 -0
- package/client/dist/assets/init-Gi6I4Gst.js +1 -0
- package/client/dist/assets/ishikawaDiagram-UXIWVN3A-CVqWYfyh.js +70 -0
- package/client/dist/assets/journeyDiagram-VCZTEJTY-BM-S2Uek.js +139 -0
- package/client/dist/assets/kanban-definition-6JOO6SKY-DtfrMWsW.js +89 -0
- package/client/dist/assets/katex-DkKDou_j.js +257 -0
- package/client/dist/assets/layout-dcq7fgvs.js +1 -0
- package/client/dist/assets/linear-B-FGkSzh.js +1 -0
- package/client/dist/assets/mermaid.core-DDFJ1A5Z.js +309 -0
- package/client/dist/assets/min-DD4RQeGA.js +1 -0
- package/client/dist/assets/mindmap-definition-QFDTVHPH-B1E5NbCo.js +96 -0
- package/client/dist/assets/ordinal-Cboi1Yqb.js +1 -0
- package/client/dist/assets/pieDiagram-DEJITSTG-Ciel1XRV.js +30 -0
- package/client/dist/assets/quadrantDiagram-34T5L4WZ-D1jxP0fH.js +7 -0
- package/client/dist/assets/requirementDiagram-MS252O5E-CTXKAocB.js +84 -0
- package/client/dist/assets/sankeyDiagram-XADWPNL6-Dj-i0BnN.js +10 -0
- package/client/dist/assets/sequenceDiagram-FGHM5R23-kCI4qbes.js +157 -0
- package/client/dist/assets/stateDiagram-FHFEXIEX-CiE-lplM.js +1 -0
- package/client/dist/assets/stateDiagram-v2-QKLJ7IA2-ZPI5JmoG.js +1 -0
- package/client/dist/assets/timeline-definition-GMOUNBTQ-MoAQpvF_.js +120 -0
- package/client/dist/assets/vennDiagram-DHZGUBPP-BqTjHn6B.js +34 -0
- package/client/dist/assets/wardley-RL74JXVD-C3OshQuH.js +162 -0
- package/client/dist/assets/wardleyDiagram-NUSXRM2D-Dl-s9oxJ.js +20 -0
- package/client/dist/assets/xychartDiagram-5P7HB3ND-BqyQEiVY.js +7 -0
- package/client/dist/index.html +16 -0
- package/package.json +57 -0
- package/server/src/index.ts +52 -0
- package/server/src/session-manager.ts +765 -0
- package/server/src/types.ts +295 -0
- package/server/src/ws-handler.ts +355 -0
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
// === Client → Server Messages ===
|
|
2
|
+
|
|
3
|
+
export interface CreateSessionMessage {
|
|
4
|
+
type: 'create_session'
|
|
5
|
+
options?: {
|
|
6
|
+
model?: string
|
|
7
|
+
cwd?: string
|
|
8
|
+
permissionMode?: string
|
|
9
|
+
executableArgs?: string[]
|
|
10
|
+
env?: Record<string, string>
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export interface SendMessageMessage {
|
|
15
|
+
type: 'send_message'
|
|
16
|
+
sessionId: string
|
|
17
|
+
content: string
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export interface SwitchSessionMessage {
|
|
21
|
+
type: 'switch_session'
|
|
22
|
+
sessionId: string
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface PermissionDecisionMessage {
|
|
26
|
+
type: 'permission_decision'
|
|
27
|
+
toolUseId: string
|
|
28
|
+
approved: boolean
|
|
29
|
+
reason?: string
|
|
30
|
+
alwaysAllow?: boolean
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export interface ListSessionsMessage {
|
|
34
|
+
type: 'list_sessions'
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ListFilesMessage {
|
|
38
|
+
type: 'list_files'
|
|
39
|
+
prefix: string
|
|
40
|
+
sessionId?: string
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface GetDefaultCwdMessage {
|
|
44
|
+
type: 'get_default_cwd'
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export interface ListCommandsMessage {
|
|
48
|
+
type: 'list_commands'
|
|
49
|
+
sessionId: string
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export interface ResumeSessionMessage {
|
|
53
|
+
type: 'resume_session'
|
|
54
|
+
sessionId: string
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
export interface CloseSessionMessage {
|
|
58
|
+
type: 'close_session'
|
|
59
|
+
sessionId: string
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
export interface SetModelMessage {
|
|
63
|
+
type: 'set_model'
|
|
64
|
+
sessionId: string
|
|
65
|
+
model: string
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export interface ListModelsMessage {
|
|
69
|
+
type: 'list_models'
|
|
70
|
+
sessionId: string
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export interface RenameSessionMessage {
|
|
74
|
+
type: 'rename_session'
|
|
75
|
+
sessionId: string
|
|
76
|
+
title: string
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
export interface ForkSessionMessage {
|
|
80
|
+
type: 'fork_session'
|
|
81
|
+
sessionId: string
|
|
82
|
+
upToMessageId: string
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export type EffortLevel = 'low' | 'medium' | 'high' | 'max'
|
|
86
|
+
|
|
87
|
+
export interface SetEffortLevelMessage {
|
|
88
|
+
type: 'set_effort_level'
|
|
89
|
+
sessionId: string
|
|
90
|
+
level: EffortLevel
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
export interface GetSubagentMessagesMessage {
|
|
94
|
+
type: 'get_subagent_messages'
|
|
95
|
+
sessionId: string
|
|
96
|
+
agentId: string
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
export interface ElicitationResponseMessage {
|
|
100
|
+
type: 'elicitation_response'
|
|
101
|
+
id: string
|
|
102
|
+
action: 'accept' | 'decline' | 'cancel'
|
|
103
|
+
content?: Record<string, unknown>
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export interface GetSessionSettingsMessage {
|
|
107
|
+
type: 'get_session_settings'
|
|
108
|
+
sessionId: string
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
export type ClientMessage =
|
|
112
|
+
| CreateSessionMessage
|
|
113
|
+
| SendMessageMessage
|
|
114
|
+
| SwitchSessionMessage
|
|
115
|
+
| PermissionDecisionMessage
|
|
116
|
+
| ListSessionsMessage
|
|
117
|
+
| ListFilesMessage
|
|
118
|
+
| GetDefaultCwdMessage
|
|
119
|
+
| ListCommandsMessage
|
|
120
|
+
| ResumeSessionMessage
|
|
121
|
+
| CloseSessionMessage
|
|
122
|
+
| SetModelMessage
|
|
123
|
+
| ListModelsMessage
|
|
124
|
+
| RenameSessionMessage
|
|
125
|
+
| ForkSessionMessage
|
|
126
|
+
| SetEffortLevelMessage
|
|
127
|
+
| GetSubagentMessagesMessage
|
|
128
|
+
| ElicitationResponseMessage
|
|
129
|
+
| GetSessionSettingsMessage
|
|
130
|
+
|
|
131
|
+
// === Server → Client Messages ===
|
|
132
|
+
|
|
133
|
+
export interface SessionCreatedMessage {
|
|
134
|
+
type: 'session_created'
|
|
135
|
+
sessionId: string
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
export interface SessionListMessage {
|
|
139
|
+
type: 'session_list'
|
|
140
|
+
sessions: SessionInfo[]
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export interface SessionInfo {
|
|
144
|
+
sessionId: string
|
|
145
|
+
summary: string
|
|
146
|
+
lastModified: number
|
|
147
|
+
status: 'idle' | 'running'
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
export interface SdkEventMessage {
|
|
151
|
+
type: 'sdk_message'
|
|
152
|
+
sessionId: string
|
|
153
|
+
message: unknown // SDK message, typed on client side
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
export interface PermissionRequestMessage {
|
|
157
|
+
type: 'permission_request'
|
|
158
|
+
sessionId: string
|
|
159
|
+
toolUseId: string
|
|
160
|
+
toolName: string
|
|
161
|
+
input: Record<string, unknown>
|
|
162
|
+
agentId?: string
|
|
163
|
+
title?: string
|
|
164
|
+
description?: string
|
|
165
|
+
hasSuggestions?: boolean
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
export interface ErrorMessage {
|
|
169
|
+
type: 'error'
|
|
170
|
+
message: string
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
export interface SessionEndMessage {
|
|
174
|
+
type: 'session_end'
|
|
175
|
+
sessionId: string
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
export interface SessionHistoryMessage {
|
|
179
|
+
type: 'session_history'
|
|
180
|
+
sessionId: string
|
|
181
|
+
messages: unknown[]
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
export interface SessionIdResolvedMessage {
|
|
185
|
+
type: 'session_id_resolved'
|
|
186
|
+
tempId: string
|
|
187
|
+
sessionId: string
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
export interface FileListMessage {
|
|
191
|
+
type: 'file_list'
|
|
192
|
+
files: FileEntry[]
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
export interface FileEntry {
|
|
196
|
+
name: string
|
|
197
|
+
path: string
|
|
198
|
+
isDir: boolean
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
export interface DefaultCwdMessage {
|
|
202
|
+
type: 'default_cwd'
|
|
203
|
+
cwd: string
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
export interface CommandListMessage {
|
|
207
|
+
type: 'command_list'
|
|
208
|
+
commands: { name: string; description: string }[]
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export interface PermissionDecidedMessage {
|
|
212
|
+
type: 'permission_decided'
|
|
213
|
+
toolUseId: string
|
|
214
|
+
approved: boolean
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
export interface SessionResumedMessage {
|
|
218
|
+
type: 'session_resumed'
|
|
219
|
+
sessionId: string
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
export interface ModelInfo {
|
|
223
|
+
value: string
|
|
224
|
+
displayName: string
|
|
225
|
+
description: string
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
export interface ModelListMessage {
|
|
229
|
+
type: 'model_list'
|
|
230
|
+
sessionId: string
|
|
231
|
+
models: ModelInfo[]
|
|
232
|
+
currentModel?: string
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
export interface SessionRenamedMessage {
|
|
236
|
+
type: 'session_renamed'
|
|
237
|
+
sessionId: string
|
|
238
|
+
title: string
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export interface SessionForkedMessage {
|
|
242
|
+
type: 'session_forked'
|
|
243
|
+
sessionId: string
|
|
244
|
+
newSessionId: string
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
export interface EffortLevelChangedMessage {
|
|
248
|
+
type: 'effort_level_changed'
|
|
249
|
+
sessionId: string
|
|
250
|
+
level: EffortLevel
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export interface SubagentMessagesMessage {
|
|
254
|
+
type: 'subagent_messages'
|
|
255
|
+
agentId: string
|
|
256
|
+
messages: unknown[]
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
export interface ElicitationRequestMessage {
|
|
260
|
+
type: 'elicitation_request'
|
|
261
|
+
id: string
|
|
262
|
+
serverName: string
|
|
263
|
+
message: string
|
|
264
|
+
mode?: string
|
|
265
|
+
requestedSchema?: Record<string, unknown>
|
|
266
|
+
url?: string
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export interface SessionSettingsMessage {
|
|
270
|
+
type: 'session_settings'
|
|
271
|
+
sessionId: string
|
|
272
|
+
settings: Record<string, unknown>
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
export type ServerMessage =
|
|
276
|
+
| SessionCreatedMessage
|
|
277
|
+
| SessionListMessage
|
|
278
|
+
| SdkEventMessage
|
|
279
|
+
| PermissionRequestMessage
|
|
280
|
+
| ErrorMessage
|
|
281
|
+
| SessionEndMessage
|
|
282
|
+
| SessionHistoryMessage
|
|
283
|
+
| SessionIdResolvedMessage
|
|
284
|
+
| FileListMessage
|
|
285
|
+
| DefaultCwdMessage
|
|
286
|
+
| CommandListMessage
|
|
287
|
+
| PermissionDecidedMessage
|
|
288
|
+
| SessionResumedMessage
|
|
289
|
+
| ModelListMessage
|
|
290
|
+
| SessionRenamedMessage
|
|
291
|
+
| SessionForkedMessage
|
|
292
|
+
| EffortLevelChangedMessage
|
|
293
|
+
| SubagentMessagesMessage
|
|
294
|
+
| ElicitationRequestMessage
|
|
295
|
+
| SessionSettingsMessage
|
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
import type { WebSocket } from '@fastify/websocket'
|
|
2
|
+
import { randomUUID } from 'node:crypto'
|
|
3
|
+
import { readdir, mkdir } from 'node:fs/promises'
|
|
4
|
+
import { join, resolve, relative, basename, dirname } from 'node:path'
|
|
5
|
+
import type { SessionManager, PermissionMeta, SessionListener } from './session-manager.js'
|
|
6
|
+
import type { ClientMessage, ServerMessage, FileEntry, EffortLevel } from './types.js'
|
|
7
|
+
|
|
8
|
+
export function createWsHandler(sessionManager: SessionManager) {
|
|
9
|
+
return async function handleConnection(socket: WebSocket): Promise<void> {
|
|
10
|
+
const listenerId = randomUUID()
|
|
11
|
+
let alive = true
|
|
12
|
+
|
|
13
|
+
// --- Heartbeat: 30s ping interval ---
|
|
14
|
+
const pingInterval = setInterval(() => {
|
|
15
|
+
if (!alive) {
|
|
16
|
+
socket.terminate()
|
|
17
|
+
return
|
|
18
|
+
}
|
|
19
|
+
alive = false
|
|
20
|
+
socket.ping()
|
|
21
|
+
}, 30_000)
|
|
22
|
+
|
|
23
|
+
socket.on('pong', () => {
|
|
24
|
+
alive = true
|
|
25
|
+
})
|
|
26
|
+
|
|
27
|
+
function send(msg: ServerMessage): void {
|
|
28
|
+
if (socket.readyState === socket.OPEN) {
|
|
29
|
+
socket.send(JSON.stringify(msg))
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function makeListener(sessionId: string): SessionListener {
|
|
34
|
+
return {
|
|
35
|
+
id: listenerId,
|
|
36
|
+
onMessage(sid, message) {
|
|
37
|
+
const msg = message as Record<string, unknown>
|
|
38
|
+
|
|
39
|
+
// Handle synthetic messages from SessionManager
|
|
40
|
+
if (msg.type === 'permission_decided') {
|
|
41
|
+
send({
|
|
42
|
+
type: 'permission_decided',
|
|
43
|
+
toolUseId: msg.toolUseId as string,
|
|
44
|
+
approved: msg.approved as boolean,
|
|
45
|
+
})
|
|
46
|
+
return
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (msg.type === 'session_id_resolved') {
|
|
50
|
+
send({
|
|
51
|
+
type: 'session_id_resolved',
|
|
52
|
+
tempId: msg.tempId as string,
|
|
53
|
+
sessionId: msg.sessionId as string,
|
|
54
|
+
})
|
|
55
|
+
return
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
if (msg.type === 'session_resumed') {
|
|
59
|
+
send({
|
|
60
|
+
type: 'session_resumed',
|
|
61
|
+
sessionId: msg.sessionId as string,
|
|
62
|
+
})
|
|
63
|
+
return
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
if (msg.type === 'session_renamed') {
|
|
67
|
+
send({
|
|
68
|
+
type: 'session_renamed',
|
|
69
|
+
sessionId: msg.sessionId as string,
|
|
70
|
+
title: msg.title as string,
|
|
71
|
+
})
|
|
72
|
+
return
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
if (msg.type === 'session_forked') {
|
|
76
|
+
send({
|
|
77
|
+
type: 'session_forked',
|
|
78
|
+
sessionId: msg.sessionId as string,
|
|
79
|
+
newSessionId: msg.newSessionId as string,
|
|
80
|
+
})
|
|
81
|
+
return
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (msg.type === 'effort_level_changed') {
|
|
85
|
+
send({
|
|
86
|
+
type: 'effort_level_changed',
|
|
87
|
+
sessionId: msg.sessionId as string,
|
|
88
|
+
level: msg.level as EffortLevel,
|
|
89
|
+
})
|
|
90
|
+
return
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
if (msg.type === 'elicitation_request') {
|
|
94
|
+
send({
|
|
95
|
+
type: 'elicitation_request',
|
|
96
|
+
id: msg.id as string,
|
|
97
|
+
serverName: msg.serverName as string,
|
|
98
|
+
message: msg.message as string,
|
|
99
|
+
mode: msg.mode as string | undefined,
|
|
100
|
+
requestedSchema: msg.requestedSchema as Record<string, unknown> | undefined,
|
|
101
|
+
url: msg.url as string | undefined,
|
|
102
|
+
})
|
|
103
|
+
return
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// Auto-push command list on init or commands_updated
|
|
107
|
+
if ((msg.type === 'system' && msg.subtype === 'init') || msg.type === 'commands_updated') {
|
|
108
|
+
sessionManager.getCommands(sid).then((commands) => {
|
|
109
|
+
if (commands.length > 0) {
|
|
110
|
+
send({ type: 'command_list', commands })
|
|
111
|
+
}
|
|
112
|
+
}).catch(() => {})
|
|
113
|
+
if (msg.type === 'commands_updated') return // don't forward synthetic message
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
// Push model list when available
|
|
117
|
+
if (msg.type === 'models_updated') {
|
|
118
|
+
send({
|
|
119
|
+
type: 'model_list',
|
|
120
|
+
sessionId: sid,
|
|
121
|
+
models: msg.models as { value: string; displayName: string; description: string }[],
|
|
122
|
+
currentModel: msg.currentModel as string | undefined,
|
|
123
|
+
})
|
|
124
|
+
return // don't forward synthetic message
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
send({ type: 'sdk_message', sessionId: sid, message })
|
|
128
|
+
},
|
|
129
|
+
onPermissionRequest(sid, toolUseId, toolName, input, meta) {
|
|
130
|
+
send({ type: 'permission_request', sessionId: sid, toolUseId, toolName, input, ...meta })
|
|
131
|
+
},
|
|
132
|
+
onEnd(sid) {
|
|
133
|
+
send({ type: 'session_end', sessionId: sid })
|
|
134
|
+
},
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
// --- Cleanup on close/error ---
|
|
139
|
+
function cleanup(): void {
|
|
140
|
+
clearInterval(pingInterval)
|
|
141
|
+
sessionManager.unsubscribeAll(listenerId)
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
socket.on('close', cleanup)
|
|
145
|
+
socket.on('error', cleanup)
|
|
146
|
+
|
|
147
|
+
// --- Message handling ---
|
|
148
|
+
socket.on('message', async (raw: Buffer) => {
|
|
149
|
+
alive = true
|
|
150
|
+
|
|
151
|
+
let msg: ClientMessage
|
|
152
|
+
try {
|
|
153
|
+
msg = JSON.parse(raw.toString()) as ClientMessage
|
|
154
|
+
} catch {
|
|
155
|
+
send({ type: 'error', message: 'Invalid JSON' })
|
|
156
|
+
return
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
switch (msg.type) {
|
|
161
|
+
case 'create_session': {
|
|
162
|
+
// Ensure cwd exists before creating session
|
|
163
|
+
const cwd = msg.options?.cwd
|
|
164
|
+
if (cwd) {
|
|
165
|
+
await mkdir(cwd, { recursive: true })
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const tempId = await sessionManager.createSession(msg.options)
|
|
169
|
+
sessionManager.subscribe(tempId, makeListener(tempId))
|
|
170
|
+
send({ type: 'session_created', sessionId: tempId })
|
|
171
|
+
break
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
case 'resume_session': {
|
|
175
|
+
// Subscribe BEFORE resume so this connection receives the broadcast too
|
|
176
|
+
sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
|
|
177
|
+
await sessionManager.resumeSession(msg.sessionId)
|
|
178
|
+
break
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
case 'send_message': {
|
|
182
|
+
// Subscribe for live updates (idempotent via listener dedup)
|
|
183
|
+
sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
|
|
184
|
+
await sessionManager.sendMessage(msg.sessionId, msg.content)
|
|
185
|
+
console.log('[ws] Message sent to session', msg.sessionId)
|
|
186
|
+
break
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
case 'switch_session': {
|
|
190
|
+
// Load historical messages
|
|
191
|
+
const history = await sessionManager.getHistory(msg.sessionId)
|
|
192
|
+
send({ type: 'session_history', sessionId: msg.sessionId, messages: history })
|
|
193
|
+
// Subscribe for live updates if session is running
|
|
194
|
+
sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
|
|
195
|
+
break
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
case 'close_session': {
|
|
199
|
+
sessionManager.closeSession(msg.sessionId)
|
|
200
|
+
// Listeners get session_end via broadcast inside closeSession
|
|
201
|
+
break
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
case 'permission_decision': {
|
|
205
|
+
sessionManager.resolvePermission(
|
|
206
|
+
msg.toolUseId,
|
|
207
|
+
msg.approved,
|
|
208
|
+
msg.reason,
|
|
209
|
+
msg.alwaysAllow,
|
|
210
|
+
)
|
|
211
|
+
break
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
case 'list_sessions': {
|
|
215
|
+
const sessions = await sessionManager.listSessions()
|
|
216
|
+
send({ type: 'session_list', sessions })
|
|
217
|
+
break
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
case 'list_files': {
|
|
221
|
+
const cwd = sessionManager.getCwd(msg.sessionId)
|
|
222
|
+
const prefix = msg.prefix || ''
|
|
223
|
+
const files = await listFiles(cwd, prefix)
|
|
224
|
+
send({ type: 'file_list', files })
|
|
225
|
+
break
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
case 'get_default_cwd': {
|
|
229
|
+
const cwd = sessionManager.getCwd()
|
|
230
|
+
send({ type: 'default_cwd', cwd })
|
|
231
|
+
break
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
case 'list_commands': {
|
|
235
|
+
const commands = await sessionManager.getCommands(msg.sessionId)
|
|
236
|
+
send({ type: 'command_list', commands })
|
|
237
|
+
break
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
case 'set_model': {
|
|
241
|
+
await sessionManager.setModel(msg.sessionId, msg.model)
|
|
242
|
+
break
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
case 'list_models': {
|
|
246
|
+
const models = await sessionManager.getSupportedModels(msg.sessionId)
|
|
247
|
+
if (models.length > 0) {
|
|
248
|
+
send({ type: 'model_list', sessionId: msg.sessionId, models })
|
|
249
|
+
}
|
|
250
|
+
break
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
case 'rename_session': {
|
|
254
|
+
await sessionManager.renameSession(msg.sessionId, msg.title)
|
|
255
|
+
break
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
case 'fork_session': {
|
|
259
|
+
await sessionManager.forkSession(msg.sessionId, msg.upToMessageId)
|
|
260
|
+
break
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
case 'set_effort_level': {
|
|
264
|
+
await sessionManager.setEffortLevel(msg.sessionId, msg.level)
|
|
265
|
+
break
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
case 'get_subagent_messages': {
|
|
269
|
+
const messages = await sessionManager.getSubagentMessages(msg.sessionId, msg.agentId)
|
|
270
|
+
send({ type: 'subagent_messages', sessionId: msg.sessionId, agentId: msg.agentId, messages })
|
|
271
|
+
break
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
case 'elicitation_response': {
|
|
275
|
+
sessionManager.resolveElicitation(msg.id, msg.action, msg.content as Record<string, unknown> | undefined)
|
|
276
|
+
break
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
case 'get_session_settings': {
|
|
280
|
+
const settings = await sessionManager.getSessionSettings(msg.sessionId)
|
|
281
|
+
send({ type: 'session_settings', sessionId: msg.sessionId, settings })
|
|
282
|
+
break
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
} catch (err) {
|
|
286
|
+
send({ type: 'error', message: err instanceof Error ? err.message : 'Unknown error' })
|
|
287
|
+
}
|
|
288
|
+
})
|
|
289
|
+
|
|
290
|
+
// Send initial session list on connect
|
|
291
|
+
try {
|
|
292
|
+
const sessions = await sessionManager.listSessions()
|
|
293
|
+
send({ type: 'session_list', sessions })
|
|
294
|
+
} catch {
|
|
295
|
+
// Non-fatal: proceed without initial session list
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
const IGNORED = new Set(['.git', 'node_modules', '.next', 'dist', '__pycache__', '.venv', 'venv', '.cache'])
|
|
301
|
+
const MAX_FILES = 50
|
|
302
|
+
|
|
303
|
+
async function listFiles(cwd: string, prefix: string): Promise<FileEntry[]> {
|
|
304
|
+
// Determine which directory to read based on the prefix
|
|
305
|
+
// e.g. prefix="src/comp" → dir="src", namePrefix="comp"
|
|
306
|
+
const targetPath = resolve(cwd, prefix)
|
|
307
|
+
let dir: string
|
|
308
|
+
let namePrefix: string
|
|
309
|
+
|
|
310
|
+
try {
|
|
311
|
+
const entries = await readdir(targetPath, { withFileTypes: true })
|
|
312
|
+
// prefix points to a directory, list its contents
|
|
313
|
+
dir = targetPath
|
|
314
|
+
namePrefix = ''
|
|
315
|
+
return entriesToFiles(entries, dir, cwd, namePrefix)
|
|
316
|
+
} catch {
|
|
317
|
+
// prefix is partial — list the parent directory and filter
|
|
318
|
+
dir = dirname(targetPath)
|
|
319
|
+
namePrefix = basename(targetPath).toLowerCase()
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
try {
|
|
323
|
+
const entries = await readdir(dir, { withFileTypes: true })
|
|
324
|
+
return entriesToFiles(entries, dir, cwd, namePrefix)
|
|
325
|
+
} catch {
|
|
326
|
+
return []
|
|
327
|
+
}
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function entriesToFiles(
|
|
331
|
+
entries: import('node:fs').Dirent[],
|
|
332
|
+
dir: string,
|
|
333
|
+
cwd: string,
|
|
334
|
+
namePrefix: string,
|
|
335
|
+
): FileEntry[] {
|
|
336
|
+
const results: FileEntry[] = []
|
|
337
|
+
for (const entry of entries) {
|
|
338
|
+
if (entry.name.startsWith('.') && !namePrefix.startsWith('.')) continue
|
|
339
|
+
if (IGNORED.has(entry.name)) continue
|
|
340
|
+
if (namePrefix && !entry.name.toLowerCase().startsWith(namePrefix)) continue
|
|
341
|
+
const fullPath = join(dir, entry.name)
|
|
342
|
+
results.push({
|
|
343
|
+
name: entry.name,
|
|
344
|
+
path: fullPath + (entry.isDirectory() ? '/' : ''),
|
|
345
|
+
isDir: entry.isDirectory(),
|
|
346
|
+
})
|
|
347
|
+
if (results.length >= MAX_FILES) break
|
|
348
|
+
}
|
|
349
|
+
// Directories first, then files, alphabetical
|
|
350
|
+
results.sort((a, b) => {
|
|
351
|
+
if (a.isDir !== b.isDir) return a.isDir ? -1 : 1
|
|
352
|
+
return a.name.localeCompare(b.name)
|
|
353
|
+
})
|
|
354
|
+
return results
|
|
355
|
+
}
|