@adhdev/daemon-core 0.9.82-rc.220 → 0.9.82-rc.221
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.d.ts +7 -6
- package/dist/index.js +844 -2657
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +839 -2650
- package/dist/index.mjs.map +1 -1
- package/dist/providers/spec/evaluator.d.ts +1 -48
- package/dist/providers/spec/fsm-driver.d.ts +122 -1
- package/dist/providers/spec/types.d.ts +7 -189
- package/package.json +1 -1
- package/src/index.ts +9 -8
- package/src/providers/provider-loader.ts +3 -11
- package/src/providers/spec/cli-adapter.ts +10 -38
- package/src/providers/spec/evaluator.ts +9 -292
- package/src/providers/spec/fsm-driver.ts +92 -6
- package/src/providers/spec/types.ts +24 -216
- package/dist/providers/spec/driver.d.ts +0 -287
- package/dist/providers/spec/loader.d.ts +0 -15
- package/dist/providers/spec/schema.gen.d.ts +0 -2039
- package/src/providers/spec/driver.ts +0 -1082
- package/src/providers/spec/loader.ts +0 -325
- package/src/providers/spec/schema.gen.ts +0 -693
- package/src/providers/spec/schema.json +0 -342
|
@@ -1,23 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
*
|
|
2
|
+
* Section + condition evaluator — pure functions shared by the v4 FSM engine.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
* - extracted modal title + buttons (if any)
|
|
8
|
-
* - which control_bar entries are visible
|
|
9
|
-
* - which notifications/delegates this evaluation activates
|
|
10
|
-
* - a trace object that explains every decision (for the inspector)
|
|
11
|
-
*
|
|
12
|
-
* No I/O, no state. Pass prevLines for delta (changed) condition support.
|
|
4
|
+
* Provides: section resolution, condition evaluation (regex/changed/all/any),
|
|
5
|
+
* title/button extraction. The v4 FSM driver and evaluator import these
|
|
6
|
+
* directly; there is no v3 SpecDriver anymore.
|
|
13
7
|
*/
|
|
14
8
|
'use strict';
|
|
15
9
|
|
|
16
10
|
import type {
|
|
17
|
-
|
|
11
|
+
SectionDef, Condition, RegexCondition,
|
|
18
12
|
ChangedCondition, AllCondition, AnyCondition,
|
|
19
13
|
ExtractTitle, ExtractButtons,
|
|
20
|
-
ControlAction, NotificationRule, DelegateTrigger,
|
|
21
14
|
} from './types.js';
|
|
22
15
|
|
|
23
16
|
export interface ResolvedSection {
|
|
@@ -27,45 +20,13 @@ export interface ResolvedSection {
|
|
|
27
20
|
text: string;
|
|
28
21
|
}
|
|
29
22
|
|
|
30
|
-
export interface ModalSnapshot {
|
|
31
|
-
title: string | null;
|
|
32
|
-
buttons: { index: number; label: string; key: string }[];
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
export interface VisibleControl {
|
|
36
|
-
id: string;
|
|
37
|
-
label: string;
|
|
38
|
-
actionType: 'send_keys' | 'open_picker' | 'attach_image';
|
|
39
|
-
}
|
|
40
|
-
|
|
41
|
-
export interface FiredNotification {
|
|
42
|
-
id: string;
|
|
43
|
-
title: string;
|
|
44
|
-
body: string;
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
export interface FiredDelegate {
|
|
48
|
-
id: string;
|
|
49
|
-
task: string;
|
|
50
|
-
}
|
|
51
|
-
|
|
52
23
|
export interface TraceEntry {
|
|
53
24
|
kind: 'section' | 'state_match' | 'state_skip' | 'modal' | 'control' | 'notification' | 'delegate';
|
|
54
25
|
text: string;
|
|
55
26
|
}
|
|
56
27
|
|
|
57
|
-
export interface SpecEvaluation {
|
|
58
|
-
state: { id: string; label: string; title: string | null };
|
|
59
|
-
modal: ModalSnapshot | null;
|
|
60
|
-
controls: VisibleControl[];
|
|
61
|
-
notifications: FiredNotification[];
|
|
62
|
-
delegates: FiredDelegate[];
|
|
63
|
-
sections: ResolvedSection[];
|
|
64
|
-
trace: TraceEntry[];
|
|
65
|
-
}
|
|
66
|
-
|
|
67
28
|
// ────────────────────────────────────────────────────────────────────────────
|
|
68
|
-
// Layout —
|
|
29
|
+
// Layout — sections{} object
|
|
69
30
|
// ────────────────────────────────────────────────────────────────────────────
|
|
70
31
|
|
|
71
32
|
function resolveSize(size: number | string | undefined, total: number): number {
|
|
@@ -77,10 +38,6 @@ function resolveSize(size: number | string | undefined, total: number): number {
|
|
|
77
38
|
return Math.max(0, Math.min(total, Math.round((total * pct) / 100)));
|
|
78
39
|
}
|
|
79
40
|
|
|
80
|
-
/**
|
|
81
|
-
* Resolve v3 sections{} object into an ordered array of ResolvedSection.
|
|
82
|
-
* Two-pass: first anchor/positional, then apply `until` cross-references.
|
|
83
|
-
*/
|
|
84
41
|
export function resolveSections(
|
|
85
42
|
sectionsObj: Record<string, SectionDef>,
|
|
86
43
|
lines: string[],
|
|
@@ -114,7 +71,6 @@ export function resolveSections(
|
|
|
114
71
|
from = idx;
|
|
115
72
|
to = total;
|
|
116
73
|
if (sec.until_regex !== undefined) {
|
|
117
|
-
// until_regex on anchor-based sections (extension field)
|
|
118
74
|
try {
|
|
119
75
|
const ure = new RegExp(sec.until_regex, sec.until_regex_flags ?? '');
|
|
120
76
|
const end = lines.findIndex((l, i) => i > idx && ure.test(l));
|
|
@@ -123,8 +79,6 @@ export function resolveSections(
|
|
|
123
79
|
} else if (sec.lines !== undefined) {
|
|
124
80
|
to = Math.min(total, from + sec.lines);
|
|
125
81
|
}
|
|
126
|
-
// `until` regex check on anchor sections (starts with ^)
|
|
127
|
-
// handled in second pass
|
|
128
82
|
}
|
|
129
83
|
} catch { /* bad anchor regex */ }
|
|
130
84
|
} else if (sec.from_top !== undefined) {
|
|
@@ -146,14 +100,12 @@ export function resolveSections(
|
|
|
146
100
|
|
|
147
101
|
if (sec.until !== undefined) {
|
|
148
102
|
if (sec.until.startsWith('^')) {
|
|
149
|
-
// `until` is a regex: stop at the first matching line after fromLine
|
|
150
103
|
try {
|
|
151
104
|
const ure = new RegExp(sec.until);
|
|
152
105
|
const end = lines.findIndex((l, i) => i > fromLine && ure.test(l));
|
|
153
106
|
if (end !== -1) toLine = end;
|
|
154
107
|
} catch { /* bad until regex */ }
|
|
155
108
|
} else {
|
|
156
|
-
// `until` is a section id reference
|
|
157
109
|
const target = anchored.get(sec.until);
|
|
158
110
|
if (target) toLine = target.fromLine;
|
|
159
111
|
}
|
|
@@ -174,7 +126,7 @@ export function sectionText(sections: ResolvedSection[], sectionId: string | und
|
|
|
174
126
|
}
|
|
175
127
|
|
|
176
128
|
// ────────────────────────────────────────────────────────────────────────────
|
|
177
|
-
// Condition evaluation
|
|
129
|
+
// Condition evaluation
|
|
178
130
|
// ────────────────────────────────────────────────────────────────────────────
|
|
179
131
|
|
|
180
132
|
function isRegexCondition(c: Condition): c is RegexCondition {
|
|
@@ -224,12 +176,10 @@ export function evaluateCondition(
|
|
|
224
176
|
if (!cursor || !prevLines || prevLines.length === 0) return false;
|
|
225
177
|
const curLines = fullScreen.split('\n');
|
|
226
178
|
const startRow = Math.max(0, cursor.row - cond.cursor_above);
|
|
227
|
-
const endRow = cursor.row;
|
|
179
|
+
const endRow = cursor.row;
|
|
228
180
|
const currentSlice = curLines.slice(startRow, endRow).join('\n');
|
|
229
181
|
const prevSlice = prevLines.slice(startRow, endRow).join('\n');
|
|
230
182
|
const didChange = currentSlice !== prevSlice;
|
|
231
|
-
// changed:false means "region is currently stable" — stable_ms duration
|
|
232
|
-
// is enforced by the driver, not here.
|
|
233
183
|
const result = cond.changed ? didChange : !didChange;
|
|
234
184
|
const stableSuffix = cond.stable_ms != null ? ` stable_ms=${cond.stable_ms}` : '';
|
|
235
185
|
trace.push({
|
|
@@ -252,7 +202,6 @@ export function evaluateCondition(
|
|
|
252
202
|
return false;
|
|
253
203
|
}
|
|
254
204
|
|
|
255
|
-
// Cursor-position guards
|
|
256
205
|
if (cursor !== undefined) {
|
|
257
206
|
if (cond.cursor_row_min !== undefined && cursor.row < cond.cursor_row_min) {
|
|
258
207
|
trace.push({ kind: 'state_skip', text: `state[${stateId}] cursor row ${cursor.row} < cursor_row_min ${cond.cursor_row_min}` });
|
|
@@ -280,47 +229,9 @@ export function evaluateCondition(
|
|
|
280
229
|
}
|
|
281
230
|
|
|
282
231
|
// ────────────────────────────────────────────────────────────────────────────
|
|
283
|
-
//
|
|
232
|
+
// Extraction helpers (used by FsmDriver for modal/title)
|
|
284
233
|
// ────────────────────────────────────────────────────────────────────────────
|
|
285
234
|
|
|
286
|
-
function matchState(
|
|
287
|
-
state: SpecStateV3,
|
|
288
|
-
sections: ResolvedSection[],
|
|
289
|
-
fullScreen: string,
|
|
290
|
-
trace: TraceEntry[],
|
|
291
|
-
cursor: { row: number; col: number } | undefined,
|
|
292
|
-
prevLines: string[] | undefined,
|
|
293
|
-
): { matched: boolean; title: string | null } {
|
|
294
|
-
// Support v1-shaped state.when ({ regex, section }) for backward compat
|
|
295
|
-
// with test objects that don't go through the loader.
|
|
296
|
-
let effectiveWhen = state.when;
|
|
297
|
-
const stateAny = state as any;
|
|
298
|
-
if (stateAny.when && 'regex' in stateAny.when && !('all' in stateAny.when) && !('any' in stateAny.when)) {
|
|
299
|
-
effectiveWhen = normalizeV1When(stateAny.when);
|
|
300
|
-
}
|
|
301
|
-
const condMatched = evaluateCondition(effectiveWhen, sections, fullScreen, cursor, prevLines, trace, state.id);
|
|
302
|
-
|
|
303
|
-
if (!condMatched) {
|
|
304
|
-
trace.push({ kind: 'state_skip', text: `state[${state.id}] when condition not met` });
|
|
305
|
-
return { matched: false, title: null };
|
|
306
|
-
}
|
|
307
|
-
|
|
308
|
-
trace.push({ kind: 'state_match', text: `state[${state.id}] matched` });
|
|
309
|
-
|
|
310
|
-
let title: string | null = null;
|
|
311
|
-
const stateAny2 = state as any;
|
|
312
|
-
const extract = state.extract;
|
|
313
|
-
// v1 compat: extract_title
|
|
314
|
-
const titleRule: ExtractTitle | undefined = extract?.title
|
|
315
|
-
?? (stateAny2.extract_title ? v1ExtractTitleToV3(stateAny2.extract_title) : undefined);
|
|
316
|
-
if (titleRule) {
|
|
317
|
-
title = extractTitle(titleRule, sections, fullScreen);
|
|
318
|
-
trace.push({ kind: 'state_match', text: `state[${state.id}] extract.title → ${title ?? '(none)'}` });
|
|
319
|
-
}
|
|
320
|
-
|
|
321
|
-
return { matched: true, title };
|
|
322
|
-
}
|
|
323
|
-
|
|
324
235
|
export function extractTitle(
|
|
325
236
|
rule: ExtractTitle,
|
|
326
237
|
sections: ResolvedSection[],
|
|
@@ -330,7 +241,6 @@ export function extractTitle(
|
|
|
330
241
|
if (!hay) return null;
|
|
331
242
|
|
|
332
243
|
if (rule.first_line) {
|
|
333
|
-
// Take the first non-separator, non-empty line
|
|
334
244
|
const lines = hay.split('\n');
|
|
335
245
|
for (const line of lines) {
|
|
336
246
|
const stripped = line.trim();
|
|
@@ -409,196 +319,3 @@ export function extractButtonsFromRule(
|
|
|
409
319
|
buttons.sort((a, b) => a.index - b.index);
|
|
410
320
|
return buttons;
|
|
411
321
|
}
|
|
412
|
-
|
|
413
|
-
function extractModal(
|
|
414
|
-
state: SpecStateV3,
|
|
415
|
-
sections: ResolvedSection[],
|
|
416
|
-
fullScreen: string,
|
|
417
|
-
title: string | null,
|
|
418
|
-
trace: TraceEntry[],
|
|
419
|
-
): ModalSnapshot | null {
|
|
420
|
-
const stateAny = state as any;
|
|
421
|
-
// v1 compat: modal_buttons
|
|
422
|
-
const buttonsRule: ExtractButtons | undefined = state.extract?.buttons
|
|
423
|
-
?? (stateAny.modal_buttons ? v1ModalButtonsToV3(stateAny.modal_buttons) : undefined);
|
|
424
|
-
if (!buttonsRule) return null;
|
|
425
|
-
|
|
426
|
-
const hay = sectionText(sections, buttonsRule.section, fullScreen);
|
|
427
|
-
const minCount = buttonsRule.min_count ?? 2;
|
|
428
|
-
const buttons = extractButtonsFromRule(buttonsRule, hay);
|
|
429
|
-
|
|
430
|
-
if (buttons.length < minCount) {
|
|
431
|
-
trace.push({ kind: 'modal', text: `extract.buttons matched ${buttons.length}/${minCount} required — discarded` });
|
|
432
|
-
return null;
|
|
433
|
-
}
|
|
434
|
-
trace.push({ kind: 'modal', text: `extract.buttons matched ${buttons.length} choices` });
|
|
435
|
-
return { title, buttons };
|
|
436
|
-
}
|
|
437
|
-
|
|
438
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
439
|
-
// v1 backward-compat helpers
|
|
440
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
441
|
-
|
|
442
|
-
/**
|
|
443
|
-
* Convert a v1 sections array to a v3 sections object (for inline test specs
|
|
444
|
-
* that bypass the loader and thus the migration path).
|
|
445
|
-
*/
|
|
446
|
-
function buildSectionsMapFromV1(v1Sections: any[]): Record<string, SectionDef> {
|
|
447
|
-
const map: Record<string, SectionDef> = {};
|
|
448
|
-
for (const sec of v1Sections) {
|
|
449
|
-
const { id, anchor_regex, ...rest } = sec;
|
|
450
|
-
const def: SectionDef = { ...rest };
|
|
451
|
-
if (anchor_regex) def.anchor = anchor_regex;
|
|
452
|
-
// v1 until: { section: id } → v3 until: id
|
|
453
|
-
if (rest.until?.section) def.until = rest.until.section;
|
|
454
|
-
else if (rest.until && typeof rest.until === 'string') def.until = rest.until;
|
|
455
|
-
else delete (def as any).until;
|
|
456
|
-
map[id] = def;
|
|
457
|
-
}
|
|
458
|
-
return map;
|
|
459
|
-
}
|
|
460
|
-
|
|
461
|
-
/** Convert v1 SectionRegex (when.regex) to v3 AllCondition. */
|
|
462
|
-
function normalizeV1When(v1When: any): AllCondition {
|
|
463
|
-
if (v1When?.cursor_above_lines && v1When?.changed) {
|
|
464
|
-
return { all: [{ cursor_above: v1When.cursor_above_lines, changed: true as const }] };
|
|
465
|
-
}
|
|
466
|
-
const cond: any = { matches: v1When.regex };
|
|
467
|
-
if (v1When.section) cond.section = v1When.section;
|
|
468
|
-
if (v1When.flags) cond.flags = v1When.flags;
|
|
469
|
-
if (v1When.cursor_row_min !== undefined) cond.cursor_row_min = v1When.cursor_row_min;
|
|
470
|
-
if (v1When.cursor_row_max !== undefined) cond.cursor_row_max = v1When.cursor_row_max;
|
|
471
|
-
if (v1When.cursor_col_min !== undefined) cond.cursor_col_min = v1When.cursor_col_min;
|
|
472
|
-
if (v1When.cursor_col_max !== undefined) cond.cursor_col_max = v1When.cursor_col_max;
|
|
473
|
-
return { all: [cond] };
|
|
474
|
-
}
|
|
475
|
-
|
|
476
|
-
/** Convert v1 extract_title to v3 ExtractTitle. */
|
|
477
|
-
function v1ExtractTitleToV3(v1: any): ExtractTitle {
|
|
478
|
-
if (v1.first_line) return { section: v1.section, first_line: true };
|
|
479
|
-
return { section: v1.section, regex: v1.regex, flags: v1.flags };
|
|
480
|
-
}
|
|
481
|
-
|
|
482
|
-
/** Convert v1 modal_buttons to v3 ExtractButtons. */
|
|
483
|
-
function v1ModalButtonsToV3(v1: any): ExtractButtons | undefined {
|
|
484
|
-
// Use first pattern from patterns[] or single pattern
|
|
485
|
-
const pat = v1.patterns?.length ? v1.patterns[0].pattern : v1.pattern;
|
|
486
|
-
if (!pat) return undefined;
|
|
487
|
-
const flg = v1.patterns?.length ? v1.patterns[0].flags : v1.flags;
|
|
488
|
-
return {
|
|
489
|
-
section: v1.section,
|
|
490
|
-
pattern: pat,
|
|
491
|
-
flags: flg,
|
|
492
|
-
key_for_index: v1.key_for_index,
|
|
493
|
-
min_count: v1.min_count,
|
|
494
|
-
continuation_lines: v1.continuation_lines,
|
|
495
|
-
};
|
|
496
|
-
}
|
|
497
|
-
|
|
498
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
499
|
-
// Public evaluator
|
|
500
|
-
// ────────────────────────────────────────────────────────────────────────────
|
|
501
|
-
|
|
502
|
-
export function evaluate(
|
|
503
|
-
spec: CliSpec,
|
|
504
|
-
screenText: string,
|
|
505
|
-
/** Optional cursor position (0-based row and col). */
|
|
506
|
-
cursor?: { row: number; col: number },
|
|
507
|
-
/** Optional previous screen lines for `changed` condition detection. */
|
|
508
|
-
prevLines?: string[],
|
|
509
|
-
): SpecEvaluation {
|
|
510
|
-
const trace: TraceEntry[] = [];
|
|
511
|
-
const lines = screenText.split('\n').map(l => l.endsWith('\r') ? l.slice(0, -1) : l);
|
|
512
|
-
const cleanScreen = lines.join('\n');
|
|
513
|
-
|
|
514
|
-
// Support both v3 (sections{}) and v1-shaped objects (layout.sections[])
|
|
515
|
-
// The v1 path exists for tests that build raw spec objects without going
|
|
516
|
-
// through the loader (which would normally migrate v1 → v3).
|
|
517
|
-
const specAny = spec as any;
|
|
518
|
-
const effectiveSectionsMap: Record<string, SectionDef> = spec.sections
|
|
519
|
-
?? buildSectionsMapFromV1(specAny.layout?.sections ?? []);
|
|
520
|
-
const sections = resolveSections(effectiveSectionsMap, lines);
|
|
521
|
-
|
|
522
|
-
for (const s of sections) {
|
|
523
|
-
trace.push({ kind: 'section', text: `section[${s.id}] lines [${s.fromLine}, ${s.toLine}) (${s.toLine - s.fromLine} lines)` });
|
|
524
|
-
}
|
|
525
|
-
if (cursor !== undefined) {
|
|
526
|
-
trace.push({ kind: 'section', text: `cursor (${cursor.row}, ${cursor.col})` });
|
|
527
|
-
}
|
|
528
|
-
|
|
529
|
-
let activeState: { id: string; label: string; title: string | null } | null = null;
|
|
530
|
-
let modal: ModalSnapshot | null = null;
|
|
531
|
-
|
|
532
|
-
for (const st of spec.states) {
|
|
533
|
-
const { matched, title } = matchState(st, sections, cleanScreen, trace, cursor, prevLines);
|
|
534
|
-
if (!matched) continue;
|
|
535
|
-
const extractedModal = extractModal(st, sections, cleanScreen, title, trace);
|
|
536
|
-
// If the state declares extract.buttons (or v1 modal_buttons) but
|
|
537
|
-
// extraction failed, don't promote the state (avoids phantom approvals).
|
|
538
|
-
const stAny = st as any;
|
|
539
|
-
if ((st.extract?.buttons || stAny.modal_buttons) && !extractedModal) {
|
|
540
|
-
trace.push({ kind: 'state_skip', text: `state[${st.id}] matched but extract.buttons failed — not promoting` });
|
|
541
|
-
continue;
|
|
542
|
-
}
|
|
543
|
-
activeState = { id: st.id, label: st.label, title };
|
|
544
|
-
modal = extractedModal;
|
|
545
|
-
break;
|
|
546
|
-
}
|
|
547
|
-
|
|
548
|
-
if (!activeState) {
|
|
549
|
-
const def = spec.states.find(s => s.id === spec.default_state);
|
|
550
|
-
if (def) {
|
|
551
|
-
activeState = { id: def.id, label: def.label, title: null };
|
|
552
|
-
trace.push({ kind: 'state_match', text: `(no state matched — fallback to default_state ${def.id})` });
|
|
553
|
-
} else {
|
|
554
|
-
activeState = { id: spec.default_state, label: spec.default_state, title: null };
|
|
555
|
-
trace.push({ kind: 'state_match', text: `(no state matched, no default_state defined — using id "${spec.default_state}")` });
|
|
556
|
-
}
|
|
557
|
-
}
|
|
558
|
-
|
|
559
|
-
const controls: VisibleControl[] = [];
|
|
560
|
-
for (const c of spec.control_bar ?? []) {
|
|
561
|
-
const visible = !c.visible_when_state || c.visible_when_state.includes(activeState.id);
|
|
562
|
-
trace.push({ kind: 'control', text: `control[${c.id}] visible=${visible}${c.visible_when_state ? ` (when_state ${c.visible_when_state.join('|')})` : ''}` });
|
|
563
|
-
if (visible) controls.push({ id: c.id, label: c.label, actionType: c.action.type });
|
|
564
|
-
}
|
|
565
|
-
|
|
566
|
-
const notifications: FiredNotification[] = [];
|
|
567
|
-
for (const n of spec.notifications ?? []) {
|
|
568
|
-
if (n.when_state !== activeState.id) continue;
|
|
569
|
-
const body = interpolate(n.body ?? '', activeState, sections);
|
|
570
|
-
notifications.push({ id: n.id, title: n.title, body });
|
|
571
|
-
trace.push({ kind: 'notification', text: `notification[${n.id}] fired (when_state=${n.when_state})` });
|
|
572
|
-
}
|
|
573
|
-
|
|
574
|
-
const delegates: FiredDelegate[] = [];
|
|
575
|
-
for (const d of spec.delegate ?? []) {
|
|
576
|
-
if (d.when_state !== activeState.id) continue;
|
|
577
|
-
delegates.push({ id: d.id, task: interpolate(d.task_template, activeState, sections) });
|
|
578
|
-
}
|
|
579
|
-
|
|
580
|
-
return {
|
|
581
|
-
state: activeState,
|
|
582
|
-
modal,
|
|
583
|
-
controls,
|
|
584
|
-
notifications,
|
|
585
|
-
delegates,
|
|
586
|
-
sections,
|
|
587
|
-
trace,
|
|
588
|
-
};
|
|
589
|
-
}
|
|
590
|
-
|
|
591
|
-
function interpolate(
|
|
592
|
-
template: string,
|
|
593
|
-
state: { id: string; label: string; title: string | null },
|
|
594
|
-
sections: ResolvedSection[],
|
|
595
|
-
): string {
|
|
596
|
-
return template
|
|
597
|
-
.replace(/\{state\.label\}/g, state.label)
|
|
598
|
-
.replace(/\{state\.title\}/g, state.title ?? '')
|
|
599
|
-
.replace(/\{state\.id\}/g, state.id)
|
|
600
|
-
.replace(/\{screen\.([a-z][a-z0-9_]*)\}/g, (_, id: string) => {
|
|
601
|
-
const s = sections.find(s => s.id === id);
|
|
602
|
-
return s ? s.text : '';
|
|
603
|
-
});
|
|
604
|
-
}
|
|
@@ -26,7 +26,7 @@ import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
|
26
26
|
import { DEFAULT_SESSION_HOST_COLS, DEFAULT_SESSION_HOST_ROWS } from '@adhdev/session-host-core';
|
|
27
27
|
import {
|
|
28
28
|
resolveSections, sectionText, extractTitle, extractButtonsFromRule,
|
|
29
|
-
type ResolvedSection,
|
|
29
|
+
type ResolvedSection, type TraceEntry,
|
|
30
30
|
} from './evaluator.js';
|
|
31
31
|
import { evaluateFsm, type FsmClock, type TransitionEval } from './fsm-evaluator.js';
|
|
32
32
|
import {
|
|
@@ -35,13 +35,99 @@ import {
|
|
|
35
35
|
} from './fsm-types.js';
|
|
36
36
|
import { loadFsmSpec } from './fsm-loader.js';
|
|
37
37
|
import type { Control, DelegateTrigger } from './types.js';
|
|
38
|
-
import {
|
|
39
|
-
type DashboardEvent, type DashboardCommand, type SpecDriverOpts,
|
|
40
|
-
type ISpecDriver, type DriverHistoryEntry,
|
|
41
|
-
resolveSubmitDelayMs, guessExt,
|
|
42
|
-
} from './driver.js';
|
|
43
38
|
import { LOG } from '../../logging/logger.js';
|
|
44
39
|
|
|
40
|
+
// ── Shared driver types (formerly in driver.ts) ───────────────────────────
|
|
41
|
+
|
|
42
|
+
export type DashboardEvent =
|
|
43
|
+
| { kind: 'pty_data'; chunk: string }
|
|
44
|
+
| { kind: 'state_changed'; state: { id: string; label: string; title: string | null };
|
|
45
|
+
modal: { title: string | null; buttons: { index: number; label: string }[] } | null;
|
|
46
|
+
controls: { id: string; label: string; action_type: string }[] }
|
|
47
|
+
| { kind: 'notification'; id: string; title: string; body: string }
|
|
48
|
+
| { kind: 'delegate'; id: string; task: string }
|
|
49
|
+
| { kind: 'spec_trace'; entries: TraceEntry[] }
|
|
50
|
+
| { kind: 'exit'; exit_code: number }
|
|
51
|
+
| { kind: 'spec_error'; errors: string[] };
|
|
52
|
+
|
|
53
|
+
export type DashboardCommand =
|
|
54
|
+
| { kind: 'send_message'; text: string }
|
|
55
|
+
| { kind: 'pty_write'; data: string }
|
|
56
|
+
| { kind: 'click_control'; control_id: string; payload?: unknown }
|
|
57
|
+
| { kind: 'click_modal_button'; index: number }
|
|
58
|
+
| { kind: 'attach_image'; blob: string; mime: string }
|
|
59
|
+
| { kind: 'resize'; cols: number; rows: number }
|
|
60
|
+
| { kind: 'cancel' }
|
|
61
|
+
| { kind: 'shutdown' };
|
|
62
|
+
|
|
63
|
+
export interface DriverHistoryEntry {
|
|
64
|
+
stateId: string;
|
|
65
|
+
label: string;
|
|
66
|
+
at: number;
|
|
67
|
+
durationMs: number;
|
|
68
|
+
reason: string;
|
|
69
|
+
matchedStateId?: string;
|
|
70
|
+
matchedRules?: string[];
|
|
71
|
+
debounceKind?: string;
|
|
72
|
+
idleHoldMs?: number;
|
|
73
|
+
busyHoldMs?: number;
|
|
74
|
+
via?: string;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export interface ISpecDriver {
|
|
78
|
+
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
79
|
+
start(): void;
|
|
80
|
+
dispatch(cmd: DashboardCommand): void;
|
|
81
|
+
snapshot(): string;
|
|
82
|
+
getCursorPosition(): { row: number; col: number };
|
|
83
|
+
getScreen(): string;
|
|
84
|
+
getSpecPath(): string;
|
|
85
|
+
shutdown(): void;
|
|
86
|
+
getStateHistory(): ReadonlyArray<DriverHistoryEntry>;
|
|
87
|
+
getSections(): Array<{ id: string; text: string }> | null;
|
|
88
|
+
getLastBusyAt(): number;
|
|
89
|
+
hasIdleHoldPending(): boolean;
|
|
90
|
+
getCompletionIdleDebounceState(): { active: boolean; ageMs: number; holdMs: number; forceAfterMs: number } | null;
|
|
91
|
+
getFsmDebug?(): unknown;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
export interface SpecDriverOpts {
|
|
95
|
+
specPath: string;
|
|
96
|
+
workingDir: string;
|
|
97
|
+
extraEnv?: Record<string, string>;
|
|
98
|
+
cols?: number;
|
|
99
|
+
rows?: number;
|
|
100
|
+
hotReload?: boolean;
|
|
101
|
+
emitTrace?: boolean;
|
|
102
|
+
transportFactory?: PtyTransportFactory;
|
|
103
|
+
extraCliArgs?: string[];
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function countNewlines(s: string): number {
|
|
107
|
+
let n = 0;
|
|
108
|
+
for (let i = 0; i < s.length; i += 1) if (s.charCodeAt(i) === 10) n += 1;
|
|
109
|
+
return n;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
const SUBMIT_DELAY_FLOOR_MS = 200;
|
|
113
|
+
|
|
114
|
+
export function resolveSubmitDelayMs(specBeforeSubmit: number | undefined, text: string): number {
|
|
115
|
+
const lines = countNewlines(text);
|
|
116
|
+
const linesBonus = Math.min(800, lines * 80);
|
|
117
|
+
const spec = typeof specBeforeSubmit === 'number' && specBeforeSubmit > 0 ? specBeforeSubmit : 0;
|
|
118
|
+
return Math.max(spec, SUBMIT_DELAY_FLOOR_MS + linesBonus);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function guessExt(mime: string): string {
|
|
122
|
+
if (/png/i.test(mime)) return '.png';
|
|
123
|
+
if (/jpe?g/i.test(mime)) return '.jpg';
|
|
124
|
+
if (/gif/i.test(mime)) return '.gif';
|
|
125
|
+
if (/webp/i.test(mime)) return '.webp';
|
|
126
|
+
return '.bin';
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
130
|
+
|
|
45
131
|
interface ModalSnapshot {
|
|
46
132
|
title: string | null;
|
|
47
133
|
buttons: { index: number; label: string; key: string }[];
|