@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.
@@ -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
+ }
@@ -180,6 +180,10 @@ export class SpecDriver {
180
180
  }
181
181
  }
182
182
 
183
+ snapshot(): string {
184
+ return this.adapter.snapshot();
185
+ }
186
+
183
187
  shutdown(): void {
184
188
  for (const t of this.delegateTimers.values()) clearTimeout(t);
185
189
  this.delegateTimers.clear();
@@ -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
+ }
@@ -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;
@@ -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,