@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.
- package/dist/index.js +251 -21
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +251 -21
- package/dist/index.mjs.map +1 -1
- package/dist/providers/cli-provider-instance.d.ts +20 -0
- package/dist/providers/provider-instance-manager.d.ts +12 -0
- package/dist/providers/provider-instance.d.ts +9 -0
- package/dist/providers/spec/cli-adapter.d.ts +2 -0
- package/package.json +1 -1
- package/src/commands/cli-manager.ts +16 -0
- package/src/mesh/mesh-events.ts +0 -19
- package/src/providers/cli-provider-instance.ts +53 -2
- package/src/providers/provider-instance-manager.ts +30 -0
- package/src/providers/provider-instance.ts +6 -0
- package/src/providers/spec/cli-adapter.ts +56 -2
- package/src/providers/types/interactive-prompt.ts +103 -4
|
@@ -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
|
@@ -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') {
|
package/src/mesh/mesh-events.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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(
|
|
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
|
-
&& !
|
|
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;
|