@adhdev/daemon-core 0.9.82-rc.169 → 0.9.82-rc.170
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/cli-adapter-types.d.ts +3 -0
- package/dist/cli-adapters/provider-cli-adapter.d.ts +2 -0
- package/dist/cli-adapters/provider-cli-shared.d.ts +2 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +474 -74
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +467 -74
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-events.d.ts +19 -0
- package/dist/providers/cli-provider-instance.d.ts +2 -1
- package/dist/providers/provider-instance.d.ts +3 -0
- package/dist/providers/spec/cli-adapter.d.ts +10 -0
- package/dist/providers/spec/driver.d.ts +1 -0
- package/dist/providers/types/interactive-prompt.d.ts +48 -0
- package/dist/shared-types.d.ts +3 -0
- package/package.json +1 -1
- package/src/cli-adapter-types.ts +3 -0
- package/src/cli-adapters/provider-cli-adapter.ts +6 -0
- package/src/cli-adapters/provider-cli-shared.ts +2 -0
- package/src/index.ts +15 -0
- package/src/mesh/mesh-events.ts +41 -0
- package/src/providers/cli-provider-instance.ts +61 -12
- package/src/providers/provider-instance.ts +3 -0
- package/src/providers/spec/cli-adapter.ts +109 -6
- package/src/providers/spec/driver.ts +4 -0
- package/src/providers/types/interactive-prompt.ts +297 -0
- package/src/shared-types.ts +3 -0
- package/src/status/builders.ts +1 -0
|
@@ -25,6 +25,15 @@ import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
|
|
|
25
25
|
import type { ChatMessage } from '../../types.js';
|
|
26
26
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
27
27
|
import { LOG } from '../../logging/logger.js';
|
|
28
|
+
import {
|
|
29
|
+
buildClaudeInteractiveTuiAnswerSteps,
|
|
30
|
+
buildClaudeInteractiveToolResult,
|
|
31
|
+
detectClaudeAskUserQuestionPromptFromJson,
|
|
32
|
+
detectClaudeAskUserQuestionPromptFromTuiPages,
|
|
33
|
+
type ClaudeInteractiveTuiPage,
|
|
34
|
+
type InteractivePrompt,
|
|
35
|
+
type InteractivePromptResponse,
|
|
36
|
+
} from '../types/interactive-prompt.js';
|
|
28
37
|
|
|
29
38
|
export class SpecCliAdapter implements CliAdapter {
|
|
30
39
|
readonly cliType: string;
|
|
@@ -48,6 +57,10 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
48
57
|
private statusCallback: (() => void) | null = null;
|
|
49
58
|
private ptyDataCallback: ((data: string) => void) | null = null;
|
|
50
59
|
private partialResponse = '';
|
|
60
|
+
private activeInteractivePrompt: InteractivePrompt | null = null;
|
|
61
|
+
private interactivePromptTransport: 'stream-json' | 'tui' | null = null;
|
|
62
|
+
private claudeTuiPromptCaptureInFlight = false;
|
|
63
|
+
private jsonLineTail = '';
|
|
51
64
|
private exited = false;
|
|
52
65
|
private spawned = false;
|
|
53
66
|
private providerSessionId: string | undefined;
|
|
@@ -115,15 +128,15 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
115
128
|
}
|
|
116
129
|
|
|
117
130
|
getStatus(): CliAdapterStatus {
|
|
118
|
-
if (this.exited) return { status: 'stopped', messages: [], activeModal: null };
|
|
119
|
-
if (!this.spawned) return { status: 'starting', messages: [], activeModal: null };
|
|
131
|
+
if (this.exited) return { status: 'stopped', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
132
|
+
if (!this.spawned) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
120
133
|
|
|
121
134
|
// Refresh native history lazily — the watch_path is cheap to stat,
|
|
122
135
|
// but parsing a full session.jsonl every call would be wasteful.
|
|
123
136
|
this.maybeRefreshNativeHistory();
|
|
124
137
|
|
|
125
138
|
const state = this.latestState;
|
|
126
|
-
if (!state) return { status: 'starting', messages: [], activeModal: null };
|
|
139
|
+
if (!state) return { status: 'starting', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
127
140
|
|
|
128
141
|
const modal = this.latestModal;
|
|
129
142
|
const lc = state.id.toLowerCase();
|
|
@@ -135,12 +148,13 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
135
148
|
message: modal.title ?? state.label,
|
|
136
149
|
buttons: modal.buttons.map(b => b.label),
|
|
137
150
|
},
|
|
151
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
138
152
|
};
|
|
139
153
|
}
|
|
140
154
|
if (/busy|generating|working|running|thinking/i.test(lc + ' ' + state.label)) {
|
|
141
|
-
return { status: 'generating', messages: [], activeModal: null };
|
|
155
|
+
return { status: 'generating', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
142
156
|
}
|
|
143
|
-
return { status: 'idle', messages: [], activeModal: null };
|
|
157
|
+
return { status: 'idle', messages: [], activeModal: null, activeInteractivePrompt: this.activeInteractivePrompt };
|
|
144
158
|
}
|
|
145
159
|
|
|
146
160
|
private maybeRefreshNativeHistory(): void {
|
|
@@ -216,6 +230,24 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
216
230
|
this.resolveModal(target);
|
|
217
231
|
}
|
|
218
232
|
|
|
233
|
+
async setInteractivePromptResponse(response: InteractivePromptResponse): Promise<void> {
|
|
234
|
+
const prompt = this.activeInteractivePrompt;
|
|
235
|
+
if (!prompt || prompt.promptId !== response.promptId) throw new Error('Interactive prompt response does not match active prompt');
|
|
236
|
+
if (this.cliType !== 'claude-cli') return;
|
|
237
|
+
if (this.interactivePromptTransport === 'tui') {
|
|
238
|
+
const steps = buildClaudeInteractiveTuiAnswerSteps(prompt, response);
|
|
239
|
+
for (const step of steps) {
|
|
240
|
+
this.driver.dispatch({ kind: 'pty_write', data: step });
|
|
241
|
+
await new Promise(resolve => setTimeout(resolve, 180));
|
|
242
|
+
}
|
|
243
|
+
} else {
|
|
244
|
+
this.driver.dispatch({ kind: 'pty_write', data: `${buildClaudeInteractiveToolResult(response)}\n` });
|
|
245
|
+
}
|
|
246
|
+
this.activeInteractivePrompt = null;
|
|
247
|
+
this.interactivePromptTransport = null;
|
|
248
|
+
this.statusCallback?.();
|
|
249
|
+
}
|
|
250
|
+
|
|
219
251
|
isApprovalRecentlyResolved(): boolean { return false; }
|
|
220
252
|
clearHistory(): void { /* no transcript buffer yet */ }
|
|
221
253
|
updateRuntimeSettings(): void { /* no runtime settings in spec model yet */ }
|
|
@@ -274,6 +306,7 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
274
306
|
spec_id: this.spec.id,
|
|
275
307
|
current_state: this.latestState,
|
|
276
308
|
current_modal: this.latestModal,
|
|
309
|
+
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
277
310
|
exited: this.exited,
|
|
278
311
|
};
|
|
279
312
|
}
|
|
@@ -307,9 +340,12 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
307
340
|
if (ev.state.title) {
|
|
308
341
|
LOG.debug('SpecAdapter', `[${this.cliType}] state.title=${JSON.stringify(ev.state.title)}`);
|
|
309
342
|
}
|
|
343
|
+
this.maybeCaptureClaudeTuiPrompt();
|
|
310
344
|
this.statusCallback?.();
|
|
311
345
|
return;
|
|
312
346
|
case 'pty_data':
|
|
347
|
+
this.detectInteractivePromptFromPtyChunk(ev.chunk);
|
|
348
|
+
this.maybeCaptureClaudeTuiPrompt();
|
|
313
349
|
try { this.ptyDataCallback?.(ev.chunk); } catch { /* ignore */ }
|
|
314
350
|
return;
|
|
315
351
|
case 'exit':
|
|
@@ -323,5 +359,72 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
323
359
|
return;
|
|
324
360
|
}
|
|
325
361
|
}
|
|
326
|
-
}
|
|
327
362
|
|
|
363
|
+
private detectInteractivePromptFromPtyChunk(chunk: string): void {
|
|
364
|
+
if (this.cliType !== 'claude-cli' || !chunk) return;
|
|
365
|
+
this.jsonLineTail += chunk;
|
|
366
|
+
if (this.jsonLineTail.length > 64 * 1024) this.jsonLineTail = this.jsonLineTail.slice(-64 * 1024);
|
|
367
|
+
const lines = this.jsonLineTail.split(/\r?\n/);
|
|
368
|
+
this.jsonLineTail = lines.pop() || '';
|
|
369
|
+
for (const line of lines) {
|
|
370
|
+
const trimmed = line.trim();
|
|
371
|
+
if (!trimmed.startsWith('{') || !trimmed.includes('AskUserQuestion')) continue;
|
|
372
|
+
try {
|
|
373
|
+
const parsed = JSON.parse(trimmed);
|
|
374
|
+
const prompt = detectClaudeAskUserQuestionPromptFromJson(parsed, this.cliType);
|
|
375
|
+
if (!prompt) continue;
|
|
376
|
+
this.activeInteractivePrompt = prompt;
|
|
377
|
+
this.interactivePromptTransport = 'stream-json';
|
|
378
|
+
this.statusCallback?.();
|
|
379
|
+
} catch {
|
|
380
|
+
// PTY output is not guaranteed to be machine JSON.
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
private maybeCaptureClaudeTuiPrompt(): void {
|
|
386
|
+
if (this.cliType !== 'claude-cli'
|
|
387
|
+
|| this.activeInteractivePrompt
|
|
388
|
+
|| this.claudeTuiPromptCaptureInFlight) return;
|
|
389
|
+
const screenText = this.driver.snapshot();
|
|
390
|
+
const headers = this.readClaudeTuiHeaders(screenText);
|
|
391
|
+
if (headers.length === 0 || !screenText.includes('Enter to select')) return;
|
|
392
|
+
this.claudeTuiPromptCaptureInFlight = true;
|
|
393
|
+
void this.captureClaudeTuiPrompt(screenText, headers).finally(() => {
|
|
394
|
+
this.claudeTuiPromptCaptureInFlight = false;
|
|
395
|
+
});
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
private readClaudeTuiHeaders(screenText: string): string[] {
|
|
399
|
+
const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
|
|
400
|
+
if (!navLine) return [];
|
|
401
|
+
const headers: string[] = [];
|
|
402
|
+
for (const match of navLine.matchAll(/[☐☒]\s+(.+?)(?=\s+[☐☒]|\s+✔\s+Submit)/g)) {
|
|
403
|
+
const header = match[1]?.trim();
|
|
404
|
+
if (header) headers.push(header);
|
|
405
|
+
}
|
|
406
|
+
return headers;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
private async captureClaudeTuiPrompt(firstScreen: string, headers: string[]): Promise<void> {
|
|
410
|
+
const pages: ClaudeInteractiveTuiPage[] = [{ screenText: firstScreen, header: headers[0] }];
|
|
411
|
+
for (let index = 1; index < headers.length; index += 1) {
|
|
412
|
+
this.driver.dispatch({ kind: 'pty_write', data: '\t' });
|
|
413
|
+
await new Promise(resolve => setTimeout(resolve, 120));
|
|
414
|
+
pages.push({ screenText: this.driver.snapshot(), header: headers[index] });
|
|
415
|
+
}
|
|
416
|
+
for (let index = headers.length - 1; index > 0; index -= 1) {
|
|
417
|
+
this.driver.dispatch({ kind: 'pty_write', data: '\x1b[Z' });
|
|
418
|
+
await new Promise(resolve => setTimeout(resolve, 80));
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
const prompt = detectClaudeAskUserQuestionPromptFromTuiPages(pages, {
|
|
422
|
+
promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
|
|
423
|
+
providerType: this.cliType,
|
|
424
|
+
});
|
|
425
|
+
if (!prompt) return;
|
|
426
|
+
this.activeInteractivePrompt = prompt;
|
|
427
|
+
this.interactivePromptTransport = 'tui';
|
|
428
|
+
this.statusCallback?.();
|
|
429
|
+
}
|
|
430
|
+
}
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
export interface InteractivePrompt {
|
|
2
|
+
promptId: string;
|
|
3
|
+
origin: 'cli' | 'mcp' | 'agent';
|
|
4
|
+
providerType: string;
|
|
5
|
+
createdAt: number;
|
|
6
|
+
questions: InteractiveQuestion[];
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export interface InteractiveQuestion {
|
|
10
|
+
questionId: string;
|
|
11
|
+
question: string;
|
|
12
|
+
header?: string;
|
|
13
|
+
multiSelect: boolean;
|
|
14
|
+
options: InteractiveOption[];
|
|
15
|
+
allowFreeform?: boolean;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export interface InteractiveOption {
|
|
19
|
+
label: string;
|
|
20
|
+
description?: string;
|
|
21
|
+
preview?: string;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
export interface InteractivePromptResponse {
|
|
25
|
+
promptId: string;
|
|
26
|
+
answers: Record<string, InteractiveAnswer>;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
export interface InteractiveAnswer {
|
|
30
|
+
selectedLabels: string[];
|
|
31
|
+
freeformText?: string;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function readString(value: unknown): string | undefined {
|
|
35
|
+
return typeof value === 'string' && value.trim() ? value.trim() : undefined;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function readStringArray(value: unknown): string[] {
|
|
39
|
+
return Array.isArray(value)
|
|
40
|
+
? value.map((item) => readString(item)).filter((item): item is string => !!item)
|
|
41
|
+
: [];
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
function normalizeOption(raw: unknown): InteractiveOption | null {
|
|
45
|
+
if (typeof raw === 'string') {
|
|
46
|
+
const label = raw.trim();
|
|
47
|
+
return label ? { label } : null;
|
|
48
|
+
}
|
|
49
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
50
|
+
const record = raw as Record<string, unknown>;
|
|
51
|
+
const label = readString(record.label);
|
|
52
|
+
if (!label) return null;
|
|
53
|
+
const description = readString(record.description);
|
|
54
|
+
const preview = readString(record.preview);
|
|
55
|
+
return {
|
|
56
|
+
label,
|
|
57
|
+
...(description ? { description } : {}),
|
|
58
|
+
...(preview ? { preview } : {}),
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function normalizeQuestion(raw: unknown, index: number): InteractiveQuestion | null {
|
|
63
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
64
|
+
const record = raw as Record<string, unknown>;
|
|
65
|
+
const question = readString(record.question);
|
|
66
|
+
if (!question) return null;
|
|
67
|
+
const questionId = readString(record.questionId) || readString(record.id) || `q${index + 1}`;
|
|
68
|
+
const options = Array.isArray(record.options)
|
|
69
|
+
? record.options.map(normalizeOption).filter((item): item is InteractiveOption => !!item)
|
|
70
|
+
: [];
|
|
71
|
+
const header = readString(record.header);
|
|
72
|
+
return {
|
|
73
|
+
questionId,
|
|
74
|
+
question,
|
|
75
|
+
...(header ? { header } : {}),
|
|
76
|
+
multiSelect: record.multiSelect === true,
|
|
77
|
+
options,
|
|
78
|
+
...(record.allowFreeform === true ? { allowFreeform: true } : {}),
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
export function normalizeInteractivePrompt(raw: unknown): InteractivePrompt | null {
|
|
83
|
+
if (!raw || typeof raw !== 'object') return null;
|
|
84
|
+
const record = raw as Record<string, unknown>;
|
|
85
|
+
const promptId = readString(record.promptId);
|
|
86
|
+
const providerType = readString(record.providerType);
|
|
87
|
+
const origin = record.origin === 'mcp' || record.origin === 'agent' ? record.origin : 'cli';
|
|
88
|
+
const questions = Array.isArray(record.questions)
|
|
89
|
+
? record.questions.map(normalizeQuestion).filter((item): item is InteractiveQuestion => !!item)
|
|
90
|
+
: [];
|
|
91
|
+
if (!promptId || !providerType || questions.length === 0) return null;
|
|
92
|
+
const createdAt = typeof record.createdAt === 'number' && Number.isFinite(record.createdAt)
|
|
93
|
+
? record.createdAt
|
|
94
|
+
: Date.now();
|
|
95
|
+
return { promptId, origin, providerType, createdAt, questions };
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export function normalizeInteractivePromptResponse(raw: unknown): InteractivePromptResponse {
|
|
99
|
+
if (!raw || typeof raw !== 'object') throw new Error('Interactive prompt response must be an object');
|
|
100
|
+
const record = raw as Record<string, unknown>;
|
|
101
|
+
const promptId = readString(record.promptId);
|
|
102
|
+
if (!promptId) throw new Error('promptId must be a non-empty string');
|
|
103
|
+
if (!record.answers || typeof record.answers !== 'object' || Array.isArray(record.answers)) {
|
|
104
|
+
throw new Error('answers must be an object');
|
|
105
|
+
}
|
|
106
|
+
const answers: Record<string, InteractiveAnswer> = {};
|
|
107
|
+
for (const [questionId, answerRaw] of Object.entries(record.answers as Record<string, unknown>)) {
|
|
108
|
+
if (!answerRaw || typeof answerRaw !== 'object' || Array.isArray(answerRaw)) continue;
|
|
109
|
+
const answer = answerRaw as Record<string, unknown>;
|
|
110
|
+
const selectedLabels = readStringArray(answer.selectedLabels);
|
|
111
|
+
const freeformText = readString(answer.freeformText);
|
|
112
|
+
answers[questionId] = {
|
|
113
|
+
selectedLabels,
|
|
114
|
+
...(freeformText ? { freeformText } : {}),
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
return { promptId, answers };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
export function buildClaudeInteractiveToolResult(response: InteractivePromptResponse): string {
|
|
121
|
+
return JSON.stringify({
|
|
122
|
+
type: 'user',
|
|
123
|
+
message: {
|
|
124
|
+
role: 'user',
|
|
125
|
+
content: [{
|
|
126
|
+
type: 'tool_result',
|
|
127
|
+
tool_use_id: response.promptId,
|
|
128
|
+
content: JSON.stringify({ answers: response.answers }),
|
|
129
|
+
is_error: false,
|
|
130
|
+
}],
|
|
131
|
+
},
|
|
132
|
+
});
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
export interface ClaudeInteractiveTuiPage {
|
|
136
|
+
screenText: string;
|
|
137
|
+
header?: string;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
function claudeTuiQuestionHeaders(screenText: string): string[] {
|
|
141
|
+
const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
|
|
142
|
+
if (!navLine) return [];
|
|
143
|
+
const headers: string[] = [];
|
|
144
|
+
const pattern = /[☐☒]\s+(.+?)(?=\s+[☐☒]|\s+✔\s+Submit)/g;
|
|
145
|
+
for (const match of navLine.matchAll(pattern)) {
|
|
146
|
+
const header = readString(match[1]);
|
|
147
|
+
if (header) headers.push(header);
|
|
148
|
+
}
|
|
149
|
+
return headers;
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index: number): InteractiveQuestion | null {
|
|
153
|
+
const lines = page.screenText.split(/\r?\n/);
|
|
154
|
+
let navIndex = -1;
|
|
155
|
+
for (let i = lines.length - 1; i >= 0; i -= 1) {
|
|
156
|
+
if (lines[i].includes('✔ Submit') && /[☐☒]/.test(lines[i])) {
|
|
157
|
+
navIndex = i;
|
|
158
|
+
break;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
if (navIndex < 0 || !page.screenText.includes('Enter to select')) return null;
|
|
162
|
+
|
|
163
|
+
let question = '';
|
|
164
|
+
let questionLineIndex = -1;
|
|
165
|
+
for (let i = navIndex + 1; i < lines.length; i += 1) {
|
|
166
|
+
const candidate = lines[i].trim();
|
|
167
|
+
if (!candidate || /^─+$/.test(candidate)) continue;
|
|
168
|
+
if (candidate === 'Review your answers' || candidate === 'Ready to submit your answers?') return null;
|
|
169
|
+
question = candidate;
|
|
170
|
+
questionLineIndex = i;
|
|
171
|
+
break;
|
|
172
|
+
}
|
|
173
|
+
if (!question) return null;
|
|
174
|
+
|
|
175
|
+
const options: InteractiveOption[] = [];
|
|
176
|
+
let allowFreeform = false;
|
|
177
|
+
const optionPattern = /^\s*(?:[❯›>]\s*)?(\d+)\.\s+(.+?)\s*$/;
|
|
178
|
+
for (let i = questionLineIndex + 1; i < lines.length; i += 1) {
|
|
179
|
+
const match = lines[i].match(optionPattern);
|
|
180
|
+
if (!match) continue;
|
|
181
|
+
const label = match[2].trim();
|
|
182
|
+
if (/^Type something\.?$/i.test(label)) {
|
|
183
|
+
allowFreeform = true;
|
|
184
|
+
continue;
|
|
185
|
+
}
|
|
186
|
+
if (/^Chat about this$/i.test(label)) continue;
|
|
187
|
+
|
|
188
|
+
let description: string | undefined;
|
|
189
|
+
const nextLine = lines[i + 1]?.trim();
|
|
190
|
+
if (nextLine
|
|
191
|
+
&& !optionPattern.test(lines[i + 1])
|
|
192
|
+
&& !/^─+$/.test(nextLine)
|
|
193
|
+
&& !/^Enter to select\b/.test(nextLine)) {
|
|
194
|
+
description = nextLine;
|
|
195
|
+
}
|
|
196
|
+
options.push({ label, ...(description ? { description } : {}) });
|
|
197
|
+
}
|
|
198
|
+
if (options.length === 0) return null;
|
|
199
|
+
|
|
200
|
+
const header = readString(page.header);
|
|
201
|
+
return {
|
|
202
|
+
questionId: `q${index + 1}`,
|
|
203
|
+
question,
|
|
204
|
+
...(header ? { header } : {}),
|
|
205
|
+
multiSelect: /Space to select|toggle selections/i.test(page.screenText),
|
|
206
|
+
options,
|
|
207
|
+
...(allowFreeform ? { allowFreeform: true } : {}),
|
|
208
|
+
};
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
export function detectClaudeAskUserQuestionPromptFromTuiPages(
|
|
212
|
+
pages: ClaudeInteractiveTuiPage[],
|
|
213
|
+
options: { promptId: string; providerType?: string; createdAt?: number },
|
|
214
|
+
): InteractivePrompt | null {
|
|
215
|
+
if (pages.length === 0) return null;
|
|
216
|
+
const headers = claudeTuiQuestionHeaders(pages[0].screenText);
|
|
217
|
+
const questions = pages.map((page, index) => parseClaudeInteractiveTuiQuestion({
|
|
218
|
+
...page,
|
|
219
|
+
header: page.header || headers[index],
|
|
220
|
+
}, index)).filter((question): question is InteractiveQuestion => !!question);
|
|
221
|
+
if (questions.length !== pages.length) return null;
|
|
222
|
+
return {
|
|
223
|
+
promptId: options.promptId,
|
|
224
|
+
origin: 'cli',
|
|
225
|
+
providerType: options.providerType || 'claude-cli',
|
|
226
|
+
createdAt: options.createdAt || Date.now(),
|
|
227
|
+
questions,
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
export function buildClaudeInteractiveTuiAnswerSteps(
|
|
232
|
+
prompt: InteractivePrompt,
|
|
233
|
+
response: InteractivePromptResponse,
|
|
234
|
+
): string[] {
|
|
235
|
+
if (response.promptId !== prompt.promptId) throw new Error('Interactive prompt response does not match active prompt');
|
|
236
|
+
const steps: string[] = [];
|
|
237
|
+
for (const question of prompt.questions) {
|
|
238
|
+
if (question.multiSelect) throw new Error('Claude TUI multi-select prompts are not supported yet');
|
|
239
|
+
const answer = response.answers[question.questionId];
|
|
240
|
+
if (!answer) throw new Error(`Missing answer for ${question.questionId}`);
|
|
241
|
+
if (answer.freeformText) throw new Error('Claude TUI freeform answers are not supported yet');
|
|
242
|
+
if (answer.selectedLabels.length !== 1) throw new Error(`Expected one selected label for ${question.questionId}`);
|
|
243
|
+
const selectedIndex = question.options.findIndex(option => option.label === answer.selectedLabels[0]);
|
|
244
|
+
if (selectedIndex < 0) throw new Error(`Unknown option for ${question.questionId}: ${answer.selectedLabels[0]}`);
|
|
245
|
+
steps.push(`${'\x1b[B'.repeat(selectedIndex)}\r`);
|
|
246
|
+
}
|
|
247
|
+
steps.push('\r');
|
|
248
|
+
return steps;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
export function interactivePromptFromClaudeAskUserQuestion(input: unknown, options: {
|
|
252
|
+
promptId: string;
|
|
253
|
+
providerType: string;
|
|
254
|
+
createdAt?: number;
|
|
255
|
+
origin?: InteractivePrompt['origin'];
|
|
256
|
+
}): InteractivePrompt | null {
|
|
257
|
+
if (!input || typeof input !== 'object') return null;
|
|
258
|
+
const record = input as Record<string, unknown>;
|
|
259
|
+
const questions = Array.isArray(record.questions)
|
|
260
|
+
? record.questions.map(normalizeQuestion).filter((item): item is InteractiveQuestion => !!item)
|
|
261
|
+
: [];
|
|
262
|
+
if (questions.length === 0) return null;
|
|
263
|
+
return {
|
|
264
|
+
promptId: options.promptId,
|
|
265
|
+
origin: options.origin || 'cli',
|
|
266
|
+
providerType: options.providerType,
|
|
267
|
+
createdAt: options.createdAt || Date.now(),
|
|
268
|
+
questions,
|
|
269
|
+
};
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
export function detectClaudeAskUserQuestionPromptFromJson(value: unknown, providerType = 'claude-cli'): InteractivePrompt | null {
|
|
273
|
+
if (!value || typeof value !== 'object') return null;
|
|
274
|
+
const record = value as Record<string, unknown>;
|
|
275
|
+
const blocks: unknown[] = [];
|
|
276
|
+
if (Array.isArray(record.content)) blocks.push(...record.content);
|
|
277
|
+
const message = record.message;
|
|
278
|
+
if (message && typeof message === 'object' && Array.isArray((message as Record<string, unknown>).content)) {
|
|
279
|
+
blocks.push(...((message as Record<string, unknown>).content as unknown[]));
|
|
280
|
+
}
|
|
281
|
+
if (record.type === 'tool_use') blocks.push(record);
|
|
282
|
+
|
|
283
|
+
for (const block of blocks) {
|
|
284
|
+
if (!block || typeof block !== 'object') continue;
|
|
285
|
+
const b = block as Record<string, unknown>;
|
|
286
|
+
const name = readString(b.name);
|
|
287
|
+
if (b.type !== 'tool_use' || name !== 'AskUserQuestion') continue;
|
|
288
|
+
const id = readString(b.id) || readString(record.id) || `ask-user-${Date.now()}`;
|
|
289
|
+
const prompt = interactivePromptFromClaudeAskUserQuestion(b.input, {
|
|
290
|
+
promptId: id,
|
|
291
|
+
providerType,
|
|
292
|
+
origin: 'cli',
|
|
293
|
+
});
|
|
294
|
+
if (prompt) return prompt;
|
|
295
|
+
}
|
|
296
|
+
return null;
|
|
297
|
+
}
|
package/src/shared-types.ts
CHANGED
|
@@ -56,6 +56,7 @@ import type {
|
|
|
56
56
|
GitWorkspaceUpdate,
|
|
57
57
|
WorkspaceGitSubscriptionParams,
|
|
58
58
|
} from './git/git-types.js';
|
|
59
|
+
import type { InteractivePrompt } from './providers/types/interactive-prompt.js';
|
|
59
60
|
|
|
60
61
|
export type {
|
|
61
62
|
GitCommandName,
|
|
@@ -110,6 +111,7 @@ export interface ReadChatSyncResult {
|
|
|
110
111
|
status: string;
|
|
111
112
|
title?: string;
|
|
112
113
|
activeModal?: { message: string; buttons: string[] } | null;
|
|
114
|
+
activeInteractivePrompt?: InteractivePrompt | null;
|
|
113
115
|
/**
|
|
114
116
|
* Chat source provenance from ChatSourceMachine (A2). Carries the
|
|
115
117
|
* selected source, transition cause, lock state, and legacy
|
|
@@ -372,6 +374,7 @@ export interface SessionEntry {
|
|
|
372
374
|
runtimeRecoveryState?: string | null;
|
|
373
375
|
resume?: ProviderResumeCapability;
|
|
374
376
|
activeChat: SessionActiveChatData | null;
|
|
377
|
+
activeInteractivePrompt?: InteractivePrompt | null;
|
|
375
378
|
capabilities?: SessionCapability[];
|
|
376
379
|
/** Effective message input/media support for this session. Defaults fail-closed to text-only. */
|
|
377
380
|
messageInput?: MessageInputSupport;
|
package/src/status/builders.ts
CHANGED
|
@@ -326,6 +326,7 @@ function buildCliSession(state: CliProviderState, options: SessionEntryBuildOpti
|
|
|
326
326
|
mode: state.mode,
|
|
327
327
|
resume: state.resume,
|
|
328
328
|
activeChat,
|
|
329
|
+
...(state.activeInteractivePrompt ? { activeInteractivePrompt: state.activeInteractivePrompt } : {}),
|
|
329
330
|
...(summaryMetadata && { summaryMetadata }),
|
|
330
331
|
...(includeSessionMetadata && {
|
|
331
332
|
capabilities: state.mode === 'terminal' ? PTY_SESSION_CAPABILITIES : CLI_CHAT_SESSION_CAPABILITIES,
|