@adhdev/daemon-core 0.9.82-rc.357 → 0.9.82-rc.359

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.
@@ -338,7 +338,10 @@ export function extractButtonsFromRule(
338
338
  label += ' ' + next.trim();
339
339
  j += 1;
340
340
  }
341
- if (buttons.some(b => b.index === idx)) continue;
341
+ // No top-down de-dup here: a stray body "1." above the modal would
342
+ // otherwise claim the index and shadow the real choice. Collect
343
+ // every match in screen order and let lastContiguousNumberedBlock
344
+ // pick the bottom-most option block below.
342
345
  const key = keyTemplate.replace(/\{index\}/g, String(idx));
343
346
  buttons.push({ index: idx, label, key, current });
344
347
  i = j - 1;
@@ -350,17 +353,51 @@ export function extractButtonsFromRule(
350
353
  const idx = Number(m[1]);
351
354
  const label = String(m[2] ?? '').trim();
352
355
  if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
353
- if (buttons.some(b => b.index === idx)) continue;
354
356
  const key = keyTemplate.replace(/\{index\}/g, String(idx));
355
357
  // The matched text begins at the cursor marker (the pattern's
356
358
  // optional `[❯›>]` prefix); flag this row as the cursor's current
357
359
  // position so `select_mode: 'arrow_keys'` can step from it.
360
+ // Like the continuation path, no top-down de-dup — body numbered
361
+ // lines are filtered out by the bottom-block selection below.
358
362
  buttons.push({ index: idx, label, key, current: hasCursorMarker(m[0]) });
359
363
  }
360
364
  }
361
365
 
362
- buttons.sort((a, b) => a.index - b.index);
363
- return buttons;
366
+ // Buttons were collected in screen (top→bottom) order. The real choices are
367
+ // the bottom-most contiguous numbered block (the modal/picker always renders
368
+ // them last); reduce to that block so conversation-history numbered lists
369
+ // pulled into the modal section can never be mistaken for options.
370
+ const block = lastContiguousNumberedBlock(buttons);
371
+ block.sort((a, b) => a.index - b.index);
372
+ return block;
373
+ }
374
+
375
+ /**
376
+ * Reduce a top→bottom-ordered list of parsed numbered entries to only the
377
+ * bottom-most contiguous block — the run whose indices descend by exactly 1
378
+ * scanning upward from the last entry.
379
+ *
380
+ * Picker and approval choices always render as the LAST contiguous numbered
381
+ * block at the bottom of the modal section. The conversation history above can
382
+ * carry its own stray "1./2./3." numbered lists (and blockquote `>` lines), and
383
+ * because section anchoring can pull body lines into the modal section, a naive
384
+ * top-down scan would bind the low option indices to those body lines and drop
385
+ * the real choices (the dashboard would show, and an arrow-key picker would
386
+ * commit, the wrong row). Selecting the bottom block makes the on-screen picker
387
+ * win regardless of body content. Since pickers number their options 1..N
388
+ * contiguously and bodies use indices ≥ 1, the upward chain always breaks the
389
+ * moment it would need a "0." above the picker's "1." — so the picker block is
390
+ * isolated cleanly. Entries are assumed already in screen order; the returned
391
+ * slice keeps that order.
392
+ */
393
+ export function lastContiguousNumberedBlock<T extends { index: number }>(entries: T[]): T[] {
394
+ if (entries.length <= 1) return entries.slice();
395
+ let start = entries.length - 1;
396
+ for (let i = entries.length - 1; i > 0; i -= 1) {
397
+ if (entries[i - 1].index === entries[i].index - 1) start = i - 1;
398
+ else break;
399
+ }
400
+ return entries.slice(start);
364
401
  }
365
402
 
366
403
  /** True when a button line carries a TUI cursor marker (`❯`, `›`, `>`) before