@adhdev/daemon-core 0.9.82-rc.294 → 0.9.82-rc.296
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 +81 -14
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +81 -14
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/cli-adapter.d.ts +32 -0
- package/dist/providers/types/interactive-prompt.d.ts +17 -0
- package/package.json +2 -2
- package/src/providers/spec/cli-adapter.ts +80 -12
- package/src/providers/types/interactive-prompt.ts +48 -8
|
@@ -118,6 +118,15 @@ export declare class SpecCliAdapter implements CliAdapter {
|
|
|
118
118
|
* spirit as the approval FSM's `approvalCooldown` modal-lost hysteresis.
|
|
119
119
|
*/
|
|
120
120
|
private static readonly INTERACTIVE_PROMPT_LOST_GRACE_MS;
|
|
121
|
+
/**
|
|
122
|
+
* Multi-question TUI capture: after Tabbing to a page, re-snapshot at this
|
|
123
|
+
* interval until its checkbox glyph column has settled, up to the timeout.
|
|
124
|
+
* The interval matches the legacy single fixed wait (120ms); the timeout
|
|
125
|
+
* bounds total capture time so a single-select page (which never shows
|
|
126
|
+
* glyphs) doesn't stall the capture indefinitely.
|
|
127
|
+
*/
|
|
128
|
+
private static readonly CLAUDE_TUI_PAGE_POLL_INTERVAL_MS;
|
|
129
|
+
private static readonly CLAUDE_TUI_PAGE_SETTLE_TIMEOUT_MS;
|
|
121
130
|
/**
|
|
122
131
|
* Clear a held interactive prompt once the user has resolved it directly
|
|
123
132
|
* in the terminal (the choice picker leaves the screen without going
|
|
@@ -144,9 +153,32 @@ export declare class SpecCliAdapter implements CliAdapter {
|
|
|
144
153
|
* question to multi-select and re-emit status. Promotion is one-way
|
|
145
154
|
* (false→true only) — once a question is known multi-select we never demote
|
|
146
155
|
* it, since the glyph column can scroll out of view on later frames.
|
|
156
|
+
*
|
|
157
|
+
* For MULTI-question prompts the per-page Tab capture is the actual source
|
|
158
|
+
* of the bug: pages 2..N are snapshotted ~120ms after the Tab keypress,
|
|
159
|
+
* before their option-row glyph column has redrawn, so those questions
|
|
160
|
+
* freeze as single-select while page 1 (already settled) is correct. We
|
|
161
|
+
* cannot upgrade blindly — the live snapshot shows only ONE focused page —
|
|
162
|
+
* but we CAN read that page's question text/header and upgrade the matching
|
|
163
|
+
* question. As the user navigates the picker (or it settles), each page is
|
|
164
|
+
* eventually re-read and repaired.
|
|
147
165
|
*/
|
|
148
166
|
private maybeUpgradeClaudeTuiMultiSelect;
|
|
149
167
|
private readClaudeTuiHeaders;
|
|
168
|
+
/**
|
|
169
|
+
* Snapshot the currently-focused claude TUI page, polling until its
|
|
170
|
+
* option-row checkbox glyph column has settled (or a bounded timeout).
|
|
171
|
+
*
|
|
172
|
+
* Why poll: right after a Tab keypress the newly-focused page's glyph
|
|
173
|
+
* column has not redrawn yet, so an immediate snapshot shows the question +
|
|
174
|
+
* option labels but NO per-option checkbox markers — freezing that page as
|
|
175
|
+
* single-select. A fixed delay either races (too short) or is wasteful (too
|
|
176
|
+
* long). Instead we re-snapshot at a fixed interval and stop as soon as the
|
|
177
|
+
* frame shows multi-select glyphs, falling back to the last frame at the
|
|
178
|
+
* timeout. Single-select pages never show glyphs, so they always poll to the
|
|
179
|
+
* timeout — bounded small to keep capture snappy.
|
|
180
|
+
*/
|
|
181
|
+
private snapshotSettledClaudeTuiPage;
|
|
150
182
|
private captureClaudeTuiPrompt;
|
|
151
183
|
getDebugState(): Record<string, any>;
|
|
152
184
|
getTraceState(limit?: number): Record<string, any>;
|
|
@@ -52,6 +52,23 @@ export interface ClaudeInteractiveTuiPage {
|
|
|
52
52
|
* as a secondary signal for layouts that render markers differently.
|
|
53
53
|
*/
|
|
54
54
|
export declare function detectClaudeTuiMultiSelect(screenText: string): boolean;
|
|
55
|
+
/**
|
|
56
|
+
* Read the question the live claude TUI picker is CURRENTLY focused on, plus
|
|
57
|
+
* whether that focused page renders multi-select checkbox markers.
|
|
58
|
+
*
|
|
59
|
+
* Used to repair a multi-question prompt after the fact: when the daemon
|
|
60
|
+
* Tab-captures pages 2..N it snapshots ~120ms after the Tab keypress, before
|
|
61
|
+
* the newly-focused page's option-row checkbox column has redrawn — so those
|
|
62
|
+
* questions get frozen as multiSelect:false even though the picker is
|
|
63
|
+
* multi-select. Re-reading the focused page on a later status tick (once it has
|
|
64
|
+
* settled) lets us attribute the now-visible glyphs to the matching question
|
|
65
|
+
* and upgrade just that one. Returns null when no picker question is on screen.
|
|
66
|
+
*/
|
|
67
|
+
export declare function readFocusedClaudeTuiQuestion(screenText: string): {
|
|
68
|
+
question: string;
|
|
69
|
+
header?: string;
|
|
70
|
+
multiSelect: boolean;
|
|
71
|
+
} | null;
|
|
55
72
|
export declare function detectClaudeAskUserQuestionPromptFromTuiPages(pages: ClaudeInteractiveTuiPage[], options: {
|
|
56
73
|
promptId: string;
|
|
57
74
|
providerType?: string;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@adhdev/daemon-core",
|
|
3
|
-
"version": "0.9.82-rc.
|
|
3
|
+
"version": "0.9.82-rc.296",
|
|
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",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"author": "vilmire",
|
|
47
47
|
"license": "AGPL-3.0-or-later",
|
|
48
48
|
"dependencies": {
|
|
49
|
-
"@adhdev/mesh-shared": "0.9.82-rc.
|
|
49
|
+
"@adhdev/mesh-shared": "0.9.82-rc.296",
|
|
50
50
|
"@adhdev/session-host-core": "*",
|
|
51
51
|
"@agentclientprotocol/sdk": "^0.16.1",
|
|
52
52
|
"ajv": "^8.20.0",
|
|
@@ -32,6 +32,7 @@ import {
|
|
|
32
32
|
detectClaudeAskUserQuestionPromptFromJson,
|
|
33
33
|
detectClaudeAskUserQuestionPromptFromTuiPages,
|
|
34
34
|
detectClaudeTuiMultiSelect,
|
|
35
|
+
readFocusedClaudeTuiQuestion,
|
|
35
36
|
type ClaudeInteractiveTuiPage,
|
|
36
37
|
type InteractivePrompt,
|
|
37
38
|
type InteractivePromptResponse,
|
|
@@ -551,6 +552,16 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
551
552
|
*/
|
|
552
553
|
private static readonly INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
|
|
553
554
|
|
|
555
|
+
/**
|
|
556
|
+
* Multi-question TUI capture: after Tabbing to a page, re-snapshot at this
|
|
557
|
+
* interval until its checkbox glyph column has settled, up to the timeout.
|
|
558
|
+
* The interval matches the legacy single fixed wait (120ms); the timeout
|
|
559
|
+
* bounds total capture time so a single-select page (which never shows
|
|
560
|
+
* glyphs) doesn't stall the capture indefinitely.
|
|
561
|
+
*/
|
|
562
|
+
private static readonly CLAUDE_TUI_PAGE_POLL_INTERVAL_MS = 120;
|
|
563
|
+
private static readonly CLAUDE_TUI_PAGE_SETTLE_TIMEOUT_MS = 600;
|
|
564
|
+
|
|
554
565
|
/**
|
|
555
566
|
* Clear a held interactive prompt once the user has resolved it directly
|
|
556
567
|
* in the terminal (the choice picker leaves the screen without going
|
|
@@ -629,19 +640,22 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
629
640
|
* question to multi-select and re-emit status. Promotion is one-way
|
|
630
641
|
* (false→true only) — once a question is known multi-select we never demote
|
|
631
642
|
* it, since the glyph column can scroll out of view on later frames.
|
|
643
|
+
*
|
|
644
|
+
* For MULTI-question prompts the per-page Tab capture is the actual source
|
|
645
|
+
* of the bug: pages 2..N are snapshotted ~120ms after the Tab keypress,
|
|
646
|
+
* before their option-row glyph column has redrawn, so those questions
|
|
647
|
+
* freeze as single-select while page 1 (already settled) is correct. We
|
|
648
|
+
* cannot upgrade blindly — the live snapshot shows only ONE focused page —
|
|
649
|
+
* but we CAN read that page's question text/header and upgrade the matching
|
|
650
|
+
* question. As the user navigates the picker (or it settles), each page is
|
|
651
|
+
* eventually re-read and repaired.
|
|
632
652
|
*/
|
|
633
653
|
private maybeUpgradeClaudeTuiMultiSelect(): void {
|
|
634
654
|
if (this.cliType !== 'claude-cli'
|
|
635
655
|
|| this.interactivePromptTransport !== 'tui'
|
|
636
656
|
|| !this.activeInteractivePrompt) return;
|
|
637
657
|
const questions = this.activeInteractivePrompt.questions;
|
|
638
|
-
|
|
639
|
-
// we can only attribute the glyphs to a specific question when there is
|
|
640
|
-
// exactly one. Multi-question prompts are tab-captured per page at
|
|
641
|
-
// capture time and aren't re-evaluated here to avoid cross-contaminating
|
|
642
|
-
// a mixed single/multi prompt.
|
|
643
|
-
if (questions.length !== 1) return;
|
|
644
|
-
if (questions[0].multiSelect) return;
|
|
658
|
+
if (questions.every(q => q.multiSelect)) return;
|
|
645
659
|
let screenText = '';
|
|
646
660
|
try {
|
|
647
661
|
screenText = this.driver.snapshot();
|
|
@@ -649,8 +663,25 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
649
663
|
return;
|
|
650
664
|
}
|
|
651
665
|
if (!screenText.includes('Enter to select')) return;
|
|
652
|
-
|
|
653
|
-
questions
|
|
666
|
+
|
|
667
|
+
if (questions.length === 1) {
|
|
668
|
+
if (questions[0].multiSelect) return;
|
|
669
|
+
if (!detectClaudeTuiMultiSelect(screenText)) return;
|
|
670
|
+
questions[0].multiSelect = true;
|
|
671
|
+
this.statusCallback?.();
|
|
672
|
+
return;
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
// Multi-question: attribute the focused page's glyphs to its question by
|
|
676
|
+
// matching header (preferred) or question text, then upgrade just that
|
|
677
|
+
// one. Never demote — a settled non-multi page is left as captured.
|
|
678
|
+
const focused = readFocusedClaudeTuiQuestion(screenText);
|
|
679
|
+
if (!focused || !focused.multiSelect) return;
|
|
680
|
+
const match = questions.find(q =>
|
|
681
|
+
(focused.header && q.header && q.header === focused.header)
|
|
682
|
+
|| q.question === focused.question);
|
|
683
|
+
if (!match || match.multiSelect) return;
|
|
684
|
+
match.multiSelect = true;
|
|
654
685
|
this.statusCallback?.();
|
|
655
686
|
}
|
|
656
687
|
|
|
@@ -665,16 +696,53 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
665
696
|
return headers;
|
|
666
697
|
}
|
|
667
698
|
|
|
699
|
+
/**
|
|
700
|
+
* Snapshot the currently-focused claude TUI page, polling until its
|
|
701
|
+
* option-row checkbox glyph column has settled (or a bounded timeout).
|
|
702
|
+
*
|
|
703
|
+
* Why poll: right after a Tab keypress the newly-focused page's glyph
|
|
704
|
+
* column has not redrawn yet, so an immediate snapshot shows the question +
|
|
705
|
+
* option labels but NO per-option checkbox markers — freezing that page as
|
|
706
|
+
* single-select. A fixed delay either races (too short) or is wasteful (too
|
|
707
|
+
* long). Instead we re-snapshot at a fixed interval and stop as soon as the
|
|
708
|
+
* frame shows multi-select glyphs, falling back to the last frame at the
|
|
709
|
+
* timeout. Single-select pages never show glyphs, so they always poll to the
|
|
710
|
+
* timeout — bounded small to keep capture snappy.
|
|
711
|
+
*/
|
|
712
|
+
private async snapshotSettledClaudeTuiPage(): Promise<string> {
|
|
713
|
+
let screenText = this.driver.snapshot();
|
|
714
|
+
const deadline = Date.now() + SpecCliAdapter.CLAUDE_TUI_PAGE_SETTLE_TIMEOUT_MS;
|
|
715
|
+
while (!detectClaudeTuiMultiSelect(screenText) && Date.now() < deadline) {
|
|
716
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
717
|
+
screenText = this.driver.snapshot();
|
|
718
|
+
}
|
|
719
|
+
return screenText;
|
|
720
|
+
}
|
|
721
|
+
|
|
668
722
|
private async captureClaudeTuiPrompt(firstScreen: string, headers: string[]): Promise<void> {
|
|
669
723
|
const pages: ClaudeInteractiveTuiPage[] = [{ screenText: firstScreen, header: headers[0] }];
|
|
724
|
+
// Forward pass: Tab to each page 2..N and snapshot once its glyph column
|
|
725
|
+
// has settled, so pages 2+ capture their checkbox markers (not a racy
|
|
726
|
+
// pre-redraw frame).
|
|
670
727
|
for (let index = 1; index < headers.length; index += 1) {
|
|
671
728
|
this.driver.dispatch({ kind: 'pty_write', data: '\t' });
|
|
672
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
673
|
-
pages.push({ screenText: this.
|
|
729
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
730
|
+
pages.push({ screenText: await this.snapshotSettledClaudeTuiPage(), header: headers[index] });
|
|
674
731
|
}
|
|
732
|
+
// Return pass: Shift-Tab back through pages N..2. As we land on each page
|
|
733
|
+
// again re-read it and OR-in any now-visible multi-select glyphs — a
|
|
734
|
+
// second chance to repair a page whose forward-pass frame was still racy.
|
|
675
735
|
for (let index = headers.length - 1; index > 0; index -= 1) {
|
|
676
736
|
this.driver.dispatch({ kind: 'pty_write', data: '\x1b[Z' });
|
|
677
|
-
await new Promise(resolve => setTimeout(resolve,
|
|
737
|
+
await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
|
|
738
|
+
const reread = await this.snapshotSettledClaudeTuiPage();
|
|
739
|
+
// The page we just Shift-Tab'd ONTO is index-1 (we move backwards).
|
|
740
|
+
const landed = pages[index - 1];
|
|
741
|
+
if (landed
|
|
742
|
+
&& !detectClaudeTuiMultiSelect(landed.screenText)
|
|
743
|
+
&& detectClaudeTuiMultiSelect(reread)) {
|
|
744
|
+
landed.screenText = reread;
|
|
745
|
+
}
|
|
678
746
|
}
|
|
679
747
|
|
|
680
748
|
const prompt = detectClaudeAskUserQuestionPromptFromTuiPages(pages, {
|
|
@@ -184,15 +184,19 @@ export function detectClaudeTuiMultiSelect(screenText: string): boolean {
|
|
|
184
184
|
if (/Space to (?:select|toggle)|toggle selection|select multiple|select all that apply/i.test(screenText)) {
|
|
185
185
|
return true;
|
|
186
186
|
}
|
|
187
|
-
// A checkbox glyph
|
|
188
|
-
// the
|
|
189
|
-
//
|
|
190
|
-
//
|
|
191
|
-
//
|
|
192
|
-
|
|
187
|
+
// A checkbox glyph on a NUMBERED option row only appears in the multi-select
|
|
188
|
+
// picker. The glyph sits EITHER before the number ("❯ [ ] 1. TypeScript" /
|
|
189
|
+
// "☐ 2. Python") OR after it ("❯ 1. [ ] 계란말이" — claude-cli >=2.1's layout).
|
|
190
|
+
// We require the numbered "N." option marker either way so we don't
|
|
191
|
+
// false-positive on the `✔ Submit` nav line (per-question answered-state
|
|
192
|
+
// ☐/☒) or on the headerless variant where the QUESTION line itself begins
|
|
193
|
+
// with `☐ ` (single-select).
|
|
194
|
+
const optionCheckbox = `(?:\\[[ xX]\\]|[☐☒◻◼])`;
|
|
195
|
+
const beforeNumber = new RegExp(`^\\s*(?:[❯›>]\\s*)?${optionCheckbox}\\s*\\d+\\.\\s+\\S`);
|
|
196
|
+
const afterNumber = new RegExp(`^\\s*(?:[❯›>]\\s*)?\\d+\\.\\s*${optionCheckbox}\\s+\\S`);
|
|
193
197
|
for (const line of screenText.split(/\r?\n/)) {
|
|
194
198
|
if (line.includes('✔ Submit')) continue; // header/nav line
|
|
195
|
-
if (
|
|
199
|
+
if (beforeNumber.test(line) || afterNumber.test(line)) return true;
|
|
196
200
|
}
|
|
197
201
|
return false;
|
|
198
202
|
}
|
|
@@ -361,6 +365,31 @@ function parseClaudeInteractiveTuiQuestion(page: ClaudeInteractiveTuiPage, index
|
|
|
361
365
|
};
|
|
362
366
|
}
|
|
363
367
|
|
|
368
|
+
/**
|
|
369
|
+
* Read the question the live claude TUI picker is CURRENTLY focused on, plus
|
|
370
|
+
* whether that focused page renders multi-select checkbox markers.
|
|
371
|
+
*
|
|
372
|
+
* Used to repair a multi-question prompt after the fact: when the daemon
|
|
373
|
+
* Tab-captures pages 2..N it snapshots ~120ms after the Tab keypress, before
|
|
374
|
+
* the newly-focused page's option-row checkbox column has redrawn — so those
|
|
375
|
+
* questions get frozen as multiSelect:false even though the picker is
|
|
376
|
+
* multi-select. Re-reading the focused page on a later status tick (once it has
|
|
377
|
+
* settled) lets us attribute the now-visible glyphs to the matching question
|
|
378
|
+
* and upgrade just that one. Returns null when no picker question is on screen.
|
|
379
|
+
*/
|
|
380
|
+
export function readFocusedClaudeTuiQuestion(
|
|
381
|
+
screenText: string,
|
|
382
|
+
): { question: string; header?: string; multiSelect: boolean } | null {
|
|
383
|
+
if (!screenText.includes('Enter to select')) return null;
|
|
384
|
+
const parsed = parseClaudeInteractiveTuiQuestion({ screenText }, 0);
|
|
385
|
+
if (!parsed) return null;
|
|
386
|
+
return {
|
|
387
|
+
question: parsed.question,
|
|
388
|
+
...(parsed.header ? { header: parsed.header } : {}),
|
|
389
|
+
multiSelect: parsed.multiSelect,
|
|
390
|
+
};
|
|
391
|
+
}
|
|
392
|
+
|
|
364
393
|
export function detectClaudeAskUserQuestionPromptFromTuiPages(
|
|
365
394
|
pages: ClaudeInteractiveTuiPage[],
|
|
366
395
|
options: { promptId: string; providerType?: string; createdAt?: number },
|
|
@@ -392,7 +421,18 @@ export function buildClaudeInteractiveTuiAnswerSteps(
|
|
|
392
421
|
if (!answer) throw new Error(`Missing answer for ${question.questionId}`);
|
|
393
422
|
const freeformText = answer.freeformText?.trim() ?? '';
|
|
394
423
|
|
|
395
|
-
|
|
424
|
+
// Defensive multi-select fallback: a checkbox picker is the ONLY way an
|
|
425
|
+
// answer can carry more than one selected label, so treat any such answer
|
|
426
|
+
// as multi-select even when `question.multiSelect` was captured as false.
|
|
427
|
+
// This guards the multi-question capture race: pages 2..N can freeze as
|
|
428
|
+
// single-select (their glyph column hadn't redrawn at Tab-snapshot time),
|
|
429
|
+
// and the old single-select branch would then either throw on 2+ checked
|
|
430
|
+
// boxes or emit a bare digit (cursor move, no toggle) for 1 box — silently
|
|
431
|
+
// dropping that page's selection. Keying off the answer's label count makes
|
|
432
|
+
// the keystroke protocol correct regardless of the captured flag.
|
|
433
|
+
const treatAsMultiSelect = question.multiSelect || answer.selectedLabels.length > 1;
|
|
434
|
+
|
|
435
|
+
if (treatAsMultiSelect) {
|
|
396
436
|
// Multi-select: Claude TUI renders each option as a checkbox and the
|
|
397
437
|
// footer reads "Space to select". A numeric digit jumps the cursor to
|
|
398
438
|
// that option; Space toggles its checkbox. So for every selected label
|