@adhdev/daemon-core 0.9.82-rc.355 → 0.9.82-rc.356

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.
@@ -121,6 +121,12 @@ export declare class SpecCliAdapter implements CliAdapter {
121
121
  * — not this code — decides how a selection is keyed for each CLI.
122
122
  */
123
123
  private selectPickerChoice;
124
+ /** Parse the picker choices only if the picker already appears rendered on
125
+ * the live screen (its `wait_for` condition currently matches and at least
126
+ * one choice parses). Returns the parsed choices when open, else null so
127
+ * the caller knows it must send the trigger to open it. Used to de-dup the
128
+ * picker open in {@link selectPickerChoice}. */
129
+ private extractPickerChoicesIfRendered;
124
130
  /** Poll the live screen until the picker's `wait_for` condition matches,
125
131
  * up to a short budget. Returns true if it rendered, false on timeout. */
126
132
  private waitForPickerRendered;
@@ -20,4 +20,5 @@ export declare function extractButtonsFromRule(rule: ExtractButtons, hay: string
20
20
  index: number;
21
21
  label: string;
22
22
  key: string;
23
+ current: boolean;
23
24
  }[];
@@ -19,6 +19,24 @@ export type ControlAction = {
19
19
  wait_for: WaitForCondition;
20
20
  extract_choices: SectionPattern;
21
21
  submit_key: string;
22
+ /**
23
+ * How a parsed choice is committed once the picker is open.
24
+ * 'index' (default) — type the on-screen number then `submit_key`
25
+ * (`{index}\r`). Correct for CLIs whose picker is number-selectable
26
+ * (codex-cli, hermes-cli).
27
+ * 'arrow_keys' — the picker is a cursor list that ignores number keys
28
+ * (claude-cli /model): move the cursor from its current row to the
29
+ * target row with up/down arrows, then confirm with the `submit_key`
30
+ * tail (the `\r` left after stripping `{index}`). Requires the
31
+ * extracted choices to flag the current cursor row.
32
+ */
33
+ select_mode?: 'index' | 'arrow_keys';
34
+ /** Arrow byte sequences for `select_mode: 'arrow_keys'`. Defaults to
35
+ * ANSI cursor up/down (`` / ``) when omitted. */
36
+ cursor_keys?: {
37
+ up: string;
38
+ down: string;
39
+ };
22
40
  } | {
23
41
  type: 'attach_image';
24
42
  method: 'tempfile_then_keys';
@@ -181,4 +199,24 @@ export interface ExtractButtons {
181
199
  key_for_index: string;
182
200
  min_count?: number;
183
201
  continuation_lines?: boolean;
202
+ /**
203
+ * How a button is committed when its modal is resolved (auto-approve or an
204
+ * explicit dashboard click).
205
+ * 'index' (default) — send `key_for_index` with `{index}` filled in
206
+ * (`{index}\r` → `1\r`). Correct for modals whose buttons are
207
+ * number-selectable (codex, hermes, antigravity number rows).
208
+ * 'arrow_keys' — the modal is a cursor list that IGNORES number keys
209
+ * (claude-cli's new TUI approval modal): a typed `1` leaks into the
210
+ * composer as literal text and `\r` submits it. Instead move the cursor
211
+ * from its current row to the target row with up/down arrows, then
212
+ * confirm with the `key_for_index` tail (the `\r` left after stripping
213
+ * `{index}`). Mirrors the `open_picker` `select_mode` of the same name.
214
+ */
215
+ select_mode?: 'index' | 'arrow_keys';
216
+ /** Arrow byte sequences for `select_mode: 'arrow_keys'`. Defaults to ANSI
217
+ * cursor up/down (`[A` / `[B`) when omitted. */
218
+ cursor_keys?: {
219
+ up: string;
220
+ down: string;
221
+ };
184
222
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@adhdev/daemon-core",
3
- "version": "0.9.82-rc.355",
3
+ "version": "0.9.82-rc.356",
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.355",
49
+ "@adhdev/mesh-shared": "0.9.82-rc.356",
50
50
  "@adhdev/session-host-core": "*",
51
51
  "@agentclientprotocol/sdk": "^0.16.1",
52
52
  "ajv": "^8.20.0",
@@ -2034,6 +2034,23 @@ export class CliProviderInstance implements ProviderInstance {
2034
2034
  ? event.providerSessionId
2035
2035
  : this.providerSessionId,
2036
2036
  };
2037
+ // TASKIDLESS: stamp the mesh task primary key on lifecycle events emitted by
2038
+ // a mesh worker session. The consumer (updateDirectDispatchStatus) was switched
2039
+ // to key on task_id (CANON-B), but the producer never carried it — so every
2040
+ // forwarded metadataEvent.taskId arrived undefined and the coordinator fell back
2041
+ // to a session_id match, which can flip a sibling dispatch row. The session
2042
+ // already knows its own taskId via attachMeshAssignment (settings.meshActiveTaskId);
2043
+ // surface it here so updateDirectDispatchStatus hits the exact PK row and the
2044
+ // session_id fallback is never exercised. Non-mesh sessions get no taskId
2045
+ // (regression guard) — isMeshWorkerSession() gates the injection.
2046
+ if (this.isMeshWorkerSession() && this.settings.meshActiveTaskId) {
2047
+ const existingTaskId = typeof enrichedEvent.taskId === 'string' && enrichedEvent.taskId.trim()
2048
+ ? enrichedEvent.taskId
2049
+ : undefined;
2050
+ if (!existingTaskId) {
2051
+ enrichedEvent.taskId = this.settings.meshActiveTaskId;
2052
+ }
2053
+ }
2037
2054
  if (this.context?.emitProviderEvent) {
2038
2055
  this.context.emitProviderEvent(enrichedEvent);
2039
2056
  } else {
@@ -413,11 +413,18 @@ export class SpecCliAdapter implements CliAdapter {
413
413
  choiceLabel: string | undefined,
414
414
  ): Promise<unknown> {
415
415
  // Open + wait so the choice list is on screen before we resolve the
416
- // label → index mapping (a fresh invoke may arrive with the picker
417
- // closed; re-opening an open picker is a no-op on these CLIs).
418
- this.driver.dispatch({ kind: 'click_control', control_id: ctl.id });
419
- await this.waitForPickerRendered(action);
420
- const options = this.extractPickerChoices(action);
416
+ // label → index mapping. The picker is normally ALREADY open here (a
417
+ // preceding list invoke leaves it rendered), so only send the trigger
418
+ // when it is not on screen. Re-sending the trigger to an open picker is
419
+ // NOT a harmless no-op on claude-cli: the trailing CR of `/model\r`
420
+ // lands as Enter on the cursor's current row and commits the wrong
421
+ // model before we navigate. De-dup the open to avoid that.
422
+ let options = this.extractPickerChoicesIfRendered(action);
423
+ if (!options) {
424
+ this.driver.dispatch({ kind: 'click_control', control_id: ctl.id });
425
+ await this.waitForPickerRendered(action);
426
+ options = this.extractPickerChoices(action);
427
+ }
421
428
 
422
429
  let index = choiceIndex;
423
430
  if ((index == null || !Number.isFinite(index)) && choiceLabel) {
@@ -432,8 +439,34 @@ export class SpecCliAdapter implements CliAdapter {
432
439
  return { ok: false, error: 'choiceIndex or choiceLabel required to select' };
433
440
  }
434
441
 
435
- const keys = (action.submit_key || '{index}\r').replace(/\{index\}/g, String(index));
436
- this.driver.dispatch({ kind: 'pty_write', data: keys });
442
+ if (action.select_mode === 'arrow_keys') {
443
+ // Cursor-list picker (claude-cli /model): number keys are ignored.
444
+ // The cursor starts on the active row (extract flags it `current`);
445
+ // step it to the target row with arrows, then confirm.
446
+ const current = options.find(o => o.current);
447
+ if (current == null) {
448
+ // Without a known cursor position a blind Enter would commit
449
+ // whatever row the cursor sits on — fail loud instead.
450
+ return {
451
+ ok: false,
452
+ error: 'arrow-nav picker: current cursor row not detected on screen',
453
+ controlResult: { options: options.map(o => ({ value: o.label, label: o.label, current: o.current })) },
454
+ };
455
+ }
456
+ const up = action.cursor_keys?.up ?? '';
457
+ const down = action.cursor_keys?.down ?? '';
458
+ const delta = index - current.index;
459
+ const step = delta >= 0 ? down : up;
460
+ const nav = step.repeat(Math.abs(delta));
461
+ // Confirm key = submit_key with the (unused) {index} placeholder
462
+ // stripped — e.g. `{index}\r` → `\r`.
463
+ const confirm = (action.submit_key || '\r').replace(/\{index\}/g, '') || '\r';
464
+ if (nav) this.driver.dispatch({ kind: 'pty_write', data: nav });
465
+ this.driver.dispatch({ kind: 'pty_write', data: confirm });
466
+ } else {
467
+ const keys = (action.submit_key || '{index}\r').replace(/\{index\}/g, String(index));
468
+ this.driver.dispatch({ kind: 'pty_write', data: keys });
469
+ }
437
470
  const selected = options.find(o => o.index === index);
438
471
  return {
439
472
  ok: true,
@@ -446,6 +479,23 @@ export class SpecCliAdapter implements CliAdapter {
446
479
  };
447
480
  }
448
481
 
482
+ /** Parse the picker choices only if the picker already appears rendered on
483
+ * the live screen (its `wait_for` condition currently matches and at least
484
+ * one choice parses). Returns the parsed choices when open, else null so
485
+ * the caller knows it must send the trigger to open it. Used to de-dup the
486
+ * picker open in {@link selectPickerChoice}. */
487
+ private extractPickerChoicesIfRendered(
488
+ action: Extract<ControlAction, { type: 'open_picker' }>,
489
+ ): Array<{ index: number; label: string; current: boolean }> | null {
490
+ const wf = action.wait_for;
491
+ if (wf?.regex) {
492
+ const re = new RegExp(wf.regex, wf.flags ?? 'i');
493
+ if (!re.test(this.readScreenSectionText(wf.section))) return null;
494
+ }
495
+ const options = this.extractPickerChoices(action);
496
+ return options.length > 0 ? options : null;
497
+ }
498
+
449
499
  /** Poll the live screen until the picker's `wait_for` condition matches,
450
500
  * up to a short budget. Returns true if it rendered, false on timeout. */
451
501
  private async waitForPickerRendered(action: Extract<ControlAction, { type: 'open_picker' }>): Promise<boolean> {
@@ -314,10 +314,10 @@ function compileLinePattern(ref: { pattern: string; flags?: string }): RegExp {
314
314
  export function extractButtonsFromRule(
315
315
  rule: ExtractButtons,
316
316
  hay: string,
317
- ): { index: number; label: string; key: string }[] {
317
+ ): { index: number; label: string; key: string; current: boolean }[] {
318
318
  const keyTemplate = rule.key_for_index;
319
319
  const continuationLines = rule.continuation_lines ?? false;
320
- const buttons: { index: number; label: string; key: string }[] = [];
320
+ const buttons: { index: number; label: string; key: string; current: boolean }[] = [];
321
321
 
322
322
  if (continuationLines) {
323
323
  const re = compileLinePattern(rule);
@@ -328,6 +328,7 @@ export function extractButtonsFromRule(
328
328
  const idx = Number(m[1]);
329
329
  let label = String(m[2] ?? '').trim();
330
330
  if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
331
+ const current = hasCursorMarker(lines[i]);
331
332
  let j = i + 1;
332
333
  while (j < lines.length) {
333
334
  const next = lines[j];
@@ -339,7 +340,7 @@ export function extractButtonsFromRule(
339
340
  }
340
341
  if (buttons.some(b => b.index === idx)) continue;
341
342
  const key = keyTemplate.replace(/\{index\}/g, String(idx));
342
- buttons.push({ index: idx, label, key });
343
+ buttons.push({ index: idx, label, key, current });
343
344
  i = j - 1;
344
345
  }
345
346
  } else {
@@ -351,10 +352,19 @@ export function extractButtonsFromRule(
351
352
  if (!Number.isFinite(idx) || idx <= 0 || !label) continue;
352
353
  if (buttons.some(b => b.index === idx)) continue;
353
354
  const key = keyTemplate.replace(/\{index\}/g, String(idx));
354
- buttons.push({ index: idx, label, key });
355
+ // The matched text begins at the cursor marker (the pattern's
356
+ // optional `[❯›>]` prefix); flag this row as the cursor's current
357
+ // position so `select_mode: 'arrow_keys'` can step from it.
358
+ buttons.push({ index: idx, label, key, current: hasCursorMarker(m[0]) });
355
359
  }
356
360
  }
357
361
 
358
362
  buttons.sort((a, b) => a.index - b.index);
359
363
  return buttons;
360
364
  }
365
+
366
+ /** True when a button line carries a TUI cursor marker (`❯`, `›`, `>`) before
367
+ * its number — i.e. the cursor currently sits on that row. */
368
+ function hasCursorMarker(text: string): boolean {
369
+ return /^\s*[❯›>]/.test(text);
370
+ }
@@ -221,7 +221,7 @@ export function guessExt(mime: string): string {
221
221
 
222
222
  interface ModalSnapshot {
223
223
  title: string | null;
224
- buttons: { index: number; label: string; key: string }[];
224
+ buttons: { index: number; label: string; key: string; current: boolean }[];
225
225
  }
226
226
 
227
227
  interface VisibleControl {
@@ -1079,6 +1079,28 @@ export class FsmDriver implements ISpecDriver {
1079
1079
  if (!m) return;
1080
1080
  const btn = m.buttons.find(b => b.index === index);
1081
1081
  if (!btn) return;
1082
+
1083
+ const rule = stateById(this.spec, this.currentStateId)?.extract?.buttons;
1084
+ if (rule?.select_mode === 'arrow_keys') {
1085
+ // Cursor-list approval modal (claude-cli new TUI): number keys are
1086
+ // IGNORED — sending `btn.key` ("1\r") types a literal "1" into the
1087
+ // composer and the trailing CR submits it as a chat message. Drive
1088
+ // the cursor from its current row to the target row with arrows,
1089
+ // then confirm. The cursor opens on the first option, so when the
1090
+ // marker isn't detected we step down from row 1 (index - 1).
1091
+ const from = m.buttons.find(b => b.current)?.index ?? 1;
1092
+ const up = rule.cursor_keys?.up ?? '\x1b[A';
1093
+ const down = rule.cursor_keys?.down ?? '\x1b[B';
1094
+ const delta = btn.index - from;
1095
+ const step = delta >= 0 ? down : up;
1096
+ const nav = step.repeat(Math.abs(delta));
1097
+ // Confirm = key_for_index with the (now unused) {index} stripped:
1098
+ // `{index}\r` → `\r`.
1099
+ const confirm = (rule.key_for_index || '\r').replace(/\{index\}/g, '') || '\r';
1100
+ if (nav) this.adapter.send_keys(nav);
1101
+ this.adapter.send_keys(confirm);
1102
+ return;
1103
+ }
1082
1104
  this.adapter.send_keys(btn.key);
1083
1105
  }
1084
1106
 
@@ -34,6 +34,21 @@ export type ControlAction =
34
34
  wait_for: WaitForCondition;
35
35
  extract_choices: SectionPattern;
36
36
  submit_key: string;
37
+ /**
38
+ * How a parsed choice is committed once the picker is open.
39
+ * 'index' (default) — type the on-screen number then `submit_key`
40
+ * (`{index}\r`). Correct for CLIs whose picker is number-selectable
41
+ * (codex-cli, hermes-cli).
42
+ * 'arrow_keys' — the picker is a cursor list that ignores number keys
43
+ * (claude-cli /model): move the cursor from its current row to the
44
+ * target row with up/down arrows, then confirm with the `submit_key`
45
+ * tail (the `\r` left after stripping `{index}`). Requires the
46
+ * extracted choices to flag the current cursor row.
47
+ */
48
+ select_mode?: 'index' | 'arrow_keys';
49
+ /** Arrow byte sequences for `select_mode: 'arrow_keys'`. Defaults to
50
+ * ANSI cursor up/down (`` / ``) when omitted. */
51
+ cursor_keys?: { up: string; down: string };
37
52
  }
38
53
  | {
39
54
  type: 'attach_image';
@@ -227,4 +242,21 @@ export interface ExtractButtons {
227
242
  key_for_index: string;
228
243
  min_count?: number;
229
244
  continuation_lines?: boolean;
245
+ /**
246
+ * How a button is committed when its modal is resolved (auto-approve or an
247
+ * explicit dashboard click).
248
+ * 'index' (default) — send `key_for_index` with `{index}` filled in
249
+ * (`{index}\r` → `1\r`). Correct for modals whose buttons are
250
+ * number-selectable (codex, hermes, antigravity number rows).
251
+ * 'arrow_keys' — the modal is a cursor list that IGNORES number keys
252
+ * (claude-cli's new TUI approval modal): a typed `1` leaks into the
253
+ * composer as literal text and `\r` submits it. Instead move the cursor
254
+ * from its current row to the target row with up/down arrows, then
255
+ * confirm with the `key_for_index` tail (the `\r` left after stripping
256
+ * `{index}`). Mirrors the `open_picker` `select_mode` of the same name.
257
+ */
258
+ select_mode?: 'index' | 'arrow_keys';
259
+ /** Arrow byte sequences for `select_mode: 'arrow_keys'`. Defaults to ANSI
260
+ * cursor up/down (`[A` / `[B`) when omitted. */
261
+ cursor_keys?: { up: string; down: string };
230
262
  }