@adhdev/daemon-core 0.9.82-rc.294 → 0.9.82-rc.295

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.
@@ -144,6 +144,15 @@ export declare class SpecCliAdapter implements CliAdapter {
144
144
  * question to multi-select and re-emit status. Promotion is one-way
145
145
  * (false→true only) — once a question is known multi-select we never demote
146
146
  * it, since the glyph column can scroll out of view on later frames.
147
+ *
148
+ * For MULTI-question prompts the per-page Tab capture is the actual source
149
+ * of the bug: pages 2..N are snapshotted ~120ms after the Tab keypress,
150
+ * before their option-row glyph column has redrawn, so those questions
151
+ * freeze as single-select while page 1 (already settled) is correct. We
152
+ * cannot upgrade blindly — the live snapshot shows only ONE focused page —
153
+ * but we CAN read that page's question text/header and upgrade the matching
154
+ * question. As the user navigates the picker (or it settles), each page is
155
+ * eventually re-read and repaired.
147
156
  */
148
157
  private maybeUpgradeClaudeTuiMultiSelect;
149
158
  private readClaudeTuiHeaders;
@@ -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.294",
3
+ "version": "0.9.82-rc.295",
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.294",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.295",
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,
@@ -629,19 +630,22 @@ export class SpecCliAdapter implements CliAdapter {
629
630
  * question to multi-select and re-emit status. Promotion is one-way
630
631
  * (false→true only) — once a question is known multi-select we never demote
631
632
  * it, since the glyph column can scroll out of view on later frames.
633
+ *
634
+ * For MULTI-question prompts the per-page Tab capture is the actual source
635
+ * of the bug: pages 2..N are snapshotted ~120ms after the Tab keypress,
636
+ * before their option-row glyph column has redrawn, so those questions
637
+ * freeze as single-select while page 1 (already settled) is correct. We
638
+ * cannot upgrade blindly — the live snapshot shows only ONE focused page —
639
+ * but we CAN read that page's question text/header and upgrade the matching
640
+ * question. As the user navigates the picker (or it settles), each page is
641
+ * eventually re-read and repaired.
632
642
  */
633
643
  private maybeUpgradeClaudeTuiMultiSelect(): void {
634
644
  if (this.cliType !== 'claude-cli'
635
645
  || this.interactivePromptTransport !== 'tui'
636
646
  || !this.activeInteractivePrompt) return;
637
647
  const questions = this.activeInteractivePrompt.questions;
638
- // The live snapshot only shows the CURRENTLY focused question's rows, so
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;
648
+ if (questions.every(q => q.multiSelect)) return;
645
649
  let screenText = '';
646
650
  try {
647
651
  screenText = this.driver.snapshot();
@@ -649,8 +653,25 @@ export class SpecCliAdapter implements CliAdapter {
649
653
  return;
650
654
  }
651
655
  if (!screenText.includes('Enter to select')) return;
652
- if (!detectClaudeTuiMultiSelect(screenText)) return;
653
- questions[0].multiSelect = true;
656
+
657
+ if (questions.length === 1) {
658
+ if (questions[0].multiSelect) return;
659
+ if (!detectClaudeTuiMultiSelect(screenText)) return;
660
+ questions[0].multiSelect = true;
661
+ this.statusCallback?.();
662
+ return;
663
+ }
664
+
665
+ // Multi-question: attribute the focused page's glyphs to its question by
666
+ // matching header (preferred) or question text, then upgrade just that
667
+ // one. Never demote — a settled non-multi page is left as captured.
668
+ const focused = readFocusedClaudeTuiQuestion(screenText);
669
+ if (!focused || !focused.multiSelect) return;
670
+ const match = questions.find(q =>
671
+ (focused.header && q.header && q.header === focused.header)
672
+ || q.question === focused.question);
673
+ if (!match || match.multiSelect) return;
674
+ match.multiSelect = true;
654
675
  this.statusCallback?.();
655
676
  }
656
677
 
@@ -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 sitting in front of a NUMBERED option row only appears in
188
- // the multi-select picker (e.g. "❯ [ ] 1. TypeScript" / "☐ 2. Python"). We
189
- // require the numbered "N." option marker so we don't false-positive on the
190
- // `✔ Submit` nav line (per-question answered-state ☐/☒) or on the headerless
191
- // variant where the QUESTION line itself begins with `☐ ` (single-select).
192
- const optionCheckbox = /^\s*(?:[❯›>]\s*)?(?:\[[ xX]\]|[☐☒◻◼])\s*\d+\.\s+\S/;
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 (optionCheckbox.test(line)) return true;
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 },