@adhdev/daemon-core 0.9.82-rc.553 → 0.9.82-rc.554
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 +29 -0
- package/dist/index.js +341 -11
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +341 -11
- package/dist/index.mjs.map +1 -1
- package/dist/mesh/mesh-refine-gates.d.ts +86 -0
- package/dist/providers/cli-provider-instance.d.ts +16 -1
- package/dist/providers/spec/adapter.d.ts +7 -0
- package/dist/providers/spec/cli-adapter.d.ts +64 -0
- package/dist/providers/spec/fsm-driver.d.ts +11 -0
- package/package.json +3 -3
- package/src/cli-adapter-types.ts +29 -0
- package/src/commands/router-refine.ts +40 -1
- package/src/mesh/mesh-refine-gates.ts +256 -0
- package/src/providers/cli-provider-instance.ts +83 -8
- package/src/providers/spec/adapter.ts +7 -0
- package/src/providers/spec/cli-adapter.ts +122 -0
- package/src/providers/spec/fsm-driver.ts +5 -0
|
@@ -1527,6 +1527,26 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1527
1527
|
return '';
|
|
1528
1528
|
}
|
|
1529
1529
|
|
|
1530
|
+
/**
|
|
1531
|
+
* NOTIF Defect-B: the final assistant summary this instance ALREADY parsed and
|
|
1532
|
+
* cached for the current turn (lastCompletionSummary), if any. The evidence
|
|
1533
|
+
* probe (completionFinalAssistantEvidence) is a POINT-SAMPLE: on a native-source
|
|
1534
|
+
* provider (antigravity) the parsed screen and the native transcript can both
|
|
1535
|
+
* momentarily yield no in-turn final assistant at the exact instant the
|
|
1536
|
+
* completion gate fires — source='unavailable', missingEvidence=true — even
|
|
1537
|
+
* though a prior poll already read the real answer off native-history and cached
|
|
1538
|
+
* it here (the same value mesh_read_chat.summary shows). Consulting the cache at
|
|
1539
|
+
* emit time lets that already-secured summary count as evidence, so the completion
|
|
1540
|
+
* notification carries the answer instead of completion_diagnostic=missing_final_assistant
|
|
1541
|
+
* with an empty summary. Returns '' when the cache is empty or was reset by the
|
|
1542
|
+
* next turn (see lastCompletionSummary = null on onTurnStarted).
|
|
1543
|
+
*/
|
|
1544
|
+
private cachedCompletionSummaryContent(): string {
|
|
1545
|
+
const cached = this.lastCompletionSummary;
|
|
1546
|
+
const content = typeof cached?.content === 'string' ? cached.content.trim() : '';
|
|
1547
|
+
return content;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1530
1550
|
private completionFinalAssistantEvidence(parsedMessages: unknown, turnStartedAt?: number): CompletionFinalAssistantEvidence {
|
|
1531
1551
|
// (FALSEIDLE FixB) UPPER-BOUND turn-end evidence. completionHasFinalAssistantMessage is a
|
|
1532
1552
|
// pure message-content check ("does the last visible bubble read as a finalized assistant
|
|
@@ -1679,12 +1699,39 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1679
1699
|
const lastVisibleKind = typeof (lastVisible as any)?.kind === 'string' ? (lastVisible as any).kind : null;
|
|
1680
1700
|
const lastVisibleContentLength = lastVisible ? flattenContent(lastVisible.content).trim().length : 0;
|
|
1681
1701
|
|
|
1702
|
+
// NOTIF Defect-B: when the live evidence probe momentarily yields no in-turn
|
|
1703
|
+
// final assistant (source='unavailable'/external-native with present=false) but
|
|
1704
|
+
// a prior poll already parsed and CACHED the real answer for this turn
|
|
1705
|
+
// (lastCompletionSummary — the same value mesh_read_chat.summary surfaces),
|
|
1706
|
+
// credit the cache as evidence. This flips finalAssistantPresent to true and
|
|
1707
|
+
// records the cached source so the completion notification carries
|
|
1708
|
+
// completion_diagnostic=present with the summary, instead of
|
|
1709
|
+
// missing_final_assistant with an empty payload. Only ever UPGRADES a
|
|
1710
|
+
// point-sample miss — a genuine present=true is unchanged, and an empty cache
|
|
1711
|
+
// leaves the missing-evidence diagnostic exactly as before.
|
|
1712
|
+
const cachedSummary = evidence.present ? '' : this.cachedCompletionSummaryContent();
|
|
1713
|
+
const creditedFromCache = !evidence.present && cachedSummary.length > 0;
|
|
1714
|
+
const finalAssistantPresent = evidence.present || creditedFromCache;
|
|
1715
|
+
const finalAssistantEvidenceSource = evidence.present
|
|
1716
|
+
? evidence.source
|
|
1717
|
+
: (creditedFromCache ? 'cached-summary' : evidence.source);
|
|
1718
|
+
// When the cached summary rescues the evidence, the turn is no longer
|
|
1719
|
+
// "missing final assistant" — clear that blockReason so isMissingFinalAssistant‑
|
|
1720
|
+
// Diagnostic()/isWeakCompletionEvidence() no longer flag it (both key off
|
|
1721
|
+
// blockReason='missing_final_assistant' independently of finalAssistantPresent)
|
|
1722
|
+
// and the coordinator log's formatCompletionMetadata reads
|
|
1723
|
+
// completion_diagnostic=present (empty blockReason → 'present'). The ORIGINAL
|
|
1724
|
+
// reason is preserved under originalBlockReason for diagnostics.
|
|
1725
|
+
const clearMissingBlock = creditedFromCache && args.blockReason === 'missing_final_assistant';
|
|
1726
|
+
const effectiveBlockReason = clearMissingBlock ? undefined : args.blockReason;
|
|
1727
|
+
|
|
1682
1728
|
return {
|
|
1683
1729
|
providerType: this.type,
|
|
1684
1730
|
sessionId: this.instanceId,
|
|
1685
1731
|
providerSessionId: this.providerSessionId || null,
|
|
1686
1732
|
workspace: this.workingDir,
|
|
1687
|
-
blockReason:
|
|
1733
|
+
...(effectiveBlockReason ? { blockReason: effectiveBlockReason } : {}),
|
|
1734
|
+
...(clearMissingBlock ? { originalBlockReason: args.blockReason } : {}),
|
|
1688
1735
|
emittedAfterFinalizationTimeout: args.emittedAfterFinalizationTimeout,
|
|
1689
1736
|
waitedMs: args.waitedMs,
|
|
1690
1737
|
maxWaitMs: COMPLETED_FINALIZATION_MAX_WAIT_MS,
|
|
@@ -1692,8 +1739,9 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
1692
1739
|
latestVisibleStatus: args.latestVisibleStatus,
|
|
1693
1740
|
parsedStatus: typeof parsed?.status === 'string' ? parsed.status : (parseError ? 'parse_error' : 'unknown'),
|
|
1694
1741
|
parseError: parseError || undefined,
|
|
1695
|
-
finalAssistantPresent
|
|
1696
|
-
|
|
1742
|
+
finalAssistantPresent,
|
|
1743
|
+
finalAssistantFromCachedSummary: !evidence.present && cachedSummary.length > 0,
|
|
1744
|
+
finalAssistantEvidenceSource,
|
|
1697
1745
|
visibleMessageCount: visibleMessages.length,
|
|
1698
1746
|
lastVisibleRole,
|
|
1699
1747
|
lastVisibleKind,
|
|
@@ -2012,6 +2060,13 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2012
2060
|
hash: string;
|
|
2013
2061
|
} | null {
|
|
2014
2062
|
if (!this.isMeshWorkerSession()) return null;
|
|
2063
|
+
// Defensive: not every CliAdapter implementation exposes the raw-terminal
|
|
2064
|
+
// read (the surface is declared optional on CliAdapter). Returning null
|
|
2065
|
+
// for an adapter that lacks it surfaces a clean unsupported refusal at
|
|
2066
|
+
// the daemon command instead of "getTerminalScreenSnapshot is not a
|
|
2067
|
+
// function" — the failure mode that broke mesh_read_terminal on the
|
|
2068
|
+
// spec-driven path before SpecCliAdapter implemented it.
|
|
2069
|
+
if (typeof this.adapter.getTerminalScreenSnapshot !== 'function') return null;
|
|
2015
2070
|
return this.adapter.getTerminalScreenSnapshot(maxBytes);
|
|
2016
2071
|
}
|
|
2017
2072
|
|
|
@@ -2032,11 +2087,18 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2032
2087
|
opts: { allowModalOverride?: boolean } = {},
|
|
2033
2088
|
): Promise<
|
|
2034
2089
|
| { ok: true; keys: MeshSendKeyName[]; hasDestructive: boolean; submits: boolean; bytes: number }
|
|
2035
|
-
| { ok: false; refused: 'submit_race' | 'actionable_modal' | 'not_mesh_worker'; keys: MeshSendKeyName[]; hasDestructive: boolean }
|
|
2090
|
+
| { ok: false; refused: 'submit_race' | 'actionable_modal' | 'not_mesh_worker' | 'unsupported'; keys: MeshSendKeyName[]; hasDestructive: boolean }
|
|
2036
2091
|
> {
|
|
2037
2092
|
if (!this.isMeshWorkerSession()) {
|
|
2038
2093
|
return { ok: false, refused: 'not_mesh_worker', keys: [], hasDestructive: false };
|
|
2039
2094
|
}
|
|
2095
|
+
// Defensive: injectKeys is optional on CliAdapter. An adapter without it
|
|
2096
|
+
// yields a clean 'unsupported' refusal instead of throwing
|
|
2097
|
+
// "injectKeys is not a function" — the failure that broke mesh_send_keys
|
|
2098
|
+
// on the spec-driven path before SpecCliAdapter implemented it.
|
|
2099
|
+
if (typeof this.adapter.injectKeys !== 'function') {
|
|
2100
|
+
return { ok: false, refused: 'unsupported', keys: [], hasDestructive: false };
|
|
2101
|
+
}
|
|
2040
2102
|
return this.adapter.injectKeys(items, opts);
|
|
2041
2103
|
}
|
|
2042
2104
|
|
|
@@ -2072,7 +2134,14 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2072
2134
|
this.meshStallEmittedForAnchor = false;
|
|
2073
2135
|
return;
|
|
2074
2136
|
}
|
|
2075
|
-
|
|
2137
|
+
// Defensive: not every CliAdapter implementation exposes isAlive() — the
|
|
2138
|
+
// spec-driven adapter (SpecCliAdapter, used by native-source providers like
|
|
2139
|
+
// antigravity-cli) historically had none, so an unguarded call threw
|
|
2140
|
+
// `this.adapter.isAlive is not a function` on EVERY 5s tick, disabling stall
|
|
2141
|
+
// detection for those sessions entirely. A missing method is treated as alive
|
|
2142
|
+
// (the session lifecycle drops the anchor via isMeshWorkerSession()/exit paths),
|
|
2143
|
+
// never as a throw. When present, a dead process drops the armed episode.
|
|
2144
|
+
if (typeof this.adapter.isAlive === 'function' && !this.adapter.isAlive()) {
|
|
2076
2145
|
// Dead PTY: nothing to watch. agent:stopped covers the exit; re-arm on
|
|
2077
2146
|
// the next live session so a restart starts a fresh episode.
|
|
2078
2147
|
this.meshStallAnchorAt = -1;
|
|
@@ -2541,9 +2610,15 @@ export class CliProviderInstance implements ProviderInstance {
|
|
|
2541
2610
|
// delegated session's inbox preview blank — or, for a LOCAL worktree session,
|
|
2542
2611
|
// stuck on the dispatched user task. If the parser DID surface assistant text,
|
|
2543
2612
|
// prefer it; only fall back to '' when no assistant summary can be derived.
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2613
|
+
// NOTIF Defect-B: completionFinalSummary is a point-sample of native-history/
|
|
2614
|
+
// screen at THIS instant; on a native-source provider (antigravity) it can be
|
|
2615
|
+
// empty at the forced-emit instant even though a prior poll already cached the
|
|
2616
|
+
// real answer (lastCompletionSummary). Fall back to the cache so the notification
|
|
2617
|
+
// carries the summary that mesh_read_chat.summary already shows — consistent with
|
|
2618
|
+
// completionDiagnostic.finalAssistantPresent being credited from the same cache.
|
|
2619
|
+
finalSummary: (this.completionFinalSummary(this.adapter?.getScriptParsedStatus()?.messages, pending.turnStartedAt)
|
|
2620
|
+
|| this.cachedCompletionSummaryContent()
|
|
2621
|
+
|| (blockReason.startsWith('parsed_status:') ? '' : undefined)),
|
|
2547
2622
|
completionDiagnostic,
|
|
2548
2623
|
});
|
|
2549
2624
|
this.completedDebouncePending = null;
|
|
@@ -175,6 +175,13 @@ export class TerminalAdapter {
|
|
|
175
175
|
return { row: pos.row, col: pos.col };
|
|
176
176
|
}
|
|
177
177
|
|
|
178
|
+
/** Current terminal geometry (columns × rows). Tracked here rather than
|
|
179
|
+
* read off the screen buffer so a resize is reflected immediately, before
|
|
180
|
+
* the next repaint. Consumed by the mesh_read_terminal viewport read. */
|
|
181
|
+
getScreenSize(): { cols: number; rows: number } {
|
|
182
|
+
return { cols: this.cols, rows: this.rows };
|
|
183
|
+
}
|
|
184
|
+
|
|
178
185
|
send_keys(text: string): void {
|
|
179
186
|
this.recordEvent('input', capPreview(escapeControl(text)), text.length);
|
|
180
187
|
this.pty?.write(text);
|
|
@@ -23,10 +23,17 @@ import { lastContiguousNumberedBlock } from './evaluator.js';
|
|
|
23
23
|
import { executeNativeHistory } from './native-history-executor.js';
|
|
24
24
|
import { detectBackgroundTaskActive } from './background-task-detector.js';
|
|
25
25
|
import * as fs from 'node:fs';
|
|
26
|
+
import { createHash } from 'node:crypto';
|
|
26
27
|
import type { NativeHistoryConfig, Control, ControlAction } from './types.js';
|
|
27
28
|
import type { CliAdapter, CliAdapterStatus } from '../../cli-adapter-types.js';
|
|
28
29
|
import type { ChatMessage } from '../../types.js';
|
|
29
30
|
import type { PtyTransportFactory } from '../../cli-adapters/pty-transport.js';
|
|
31
|
+
import {
|
|
32
|
+
encodeMeshSendKeys,
|
|
33
|
+
truncateToByteTailByLine,
|
|
34
|
+
type MeshSendKeyItem,
|
|
35
|
+
type MeshSendKeyName,
|
|
36
|
+
} from '../../cli-adapters/provider-cli-shared.js';
|
|
30
37
|
import { LOG } from '../../logging/logger.js';
|
|
31
38
|
import {
|
|
32
39
|
buildClaudeInteractiveTuiAnswerSteps,
|
|
@@ -277,6 +284,121 @@ export class SpecCliAdapter implements CliAdapter {
|
|
|
277
284
|
return this.spawned && !this.exited;
|
|
278
285
|
}
|
|
279
286
|
|
|
287
|
+
// Process liveness for the MESH-STALL-WATCH watchdog (checkMeshWorkerStall).
|
|
288
|
+
// The spec path drives the child through the transport/driver rather than a
|
|
289
|
+
// directly-held ptyProcess handle, so liveness is tracked by the spawned/exited
|
|
290
|
+
// lifecycle flags — the same pair isReady() uses. A spawned, not-yet-exited
|
|
291
|
+
// session is alive. ProviderCliAdapter exposes the equivalent via `ptyProcess !== null`.
|
|
292
|
+
isAlive(): boolean {
|
|
293
|
+
return this.spawned && !this.exited;
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
// MESH-READ-TERMINAL / MESH-SEND-KEYS byte caps — same envelope as
|
|
297
|
+
// ProviderCliAdapter (32KiB default view, 64KiB absolute hard cap). Bytes,
|
|
298
|
+
// not chars: a multi-byte-glyph screen can exceed an MCP payload cap while
|
|
299
|
+
// the char count still looks safe.
|
|
300
|
+
private static readonly TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES = 32 * 1024;
|
|
301
|
+
private static readonly TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES = 64 * 1024;
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* MESH-READ-TERMINAL (feature 2: RAW terminal read). Least-privilege read
|
|
305
|
+
* of the CURRENT rendered viewport for mesh_read_terminal on the spec path
|
|
306
|
+
* (claude-cli / antigravity / codex-cli — the native-source providers that
|
|
307
|
+
* route through SpecCliAdapter). Mirrors ProviderCliAdapter.getTerminalScreenSnapshot:
|
|
308
|
+
* - returns ONLY the driver's current viewport snapshot, the cursor
|
|
309
|
+
* position and the terminal geometry — NO scrollback, NO parser/FSM
|
|
310
|
+
* state, NO debug buffers;
|
|
311
|
+
* - the payload is byte-bounded (UTF-8) with bottom-tail preservation so a
|
|
312
|
+
* screen of multi-byte glyphs can never exceed the MCP payload cap;
|
|
313
|
+
* - `hash` is over the FULL untruncated viewport so a caller can detect a
|
|
314
|
+
* screen change across polls even when the returned text was truncated.
|
|
315
|
+
*
|
|
316
|
+
* SECURITY: the raw viewport can carry tokens / command args / env / user
|
|
317
|
+
* data. Callers MUST gate this on mesh ownership and MUST NOT log the text.
|
|
318
|
+
*/
|
|
319
|
+
getTerminalScreenSnapshot(maxBytes = SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES): {
|
|
320
|
+
text: string;
|
|
321
|
+
cursor: { col: number; row: number };
|
|
322
|
+
cols: number;
|
|
323
|
+
rows: number;
|
|
324
|
+
truncated: boolean;
|
|
325
|
+
originalBytes: number;
|
|
326
|
+
returnedBytes: number;
|
|
327
|
+
hash: string;
|
|
328
|
+
} {
|
|
329
|
+
const cap = Math.min(
|
|
330
|
+
SpecCliAdapter.TERMINAL_SNAPSHOT_ABSOLUTE_MAX_BYTES,
|
|
331
|
+
Math.max(1024, Math.floor(maxBytes) || SpecCliAdapter.TERMINAL_SNAPSHOT_DEFAULT_MAX_BYTES),
|
|
332
|
+
);
|
|
333
|
+
let rawViewport = '';
|
|
334
|
+
try { rawViewport = this.driver.snapshot() || ''; } catch { rawViewport = ''; }
|
|
335
|
+
let cursor = { row: 0, col: 0 };
|
|
336
|
+
try { cursor = this.driver.getCursorPosition(); } catch { /* keep 0,0 */ }
|
|
337
|
+
// getScreenSize is optional on ISpecDriver; a test double may omit it.
|
|
338
|
+
let size = { cols: 0, rows: 0 };
|
|
339
|
+
try { size = this.driver.getScreenSize?.() ?? size; } catch { /* keep 0,0 */ }
|
|
340
|
+
const truncation = truncateToByteTailByLine(rawViewport, cap);
|
|
341
|
+
const hash = createHash('sha256').update(rawViewport, 'utf8').digest('hex').slice(0, 16);
|
|
342
|
+
return {
|
|
343
|
+
text: truncation.text,
|
|
344
|
+
cursor: { col: cursor.col, row: cursor.row },
|
|
345
|
+
cols: size.cols,
|
|
346
|
+
rows: size.rows,
|
|
347
|
+
truncated: truncation.truncated,
|
|
348
|
+
originalBytes: truncation.originalBytes,
|
|
349
|
+
returnedBytes: truncation.returnedBytes,
|
|
350
|
+
hash,
|
|
351
|
+
};
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/**
|
|
355
|
+
* MESH-SEND-KEYS (feature 3: key injection). Inject a STRUCTURED key
|
|
356
|
+
* sequence into the spec-driven PTY for mesh_send_keys. Mirrors
|
|
357
|
+
* ProviderCliAdapter.injectKeys' modal fail-closed guard, then writes the
|
|
358
|
+
* whole encoded sequence in ONE pty_write dispatch (text+ENTER is a single
|
|
359
|
+
* contiguous string, so a submit key can never be separated from the text
|
|
360
|
+
* it submits).
|
|
361
|
+
*
|
|
362
|
+
* The spec path drives the child through the FsmDriver, not a directly-held
|
|
363
|
+
* ptyProcess — there is no adapter-level echo-gate/submit-retry FIFO to race
|
|
364
|
+
* against here (the driver serializes its own writes), so the only guard is
|
|
365
|
+
* the modal fail-closed: a NON-destructive injection into an actionable
|
|
366
|
+
* approval modal is refused (use mesh_approve) unless explicitly overridden.
|
|
367
|
+
* A destructive ESC/CTRL_C dismisses rather than confirms, so it is allowed
|
|
368
|
+
* past this gate (the tool layer owns the destructive double-gate + audit).
|
|
369
|
+
* This method NEVER logs the literal text — only key enums / byte length.
|
|
370
|
+
*/
|
|
371
|
+
async injectKeys(
|
|
372
|
+
items: MeshSendKeyItem[],
|
|
373
|
+
opts: { allowModalOverride?: boolean } = {},
|
|
374
|
+
): Promise<
|
|
375
|
+
| { ok: true; keys: MeshSendKeyName[]; hasDestructive: boolean; submits: boolean; bytes: number }
|
|
376
|
+
| { ok: false; refused: 'submit_race' | 'actionable_modal'; keys: MeshSendKeyName[]; hasDestructive: boolean }
|
|
377
|
+
> {
|
|
378
|
+
if (!this.spawned || this.exited) throw new Error(`${this.cliName} is not running`);
|
|
379
|
+
const encoded = encodeMeshSendKeys(items);
|
|
380
|
+
|
|
381
|
+
// Modal fail-closed — a NON-destructive injection while parked on an
|
|
382
|
+
// actionable approval modal is refused so a modal choice can't be
|
|
383
|
+
// confirmed via send_keys and bypass the approval policy.
|
|
384
|
+
const modalActive = this.latestState?.status === 'approval';
|
|
385
|
+
if (modalActive && !encoded.hasDestructive && !opts.allowModalOverride) {
|
|
386
|
+
LOG.warn('SpecAdapter', `[${this.cliType}] send_keys refused (actionable_modal): keys=${encoded.keys.join(',')} — use mesh_approve`);
|
|
387
|
+
return { ok: false, refused: 'actionable_modal', keys: encoded.keys, hasDestructive: encoded.hasDestructive };
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// Atomic write: the full encoded sequence goes out in ONE pty_write.
|
|
391
|
+
this.driver.dispatch({ kind: 'pty_write', data: encoded.sequence });
|
|
392
|
+
LOG.info('SpecAdapter', `[${this.cliType}] send_keys injected keys=${encoded.keys.join(',') || '(text-only)'} bytes=${Buffer.byteLength(encoded.sequence, 'utf8')} destructive=${encoded.hasDestructive}`);
|
|
393
|
+
return {
|
|
394
|
+
ok: true,
|
|
395
|
+
keys: encoded.keys,
|
|
396
|
+
hasDestructive: encoded.hasDestructive,
|
|
397
|
+
submits: encoded.submits,
|
|
398
|
+
bytes: Buffer.byteLength(encoded.sequence, 'utf8'),
|
|
399
|
+
};
|
|
400
|
+
}
|
|
401
|
+
|
|
280
402
|
setOnStatusChange(cb: () => void): void {
|
|
281
403
|
this.statusCallback = cb;
|
|
282
404
|
}
|
|
@@ -122,6 +122,10 @@ export interface ISpecDriver {
|
|
|
122
122
|
snapshot(): string;
|
|
123
123
|
getCursorPosition(): { row: number; col: number };
|
|
124
124
|
getScreen(): string;
|
|
125
|
+
/** Current terminal geometry (columns × rows). Optional so a non-Fsm
|
|
126
|
+
* ISpecDriver implementation (test doubles) need not provide it; the
|
|
127
|
+
* mesh_read_terminal path falls back to a 0×0 geometry when absent. */
|
|
128
|
+
getScreenSize?(): { cols: number; rows: number };
|
|
125
129
|
getSpecPath(): string;
|
|
126
130
|
shutdown(): void;
|
|
127
131
|
getStateHistory(): ReadonlyArray<DriverHistoryEntry>;
|
|
@@ -443,6 +447,7 @@ export class FsmDriver implements ISpecDriver {
|
|
|
443
447
|
snapshot(): string { return this.adapter.snapshot(); }
|
|
444
448
|
getCursorPosition(): { row: number; col: number } { return this.adapter.getCursorPosition(); }
|
|
445
449
|
getScreen(): string { return this.adapter.snapshot(); }
|
|
450
|
+
getScreenSize(): { cols: number; rows: number } { return this.adapter.getScreenSize(); }
|
|
446
451
|
|
|
447
452
|
/** Scrollback-inclusive screen as line array — used only for modal/button
|
|
448
453
|
* content extraction so a tall prompt's off-screen anchors stay matchable.
|