@adhdev/daemon-core 0.9.82-rc.295 → 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.
@@ -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
@@ -156,6 +165,20 @@ export declare class SpecCliAdapter implements CliAdapter {
156
165
  */
157
166
  private maybeUpgradeClaudeTuiMultiSelect;
158
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;
159
182
  private captureClaudeTuiPrompt;
160
183
  getDebugState(): Record<string, any>;
161
184
  getTraceState(limit?: number): Record<string, any>;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.295",
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.295",
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",
@@ -552,6 +552,16 @@ export class SpecCliAdapter implements CliAdapter {
552
552
  */
553
553
  private static readonly INTERACTIVE_PROMPT_LOST_GRACE_MS = 1500;
554
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
+
555
565
  /**
556
566
  * Clear a held interactive prompt once the user has resolved it directly
557
567
  * in the terminal (the choice picker leaves the screen without going
@@ -686,16 +696,53 @@ export class SpecCliAdapter implements CliAdapter {
686
696
  return headers;
687
697
  }
688
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
+
689
722
  private async captureClaudeTuiPrompt(firstScreen: string, headers: string[]): Promise<void> {
690
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).
691
727
  for (let index = 1; index < headers.length; index += 1) {
692
728
  this.driver.dispatch({ kind: 'pty_write', data: '\t' });
693
- await new Promise(resolve => setTimeout(resolve, 120));
694
- pages.push({ screenText: this.driver.snapshot(), header: headers[index] });
729
+ await new Promise(resolve => setTimeout(resolve, SpecCliAdapter.CLAUDE_TUI_PAGE_POLL_INTERVAL_MS));
730
+ pages.push({ screenText: await this.snapshotSettledClaudeTuiPage(), header: headers[index] });
695
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.
696
735
  for (let index = headers.length - 1; index > 0; index -= 1) {
697
736
  this.driver.dispatch({ kind: 'pty_write', data: '\x1b[Z' });
698
- await new Promise(resolve => setTimeout(resolve, 80));
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
+ }
699
746
  }
700
747
 
701
748
  const prompt = detectClaudeAskUserQuestionPromptFromTuiPages(pages, {
@@ -421,7 +421,18 @@ export function buildClaudeInteractiveTuiAnswerSteps(
421
421
  if (!answer) throw new Error(`Missing answer for ${question.questionId}`);
422
422
  const freeformText = answer.freeformText?.trim() ?? '';
423
423
 
424
- if (question.multiSelect) {
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) {
425
436
  // Multi-select: Claude TUI renders each option as a checkbox and the
426
437
  // footer reads "Space to select". A numeric digit jumps the cursor to
427
438
  // that option; Space toggles its checkbox. So for every selected label