@adhdev/daemon-core 0.9.82-rc.566 → 0.9.82-rc.568
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/cli-adapter-types.d.ts +14 -0
- package/dist/index.js +194 -32
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +194 -32
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-active-work.d.ts +4 -0
- package/dist/providers/cli-provider-instance.d.ts +27 -0
- package/dist/providers/spec/cli-adapter.d.ts +1 -0
- package/dist/providers/spec/fsm-driver.d.ts +18 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.d.ts +1 -0
- package/src/cli-adapter-types.ts +17 -0
- package/src/commands/chat-commands-write.ts +15 -1
- package/src/mesh/mesh-active-work.ts +22 -4
- package/src/mesh/mesh-event-forwarding.ts +41 -23
- package/src/mesh/mesh-reconcile-loop.ts +52 -0
- package/src/providers/cli-provider-instance.ts +133 -14
- package/src/providers/spec/cli-adapter.ts +37 -3
- package/src/providers/spec/fsm-driver.ts +27 -5
|
@@ -109,6 +109,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
109
109
|
*/
|
|
110
110
|
private static readonly AUTO_APPROVE_SETTLE_MS = 600;
|
|
111
111
|
|
|
112
|
+
/**
|
|
113
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): how long after a LOCAL auto-approve fire the mesh
|
|
114
|
+
* event forwarder still treats the modal as "being resolved locally" and suppresses the
|
|
115
|
+
* coordinator notification. Chosen to comfortably cover the resolveModal → PTY absorb →
|
|
116
|
+
* status-leaves-approval round trip (incl. the win32 CR-resend loop) while staying short
|
|
117
|
+
* enough that a modal which auto-approve fired at but did NOT resolve re-surfaces to the
|
|
118
|
+
* coordinator on the next event. Aligned with the adapter's own approval cooldown scale.
|
|
119
|
+
*/
|
|
120
|
+
private static readonly APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS = 8000;
|
|
121
|
+
|
|
112
122
|
/**
|
|
113
123
|
* Busy-side hysteresis for the settle gate. A momentary `generating` flip
|
|
114
124
|
* while the SAME approval modal's button block is still on screen (its
|
|
@@ -387,6 +397,16 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
387
397
|
// signature) while a genuinely closed modal — buttons empty continuously past
|
|
388
398
|
// the continuity window — is still recognised and resets the gate.
|
|
389
399
|
private autoApproveLastModalSeenAt = 0;
|
|
400
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A): wall-clock of the last time this session actually
|
|
401
|
+
// FIRED a local auto-approve resolveModal (the settle gate passed → resolveModal
|
|
402
|
+
// dispatched). The mesh event forwarder keys its agent:waiting_approval suppression on
|
|
403
|
+
// this + a cooldown so it only drops the coordinator notification when we can positively
|
|
404
|
+
// confirm the modal was (or is being) resolved LOCALLY. If auto-approve is merely
|
|
405
|
+
// *configured* on but has NOT recently fired for this modal, the raw waiting_approval is
|
|
406
|
+
// forwarded so a task_approval_needed ledger row is created and the coordinator/inbox is
|
|
407
|
+
// told — closing the blind spot where a never-resolving worker approval was silently
|
|
408
|
+
// dropped just because settings.autoApprove===true.
|
|
409
|
+
private lastAutoApproveFiredAt = 0;
|
|
390
410
|
// AUTOAPPROVE-FLAP-INBOX-MISSING sticky-approval overlay (see APPROVAL_STICKY_FLAP_MS).
|
|
391
411
|
// The wall-clock of the last frame where the RAW adapter reported waiting_approval with
|
|
392
412
|
// a CONCRETE modal (buttons present), the cached modal to re-present across a busy blip,
|
|
@@ -1099,6 +1119,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1099
1119
|
return null;
|
|
1100
1120
|
}
|
|
1101
1121
|
|
|
1122
|
+
/**
|
|
1123
|
+
* APPROVAL-INBOX-BLINDSPOT (Fix A): true when this session's approval modal was — or is
|
|
1124
|
+
* being — resolved LOCALLY within the recent cooldown. Two independent positive signals:
|
|
1125
|
+
* (1) auto-approve fired its resolveModal within APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS
|
|
1126
|
+
* (lastAutoApproveFiredAt), or
|
|
1127
|
+
* (2) the underlying adapter reports isApprovalRecentlyResolved() — its own resolve
|
|
1128
|
+
* cooldown, which also covers a dashboard / mesh_approve resolution.
|
|
1129
|
+
* The mesh event forwarder uses this to decide whether an agent:waiting_approval from an
|
|
1130
|
+
* auto-approving worker can be safely SUPPRESSED (a local resolution is in flight) or must
|
|
1131
|
+
* be FORWARDED (auto-approve is configured but has NOT actually resolved this modal, so the
|
|
1132
|
+
* coordinator/inbox must be told). Keying suppression on real resolution — not just the
|
|
1133
|
+
* autoApprove *intent* — is the blind-spot fix: a never-resolving worker approval is no
|
|
1134
|
+
* longer silently dropped.
|
|
1135
|
+
*/
|
|
1136
|
+
approvalRecentlyResolvedLocally(now = Date.now()): boolean {
|
|
1137
|
+
if (this.lastAutoApproveFiredAt
|
|
1138
|
+
&& now - this.lastAutoApproveFiredAt < CliProviderInstance.APPROVAL_LOCAL_RESOLUTION_COOLDOWN_MS) {
|
|
1139
|
+
return true;
|
|
1140
|
+
}
|
|
1141
|
+
try {
|
|
1142
|
+
const adapter = this.adapter as { isApprovalRecentlyResolved?: () => boolean };
|
|
1143
|
+
if (typeof adapter.isApprovalRecentlyResolved === 'function') {
|
|
1144
|
+
return adapter.isApprovalRecentlyResolved() === true;
|
|
1145
|
+
}
|
|
1146
|
+
} catch { /* adapter gone / transient */ }
|
|
1147
|
+
return false;
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1102
1150
|
/**
|
|
1103
1151
|
* NOTIF-HELD-DRAIN: true when this `waiting_approval` is a routine, transient tool-consent
|
|
1104
1152
|
* of an autonomously-progressing mesh session rather than a genuine human-await modal —
|
|
@@ -1344,7 +1392,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1344
1392
|
* real last answer with ZERO per-tick native reads (the native read already ran
|
|
1345
1393
|
* once at completion). Reset on the next turn's start.
|
|
1346
1394
|
*/
|
|
1347
|
-
private lastCompletionSummary: { content: string; receivedAt: number } | null = null;
|
|
1395
|
+
private lastCompletionSummary: { content: string; receivedAt: number; sourceTimestampMs?: number } | null = null;
|
|
1348
1396
|
|
|
1349
1397
|
private async enforceFreshSessionLaunchIfNeeded(): Promise<void> {
|
|
1350
1398
|
const scriptName = getForcedNewSessionScriptName(this.provider, this.launchMode);
|
|
@@ -1546,18 +1594,31 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1546
1594
|
* the dashboard tail-repair cache — a display value, not a completion decision.
|
|
1547
1595
|
*/
|
|
1548
1596
|
private lastVisibleAssistantSummary(messages: unknown): string {
|
|
1549
|
-
|
|
1597
|
+
return this.lastVisibleAssistantSummaryDetail(messages).content;
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
// Like lastVisibleAssistantSummary but also returns the source bubble's own
|
|
1601
|
+
// timestamp (ms), so a cached summary can later be turn-scoped: the display
|
|
1602
|
+
// cache is populated from an UNSCOPED tail read (it must show the answer as
|
|
1603
|
+
// soon as native-history has it), so it can hold a bubble that predates the
|
|
1604
|
+
// current turn. Recording the bubble's timestamp lets the weak-completion
|
|
1605
|
+
// fallback reject a turn-stale cached summary instead of re-leaking the exact
|
|
1606
|
+
// stale bubble the turn-boundary gate already rejected (FALSE-IDLE Defect 1c).
|
|
1607
|
+
private lastVisibleAssistantSummaryDetail(messages: unknown): { content: string; timestampMs?: number } {
|
|
1608
|
+
if (!Array.isArray(messages)) return { content: '' };
|
|
1550
1609
|
for (let i = messages.length - 1; i >= 0; i -= 1) {
|
|
1551
1610
|
const m = messages[i] as { role?: string; kind?: string; content?: unknown };
|
|
1552
1611
|
const role = typeof m?.role === 'string' ? m.role : '';
|
|
1553
1612
|
const kind = typeof m?.kind === 'string' ? m.kind : '';
|
|
1554
1613
|
if (role === 'system') continue;
|
|
1555
1614
|
if (kind === 'tool' || kind === 'activity') continue;
|
|
1556
|
-
if (role === 'user' || role === 'human') return '';
|
|
1557
|
-
if (role === 'assistant')
|
|
1558
|
-
|
|
1615
|
+
if (role === 'user' || role === 'human') return { content: '' };
|
|
1616
|
+
if (role === 'assistant') {
|
|
1617
|
+
return { content: flattenContent(m.content as any).trim(), timestampMs: readChatMessageTimestampMs(m as any) };
|
|
1618
|
+
}
|
|
1619
|
+
return { content: '' };
|
|
1559
1620
|
}
|
|
1560
|
-
return '';
|
|
1621
|
+
return { content: '' };
|
|
1561
1622
|
}
|
|
1562
1623
|
|
|
1563
1624
|
/**
|
|
@@ -1580,6 +1641,29 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1580
1641
|
return content;
|
|
1581
1642
|
}
|
|
1582
1643
|
|
|
1644
|
+
// FALSE-IDLE Defect 1c: turn-scoped view of the cached completion summary.
|
|
1645
|
+
// The cache is populated from an UNSCOPED tail read (lastVisibleAssistant‑
|
|
1646
|
+
// SummaryDetail) so the dashboard can show the answer the instant native-history
|
|
1647
|
+
// has it — which means it can hold a bubble that PREDATES the producing turn.
|
|
1648
|
+
// The weak-completion (missing_final_assistant) emit path falls back to the cache
|
|
1649
|
+
// for finalSummary; without turn-scoping it would re-surface the exact stale
|
|
1650
|
+
// mid-turn bubble the turn-boundary gate already rejected as evidence, freezing
|
|
1651
|
+
// that stale text as the completion's finalSummary. Consult the cache only when
|
|
1652
|
+
// its source bubble is proven in-turn (timestamp at/after turnStartedAt). When no
|
|
1653
|
+
// boundary is known (turnStartedAt falsy) or the cache carries no source timestamp
|
|
1654
|
+
// (legacy writes), behaviour is identical to the unscoped read.
|
|
1655
|
+
private cachedInTurnCompletionSummaryContent(turnStartedAt?: number): string {
|
|
1656
|
+
const cached = this.lastCompletionSummary;
|
|
1657
|
+
const content = typeof cached?.content === 'string' ? cached.content.trim() : '';
|
|
1658
|
+
if (!content) return '';
|
|
1659
|
+
const hasBoundary = typeof turnStartedAt === 'number' && Number.isFinite(turnStartedAt) && turnStartedAt > 0;
|
|
1660
|
+
const ts = cached?.sourceTimestampMs;
|
|
1661
|
+
if (hasBoundary && typeof ts === 'number' && Number.isFinite(ts) && ts < (turnStartedAt as number)) {
|
|
1662
|
+
return '';
|
|
1663
|
+
}
|
|
1664
|
+
return content;
|
|
1665
|
+
}
|
|
1666
|
+
|
|
1583
1667
|
private completionFinalAssistantEvidence(parsedMessages: unknown, turnStartedAt?: number): CompletionFinalAssistantEvidence {
|
|
1584
1668
|
// (FALSEIDLE FixB) UPPER-BOUND turn-end evidence. completionHasFinalAssistantMessage is a
|
|
1585
1669
|
// pure message-content check ("does the last visible bubble read as a finalized assistant
|
|
@@ -1633,9 +1717,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1633
1717
|
// (which also requires turnClosed and turn-scoping): the dashboard should
|
|
1634
1718
|
// show the answer as soon as native-history has it, even if the FSM has
|
|
1635
1719
|
// not yet ratified the turn end. getState() replaces it on the next turn.
|
|
1636
|
-
const lastVisibleAssistant = this.
|
|
1637
|
-
if (lastVisibleAssistant) {
|
|
1638
|
-
this.lastCompletionSummary = { content: lastVisibleAssistant, receivedAt: Date.now() };
|
|
1720
|
+
const lastVisibleAssistant = this.lastVisibleAssistantSummaryDetail(externalMessages);
|
|
1721
|
+
if (lastVisibleAssistant.content) {
|
|
1722
|
+
this.lastCompletionSummary = { content: lastVisibleAssistant.content, receivedAt: Date.now(), sourceTimestampMs: lastVisibleAssistant.timestampMs };
|
|
1639
1723
|
}
|
|
1640
1724
|
return {
|
|
1641
1725
|
present,
|
|
@@ -1697,7 +1781,11 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1697
1781
|
// sessions whose generating_completed is suppressed, and short-gen
|
|
1698
1782
|
// settle paths), so the dashboard sees the answer even when no
|
|
1699
1783
|
// agent:generating_completed is ever emitted. Native read already ran.
|
|
1700
|
-
|
|
1784
|
+
// externalSummary is already turn-scoped (extractFinalSummaryFromMessagesAfter
|
|
1785
|
+
// dropped any bubble predating turnStartedAt), so it is in-turn by construction.
|
|
1786
|
+
// Record the turn boundary as its source timestamp so the weak-completion
|
|
1787
|
+
// fallback (cachedInTurnCompletionSummaryContent) accepts it.
|
|
1788
|
+
this.lastCompletionSummary = { content: externalSummary, receivedAt: Date.now(), sourceTimestampMs: typeof turnStartedAt === 'number' ? turnStartedAt : undefined };
|
|
1701
1789
|
return externalSummary;
|
|
1702
1790
|
}
|
|
1703
1791
|
return parsedSummary || undefined;
|
|
@@ -1721,7 +1809,12 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1721
1809
|
parseError = error?.message || String(error);
|
|
1722
1810
|
}
|
|
1723
1811
|
|
|
1724
|
-
|
|
1812
|
+
// FALSE-IDLE Defect 1c: turn-scope the diagnostic's evidence probe too. Passing
|
|
1813
|
+
// pending.turnStartedAt makes completionHasFinalAssistantMessage reject a stale
|
|
1814
|
+
// mid-turn bubble (predating the turn) just as the finalization gate did, so the
|
|
1815
|
+
// diagnostic cannot credit finalAssistantPresent (or clear missing_final_assistant)
|
|
1816
|
+
// off a bubble the gate already rejected. With no boundary this is unchanged.
|
|
1817
|
+
const evidence = this.completionFinalAssistantEvidence(parsed?.messages, args.pending.turnStartedAt);
|
|
1725
1818
|
if (evidence.source === 'external-native') {
|
|
1726
1819
|
this.recordPendingTranscriptProbe(args.pending);
|
|
1727
1820
|
}
|
|
@@ -1742,7 +1835,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1742
1835
|
// missing_final_assistant with an empty payload. Only ever UPGRADES a
|
|
1743
1836
|
// point-sample miss — a genuine present=true is unchanged, and an empty cache
|
|
1744
1837
|
// leaves the missing-evidence diagnostic exactly as before.
|
|
1745
|
-
const cachedSummary = evidence.present ? '' : this.
|
|
1838
|
+
const cachedSummary = evidence.present ? '' : this.cachedInTurnCompletionSummaryContent(args.pending.turnStartedAt);
|
|
1746
1839
|
const creditedFromCache = !evidence.present && cachedSummary.length > 0;
|
|
1747
1840
|
const finalAssistantPresent = evidence.present || creditedFromCache;
|
|
1748
1841
|
const finalAssistantEvidenceSource = evidence.present
|
|
@@ -2707,7 +2800,7 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2707
2800
|
// carries the summary that mesh_read_chat.summary already shows — consistent with
|
|
2708
2801
|
// completionDiagnostic.finalAssistantPresent being credited from the same cache.
|
|
2709
2802
|
finalSummary: (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt)
|
|
2710
|
-
|| this.
|
|
2803
|
+
|| this.cachedInTurnCompletionSummaryContent(pending.turnStartedAt)
|
|
2711
2804
|
|| (blockReason.startsWith('parsed_status:') ? '' : undefined)),
|
|
2712
2805
|
completionDiagnostic,
|
|
2713
2806
|
});
|
|
@@ -3139,8 +3232,34 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
3139
3232
|
this.lastAutoApprovalSignature = '';
|
|
3140
3233
|
}, 5000);
|
|
3141
3234
|
this.recordAutoApproval(modal?.message, buttonLabel, now);
|
|
3235
|
+
// APPROVAL-INBOX-BLINDSPOT (Fix A) + BUTTON-INDEX-MISMAP (Fix C): stamp the
|
|
3236
|
+
// local-resolution clock so the mesh forwarder can distinguish "auto-approve just
|
|
3237
|
+
// fired / is firing" (suppress the coordinator notification — modal is being resolved
|
|
3238
|
+
// locally) from "auto-approve is merely configured but has not fired for this modal"
|
|
3239
|
+
// (forward it so the coordinator is told and a task_approval_needed ledger row is
|
|
3240
|
+
// created). Only stamp when the click actually MATCHED a button: resolveModalMatched
|
|
3241
|
+
// maps the array position to the real FSM display index and reports whether a button
|
|
3242
|
+
// was pressed, so a mis-mapped/never-pressed modal does NOT falsely mark itself
|
|
3243
|
+
// locally-resolved (which would suppress the coordinator notification for a modal that
|
|
3244
|
+
// never got answered — the exact blind spot). Legacy adapters keep the void resolveModal.
|
|
3245
|
+
this.lastAutoApproveFiredAt = now;
|
|
3142
3246
|
setTimeout(() => {
|
|
3143
|
-
this.adapter
|
|
3247
|
+
const adapter = this.adapter as {
|
|
3248
|
+
resolveModalMatched?: (i: number) => boolean;
|
|
3249
|
+
resolveModal?: (i: number) => void;
|
|
3250
|
+
};
|
|
3251
|
+
if (typeof adapter.resolveModalMatched === 'function') {
|
|
3252
|
+
const matched = adapter.resolveModalMatched(buttonIndex);
|
|
3253
|
+
if (!matched) {
|
|
3254
|
+
// Click did not land on any button — undo the local-resolution stamp so the
|
|
3255
|
+
// next agent:waiting_approval is FORWARDED to the coordinator/inbox rather
|
|
3256
|
+
// than suppressed as "resolved locally".
|
|
3257
|
+
if (this.lastAutoApproveFiredAt === now) this.lastAutoApproveFiredAt = 0;
|
|
3258
|
+
LOG.warn('CLI', `[${this.type}] auto-approve resolveModal matched no button (index ${buttonIndex}) — surfacing approval to coordinator`);
|
|
3259
|
+
}
|
|
3260
|
+
} else {
|
|
3261
|
+
adapter.resolveModal?.(buttonIndex);
|
|
3262
|
+
}
|
|
3144
3263
|
}, 0);
|
|
3145
3264
|
return autoApproveActive;
|
|
3146
3265
|
}
|
|
@@ -204,8 +204,21 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
204
204
|
// modal this frame still stays waiting_approval (no activeModal yet).
|
|
205
205
|
// `kind` carries the semantic modal class through to the auto-approve
|
|
206
206
|
// gate so a /model picker (kind='picker') is never auto-answered.
|
|
207
|
+
// BUTTON-INDEX-MISMAP (Fix C.1): keep `buttons` as the label list every
|
|
208
|
+
// existing consumer (pickApprovalButton, mesh_approve, auto-approve) reads,
|
|
209
|
+
// but ALSO surface `buttonMeta` carrying each button's real FSM display index
|
|
210
|
+
// alongside its label. A partial/non-contiguous modal (display indices [1,3,4]
|
|
211
|
+
// at array positions [0,1,2]) then no longer loses the index → label mapping
|
|
212
|
+
// once it leaves the adapter: a consumer that has an array position can recover
|
|
213
|
+
// the true FSM index without re-parsing. resolveModal() below relies on the same
|
|
214
|
+
// ordered list to translate an array position to the correct FSM index.
|
|
207
215
|
activeModal: modal
|
|
208
|
-
? {
|
|
216
|
+
? {
|
|
217
|
+
message: modal.title ?? state.label,
|
|
218
|
+
buttons: modal.buttons.map(b => b.label),
|
|
219
|
+
buttonMeta: modal.buttons.map(b => ({ index: b.index, label: b.label })),
|
|
220
|
+
kind: modal.kind ?? null,
|
|
221
|
+
}
|
|
209
222
|
: null,
|
|
210
223
|
activeInteractivePrompt: this.activeInteractivePrompt,
|
|
211
224
|
...sessionFields,
|
|
@@ -420,8 +433,29 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
420
433
|
}
|
|
421
434
|
|
|
422
435
|
resolveModal(buttonIndex: number): void {
|
|
423
|
-
|
|
424
|
-
|
|
436
|
+
this.resolveModalMatched(buttonIndex);
|
|
437
|
+
}
|
|
438
|
+
|
|
439
|
+
resolveModalMatched(buttonIndex: number): boolean {
|
|
440
|
+
// BUTTON-INDEX-MISMAP (Fix C): `buttonIndex` is an ARRAY POSITION into the
|
|
441
|
+
// label list this adapter surfaced via getStatus().activeModal.buttons (the
|
|
442
|
+
// same order pickApprovalButton / mesh_approve pick from). The FSM matches a
|
|
443
|
+
// click by the button's DISPLAYED number (evaluator sets button.index =
|
|
444
|
+
// Number(m[1])), which is NOT `arrayPos + 1` for a partial / non-contiguous
|
|
445
|
+
// modal — e.g. a "1. Yes / 3. Always / 4. No" set parses to display indices
|
|
446
|
+
// [1,3,4] at array positions [0,1,2]. Blindly sending `arrayPos + 1` then
|
|
447
|
+
// targets a non-existent display index (2) and handleClickModalButton finds
|
|
448
|
+
// no button → nothing is pressed. Look up the real FSM display index from the
|
|
449
|
+
// same ordered button list instead, and fall back to the legacy +1 only when
|
|
450
|
+
// no modal is captured (defensive; the driver's own guard rejects a miss).
|
|
451
|
+
const buttons = this.latestModal?.buttons ?? [];
|
|
452
|
+
const target = (buttonIndex >= 0 && buttonIndex < buttons.length)
|
|
453
|
+
? buttons[buttonIndex].index
|
|
454
|
+
: buttonIndex + 1;
|
|
455
|
+
// clickModalButton returns whether the FSM actually found a button for `target`
|
|
456
|
+
// and dispatched its confirm keys — surfaced so mesh_approve can distinguish a
|
|
457
|
+
// real press from a silent miss (the exact false-success the mis-map produced).
|
|
458
|
+
return this.driver.clickModalButton(target);
|
|
425
459
|
}
|
|
426
460
|
|
|
427
461
|
async resolveAction(data: unknown): Promise<void> {
|
|
@@ -118,6 +118,13 @@ export interface ISpecDriver {
|
|
|
118
118
|
subscribe(listener: (ev: DashboardEvent) => void): () => void;
|
|
119
119
|
start(): void;
|
|
120
120
|
dispatch(cmd: DashboardCommand): void;
|
|
121
|
+
/**
|
|
122
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): click a modal button by its FSM display index and report
|
|
123
|
+
* whether a button was actually matched and its confirm keys dispatched. Unlike the
|
|
124
|
+
* fire-and-forget `dispatch('click_modal_button')`, callers that need to know the click
|
|
125
|
+
* landed (mesh_approve → SpecCliAdapter.resolveModalMatched) can observe a miss.
|
|
126
|
+
*/
|
|
127
|
+
clickModalButton(index: number): boolean;
|
|
121
128
|
updateMeta(meta: Record<string, unknown>, replace?: boolean): void;
|
|
122
129
|
snapshot(): string;
|
|
123
130
|
getCursorPosition(): { row: number; col: number };
|
|
@@ -428,7 +435,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
428
435
|
case 'send_message': this.handleSendMessage(cmd.text); return;
|
|
429
436
|
case 'pty_write': this.adapter.send_keys(cmd.data); return;
|
|
430
437
|
case 'click_control': this.handleClickControl(cmd.control_id, cmd.payload); return;
|
|
431
|
-
case 'click_modal_button': this.
|
|
438
|
+
case 'click_modal_button': this.clickModalButton(cmd.index); return;
|
|
432
439
|
case 'attach_image': this.handleAttachImage(cmd.blob, cmd.mime); return;
|
|
433
440
|
case 'resize': this.adapter.resize(cmd.cols, cmd.rows); return;
|
|
434
441
|
case 'cancel': this.adapter.send_keys('\x03'); return;
|
|
@@ -1318,11 +1325,25 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1318
1325
|
}
|
|
1319
1326
|
}
|
|
1320
1327
|
|
|
1321
|
-
|
|
1328
|
+
/**
|
|
1329
|
+
* BUTTON-INDEX-MISMAP (Fix C.3): public modal-click entry that returns whether a button
|
|
1330
|
+
* matching the requested FSM display index was actually found and its confirm keys were
|
|
1331
|
+
* dispatched. The old private handleClickModalButton silently `return`ed on a miss (no
|
|
1332
|
+
* modal captured, or no button whose `.index` equals the requested display index), so a
|
|
1333
|
+
* mis-mapped index looked identical to a successful press. Callers that need to know
|
|
1334
|
+
* whether the click landed (mesh_approve → resolveModal) can now observe the miss instead
|
|
1335
|
+
* of reporting success into the void. The generic `dispatch('click_modal_button')` path
|
|
1336
|
+
* keeps ignoring the return (fire-and-forget UI clicks).
|
|
1337
|
+
*/
|
|
1338
|
+
clickModalButton(index: number): boolean {
|
|
1339
|
+
return this.handleClickModalButton(index);
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
private handleClickModalButton(index: number): boolean {
|
|
1322
1343
|
const m = this.currentEval?.modal;
|
|
1323
|
-
if (!m) return;
|
|
1344
|
+
if (!m) return false;
|
|
1324
1345
|
const btn = m.buttons.find(b => b.index === index);
|
|
1325
|
-
if (!btn) return;
|
|
1346
|
+
if (!btn) return false;
|
|
1326
1347
|
|
|
1327
1348
|
const rule = stateById(this.spec, this.currentStateId)?.extract?.buttons;
|
|
1328
1349
|
if (rule?.select_mode === 'arrow_keys') {
|
|
@@ -1343,9 +1364,10 @@ export class FsmDriver implements ISpecDriver {
|
|
|
1343
1364
|
const confirm = (rule.key_for_index || '\r').replace(/\{index\}/g, '') || '\r';
|
|
1344
1365
|
if (nav) this.adapter.send_keys(nav);
|
|
1345
1366
|
this.submitModalConfirm(confirm);
|
|
1346
|
-
return;
|
|
1367
|
+
return true;
|
|
1347
1368
|
}
|
|
1348
1369
|
this.submitModalConfirm(btn.key);
|
|
1370
|
+
return true;
|
|
1349
1371
|
}
|
|
1350
1372
|
|
|
1351
1373
|
/**
|