@adhdev/daemon-core 0.9.82-rc.180 → 0.9.82-rc.182

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.
@@ -97,6 +97,26 @@ export declare class CliProviderInstance implements ProviderInstance {
97
97
  getHotChatSessionState(): HotChatSessionState;
98
98
  getSessionModalState(sessionId?: string): SessionModalState;
99
99
  updateSettings(newSettings: Record<string, any>): void;
100
+ /**
101
+ * Stamp a direct-dispatch mesh assignment on this instance.
102
+ * setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
103
+ * route generating_completed back to the originating coordinator. Without
104
+ * this stamp, mesh_send_task --direct targets a plain CLI session whose
105
+ * completion events silently drop because the forwarder has nothing to
106
+ * match against.
107
+ */
108
+ attachMeshAssignment(assignment: {
109
+ meshId: string;
110
+ nodeId?: string;
111
+ taskId?: string;
112
+ }): void;
113
+ /**
114
+ * Clear a previously-attached mesh assignment after the task reaches a
115
+ * terminal state. Leaving meshNodeFor pinned would route this session's
116
+ * subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
117
+ * coordinator as if they were task completions.
118
+ */
119
+ detachMeshAssignment(): void;
100
120
  onEvent(event: string, data?: any): void;
101
121
  recordAcknowledgedUserInput(input: InputEnvelope | string): void;
102
122
  dispose(): void;
@@ -82,6 +82,18 @@ export declare class ProviderInstanceManager {
82
82
  * Called when user changes settings from dashboard.
83
83
  */
84
84
  updateInstanceSettings(providerType: string, settings: Record<string, any>): number;
85
+ /** Stamp a mesh assignment on a single instance (used by mesh_send_task
86
+ * --direct so the worker's completion event has a coordinator routing
87
+ * marker in state.settings). Returns true if the instance existed and
88
+ * the stamp was applied. */
89
+ attachMeshAssignmentToInstance(instanceId: string, assignment: {
90
+ meshId: string;
91
+ nodeId?: string;
92
+ taskId?: string;
93
+ }): boolean;
94
+ /** Clear a mesh assignment after the dispatched task reaches a terminal
95
+ * state (generating_completed / stopped / failed). */
96
+ detachMeshAssignmentFromInstance(instanceId: string): boolean;
85
97
  refreshProviderDefinitions(resolveProvider: (providerType: string) => unknown): number;
86
98
  /**
87
99
  * All terminate
@@ -181,6 +181,15 @@ export interface ProviderInstance {
181
181
  onEvent(event: string, data?: any): void;
182
182
  /** Update settings at runtime (called when user changes settings from dashboard) */
183
183
  updateSettings?(newSettings: Record<string, any>): void;
184
+ /** Stamp a direct-dispatch mesh task assignment so generating_completed
185
+ * events route back to the originating coordinator. Cleared by
186
+ * detachMeshAssignment when the task reaches a terminal state. */
187
+ attachMeshAssignment?(assignment: {
188
+ meshId: string;
189
+ nodeId?: string;
190
+ taskId?: string;
191
+ }): void;
192
+ detachMeshAssignment?(): void;
184
193
  /** Refresh static provider definition/scripts without restarting the live runtime. */
185
194
  refreshProviderDefinition?(provider: ProviderModule): void;
186
195
  /** cleanup */
@@ -89,6 +89,8 @@ export declare class SpecCliAdapter implements CliAdapter {
89
89
  refreshProviderDefinition(): void;
90
90
  private handleEvent;
91
91
  private detectInteractivePromptFromPtyChunk;
92
+ private readCurrentScreenSections;
93
+ private readClaudeScreenAssistantMessages;
92
94
  private maybeCaptureClaudeTuiPrompt;
93
95
  private readClaudeTuiHeaders;
94
96
  private captureClaudeTuiPrompt;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.180",
3
+ "version": "0.9.82-rc.182",
4
4
  "description": "ADHDev daemon core — CDP, IDE detection, providers, command execution",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -1272,6 +1272,22 @@ export class DaemonCliManager {
1272
1272
  } else if (currentStatus === 'starting') {
1273
1273
  currentStatus = getEffectiveAgentSendStatus(adapter);
1274
1274
  }
1275
+ // Stamp mesh direct-dispatch assignment on the target
1276
+ // instance BEFORE sending the prompt so the completion
1277
+ // event has a routing marker by the time it fires.
1278
+ // mesh_send_task --direct ships meshContext for plain CLI
1279
+ // sessions that were never launched as mesh delegates.
1280
+ const meshContext = (args as any)?.meshContext;
1281
+ if (meshContext && typeof meshContext === 'object' && typeof meshContext.meshId === 'string' && meshContext.meshId) {
1282
+ const targetInstanceId = key;
1283
+ try {
1284
+ this.deps.getInstanceManager()?.attachMeshAssignmentToInstance(targetInstanceId, {
1285
+ meshId: meshContext.meshId,
1286
+ ...(typeof meshContext.nodeId === 'string' && meshContext.nodeId ? { nodeId: meshContext.nodeId } : {}),
1287
+ ...(typeof meshContext.taskId === 'string' && meshContext.taskId ? { taskId: meshContext.taskId } : {}),
1288
+ });
1289
+ } catch { /* best-effort */ }
1290
+ }
1275
1291
  const input = normalizeInputEnvelope(args?.input ? { input: args.input } : args);
1276
1292
  const provider = this.providerLoader.resolve(agentType) || this.providerLoader.getMeta(agentType);
1277
1293
  if (provider?.category === 'acp') {
@@ -1769,25 +1769,6 @@ export function setupMeshEventForwarding(components: DaemonComponents) {
1769
1769
  }
1770
1770
  } catch { /* best-effort */ }
1771
1771
  if (!meshIdFromDirectDispatch) return;
1772
- } else {
1773
- // Plain session (no meshNodeFor / meshCoordinatorFor / launchedByCoordinator)
1774
- // that received a direct dispatch via mesh_send_task: the dispatcher logs
1775
- // the dispatch against the workspace-resolved mesh but never stamps the
1776
- // target session's settings. Without this fallback, the worker's
1777
- // generating_completed event silently drops here and task_completed
1778
- // never lands in the ledger — coordinator sees the task as still in
1779
- // flight forever. Resolve the mesh via workspace and accept the event
1780
- // when an active direct dispatch points at this session.
1781
- const workspaceMesh = getCachedMeshByWorkspace(workspace);
1782
- const workspaceMeshId = readNonEmptyString(workspaceMesh?.id);
1783
- if (workspaceMeshId) {
1784
- try {
1785
- const activeDispatches = getActiveDirectDispatches(workspaceMeshId);
1786
- if (activeDispatches.some(d => d.sessionId === instanceId) || hasUnterminalDirectDispatchLedgerEntry(workspaceMeshId, instanceId)) {
1787
- meshIdFromDirectDispatch = workspaceMeshId;
1788
- }
1789
- } catch { /* best-effort */ }
1790
- }
1791
1772
  }
1792
1773
 
1793
1774
  const meshIdFromRuntime = readNonEmptyString(settings.meshNodeFor) || meshIdFromDirectDispatch;
@@ -86,6 +86,15 @@ type ExternalTranscriptProbe = {
86
86
  const COMPLETED_FINALIZATION_RETRY_MS = 1000;
87
87
  const COMPLETED_FINALIZATION_MAX_WAIT_MS = 30_000;
88
88
 
89
+ /** Events that signal a dispatched mesh task has reached a terminal state.
90
+ * Detach the mesh assignment after emitting one of these so the worker's
91
+ * next unrelated turn doesn't impersonate another completion. */
92
+ const TERMINAL_MESH_EVENTS = new Set([
93
+ 'agent:generating_completed',
94
+ 'agent:stopped',
95
+ 'agent:ready',
96
+ ]);
97
+
89
98
  const IMAGE_MIME_EXTENSIONS: Record<string, string> = {
90
99
  'image/png': '.png',
91
100
  'image/jpeg': '.jpg',
@@ -782,6 +791,39 @@ export class CliProviderInstance implements ProviderInstance {
782
791
  });
783
792
  }
784
793
 
794
+ /**
795
+ * Stamp a direct-dispatch mesh assignment on this instance.
796
+ * setupMeshEventForwarding reads settings.meshNodeFor + meshActiveTaskId to
797
+ * route generating_completed back to the originating coordinator. Without
798
+ * this stamp, mesh_send_task --direct targets a plain CLI session whose
799
+ * completion events silently drop because the forwarder has nothing to
800
+ * match against.
801
+ */
802
+ attachMeshAssignment(assignment: { meshId: string; nodeId?: string; taskId?: string }): void {
803
+ if (!assignment?.meshId) return;
804
+ this.settings = {
805
+ ...this.settings,
806
+ meshNodeFor: assignment.meshId,
807
+ ...(assignment.nodeId ? { meshNodeId: assignment.nodeId } : {}),
808
+ ...(assignment.taskId ? { meshActiveTaskId: assignment.taskId } : {}),
809
+ };
810
+ this.adapter.updateRuntimeSettings?.(this.settings);
811
+ }
812
+
813
+ /**
814
+ * Clear a previously-attached mesh assignment after the task reaches a
815
+ * terminal state. Leaving meshNodeFor pinned would route this session's
816
+ * subsequent unrelated turns (e.g. ad-hoc dashboard chats) to the
817
+ * coordinator as if they were task completions.
818
+ */
819
+ detachMeshAssignment(): void {
820
+ if (!this.settings.meshNodeFor && !this.settings.meshActiveTaskId && !this.settings.meshNodeId) return;
821
+ const { meshNodeFor, meshNodeId, meshActiveTaskId, ...rest } = this.settings;
822
+ void meshNodeFor; void meshNodeId; void meshActiveTaskId;
823
+ this.settings = rest;
824
+ this.adapter.updateRuntimeSettings?.(this.settings);
825
+ }
826
+
785
827
  onEvent(event: string, data?: any): void {
786
828
  if (event === 'send_message') {
787
829
  const input = normalizeInputEnvelope(data);
@@ -1513,9 +1555,18 @@ export class CliProviderInstance implements ProviderInstance {
1513
1555
  };
1514
1556
  if (this.context?.emitProviderEvent) {
1515
1557
  this.context.emitProviderEvent(enrichedEvent);
1516
- return;
1558
+ } else {
1559
+ this.events.push(enrichedEvent);
1560
+ }
1561
+ // Auto-detach a direct-dispatch mesh assignment once the dispatched
1562
+ // task reaches a terminal state. Leaving meshNodeFor pinned would
1563
+ // route this session's next unrelated turn (a dashboard chat) into
1564
+ // the coordinator as if it were the completion of another task.
1565
+ // We schedule after the emit so the originating coordinator still
1566
+ // observes the completion event with its routing marker intact.
1567
+ if (TERMINAL_MESH_EVENTS.has(event.event) && this.settings.meshActiveTaskId) {
1568
+ try { this.detachMeshAssignment(); } catch { /* best-effort */ }
1517
1569
  }
1518
- this.events.push(enrichedEvent);
1519
1570
  }
1520
1571
 
1521
1572
  private flushEvents(): ProviderEvent[] {
@@ -306,6 +306,36 @@ export class ProviderInstanceManager {
306
306
  return updated;
307
307
  }
308
308
 
309
+ /** Stamp a mesh assignment on a single instance (used by mesh_send_task
310
+ * --direct so the worker's completion event has a coordinator routing
311
+ * marker in state.settings). Returns true if the instance existed and
312
+ * the stamp was applied. */
313
+ attachMeshAssignmentToInstance(instanceId: string, assignment: { meshId: string; nodeId?: string; taskId?: string }): boolean {
314
+ const inst = this.instances.get(instanceId);
315
+ if (!inst || typeof inst.attachMeshAssignment !== 'function') {
316
+ try {
317
+ const { LOG } = require('../logging/logger.js');
318
+ LOG.warn?.('MeshDispatch', `attachMeshAssignment skipped: instance ${instanceId} ${inst ? 'has no attach method' : 'not found'}`);
319
+ } catch { /* noop */ }
320
+ return false;
321
+ }
322
+ inst.attachMeshAssignment(assignment);
323
+ try {
324
+ const { LOG } = require('../logging/logger.js');
325
+ LOG.info?.('MeshDispatch', `stamped mesh assignment on ${instanceId}: mesh=${assignment.meshId} node=${assignment.nodeId || ''} task=${assignment.taskId || ''}`);
326
+ } catch { /* noop */ }
327
+ return true;
328
+ }
329
+
330
+ /** Clear a mesh assignment after the dispatched task reaches a terminal
331
+ * state (generating_completed / stopped / failed). */
332
+ detachMeshAssignmentFromInstance(instanceId: string): boolean {
333
+ const inst = this.instances.get(instanceId);
334
+ if (!inst || typeof inst.detachMeshAssignment !== 'function') return false;
335
+ inst.detachMeshAssignment();
336
+ return true;
337
+ }
338
+
309
339
  refreshProviderDefinitions(resolveProvider: (providerType: string) => unknown): number {
310
340
  let refreshed = 0;
311
341
  for (const instance of this.instances.values()) {
@@ -217,6 +217,12 @@ export interface ProviderInstance {
217
217
  /** Update settings at runtime (called when user changes settings from dashboard) */
218
218
  updateSettings?(newSettings: Record<string, any>): void;
219
219
 
220
+ /** Stamp a direct-dispatch mesh task assignment so generating_completed
221
+ * events route back to the originating coordinator. Cleared by
222
+ * detachMeshAssignment when the task reaches a terminal state. */
223
+ attachMeshAssignment?(assignment: { meshId: string; nodeId?: string; taskId?: string }): void;
224
+ detachMeshAssignment?(): void;
225
+
220
226
  /** Refresh static provider definition/scripts without restarting the live runtime. */
221
227
  refreshProviderDefinition?(provider: ProviderModule): void;
222
228
 
@@ -19,6 +19,7 @@
19
19
  'use strict';
20
20
 
21
21
  import { SpecDriver, type DashboardEvent } from './driver.js';
22
+ import { evaluate } from './evaluator.js';
22
23
  import { loadSpec } from './loader.js';
23
24
  import type { CliSpec } from './types.js';
24
25
  import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
@@ -164,7 +165,11 @@ export class SpecCliAdapter implements CliAdapter {
164
165
  }
165
166
 
166
167
  getScriptParsedStatus(): unknown {
167
- return this.getStatus();
168
+ const status = this.getStatus();
169
+ return {
170
+ ...status,
171
+ messages: this.readClaudeScreenAssistantMessages(),
172
+ };
168
173
  }
169
174
 
170
175
  getPartialResponse(): string {
@@ -405,13 +410,62 @@ export class SpecCliAdapter implements CliAdapter {
405
410
  }
406
411
  }
407
412
 
413
+ private readCurrentScreenSections(screenText: string): Record<string, string> {
414
+ try {
415
+ const ev = evaluate(this.spec, screenText);
416
+ return Object.fromEntries(ev.sections.map(section => [section.id, section.text]));
417
+ } catch {
418
+ return {};
419
+ }
420
+ }
421
+
422
+ private readClaudeScreenAssistantMessages(): ChatMessage[] {
423
+ if (this.cliType !== 'claude-cli') return [];
424
+ let screenText = '';
425
+ try {
426
+ screenText = this.driver.snapshot();
427
+ } catch {
428
+ return [];
429
+ }
430
+ const sections = this.readCurrentScreenSections(screenText);
431
+ const body = sections.body || screenText;
432
+ const messages: ChatMessage[] = [];
433
+ const seen = new Set<string>();
434
+ for (const line of body.split(/\r?\n/)) {
435
+ const match = line.match(/^\s*⏺\s+(.+?)\s*$/);
436
+ const content = match?.[1]?.trim();
437
+ if (!content || seen.has(content)) continue;
438
+ seen.add(content);
439
+ messages.push({
440
+ role: 'assistant',
441
+ kind: 'standard',
442
+ content,
443
+ source: 'assistant_text',
444
+ userFacing: true,
445
+ bubbleState: 'final',
446
+ });
447
+ }
448
+ return messages;
449
+ }
450
+
408
451
  private maybeCaptureClaudeTuiPrompt(): void {
409
452
  if (this.cliType !== 'claude-cli'
410
453
  || this.activeInteractivePrompt
411
454
  || this.claudeTuiPromptCaptureInFlight) return;
412
455
  const screenText = this.driver.snapshot();
413
456
  const headers = this.readClaudeTuiHeaders(screenText);
414
- if (headers.length === 0 || !screenText.includes('Enter to select')) return;
457
+ if (!screenText.includes('Enter to select')) return;
458
+ if (headers.length === 0) {
459
+ const prompt = detectClaudeAskUserQuestionPromptFromTuiPages([{ screenText }], {
460
+ promptId: `ask-user-${this.providerSessionId || 'claude'}-${Date.now()}`,
461
+ providerType: this.cliType,
462
+ });
463
+ if (!prompt) return;
464
+ this.activeInteractivePrompt = prompt;
465
+ this.interactivePromptTransport = 'tui';
466
+ this.statusCallback?.();
467
+ return;
468
+ }
415
469
  this.claudeTuiPromptCaptureInFlight = true;
416
470
  void this.captureClaudeTuiPrompt(screenText, headers).finally(() => {
417
471
  this.claudeTuiPromptCaptureInFlight = false;
@@ -137,6 +137,8 @@ export interface ClaudeInteractiveTuiPage {
137
137
  header?: string;
138
138
  }
139
139
 
140
+ const CLAUDE_TUI_OPTION_PATTERN = /^\s*(?:[❯›>]\s*)?(\d+)\.\s+(.+?)\s*$/;
141
+
140
142
  function claudeTuiQuestionHeaders(screenText: string): string[] {
141
143
  const navLine = screenText.split(/\r?\n/).find(line => line.includes('✔ Submit') && /[☐☒]/.test(line));
142
144
  if (!navLine) return [];
@@ -149,6 +151,103 @@ function claudeTuiQuestionHeaders(screenText: string): string[] {
149
151
  return headers;
150
152
  }
151
153
 
154
+ function isClaudeTuiSelectFooter(text: string): boolean {
155
+ return /Enter to select/i.test(text) && /Esc to cancel/i.test(text);
156
+ }
157
+
158
+ function readClaudeHeaderLine(lines: string[], beforeIndex: number): string | undefined {
159
+ for (let i = beforeIndex; i >= 0; i -= 1) {
160
+ const candidate = lines[i].trim();
161
+ if (!candidate) continue;
162
+ const match = candidate.match(/^[☐☒]\s+(.+?)\s*$/);
163
+ if (match?.[1]) return readString(match[1]);
164
+ if (/^─+$/.test(candidate)) break;
165
+ }
166
+ return undefined;
167
+ }
168
+
169
+ function readClaudeOptionDescription(lines: string[], optionLineIndex: number): string | undefined {
170
+ const nextLine = lines[optionLineIndex + 1];
171
+ const next = nextLine?.trim();
172
+ if (!next
173
+ || CLAUDE_TUI_OPTION_PATTERN.test(nextLine)
174
+ || /^─+$/.test(next)
175
+ || /^Enter to select\b/i.test(next)
176
+ || /^[☐☒]\s+/.test(next)) {
177
+ return undefined;
178
+ }
179
+ return next;
180
+ }
181
+
182
+ function parseClaudeHeaderlessInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index: number): InteractiveQuestion | null {
183
+ if (!isClaudeTuiSelectFooter(page.screenText)) return null;
184
+ if (!/Type something\.?|Chat about this/i.test(page.screenText)) return null;
185
+
186
+ const lines = page.screenText.split(/\r?\n/);
187
+ let footerIndex = -1;
188
+ for (let i = lines.length - 1; i >= 0; i -= 1) {
189
+ if (/Enter to select/i.test(lines[i])) {
190
+ footerIndex = i;
191
+ break;
192
+ }
193
+ }
194
+ if (footerIndex < 0) return null;
195
+
196
+ let optionBlockEnd = footerIndex - 1;
197
+ for (let i = footerIndex - 1; i >= 0; i -= 1) {
198
+ if (/^─+$/.test(lines[i].trim())) {
199
+ optionBlockEnd = i - 1;
200
+ break;
201
+ }
202
+ }
203
+
204
+ const optionLineIndexes: number[] = [];
205
+ for (let i = optionBlockEnd; i >= 0; i -= 1) {
206
+ const line = lines[i];
207
+ if (CLAUDE_TUI_OPTION_PATTERN.test(line)) {
208
+ optionLineIndexes.push(i);
209
+ continue;
210
+ }
211
+ if (optionLineIndexes.length > 0 && (!line.trim() || /^─+$/.test(line.trim()))) break;
212
+ }
213
+ optionLineIndexes.reverse();
214
+ if (optionLineIndexes.length === 0) return null;
215
+
216
+ const firstOptionIndex = optionLineIndexes[0];
217
+ let question = '';
218
+ for (let i = firstOptionIndex - 1; i >= 0; i -= 1) {
219
+ const candidate = lines[i].trim();
220
+ if (!candidate || /^─+$/.test(candidate) || /^[☐☒]\s+/.test(candidate)) continue;
221
+ question = candidate;
222
+ break;
223
+ }
224
+ if (!question) return null;
225
+
226
+ const options: InteractiveOption[] = [];
227
+ let allowFreeform = false;
228
+ for (const optionLineIndex of optionLineIndexes) {
229
+ const match = lines[optionLineIndex].match(CLAUDE_TUI_OPTION_PATTERN);
230
+ if (!match) continue;
231
+ const label = match[2].trim();
232
+ if (/^Chat about this$/i.test(label)) continue;
233
+ if (/^Type something\.?$/i.test(label)) allowFreeform = true;
234
+
235
+ const description = readClaudeOptionDescription(lines, optionLineIndex);
236
+ options.push({ label, ...(description ? { description } : {}) });
237
+ }
238
+ if (options.length === 0) return null;
239
+
240
+ const header = readString(page.header) || readClaudeHeaderLine(lines, firstOptionIndex - 1);
241
+ return {
242
+ questionId: `q${index + 1}`,
243
+ question,
244
+ ...(header ? { header } : {}),
245
+ multiSelect: /Space to select|toggle selections/i.test(page.screenText),
246
+ options,
247
+ ...(allowFreeform ? { allowFreeform: true } : {}),
248
+ };
249
+ }
250
+
152
251
  function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index: number): InteractiveQuestion | null {
153
252
  const lines = page.screenText.split(/\r?\n/);
154
253
  let navIndex = -1;
@@ -158,7 +257,8 @@ function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index
158
257
  break;
159
258
  }
160
259
  }
161
- if (navIndex < 0 || !page.screenText.includes('Enter to select')) return null;
260
+ if (navIndex < 0) return parseClaudeHeaderlessInteractiveTuiQuestion(page, index);
261
+ if (!page.screenText.includes('Enter to select')) return null;
162
262
 
163
263
  let question = '';
164
264
  let questionLineIndex = -1;
@@ -174,9 +274,8 @@ function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index
174
274
 
175
275
  const options: InteractiveOption[] = [];
176
276
  let allowFreeform = false;
177
- const optionPattern = /^\s*(?:[❯›>]\s*)?(\d+)\.\s+(.+?)\s*$/;
178
277
  for (let i = questionLineIndex + 1; i < lines.length; i += 1) {
179
- const match = lines[i].match(optionPattern);
278
+ const match = lines[i].match(CLAUDE_TUI_OPTION_PATTERN);
180
279
  if (!match) continue;
181
280
  const label = match[2].trim();
182
281
  if (/^Type something\.?$/i.test(label)) {
@@ -188,7 +287,7 @@ function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index
188
287
  let description: string | undefined;
189
288
  const nextLine = lines[i + 1]?.trim();
190
289
  if (nextLine
191
- && !optionPattern.test(lines[i + 1])
290
+ && !CLAUDE_TUI_OPTION_PATTERN.test(lines[i + 1])
192
291
  && !/^─+$/.test(nextLine)
193
292
  && !/^Enter to select\b/.test(nextLine)) {
194
293
  description = nextLine;