@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.
- package/dist/index.js +70 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +70 -11
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/cli-adapter.d.ts +6 -0
- package/dist/providers/spec/evaluator.d.ts +1 -0
- package/dist/providers/spec/types.d.ts +38 -0
- package/package.json +2 -2
- package/src/providers/cli-provider-instance.ts +17 -0
- package/src/providers/spec/cli-adapter.ts +57 -7
- package/src/providers/spec/evaluator.ts +14 -4
- package/src/providers/spec/fsm-driver.ts +23 -1
- package/src/providers/spec/types.ts +32 -0
|
@@ -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;
|
|
@@ -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 (`[A` / `[B`) 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.
|
|
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.
|
|
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
|
|
417
|
-
//
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
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
|
-
|
|
436
|
-
|
|
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 ?? '[A';
|
|
457
|
+
const down = action.cursor_keys?.down ?? '[B';
|
|
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
|
-
|
|
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 (`[A` / `[B`) 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
|
}
|