mstro-app 0.5.5 → 0.5.6
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/dist/server/cli/headless/claude-invoker-process.d.ts.map +1 -1
- package/dist/server/cli/headless/claude-invoker-process.js +9 -1
- package/dist/server/cli/headless/claude-invoker-process.js.map +1 -1
- package/dist/server/cli/headless/mcp-config.d.ts +22 -5
- package/dist/server/cli/headless/mcp-config.d.ts.map +1 -1
- package/dist/server/cli/headless/mcp-config.js +7 -5
- package/dist/server/cli/headless/mcp-config.js.map +1 -1
- package/dist/server/cli/headless/runner.d.ts.map +1 -1
- package/dist/server/cli/headless/runner.js +19 -0
- package/dist/server/cli/headless/runner.js.map +1 -1
- package/dist/server/cli/headless/types.d.ts +16 -1
- package/dist/server/cli/headless/types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.d.ts +6 -0
- package/dist/server/cli/improvisation-session-manager.d.ts.map +1 -1
- package/dist/server/cli/improvisation-session-manager.js +8 -0
- package/dist/server/cli/improvisation-session-manager.js.map +1 -1
- package/dist/server/cli/improvisation-types.d.ts +7 -0
- package/dist/server/cli/improvisation-types.d.ts.map +1 -1
- package/dist/server/cli/improvisation-types.js.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.d.ts.map +1 -1
- package/dist/server/cli/retry/retry-runner-factory.js +1 -0
- package/dist/server/cli/retry/retry-runner-factory.js.map +1 -1
- package/dist/server/index.js +8 -1
- package/dist/server/index.js.map +1 -1
- package/dist/server/mcp/server.js +52 -0
- package/dist/server/mcp/server.js.map +1 -1
- package/dist/server/routes/index.d.ts +1 -0
- package/dist/server/routes/index.d.ts.map +1 -1
- package/dist/server/routes/index.js +1 -0
- package/dist/server/routes/index.js.map +1 -1
- package/dist/server/routes/internal.d.ts +16 -0
- package/dist/server/routes/internal.d.ts.map +1 -0
- package/dist/server/routes/internal.js +94 -0
- package/dist/server/routes/internal.js.map +1 -0
- package/dist/server/services/runtime-info.d.ts +3 -0
- package/dist/server/services/runtime-info.d.ts.map +1 -0
- package/dist/server/services/runtime-info.js +21 -0
- package/dist/server/services/runtime-info.js.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts +32 -0
- package/dist/server/services/websocket/ask-user-question-bridge.d.ts.map +1 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js +115 -0
- package/dist/server/services/websocket/ask-user-question-bridge.js.map +1 -0
- package/dist/server/services/websocket/handler.d.ts +8 -0
- package/dist/server/services/websocket/handler.d.ts.map +1 -1
- package/dist/server/services/websocket/handler.js +30 -0
- package/dist/server/services/websocket/handler.js.map +1 -1
- package/dist/server/services/websocket/session-handlers.d.ts.map +1 -1
- package/dist/server/services/websocket/session-handlers.js +3 -0
- package/dist/server/services/websocket/session-handlers.js.map +1 -1
- package/dist/server/services/websocket/types.d.ts +39 -2
- package/dist/server/services/websocket/types.d.ts.map +1 -1
- package/dist/server/services/websocket/types.js +2 -2
- package/dist/server/services/websocket/types.js.map +1 -1
- package/package.json +1 -1
- package/server/cli/headless/claude-invoker-process.ts +9 -1
- package/server/cli/headless/mcp-config.ts +30 -5
- package/server/cli/headless/runner.ts +21 -0
- package/server/cli/headless/types.ts +16 -1
- package/server/cli/improvisation-session-manager.ts +9 -0
- package/server/cli/improvisation-types.ts +7 -0
- package/server/cli/retry/retry-runner-factory.ts +1 -0
- package/server/index.ts +8 -0
- package/server/mcp/server.ts +57 -0
- package/server/routes/index.ts +1 -0
- package/server/routes/internal.ts +112 -0
- package/server/services/runtime-info.ts +24 -0
- package/server/services/websocket/ask-user-question-bridge.ts +148 -0
- package/server/services/websocket/handler.ts +30 -0
- package/server/services/websocket/session-handlers.ts +3 -0
- package/server/services/websocket/types.ts +48 -2
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Internal Routes
|
|
5
|
+
*
|
|
6
|
+
* HTTP endpoints used by sibling subprocesses (like the MCP bouncer) to talk
|
|
7
|
+
* back to the running CLI server. NOT mounted under `/api/*` — these are gated
|
|
8
|
+
* by the per-process bouncer secret instead of the user's session token.
|
|
9
|
+
*
|
|
10
|
+
* Currently a single endpoint:
|
|
11
|
+
* POST /internal/ask-user-question
|
|
12
|
+
* Bouncer pauses Claude on AskUserQuestion; this blocks until the web
|
|
13
|
+
* user answers, then returns the answers Claude needs to continue.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import { Hono } from 'hono'
|
|
17
|
+
import {
|
|
18
|
+
isValidBouncerSecret,
|
|
19
|
+
registerPendingQuestion,
|
|
20
|
+
} from '../services/websocket/ask-user-question-bridge.js'
|
|
21
|
+
import type { HandlerContext } from '../services/websocket/handler-context.js'
|
|
22
|
+
import { broadcastTabEvent } from '../services/websocket/tab-broadcast.js'
|
|
23
|
+
import type {
|
|
24
|
+
AskUserQuestionItem,
|
|
25
|
+
AskUserQuestionPayload,
|
|
26
|
+
} from '../services/websocket/types.js'
|
|
27
|
+
|
|
28
|
+
interface AskUserQuestionRequestBody {
|
|
29
|
+
toolUseId?: unknown
|
|
30
|
+
tabId?: unknown
|
|
31
|
+
questions?: unknown
|
|
32
|
+
/** Override default 15min timeout (ms). Optional. */
|
|
33
|
+
timeoutMs?: unknown
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Narrow an unknown into AskUserQuestionItem[] without throwing. */
|
|
37
|
+
function parseQuestions(value: unknown): AskUserQuestionItem[] | null {
|
|
38
|
+
if (!Array.isArray(value)) return null
|
|
39
|
+
const out: AskUserQuestionItem[] = []
|
|
40
|
+
for (const raw of value) {
|
|
41
|
+
if (!raw || typeof raw !== 'object') return null
|
|
42
|
+
const r = raw as Record<string, unknown>
|
|
43
|
+
if (typeof r.question !== 'string' || typeof r.header !== 'string') return null
|
|
44
|
+
if (!Array.isArray(r.options)) return null
|
|
45
|
+
const options = r.options.map((o) => {
|
|
46
|
+
if (!o || typeof o !== 'object') return null
|
|
47
|
+
const oo = o as Record<string, unknown>
|
|
48
|
+
if (typeof oo.label !== 'string') return null
|
|
49
|
+
return {
|
|
50
|
+
label: oo.label,
|
|
51
|
+
description: typeof oo.description === 'string' ? oo.description : '',
|
|
52
|
+
preview: typeof oo.preview === 'string' ? oo.preview : undefined,
|
|
53
|
+
}
|
|
54
|
+
})
|
|
55
|
+
if (options.some((o) => o === null)) return null
|
|
56
|
+
out.push({
|
|
57
|
+
question: r.question,
|
|
58
|
+
header: r.header,
|
|
59
|
+
options: options as AskUserQuestionItem['options'],
|
|
60
|
+
multiSelect: r.multiSelect === true,
|
|
61
|
+
})
|
|
62
|
+
}
|
|
63
|
+
return out
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function createInternalRoutes(ctx: HandlerContext): Hono {
|
|
67
|
+
const app = new Hono()
|
|
68
|
+
|
|
69
|
+
app.post('/ask-user-question', async (c) => {
|
|
70
|
+
const secret = c.req.header('x-mstro-bouncer-secret')
|
|
71
|
+
if (!isValidBouncerSecret(secret)) {
|
|
72
|
+
return c.json({ error: 'Forbidden' }, 403)
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
let body: AskUserQuestionRequestBody
|
|
76
|
+
try {
|
|
77
|
+
body = (await c.req.json()) as AskUserQuestionRequestBody
|
|
78
|
+
} catch {
|
|
79
|
+
return c.json({ error: 'Invalid JSON' }, 400)
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const toolUseId = typeof body.toolUseId === 'string' ? body.toolUseId : ''
|
|
83
|
+
const tabId = typeof body.tabId === 'string' ? body.tabId : ''
|
|
84
|
+
const questions = parseQuestions(body.questions)
|
|
85
|
+
if (!toolUseId || !tabId || !questions || questions.length === 0) {
|
|
86
|
+
return c.json({ error: 'toolUseId, tabId, and non-empty questions[] are required' }, 400)
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const timeoutMs =
|
|
90
|
+
typeof body.timeoutMs === 'number' && body.timeoutMs > 0 ? body.timeoutMs : undefined
|
|
91
|
+
|
|
92
|
+
const payload: AskUserQuestionPayload = { toolUseId, questions }
|
|
93
|
+
broadcastTabEvent(ctx, tabId, 'askUserQuestion', payload)
|
|
94
|
+
|
|
95
|
+
try {
|
|
96
|
+
const answers = await registerPendingQuestion({ toolUseId, tabId, timeoutMs })
|
|
97
|
+
return c.json({ answers })
|
|
98
|
+
} catch (err) {
|
|
99
|
+
const reason = err instanceof Error ? err.message : 'cancelled'
|
|
100
|
+
// Tell every web client to dismiss the card so users don't keep poking
|
|
101
|
+
// an already-dead question.
|
|
102
|
+
broadcastTabEvent(ctx, tabId, 'askUserQuestionDismissed', {
|
|
103
|
+
toolUseId,
|
|
104
|
+
reason: reason === 'timeout' ? 'timeout' : 'cancelled',
|
|
105
|
+
})
|
|
106
|
+
const status = reason === 'timeout' ? 504 : 410
|
|
107
|
+
return c.json({ error: reason }, status)
|
|
108
|
+
}
|
|
109
|
+
})
|
|
110
|
+
|
|
111
|
+
return app
|
|
112
|
+
}
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Runtime Info
|
|
5
|
+
*
|
|
6
|
+
* Holds process-wide singletons that are known after server startup but
|
|
7
|
+
* needed by code paths (e.g. the headless runner / MCP config generator)
|
|
8
|
+
* that don't have a natural reference to the Hono app or instance registry.
|
|
9
|
+
*
|
|
10
|
+
* Specifically: the port the CLI server is bound to. We need it so the MCP
|
|
11
|
+
* bouncer subprocess can call back into us via HTTP for AskUserQuestion.
|
|
12
|
+
*
|
|
13
|
+
* Set once at server startup (see `server/index.ts`).
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
let currentPort: number | undefined;
|
|
17
|
+
|
|
18
|
+
export function setCurrentMstroPort(port: number): void {
|
|
19
|
+
currentPort = port;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function getCurrentMstroPort(): number | undefined {
|
|
23
|
+
return currentPort;
|
|
24
|
+
}
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
// Copyright (c) 2025-present Mstro, Inc. All rights reserved.
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* AskUserQuestion Bridge
|
|
5
|
+
*
|
|
6
|
+
* Bridges the MCP bouncer subprocess (which receives Claude's AskUserQuestion
|
|
7
|
+
* tool calls) and the web client (which collects the user's answers).
|
|
8
|
+
*
|
|
9
|
+
* Flow:
|
|
10
|
+
* Claude → MCP bouncer (subprocess)
|
|
11
|
+
* → POST /internal/ask-user-question (this CLI server)
|
|
12
|
+
* → registerPendingQuestion() stores a resolver
|
|
13
|
+
* → broadcastTabEvent('askUserQuestion', …) pushes the question to web
|
|
14
|
+
* → web user answers → WS `askUserQuestionResponse`
|
|
15
|
+
* → resolvePendingQuestion() resolves the awaited promise
|
|
16
|
+
* → HTTP response to bouncer with answers
|
|
17
|
+
* → bouncer returns { behavior: "allow", updatedInput: { questions, answers } }
|
|
18
|
+
*
|
|
19
|
+
* Ownership of state: pending questions live only here, in-process. The
|
|
20
|
+
* registry is keyed by `toolUseId` (Claude's per-call id) which guarantees
|
|
21
|
+
* uniqueness across tabs and sessions.
|
|
22
|
+
*
|
|
23
|
+
* Timeouts: questions auto-reject after `DEFAULT_TIMEOUT_MS`. The bouncer's
|
|
24
|
+
* HTTP call gets a 504 and returns a deny to Claude rather than blocking the
|
|
25
|
+
* Claude turn forever.
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
import { randomUUID } from 'node:crypto';
|
|
29
|
+
|
|
30
|
+
/** Default per-question timeout (15 minutes). */
|
|
31
|
+
const DEFAULT_TIMEOUT_MS = 15 * 60 * 1000;
|
|
32
|
+
|
|
33
|
+
interface PendingQuestion {
|
|
34
|
+
toolUseId: string;
|
|
35
|
+
tabId: string;
|
|
36
|
+
resolve: (answers: Record<string, string>) => void;
|
|
37
|
+
reject: (reason: 'timeout' | 'cancelled' | 'session-ended') => void;
|
|
38
|
+
timer: ReturnType<typeof setTimeout>;
|
|
39
|
+
createdAt: number;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const pending = new Map<string, PendingQuestion>();
|
|
43
|
+
|
|
44
|
+
/** Per-process secret the MCP bouncer must echo to authenticate.
|
|
45
|
+
* Generated once at server start, passed to bouncers via env var. */
|
|
46
|
+
const bouncerSharedSecret = randomUUID();
|
|
47
|
+
|
|
48
|
+
/** Get the per-process bouncer secret (passed via env var to bouncer subprocesses). */
|
|
49
|
+
export function getBouncerSecret(): string {
|
|
50
|
+
return bouncerSharedSecret;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Validate a secret claimed by an inbound /internal request. */
|
|
54
|
+
export function isValidBouncerSecret(secret: string | undefined | null): boolean {
|
|
55
|
+
if (!secret) return false;
|
|
56
|
+
return secret === bouncerSharedSecret;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export interface RegisterPendingQuestionOptions {
|
|
60
|
+
toolUseId: string;
|
|
61
|
+
tabId: string;
|
|
62
|
+
timeoutMs?: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Register a pending question. The returned promise resolves when
|
|
67
|
+
* `resolvePendingQuestion` is called for the same toolUseId, or rejects on
|
|
68
|
+
* timeout / cancellation.
|
|
69
|
+
*/
|
|
70
|
+
export function registerPendingQuestion(
|
|
71
|
+
opts: RegisterPendingQuestionOptions,
|
|
72
|
+
): Promise<Record<string, string>> {
|
|
73
|
+
const { toolUseId, tabId, timeoutMs = DEFAULT_TIMEOUT_MS } = opts;
|
|
74
|
+
|
|
75
|
+
// Defensive: reject any prior pending entry for this id (shouldn't happen
|
|
76
|
+
// but a process restart or duplicate POST shouldn't leak handlers).
|
|
77
|
+
const existing = pending.get(toolUseId);
|
|
78
|
+
if (existing) {
|
|
79
|
+
clearTimeout(existing.timer);
|
|
80
|
+
existing.reject('cancelled');
|
|
81
|
+
pending.delete(toolUseId);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return new Promise<Record<string, string>>((resolve, reject) => {
|
|
85
|
+
const timer = setTimeout(() => {
|
|
86
|
+
const entry = pending.get(toolUseId);
|
|
87
|
+
if (!entry) return;
|
|
88
|
+
pending.delete(toolUseId);
|
|
89
|
+
entry.reject('timeout');
|
|
90
|
+
}, timeoutMs);
|
|
91
|
+
|
|
92
|
+
pending.set(toolUseId, {
|
|
93
|
+
toolUseId,
|
|
94
|
+
tabId,
|
|
95
|
+
resolve,
|
|
96
|
+
reject: (reason) => reject(new Error(reason)),
|
|
97
|
+
timer,
|
|
98
|
+
createdAt: Date.now(),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Resolve a pending question with the user's answers. Returns true if a
|
|
105
|
+
* pending entry was found and resolved; false if there was no matching
|
|
106
|
+
* pending question (already answered, timed out, or unknown id).
|
|
107
|
+
*/
|
|
108
|
+
export function resolvePendingQuestion(
|
|
109
|
+
toolUseId: string,
|
|
110
|
+
answers: Record<string, string>,
|
|
111
|
+
): boolean {
|
|
112
|
+
const entry = pending.get(toolUseId);
|
|
113
|
+
if (!entry) return false;
|
|
114
|
+
pending.delete(toolUseId);
|
|
115
|
+
clearTimeout(entry.timer);
|
|
116
|
+
entry.resolve(answers);
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Cancel all pending questions for a given tab. Used when a tab is removed,
|
|
122
|
+
* a session is reset, or an orchestra disconnects. Returns the toolUseIds
|
|
123
|
+
* that were cancelled so callers can broadcast `askUserQuestionDismissed`.
|
|
124
|
+
*/
|
|
125
|
+
export function cancelPendingQuestionsForTab(
|
|
126
|
+
tabId: string,
|
|
127
|
+
reason: 'cancelled' | 'session-ended' = 'cancelled',
|
|
128
|
+
): string[] {
|
|
129
|
+
const cancelled: string[] = [];
|
|
130
|
+
for (const [id, entry] of pending) {
|
|
131
|
+
if (entry.tabId !== tabId) continue;
|
|
132
|
+
pending.delete(id);
|
|
133
|
+
clearTimeout(entry.timer);
|
|
134
|
+
entry.reject(reason);
|
|
135
|
+
cancelled.push(id);
|
|
136
|
+
}
|
|
137
|
+
return cancelled;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/** Look up the tab that owns a pending question, or undefined. */
|
|
141
|
+
export function getPendingQuestionTab(toolUseId: string): string | undefined {
|
|
142
|
+
return pending.get(toolUseId)?.tabId;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/** Diagnostic: how many questions are currently waiting on user input. */
|
|
146
|
+
export function pendingQuestionCount(): number {
|
|
147
|
+
return pending.size;
|
|
148
|
+
}
|
|
@@ -15,6 +15,7 @@ import type { ImprovisationSessionManager } from '../../cli/improvisation-sessio
|
|
|
15
15
|
import type { InstanceRegistry } from '../instances.js';
|
|
16
16
|
import { captureException } from '../sentry.js';
|
|
17
17
|
import { getPTYManager } from '../terminal/pty-manager.js';
|
|
18
|
+
import { resolvePendingQuestion } from './ask-user-question-bridge.js';
|
|
18
19
|
import { AutocompleteService } from './autocomplete.js';
|
|
19
20
|
import { FileDownloadHandler } from './file-download-handler.js';
|
|
20
21
|
import { handleFileExplorerMessage, handleFileMessage } from './file-explorer-handlers.js';
|
|
@@ -217,6 +218,9 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
217
218
|
return handleListSkills(this, ws, workingDir);
|
|
218
219
|
case 'shutdownInstance':
|
|
219
220
|
return this.handleShutdownInstance(ws, permission);
|
|
221
|
+
case 'askUserQuestionResponse':
|
|
222
|
+
if (permission === 'view') return;
|
|
223
|
+
return this.handleAskUserQuestionResponse(msg, tabId);
|
|
220
224
|
}
|
|
221
225
|
|
|
222
226
|
// Dispatch table lookup for domain handlers
|
|
@@ -391,6 +395,32 @@ export class WebSocketImproviseHandler implements HandlerContext {
|
|
|
391
395
|
this.sessions.delete(sessionId);
|
|
392
396
|
}
|
|
393
397
|
|
|
398
|
+
/**
|
|
399
|
+
* Resolve a pending AskUserQuestion call with the user's answers. The
|
|
400
|
+
* bouncer subprocess is awaiting on the bridge promise; calling
|
|
401
|
+
* `resolvePendingQuestion` releases it so Claude resumes with the answers.
|
|
402
|
+
* Stale toolUseIds are no-ops — likely the question already timed out or
|
|
403
|
+
* was cancelled, and a stale web client is replaying its submission.
|
|
404
|
+
*/
|
|
405
|
+
private handleAskUserQuestionResponse(msg: WebSocketMessage, tabId: string): void {
|
|
406
|
+
const data = msg.data as { toolUseId?: unknown; answers?: unknown } | undefined;
|
|
407
|
+
const toolUseId = typeof data?.toolUseId === 'string' ? data.toolUseId : '';
|
|
408
|
+
const answersIn = data?.answers;
|
|
409
|
+
if (!toolUseId) return;
|
|
410
|
+
|
|
411
|
+
// Only accept a flat string-string map; coerce safely so a malformed
|
|
412
|
+
// payload doesn't crash the bridge.
|
|
413
|
+
const answers: Record<string, string> = {};
|
|
414
|
+
if (answersIn && typeof answersIn === 'object' && !Array.isArray(answersIn)) {
|
|
415
|
+
for (const [k, v] of Object.entries(answersIn as Record<string, unknown>)) {
|
|
416
|
+
if (typeof v === 'string') answers[k] = v;
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
void tabId; // tabId is informational; the toolUseId is the unique key
|
|
421
|
+
resolvePendingQuestion(toolUseId, answers);
|
|
422
|
+
}
|
|
423
|
+
|
|
394
424
|
/**
|
|
395
425
|
* Handle a `shutdownInstance` control message from a web client.
|
|
396
426
|
*
|
|
@@ -248,6 +248,9 @@ export function buildOutputHistory(session: ImprovisationSessionManager): Array<
|
|
|
248
248
|
*/
|
|
249
249
|
export function setupSessionListeners(ctx: HandlerContext, session: ImprovisationSessionManager, _ws: WSContext, tabId: string): void {
|
|
250
250
|
session.removeAllListeners();
|
|
251
|
+
// Bind tabId before listeners — the headless runner reads it at executePrompt
|
|
252
|
+
// time to wire AskUserQuestion routing back to this tab's web clients.
|
|
253
|
+
session.setTabId(tabId);
|
|
251
254
|
const engine = resolveEngineForSession(session);
|
|
252
255
|
|
|
253
256
|
session.on('onHistoryPersisted', () => {
|
|
@@ -19,7 +19,7 @@ export interface WSContext {
|
|
|
19
19
|
_ws?: unknown
|
|
20
20
|
}
|
|
21
21
|
|
|
22
|
-
const CoreMessages = ['execute', 'cancel', 'getHistory', 'getSessions', 'getSessionsCount', 'deleteSession', 'getSessionById', 'clearHistory', 'searchHistory', 'new', 'autocomplete', 'readFile', 'ping', 'initTab', 'resumeSession', 'approve', 'reject', 'recordSelection', 'requestNotificationSummary'] as const;
|
|
22
|
+
const CoreMessages = ['execute', 'cancel', 'getHistory', 'getSessions', 'getSessionsCount', 'deleteSession', 'getSessionById', 'clearHistory', 'searchHistory', 'new', 'autocomplete', 'readFile', 'ping', 'initTab', 'resumeSession', 'approve', 'reject', 'recordSelection', 'requestNotificationSummary', 'askUserQuestionResponse'] as const;
|
|
23
23
|
|
|
24
24
|
const TerminalMessages = ['terminalInit', 'terminalReconnect', 'terminalList', 'terminalInput', 'terminalResize', 'terminalClose'] as const;
|
|
25
25
|
|
|
@@ -108,7 +108,7 @@ export interface WebSocketMessage {
|
|
|
108
108
|
_permission?: 'view';
|
|
109
109
|
}
|
|
110
110
|
|
|
111
|
-
const CoreResponseMessages = ['output', 'thinking', 'movementStart', 'movementComplete', 'movementError', 'sessionUpdate', 'history', 'sessions', 'sessionsCount', 'sessionDeleted', 'sessionData', 'historyCleared', 'searchResults', 'newSession', 'autocomplete', 'fileContent', 'error', 'pong', 'tabInitialized', 'approvalRequired', 'toolUse', 'streamingTokens', 'notificationSummary', 'executeAck', 'clientOffline', 'clientAuthExpired'] as const;
|
|
111
|
+
const CoreResponseMessages = ['output', 'thinking', 'movementStart', 'movementComplete', 'movementError', 'sessionUpdate', 'history', 'sessions', 'sessionsCount', 'sessionDeleted', 'sessionData', 'historyCleared', 'searchResults', 'newSession', 'autocomplete', 'fileContent', 'error', 'pong', 'tabInitialized', 'approvalRequired', 'toolUse', 'streamingTokens', 'notificationSummary', 'executeAck', 'clientOffline', 'clientAuthExpired', 'askUserQuestion', 'askUserQuestionDismissed'] as const;
|
|
112
112
|
|
|
113
113
|
const TerminalResponseMessages = ['terminalOutput', 'terminalReady', 'terminalExit', 'terminalError', 'terminalList', 'terminalScrollback', 'terminalCreated', 'terminalClosed'] as const;
|
|
114
114
|
|
|
@@ -193,6 +193,52 @@ export interface ConnectionData {
|
|
|
193
193
|
workingDir: string;
|
|
194
194
|
}
|
|
195
195
|
|
|
196
|
+
// ============================================================================
|
|
197
|
+
// AskUserQuestion Types
|
|
198
|
+
// ============================================================================
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Single option Claude offers in an AskUserQuestion call. Mirrors the
|
|
202
|
+
* Claude Agent SDK shape so we can pass-through with no translation.
|
|
203
|
+
*/
|
|
204
|
+
export interface AskUserQuestionOption {
|
|
205
|
+
label: string;
|
|
206
|
+
description: string;
|
|
207
|
+
/** Optional HTML/Markdown preview snippet (when previewFormat is set). */
|
|
208
|
+
preview?: string;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* One question in an AskUserQuestion call. Per Claude SDK: each call
|
|
213
|
+
* carries 1–4 questions, each question 2–4 options. Header is a short
|
|
214
|
+
* label (≤12 chars) used for compact display.
|
|
215
|
+
*/
|
|
216
|
+
export interface AskUserQuestionItem {
|
|
217
|
+
question: string;
|
|
218
|
+
header: string;
|
|
219
|
+
options: AskUserQuestionOption[];
|
|
220
|
+
multiSelect: boolean;
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
/** Outbound payload (cli → web) when Claude pauses on an AskUserQuestion. */
|
|
224
|
+
export interface AskUserQuestionPayload {
|
|
225
|
+
toolUseId: string;
|
|
226
|
+
questions: AskUserQuestionItem[];
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/** Outbound payload (cli → web) when a pending question is no longer answerable. */
|
|
230
|
+
export interface AskUserQuestionDismissedPayload {
|
|
231
|
+
toolUseId: string;
|
|
232
|
+
reason: 'timeout' | 'cancelled' | 'session-ended';
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/** Inbound payload (web → cli) carrying the user's selections. */
|
|
236
|
+
export interface AskUserQuestionResponseData {
|
|
237
|
+
toolUseId: string;
|
|
238
|
+
/** Map of question text → selected label(s) (joined with ", " for multi-select). */
|
|
239
|
+
answers: Record<string, string>;
|
|
240
|
+
}
|
|
241
|
+
|
|
196
242
|
export interface SkillEntry {
|
|
197
243
|
name: string;
|
|
198
244
|
displayName: string;
|