@jacexh/claude-web-console 0.12.4 → 0.13.0
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/client/dist/assets/{_baseUniq-4U_gS4M1.js → _baseUniq-B6sYeW4g.js} +1 -1
- package/client/dist/assets/{arc-C59lV2XZ.js → arc-B3qp8GtI.js} +1 -1
- package/client/dist/assets/{architectureDiagram-Q4EWVU46-De8p0m6N.js → architectureDiagram-Q4EWVU46-DUaflb5F.js} +1 -1
- package/client/dist/assets/{blockDiagram-DXYQGD6D-BEzLwzF0.js → blockDiagram-DXYQGD6D-dJ5MP9NF.js} +1 -1
- package/client/dist/assets/{c4Diagram-AHTNJAMY-DGkH6t15.js → c4Diagram-AHTNJAMY-BUyLffeg.js} +1 -1
- package/client/dist/assets/channel-Hdhu6NSU.js +1 -0
- package/client/dist/assets/{chunk-4BX2VUAB-DCoVX0i8.js → chunk-4BX2VUAB-CL6RSkDB.js} +1 -1
- package/client/dist/assets/{chunk-4TB4RGXK-dGyuU23q.js → chunk-4TB4RGXK-Bpos2uLs.js} +1 -1
- package/client/dist/assets/{chunk-55IACEB6-DDiu2l5t.js → chunk-55IACEB6-DXBFgKxO.js} +1 -1
- package/client/dist/assets/{chunk-EDXVE4YY-B_5Djzmq.js → chunk-EDXVE4YY-2YRg8OOI.js} +1 -1
- package/client/dist/assets/{chunk-FMBD7UC4-O6auhXOZ.js → chunk-FMBD7UC4-BBAsqSsh.js} +1 -1
- package/client/dist/assets/{chunk-OYMX7WX6-C2l5EG0a.js → chunk-OYMX7WX6-kkD25xqj.js} +1 -1
- package/client/dist/assets/{chunk-QZHKN3VN-CGKUgT3w.js → chunk-QZHKN3VN-BFbEMvTe.js} +1 -1
- package/client/dist/assets/{chunk-YZCP3GAM-CSmbvz_K.js → chunk-YZCP3GAM-CCNsuiYn.js} +1 -1
- package/client/dist/assets/classDiagram-6PBFFD2Q-DDpCsFzN.js +1 -0
- package/client/dist/assets/classDiagram-v2-HSJHXN6E-DDpCsFzN.js +1 -0
- package/client/dist/assets/clone-CyGsvajQ.js +1 -0
- package/client/dist/assets/{cose-bilkent-S5V4N54A-DtjeJt3O.js → cose-bilkent-S5V4N54A-CFic2XgR.js} +1 -1
- package/client/dist/assets/{dagre-KV5264BT-CBGSZE2K.js → dagre-KV5264BT-DmhbJsrh.js} +1 -1
- package/client/dist/assets/{diagram-5BDNPKRD-DWej4_Dm.js → diagram-5BDNPKRD-EMWFsNjs.js} +1 -1
- package/client/dist/assets/{diagram-G4DWMVQ6-CsBZJklM.js → diagram-G4DWMVQ6-D4bNVW2H.js} +1 -1
- package/client/dist/assets/{diagram-MMDJMWI5-CAnJ5oHd.js → diagram-MMDJMWI5-C_YYb2VJ.js} +1 -1
- package/client/dist/assets/{diagram-TYMM5635-BNwTUJzz.js → diagram-TYMM5635-C-Uatjas.js} +1 -1
- package/client/dist/assets/{erDiagram-SMLLAGMA-CwJQuBj5.js → erDiagram-SMLLAGMA-Zk5yMi0J.js} +1 -1
- package/client/dist/assets/{flowDiagram-DWJPFMVM-COosf1Qi.js → flowDiagram-DWJPFMVM-DQFo3wKV.js} +1 -1
- package/client/dist/assets/{ganttDiagram-T4ZO3ILL-_aInr4aJ.js → ganttDiagram-T4ZO3ILL-BvIdlFEU.js} +1 -1
- package/client/dist/assets/{gitGraphDiagram-UUTBAWPF-DcXA_pZG.js → gitGraphDiagram-UUTBAWPF-IyBYKltU.js} +1 -1
- package/client/dist/assets/{graph-BLaCZz6d.js → graph-Cjv1Iudr.js} +1 -1
- package/client/dist/assets/index-3fTJUkcw.css +1 -0
- package/client/dist/assets/index-DyP4iMkT.js +359 -0
- package/client/dist/assets/{infoDiagram-42DDH7IO-CRQ7YC07.js → infoDiagram-42DDH7IO-BbbU3b3O.js} +1 -1
- package/client/dist/assets/{ishikawaDiagram-UXIWVN3A-DbI4M_iy.js → ishikawaDiagram-UXIWVN3A-BvTy237T.js} +1 -1
- package/client/dist/assets/{journeyDiagram-VCZTEJTY-DnivGAau.js → journeyDiagram-VCZTEJTY-CvqHRg-v.js} +1 -1
- package/client/dist/assets/{kanban-definition-6JOO6SKY-BZXMUpR7.js → kanban-definition-6JOO6SKY-_qWxod8i.js} +1 -1
- package/client/dist/assets/{layout-DaCJPJ4j.js → layout-QRga_jZh.js} +1 -1
- package/client/dist/assets/{linear-D3Tj8-2V.js → linear-dO_OA3Zq.js} +1 -1
- package/client/dist/assets/{mermaid.core-D970jp78.js → mermaid.core-DC6AubH-.js} +4 -4
- package/client/dist/assets/{min-7eDP7Usr.js → min-C-W75DLw.js} +1 -1
- package/client/dist/assets/{mindmap-definition-QFDTVHPH-DN_5Nzl7.js → mindmap-definition-QFDTVHPH-DuSy2wWg.js} +1 -1
- package/client/dist/assets/{pieDiagram-DEJITSTG-BqiFSuiC.js → pieDiagram-DEJITSTG-K-01TW82.js} +1 -1
- package/client/dist/assets/{quadrantDiagram-34T5L4WZ-1bj64KPR.js → quadrantDiagram-34T5L4WZ-CH5sCVdG.js} +1 -1
- package/client/dist/assets/{requirementDiagram-MS252O5E-d36Oj16U.js → requirementDiagram-MS252O5E-qNmh8nmD.js} +1 -1
- package/client/dist/assets/{sankeyDiagram-XADWPNL6-DWZ3s91n.js → sankeyDiagram-XADWPNL6-DDrfQolI.js} +1 -1
- package/client/dist/assets/{sequenceDiagram-FGHM5R23-C3l3gLwX.js → sequenceDiagram-FGHM5R23-BYXjyIvv.js} +1 -1
- package/client/dist/assets/{stateDiagram-FHFEXIEX-_Ee_DnHr.js → stateDiagram-FHFEXIEX-DKa3Ub_p.js} +1 -1
- package/client/dist/assets/stateDiagram-v2-QKLJ7IA2-DW-O7br7.js +1 -0
- package/client/dist/assets/{timeline-definition-GMOUNBTQ-BtuhmgP-.js → timeline-definition-GMOUNBTQ-BdeASN9k.js} +1 -1
- package/client/dist/assets/{vennDiagram-DHZGUBPP-B67BiofD.js → vennDiagram-DHZGUBPP-DjAjT_jR.js} +1 -1
- package/client/dist/assets/{wardley-RL74JXVD-Da_3UXKp.js → wardley-RL74JXVD-B0cvAuVw.js} +1 -1
- package/client/dist/assets/{wardleyDiagram-NUSXRM2D-CNL4fx4y.js → wardleyDiagram-NUSXRM2D-DSRvUIwc.js} +1 -1
- package/client/dist/assets/{xychartDiagram-5P7HB3ND-CXovLg1r.js → xychartDiagram-5P7HB3ND-Cs8afIsZ.js} +1 -1
- package/client/dist/index.html +2 -2
- package/package.json +1 -1
- package/server/src/__tests__/http-routes.test.ts +28 -14
- package/server/src/__tests__/turn-started-broadcast.test.ts +0 -20
- package/server/src/http-routes.ts +18 -6
- package/server/src/session-manager.ts +155 -147
- package/server/src/turn-lifecycle.ts +4 -1
- package/server/src/types.ts +14 -7
- package/server/src/ws-handler.ts +14 -11
- package/client/dist/assets/channel-DETbjbce.js +0 -1
- package/client/dist/assets/classDiagram-6PBFFD2Q-BcM7pkgM.js +0 -1
- package/client/dist/assets/classDiagram-v2-HSJHXN6E-BcM7pkgM.js +0 -1
- package/client/dist/assets/clone-CBKKAfIW.js +0 -1
- package/client/dist/assets/index-CQyb-QJw.js +0 -358
- package/client/dist/assets/index-MeijXGw4.css +0 -1
- package/client/dist/assets/stateDiagram-v2-QKLJ7IA2-BAk4qFFA.js +0 -1
|
@@ -159,7 +159,6 @@ export class SessionManager {
|
|
|
159
159
|
private streamingSessionIds = new Set<string>()
|
|
160
160
|
private sessionListeners = new Map<string, Set<SessionListener>>()
|
|
161
161
|
private idleTimers = new Map<string, ReturnType<typeof setTimeout>>()
|
|
162
|
-
private pendingRemaps = new Map<string, { sessionIdRef: { current: string } }>()
|
|
163
162
|
// Track cwd for each session so we can resume in the correct project
|
|
164
163
|
private sessionCwds = new Map<string, string>()
|
|
165
164
|
/** User-supplied creation options that must survive resume cycles */
|
|
@@ -350,18 +349,24 @@ export class SessionManager {
|
|
|
350
349
|
}
|
|
351
350
|
|
|
352
351
|
async createSession(
|
|
353
|
-
options
|
|
352
|
+
options: { message: string; cwd?: string; model?: string; permissionMode?: string; effortLevel?: EffortLevel; executableArgs?: string[]; env?: Record<string, string> },
|
|
354
353
|
): Promise<string> {
|
|
355
|
-
const cwd = options
|
|
354
|
+
const cwd = options.cwd ?? process.env.CC_WEB_CONSOLE_CWD ?? process.env.HOME ?? '/'
|
|
356
355
|
const sessionIdRef = { current: '' }
|
|
357
356
|
const pluginArgs = getPluginDirArgs()
|
|
358
|
-
const userArgs = options
|
|
357
|
+
const userArgs = options.executableArgs ?? []
|
|
358
|
+
const opts: SessionCreationOptions = {
|
|
359
|
+
model: options.model,
|
|
360
|
+
permissionMode: options.permissionMode,
|
|
361
|
+
executableArgs: options.executableArgs,
|
|
362
|
+
env: options.env,
|
|
363
|
+
}
|
|
359
364
|
const sessionOptions = {
|
|
360
|
-
...(options
|
|
361
|
-
permissionMode: options
|
|
365
|
+
...(options.model ? { model: options.model } : {}),
|
|
366
|
+
permissionMode: options.permissionMode ?? 'default',
|
|
362
367
|
canUseTool: this.buildCanUseTool(sessionIdRef),
|
|
363
368
|
onElicitation: this.buildOnElicitation(sessionIdRef),
|
|
364
|
-
env: { ...cleanEnv(cwd), ...options
|
|
369
|
+
env: { ...cleanEnv(cwd), ...options.env },
|
|
365
370
|
pathToClaudeCodeExecutable: CLAUDE_EXECUTABLE,
|
|
366
371
|
executableArgs: [...pluginArgs, ...userArgs],
|
|
367
372
|
} as SDKSessionOptions
|
|
@@ -371,28 +376,62 @@ export class SessionManager {
|
|
|
371
376
|
const session = unstable_v2_createSession(sessionOptions)
|
|
372
377
|
try { process.chdir(originalCwd) } catch { /* ignore */ }
|
|
373
378
|
|
|
374
|
-
//
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
//
|
|
391
|
-
|
|
379
|
+
// Send first message — SDK assigns sessionId asynchronously after send()
|
|
380
|
+
this.log.info({ cwd, model: options.model }, 'createSession: sending first message')
|
|
381
|
+
await session.send(options.message)
|
|
382
|
+
|
|
383
|
+
// SDK requires stream() to be consumed for the session to progress.
|
|
384
|
+
// Use a mutable ref so consumeStream picks up the real sessionId once resolved.
|
|
385
|
+
const streamIdRef = { current: `pending-${Date.now()}` }
|
|
386
|
+
this.log.info('createSession: starting stream + waiting for sessionId')
|
|
387
|
+
this.sessions.set(streamIdRef.current, session)
|
|
388
|
+
this.streamingSessionIds.add(streamIdRef.current)
|
|
389
|
+
this.consumeStream(streamIdRef, session)
|
|
390
|
+
|
|
391
|
+
const sessionId = await waitForSessionId(session, 30000)
|
|
392
|
+
this.log.info({ sessionId }, 'createSession: sessionId resolved')
|
|
393
|
+
sessionIdRef.current = sessionId
|
|
394
|
+
|
|
395
|
+
// Remap internal maps from tempKey → real sessionId
|
|
396
|
+
const tempKey = streamIdRef.current === sessionId ? null : streamIdRef.current
|
|
397
|
+
streamIdRef.current = sessionId
|
|
398
|
+
if (tempKey && tempKey !== sessionId) {
|
|
399
|
+
const s = this.sessions.get(tempKey)
|
|
400
|
+
if (s) { this.sessions.delete(tempKey); this.sessions.set(sessionId, s) }
|
|
401
|
+
this.streamingSessionIds.delete(tempKey)
|
|
402
|
+
this.streamingSessionIds.add(sessionId)
|
|
403
|
+
const listeners = this.sessionListeners.get(tempKey)
|
|
404
|
+
if (listeners) { this.sessionListeners.delete(tempKey); this.sessionListeners.set(sessionId, listeners) }
|
|
405
|
+
const q = this.activeQueries.get(tempKey)
|
|
406
|
+
if (q) { this.activeQueries.delete(tempKey); this.activeQueries.set(sessionId, q) }
|
|
407
|
+
const prevStatus = this.sessionStatus.get(tempKey)
|
|
408
|
+
this.sessionStatus.delete(tempKey)
|
|
409
|
+
if (prevStatus) this.sessionStatus.set(sessionId, prevStatus)
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
// Register session state under real sessionId
|
|
413
|
+
this.sessionStatus.set(sessionId, 'running')
|
|
414
|
+
this.sessionCwds.set(sessionId, cwd)
|
|
415
|
+
this.sessionCreationOptions.set(sessionId, opts)
|
|
416
|
+
saveSessionOptions(sessionId, opts)
|
|
417
|
+
|
|
418
|
+
// Pre-populate model/effort so getSessionState() returns them immediately
|
|
419
|
+
// when the client sends switch_session (before consumeStream's init message)
|
|
420
|
+
if (options.model) {
|
|
421
|
+
this.sessionModels.set(sessionId, options.model)
|
|
422
|
+
}
|
|
423
|
+
if (options.effortLevel) {
|
|
424
|
+
this.sessionEffortLevels.set(sessionId, options.effortLevel)
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// Apply effort level after session is running (SDK doesn't accept it in SDKSessionOptions)
|
|
428
|
+
if (options.effortLevel) {
|
|
429
|
+
this.setEffortLevel(sessionId, options.effortLevel).catch((err) => {
|
|
430
|
+
this.log.error({ err, sessionId }, 'Failed to set initial effort level')
|
|
431
|
+
})
|
|
432
|
+
}
|
|
392
433
|
|
|
393
|
-
|
|
394
|
-
// Return tempId now; real sessionId arrives via session_id_resolved WS event after first send().
|
|
395
|
-
return tempId
|
|
434
|
+
return sessionId
|
|
396
435
|
}
|
|
397
436
|
|
|
398
437
|
private fetchAndBroadcastModels(sessionId: string, session: SDKSession, currentModel?: string): void {
|
|
@@ -418,81 +457,68 @@ export class SessionManager {
|
|
|
418
457
|
): void {
|
|
419
458
|
if (this.streamingSessionIds.has(sessionId)) return
|
|
420
459
|
this.streamingSessionIds.add(sessionId)
|
|
421
|
-
this.consumeStream(sessionId, session)
|
|
460
|
+
this.consumeStream({ current: sessionId }, session)
|
|
422
461
|
}
|
|
423
462
|
|
|
424
463
|
private async consumeStream(
|
|
425
|
-
|
|
464
|
+
idRef: { current: string },
|
|
426
465
|
session: SDKSession,
|
|
427
466
|
): Promise<void> {
|
|
428
|
-
//
|
|
429
|
-
|
|
467
|
+
// sessionId is read from ref — may change from tempKey to real ID during initial creation
|
|
468
|
+
const getId = () => idRef.current
|
|
430
469
|
let modelsFetched = false
|
|
431
470
|
|
|
432
471
|
try {
|
|
433
472
|
// SDK's stream() ends after each turn (on "result" message).
|
|
434
473
|
// Loop to re-enter stream() for subsequent turns.
|
|
435
474
|
while (true) {
|
|
436
|
-
this.log.info({ sessionId:
|
|
475
|
+
this.log.info({ sessionId: getId() }, 'consumeStream: entering stream()')
|
|
437
476
|
const query = session.stream()
|
|
438
477
|
const turnState: TurnState = { turnStarted: false }
|
|
439
|
-
|
|
440
|
-
try {
|
|
441
|
-
const sid = session.sessionId
|
|
442
|
-
this.activeQueries.set(sid, query)
|
|
443
|
-
} catch { /* sessionId not yet available, will set after remap */ }
|
|
478
|
+
this.activeQueries.set(getId(), query)
|
|
444
479
|
|
|
445
480
|
for await (const msg of query) {
|
|
481
|
+
const sid = getId()
|
|
446
482
|
const msgAny = msg as Record<string, unknown>
|
|
447
483
|
this.log.info({
|
|
448
484
|
type: msgAny.type,
|
|
449
485
|
subtype: msgAny.subtype,
|
|
450
486
|
parentToolUseId: msgAny.parent_tool_use_id,
|
|
451
|
-
sessionId:
|
|
487
|
+
sessionId: sid,
|
|
452
488
|
message: JSON.stringify(msg).slice(0, 50),
|
|
453
489
|
}, 'Stream message received')
|
|
454
490
|
|
|
455
491
|
// Fetch models once after stream is active (works for both new and resumed sessions)
|
|
456
492
|
if (!modelsFetched) {
|
|
457
493
|
modelsFetched = true
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
// Update query ref now that sessionId is available
|
|
461
|
-
this.activeQueries.set(sid, query)
|
|
462
|
-
this.fetchAndBroadcastModels(sid, session, (msgAny.type === 'system' && msgAny.subtype === 'init') ? (msgAny.model as string) : undefined)
|
|
463
|
-
} catch { /* session not yet initialized */ }
|
|
494
|
+
this.activeQueries.set(sid, query)
|
|
495
|
+
this.fetchAndBroadcastModels(sid, session, (msgAny.type === 'system' && msgAny.subtype === 'init') ? (msgAny.model as string) : undefined)
|
|
464
496
|
}
|
|
465
497
|
|
|
466
498
|
// Extract commands from init message, then enrich with full descriptions
|
|
467
499
|
if (msgAny.type === 'system' && msgAny.subtype === 'init') {
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
const existing = this.sessionCommands.get(sid) ?? []
|
|
485
|
-
for (const prev of existing) {
|
|
486
|
-
if (!cmdMap.has(prev.name)) {
|
|
487
|
-
cmdMap.set(prev.name, prev)
|
|
488
|
-
}
|
|
500
|
+
const slashCmds = (msgAny.slash_commands as string[]) ?? []
|
|
501
|
+
const skills = (msgAny.skills as string[]) ?? []
|
|
502
|
+
const allNames = new Set([...slashCmds, ...skills])
|
|
503
|
+
this.sessionCommands.set(sid, Array.from(allNames).map((name) => ({
|
|
504
|
+
name,
|
|
505
|
+
description: skills.includes(name) ? 'skill' : '',
|
|
506
|
+
})))
|
|
507
|
+
const q = (session as unknown as { query: { supportedCommands(): Promise<{ name: string; description: string }[]> } }).query
|
|
508
|
+
q?.supportedCommands()?.then((cmds: { name: string; description: string }[]) => {
|
|
509
|
+
if (cmds.length > 0) {
|
|
510
|
+
const currentSid = getId()
|
|
511
|
+
const cmdMap = new Map(cmds.map((c) => [c.name, c]))
|
|
512
|
+
const existing = this.sessionCommands.get(currentSid) ?? []
|
|
513
|
+
for (const prev of existing) {
|
|
514
|
+
if (!cmdMap.has(prev.name)) {
|
|
515
|
+
cmdMap.set(prev.name, prev)
|
|
489
516
|
}
|
|
490
|
-
this.sessionCommands.set(sid, Array.from(cmdMap.values()))
|
|
491
|
-
// Notify via broadcast so ws-handler can push updated list
|
|
492
|
-
this.broadcast(sid, (l) => l.onMessage(sid, { type: 'commands_updated' } as unknown as SDKMessage))
|
|
493
517
|
}
|
|
494
|
-
|
|
495
|
-
|
|
518
|
+
this.sessionCommands.set(currentSid, Array.from(cmdMap.values()))
|
|
519
|
+
this.broadcast(currentSid, (l) => l.onMessage(currentSid, { type: 'commands_updated' } as unknown as SDKMessage))
|
|
520
|
+
}
|
|
521
|
+
}).catch(() => {})
|
|
496
522
|
}
|
|
497
523
|
if (msgAny.type === 'result') {
|
|
498
524
|
if (msgAny.is_error) {
|
|
@@ -500,96 +526,40 @@ export class SessionManager {
|
|
|
500
526
|
} else {
|
|
501
527
|
this.log.info({ cost: (msgAny as Record<string, unknown>).total_cost_usd }, 'Turn complete')
|
|
502
528
|
}
|
|
503
|
-
this.sessionStatus.set(
|
|
504
|
-
}
|
|
505
|
-
|
|
506
|
-
let sessionId: string
|
|
507
|
-
try {
|
|
508
|
-
sessionId = session.sessionId
|
|
509
|
-
} catch {
|
|
510
|
-
continue
|
|
529
|
+
this.sessionStatus.set(sid, 'idle')
|
|
511
530
|
}
|
|
512
531
|
|
|
513
|
-
// Remap tempId → real sessionId (O(1) lookup)
|
|
514
|
-
const remap = this.pendingRemaps.get(initialSessionId)
|
|
515
|
-
if (remap && remap.sessionIdRef.current === '' && sessionId && !sessionId.startsWith('pending-')) {
|
|
516
|
-
remap.sessionIdRef.current = sessionId
|
|
517
|
-
const tempId = initialSessionId
|
|
518
|
-
// Move session, cwd, listeners, streaming from tempId to real sessionId
|
|
519
|
-
const s = this.sessions.get(tempId)
|
|
520
|
-
if (s) { this.sessions.delete(tempId); this.sessions.set(sessionId, s) }
|
|
521
|
-
const c = this.sessionCwds.get(tempId)
|
|
522
|
-
if (c) { this.sessionCwds.delete(tempId); this.sessionCwds.set(sessionId, c) }
|
|
523
|
-
const opts = this.sessionCreationOptions.get(tempId)
|
|
524
|
-
if (opts) { this.sessionCreationOptions.delete(tempId); this.sessionCreationOptions.set(sessionId, opts); saveSessionOptions(sessionId, opts) }
|
|
525
|
-
const listeners = this.sessionListeners.get(tempId)
|
|
526
|
-
if (listeners) { this.sessionListeners.delete(tempId); this.sessionListeners.set(sessionId, listeners) }
|
|
527
|
-
this.streamingSessionIds.delete(tempId)
|
|
528
|
-
this.streamingSessionIds.add(sessionId)
|
|
529
|
-
const prevStatus = this.sessionStatus.get(tempId)
|
|
530
|
-
this.sessionStatus.delete(tempId)
|
|
531
|
-
if (prevStatus) this.sessionStatus.set(sessionId, prevStatus)
|
|
532
|
-
const q = this.activeQueries.get(tempId)
|
|
533
|
-
if (q) { this.activeQueries.delete(tempId); this.activeQueries.set(sessionId, q) }
|
|
534
|
-
this.pendingRemaps.delete(tempId)
|
|
535
|
-
// Update our tracking variable
|
|
536
|
-
currentSessionId = sessionId
|
|
537
|
-
// Notify listeners of the remap
|
|
538
|
-
this.broadcast(sessionId, (l) => l.onMessage(sessionId, {
|
|
539
|
-
type: 'session_id_resolved', tempId, sessionId,
|
|
540
|
-
} as unknown as SDKMessage))
|
|
541
|
-
}
|
|
542
|
-
|
|
543
|
-
// Set session status to running on first turn message (skip system/init)
|
|
544
532
|
if (isTurnMessage(msgAny) && shouldBroadcastTurnStarted(turnState)) {
|
|
545
|
-
this.sessionStatus.set(
|
|
533
|
+
this.sessionStatus.set(sid, 'running')
|
|
546
534
|
}
|
|
547
|
-
this.broadcast(
|
|
535
|
+
this.broadcast(sid, (l) => l.onMessage(sid, msg))
|
|
548
536
|
}
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
this.sessionStatus.set(sessionId, 'idle')
|
|
537
|
+
const sid = getId()
|
|
538
|
+
this.log.info({ sessionId: sid }, 'consumeStream: stream() ended (turn complete)')
|
|
539
|
+
if (this.closedSessionIds.has(sid)) break
|
|
540
|
+
// If stream ended without a result (e.g. control message like setModel response),
|
|
541
|
+
// reset status back to idle so the client isn't stuck in 'running'
|
|
542
|
+
if (shouldResetToIdleOnStreamEnd(this.sessionStatus.get(sid))) {
|
|
543
|
+
this.sessionStatus.set(sid, 'idle')
|
|
557
544
|
}
|
|
558
545
|
// Wait briefly before re-entering stream() for next turn
|
|
559
546
|
await new Promise((r) => setTimeout(r, 50))
|
|
560
547
|
}
|
|
561
548
|
} catch (err) {
|
|
562
|
-
// AbortError is expected when session is closed
|
|
563
549
|
if (!(err instanceof Error && err.name === 'AbortError')) {
|
|
564
550
|
this.log.error({ err }, 'Stream error')
|
|
565
551
|
}
|
|
566
552
|
} finally {
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
if (this.sessions.has(sessionId) && !this.closedSessionIds.has(sessionId)) {
|
|
576
|
-
const s = this.sessions.get(sessionId)
|
|
577
|
-
this.sessions.delete(sessionId)
|
|
578
|
-
try { s?.close() } catch { /* already closed */ }
|
|
579
|
-
}
|
|
580
|
-
this.broadcast(sessionId, (l) => l.onEnd(sessionId))
|
|
581
|
-
} catch {
|
|
582
|
-
// Session never initialized — try with our tracked id
|
|
583
|
-
this.sessionStatus.set(currentSessionId, 'stopped')
|
|
584
|
-
this.streamingSessionIds.delete(currentSessionId)
|
|
585
|
-
this.activeQueries.delete(currentSessionId)
|
|
586
|
-
if (this.sessions.has(currentSessionId) && !this.closedSessionIds.has(currentSessionId)) {
|
|
587
|
-
const s = this.sessions.get(currentSessionId)
|
|
588
|
-
this.sessions.delete(currentSessionId)
|
|
589
|
-
try { s?.close() } catch { /* already closed */ }
|
|
590
|
-
}
|
|
591
|
-
this.broadcast(currentSessionId, (l) => l.onEnd(currentSessionId))
|
|
553
|
+
const sid = getId()
|
|
554
|
+
this.sessionStatus.set(sid, 'stopped')
|
|
555
|
+
this.streamingSessionIds.delete(sid)
|
|
556
|
+
this.activeQueries.delete(sid)
|
|
557
|
+
if (this.sessions.has(sid) && !this.closedSessionIds.has(sid)) {
|
|
558
|
+
const s = this.sessions.get(sid)
|
|
559
|
+
this.sessions.delete(sid)
|
|
560
|
+
try { s?.close() } catch { /* already closed */ }
|
|
592
561
|
}
|
|
562
|
+
this.broadcast(sid, (l) => l.onEnd(sid))
|
|
593
563
|
}
|
|
594
564
|
}
|
|
595
565
|
|
|
@@ -664,6 +634,45 @@ export class SessionManager {
|
|
|
664
634
|
} as unknown as SDKMessage))
|
|
665
635
|
}
|
|
666
636
|
|
|
637
|
+
async setPermissionMode(sessionId: string, mode: string): Promise<void> {
|
|
638
|
+
const session = this.sessions.get(sessionId)
|
|
639
|
+
if (!session) {
|
|
640
|
+
throw new Error(`Session ${sessionId} not found`)
|
|
641
|
+
}
|
|
642
|
+
const query = (session as unknown as { query: { setPermissionMode(mode: string): Promise<void> } }).query
|
|
643
|
+
if (!query?.setPermissionMode) {
|
|
644
|
+
throw new Error(`Session ${sessionId} does not support setPermissionMode`)
|
|
645
|
+
}
|
|
646
|
+
await query.setPermissionMode(mode)
|
|
647
|
+
// Persist so resume uses the updated permission mode
|
|
648
|
+
const opts = this.sessionCreationOptions.get(sessionId)
|
|
649
|
+
if (opts) {
|
|
650
|
+
opts.permissionMode = mode
|
|
651
|
+
saveSessionOptions(sessionId, opts)
|
|
652
|
+
}
|
|
653
|
+
this.broadcast(sessionId, (l) => l.onMessage(sessionId, {
|
|
654
|
+
type: 'permission_mode_changed', sessionId, mode,
|
|
655
|
+
} as unknown as SDKMessage))
|
|
656
|
+
}
|
|
657
|
+
|
|
658
|
+
async setEnv(sessionId: string, env: Record<string, string>): Promise<void> {
|
|
659
|
+
const session = this.sessions.get(sessionId)
|
|
660
|
+
if (!session) {
|
|
661
|
+
throw new Error(`Session ${sessionId} not found`)
|
|
662
|
+
}
|
|
663
|
+
const query = (session as unknown as { query: { applyFlagSettings(settings: { env?: Record<string, string> }): Promise<void> } }).query
|
|
664
|
+
if (!query?.applyFlagSettings) {
|
|
665
|
+
throw new Error(`Session ${sessionId} does not support applyFlagSettings`)
|
|
666
|
+
}
|
|
667
|
+
await query.applyFlagSettings({ env })
|
|
668
|
+
// Persist so resume uses the updated env
|
|
669
|
+
const opts = this.sessionCreationOptions.get(sessionId)
|
|
670
|
+
if (opts) {
|
|
671
|
+
opts.env = env
|
|
672
|
+
saveSessionOptions(sessionId, opts)
|
|
673
|
+
}
|
|
674
|
+
}
|
|
675
|
+
|
|
667
676
|
getSessionState(sessionId: string): { model?: string; effortLevel?: EffortLevel; status: 'idle' | 'running' | 'stopped' } {
|
|
668
677
|
return {
|
|
669
678
|
model: this.sessionModels.get(sessionId),
|
|
@@ -875,7 +884,6 @@ export class SessionManager {
|
|
|
875
884
|
|
|
876
885
|
// Keep sessionCwds and sessionCommands — they're metadata needed for re-resume.
|
|
877
886
|
// Only clean up runtime state.
|
|
878
|
-
this.pendingRemaps.delete(sessionId)
|
|
879
887
|
// Deny pending permissions only for this session
|
|
880
888
|
for (const [id, entry] of this.pendingPermissions) {
|
|
881
889
|
if (entry.sessionId === sessionId) {
|
|
@@ -14,7 +14,10 @@ export function shouldResetToIdleOnStreamEnd(currentStatus: string): boolean {
|
|
|
14
14
|
return currentStatus === 'running'
|
|
15
15
|
}
|
|
16
16
|
|
|
17
|
-
/**
|
|
17
|
+
/**
|
|
18
|
+
* Returns true if this SDK message is part of an active turn (not session init
|
|
19
|
+
* or a control response like setModel/applyFlagSettings).
|
|
20
|
+
*/
|
|
18
21
|
export function isTurnMessage(msg: Record<string, unknown>): boolean {
|
|
19
22
|
if (msg.type === 'system' && msg.subtype === 'init') return false
|
|
20
23
|
return true
|
package/server/src/types.ts
CHANGED
|
@@ -99,6 +99,18 @@ export interface ElicitationResponseMessage {
|
|
|
99
99
|
content?: Record<string, unknown>
|
|
100
100
|
}
|
|
101
101
|
|
|
102
|
+
export interface SetPermissionModeMessage {
|
|
103
|
+
type: 'set_permission_mode'
|
|
104
|
+
sessionId: string
|
|
105
|
+
mode: string
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
export interface SetEnvMessage {
|
|
109
|
+
type: 'set_env'
|
|
110
|
+
sessionId: string
|
|
111
|
+
env: Record<string, string>
|
|
112
|
+
}
|
|
113
|
+
|
|
102
114
|
export interface GetSessionSettingsMessage {
|
|
103
115
|
type: 'get_session_settings'
|
|
104
116
|
sessionId: string
|
|
@@ -122,6 +134,8 @@ export type ClientMessage =
|
|
|
122
134
|
| SetEffortLevelMessage
|
|
123
135
|
| GetSubagentMessagesMessage
|
|
124
136
|
| ElicitationResponseMessage
|
|
137
|
+
| SetPermissionModeMessage
|
|
138
|
+
| SetEnvMessage
|
|
125
139
|
| GetSessionSettingsMessage
|
|
126
140
|
|
|
127
141
|
// === Server → Client Messages ===
|
|
@@ -173,12 +187,6 @@ export interface SessionHistoryMessage {
|
|
|
173
187
|
messages: unknown[]
|
|
174
188
|
}
|
|
175
189
|
|
|
176
|
-
export interface SessionIdResolvedMessage {
|
|
177
|
-
type: 'session_id_resolved'
|
|
178
|
-
tempId: string
|
|
179
|
-
sessionId: string
|
|
180
|
-
}
|
|
181
|
-
|
|
182
190
|
export interface FileListMessage {
|
|
183
191
|
type: 'file_list'
|
|
184
192
|
files: FileEntry[]
|
|
@@ -293,7 +301,6 @@ export type ServerMessage =
|
|
|
293
301
|
| ErrorMessage
|
|
294
302
|
| SessionEndMessage
|
|
295
303
|
| SessionHistoryMessage
|
|
296
|
-
| SessionIdResolvedMessage
|
|
297
304
|
| FileListMessage
|
|
298
305
|
| DefaultCwdMessage
|
|
299
306
|
| CommandListMessage
|
package/server/src/ws-handler.ts
CHANGED
|
@@ -47,15 +47,6 @@ export function createWsHandler(sessionManager: SessionManager, log: FastifyBase
|
|
|
47
47
|
return
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
-
if (msg.type === 'session_id_resolved') {
|
|
51
|
-
send({
|
|
52
|
-
type: 'session_id_resolved',
|
|
53
|
-
tempId: msg.tempId as string,
|
|
54
|
-
sessionId: msg.sessionId as string,
|
|
55
|
-
})
|
|
56
|
-
return
|
|
57
|
-
}
|
|
58
|
-
|
|
59
50
|
if (msg.type === 'session_resumed') {
|
|
60
51
|
send({
|
|
61
52
|
type: 'session_resumed',
|
|
@@ -185,6 +176,10 @@ export function createWsHandler(sessionManager: SessionManager, log: FastifyBase
|
|
|
185
176
|
}
|
|
186
177
|
|
|
187
178
|
case 'switch_session': {
|
|
179
|
+
// Subscribe FIRST so live stream messages are not lost while loading history.
|
|
180
|
+
// For new sessions, consumeStream is already running — registering early
|
|
181
|
+
// ensures we catch assistant response chunks during the getHistory() await.
|
|
182
|
+
sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
|
|
188
183
|
// Load historical messages
|
|
189
184
|
const history = await sessionManager.getHistory(msg.sessionId)
|
|
190
185
|
send({ type: 'session_history', sessionId: msg.sessionId, messages: history })
|
|
@@ -193,8 +188,6 @@ export function createWsHandler(sessionManager: SessionManager, log: FastifyBase
|
|
|
193
188
|
if (state.model || state.effortLevel) {
|
|
194
189
|
send({ type: 'session_state', sessionId: msg.sessionId, ...state })
|
|
195
190
|
}
|
|
196
|
-
// Subscribe for live updates if session is running
|
|
197
|
-
sessionManager.subscribe(msg.sessionId, makeListener(msg.sessionId))
|
|
198
191
|
break
|
|
199
192
|
}
|
|
200
193
|
|
|
@@ -292,6 +285,16 @@ export function createWsHandler(sessionManager: SessionManager, log: FastifyBase
|
|
|
292
285
|
break
|
|
293
286
|
}
|
|
294
287
|
|
|
288
|
+
case 'set_permission_mode': {
|
|
289
|
+
await sessionManager.setPermissionMode(msg.sessionId, msg.mode)
|
|
290
|
+
break
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
case 'set_env': {
|
|
294
|
+
await sessionManager.setEnv(msg.sessionId, msg.env)
|
|
295
|
+
break
|
|
296
|
+
}
|
|
297
|
+
|
|
295
298
|
case 'get_subagent_messages': {
|
|
296
299
|
const messages = await sessionManager.getSubagentMessages(msg.sessionId, msg.agentId)
|
|
297
300
|
send({ type: 'subagent_messages', sessionId: msg.sessionId, agentId: msg.agentId, messages })
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{aq as o,ar as n}from"./mermaid.core-D970jp78.js";const t=(r,a)=>o.lang.round(n.parse(r)[a]);export{t as c};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-dGyuU23q.js";import{_ as i}from"./mermaid.core-D970jp78.js";import"./chunk-FMBD7UC4-O6auhXOZ.js";import"./chunk-YZCP3GAM-CSmbvz_K.js";import"./chunk-55IACEB6-DDiu2l5t.js";import"./chunk-EDXVE4YY-B_5Djzmq.js";import"./index-CQyb-QJw.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{s as a,c as s,a as e,C as t}from"./chunk-4TB4RGXK-dGyuU23q.js";import{_ as i}from"./mermaid.core-D970jp78.js";import"./chunk-FMBD7UC4-O6auhXOZ.js";import"./chunk-YZCP3GAM-CSmbvz_K.js";import"./chunk-55IACEB6-DDiu2l5t.js";import"./chunk-EDXVE4YY-B_5Djzmq.js";import"./index-CQyb-QJw.js";var n={parser:e,get db(){return new t},renderer:s,styles:a,init:i(r=>{r.class||(r.class={}),r.class.arrowMarkerAbsolute=r.arrowMarkerAbsolute},"init")};export{n as diagram};
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
import{b as r}from"./graph-BLaCZz6d.js";var e=4;function a(o){return r(o,e)}export{a as c};
|