@huseynsnmz/pi-suite 0.0.0-stage → 0.2.0
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/CHANGELOG.md +28 -0
- package/LICENSE +21 -0
- package/README.md +88 -2
- package/SECURITY.md +5 -0
- package/package.json +112 -4
- package/packages/board/CHANGELOG.md +340 -0
- package/packages/board/CONTRIBUTING.md +7 -0
- package/packages/board/LICENSE +21 -0
- package/packages/board/README.md +88 -0
- package/packages/board/SECURITY.md +19 -0
- package/packages/board/docs/how-it-works.md +31 -0
- package/packages/board/docs/reference.md +278 -0
- package/packages/board/package.json +81 -0
- package/packages/board/runner/job-runner-legacy.mjs +68 -0
- package/packages/board/runner/job-runner.mjs +834 -0
- package/packages/board/runner/pty-runner-legacy.mjs +50 -0
- package/packages/board/runner/pty-runner.mjs +2175 -0
- package/packages/board/runner/state-coordinator.mjs +673 -0
- package/packages/board/runner/state-runner.mjs +203 -0
- package/packages/board/runner/title-runner.mjs +80 -0
- package/packages/board/src/commands/agent-board.ts +394 -0
- package/packages/board/src/commands/attach-decision.mjs +66 -0
- package/packages/board/src/commands/attach-flow.ts +251 -0
- package/packages/board/src/commands/bg.ts +103 -0
- package/packages/board/src/commands/board-model.ts +21 -0
- package/packages/board/src/core/atomic.mjs +184 -0
- package/packages/board/src/core/attach-delete.mjs +24 -0
- package/packages/board/src/core/attach-frame-proof.mjs +870 -0
- package/packages/board/src/core/attach-frame-reporter.mjs +648 -0
- package/packages/board/src/core/attach-frame-stream.mjs +266 -0
- package/packages/board/src/core/attach-view-proof.mjs +709 -0
- package/packages/board/src/core/auto-state.mjs +377 -0
- package/packages/board/src/core/background-runs.mjs +273 -0
- package/packages/board/src/core/code-refs-store.mjs +315 -0
- package/packages/board/src/core/code-refs.mjs +913 -0
- package/packages/board/src/core/config.mjs +77 -0
- package/packages/board/src/core/control-protocol.mjs +482 -0
- package/packages/board/src/core/coordinator-client.mjs +633 -0
- package/packages/board/src/core/coordinator-journal.mjs +282 -0
- package/packages/board/src/core/coordinator-protocol.mjs +14 -0
- package/packages/board/src/core/cwd-stats.mjs +114 -0
- package/packages/board/src/core/dashboard-render.mjs +10 -0
- package/packages/board/src/core/derive.mjs +117 -0
- package/packages/board/src/core/diagnostics.mjs +133 -0
- package/packages/board/src/core/double-press.mjs +26 -0
- package/packages/board/src/core/editor-state-reporter.mjs +112 -0
- package/packages/board/src/core/events.mjs +286 -0
- package/packages/board/src/core/evidence.mjs +245 -0
- package/packages/board/src/core/follow-up-queue.mjs +211 -0
- package/packages/board/src/core/foreground-preview-cache.mjs +117 -0
- package/packages/board/src/core/foreground-projection-cache.mjs +99 -0
- package/packages/board/src/core/frame-witness-update.mjs +35 -0
- package/packages/board/src/core/heuristics.mjs +313 -0
- package/packages/board/src/core/hold-input.mjs +27 -0
- package/packages/board/src/core/host-coordination.mjs +300 -0
- package/packages/board/src/core/host-crash.mjs +79 -0
- package/packages/board/src/core/host-probe.mjs +207 -0
- package/packages/board/src/core/host-protocol.mjs +73 -0
- package/packages/board/src/core/ids.mjs +35 -0
- package/packages/board/src/core/ime-cursor-coalesce.mjs +193 -0
- package/packages/board/src/core/invocation.mjs +43 -0
- package/packages/board/src/core/launch-options.mjs +418 -0
- package/packages/board/src/core/launch.mjs +157 -0
- package/packages/board/src/core/load-config.mjs +4 -0
- package/packages/board/src/core/locks.mjs +530 -0
- package/packages/board/src/core/paths.mjs +183 -0
- package/packages/board/src/core/pid.mjs +75 -0
- package/packages/board/src/core/prewarm-schedule.mjs +41 -0
- package/packages/board/src/core/process-identity-probe.mjs +228 -0
- package/packages/board/src/core/prompt-transport.mjs +13 -0
- package/packages/board/src/core/pty-attach-jiggle-controller.mjs +356 -0
- package/packages/board/src/core/pty-attach-jiggle-retry.mjs +109 -0
- package/packages/board/src/core/pty-attach-reconnect.mjs +61 -0
- package/packages/board/src/core/pty-attach-render.mjs +229 -0
- package/packages/board/src/core/pty-links.mjs +81 -0
- package/packages/board/src/core/pty-scroll.mjs +522 -0
- package/packages/board/src/core/pty-support.mjs +327 -0
- package/packages/board/src/core/repo.mjs +103 -0
- package/packages/board/src/core/rows.mjs +414 -0
- package/packages/board/src/core/screen-log-gc.mjs +198 -0
- package/packages/board/src/core/screen-log.mjs +160 -0
- package/packages/board/src/core/session-view.mjs +174 -0
- package/packages/board/src/core/slash-commands.mjs +52 -0
- package/packages/board/src/core/state-commands.mjs +757 -0
- package/packages/board/src/core/status-consistency.mjs +98 -0
- package/packages/board/src/core/steering-prompts.mjs +34 -0
- package/packages/board/src/core/steering.mjs +133 -0
- package/packages/board/src/core/stop-latch.mjs +41 -0
- package/packages/board/src/core/store.mjs +523 -0
- package/packages/board/src/core/suspend.mjs +36 -0
- package/packages/board/src/core/terminal-attach-client.mjs +981 -0
- package/packages/board/src/core/terminal-attach-protocol.mjs +460 -0
- package/packages/board/src/core/terminal-model.mjs +222 -0
- package/packages/board/src/core/terminal-query-sequences.mjs +239 -0
- package/packages/board/src/core/terminal-reply-guard.mjs +222 -0
- package/packages/board/src/core/terminal-snapshot.mjs +440 -0
- package/packages/board/src/core/test-knobs.mjs +28 -0
- package/packages/board/src/core/title.mjs +43 -0
- package/packages/board/src/core/types.mjs +414 -0
- package/packages/board/src/core/warm-host-sweeper.mjs +168 -0
- package/packages/board/src/core/worktree.mjs +65 -0
- package/packages/board/src/index.ts +303 -0
- package/packages/board/src/runtime/service.mjs +3222 -0
- package/packages/board/src/ui/chrome.ts +20 -0
- package/packages/board/src/ui/dashboard-decisions.mjs +55 -0
- package/packages/board/src/ui/dashboard-evidence.mjs +85 -0
- package/packages/board/src/ui/dashboard.ts +2237 -0
- package/packages/board/src/ui/double-left.ts +30 -0
- package/packages/board/src/ui/external-editor.ts +25 -0
- package/packages/board/src/ui/focus.ts +32 -0
- package/packages/board/src/ui/pty-attach.ts +3184 -0
- package/packages/board/src/ui/waiting.ts +30 -0
- package/packages/commands/LICENSE +21 -0
- package/packages/commands/README.md +19 -0
- package/packages/commands/package.json +22 -0
- package/packages/commands/src/index.ts +43 -0
- package/packages/commands/src/security-review.ts +109 -0
- package/packages/frame/CHANGELOG.md +113 -0
- package/packages/frame/CONTRIBUTING.md +7 -0
- package/packages/frame/LICENSE +22 -0
- package/packages/frame/README.md +105 -0
- package/packages/frame/SECURITY.md +16 -0
- package/packages/frame/docs/how-it-works.md +53 -0
- package/packages/frame/docs/reference.md +78 -0
- package/packages/frame/package.json +72 -0
- package/packages/frame/src/data/git.ts +90 -0
- package/packages/frame/src/data/github.ts +140 -0
- package/packages/frame/src/data/gitlab.ts +63 -0
- package/packages/frame/src/data/mode.ts +49 -0
- package/packages/frame/src/data/past-prompts.ts +57 -0
- package/packages/frame/src/data/runner.ts +32 -0
- package/packages/frame/src/data/snapshot.ts +223 -0
- package/packages/frame/src/data/start-notes.ts +104 -0
- package/packages/frame/src/data/telemetry.ts +337 -0
- package/packages/frame/src/data/todos.ts +162 -0
- package/packages/frame/src/data/totals.ts +94 -0
- package/packages/frame/src/data/visits.ts +45 -0
- package/packages/frame/src/index.ts +696 -0
- package/packages/frame/src/kit/LICENSE +21 -0
- package/packages/frame/src/kit/app/attribution.ts +30 -0
- package/packages/frame/src/kit/app/builder.ts +186 -0
- package/packages/frame/src/kit/app/claim.ts +27 -0
- package/packages/frame/src/kit/app/feature.ts +91 -0
- package/packages/frame/src/kit/app/host.ts +214 -0
- package/packages/frame/src/kit/app/renderers.ts +62 -0
- package/packages/frame/src/kit/app/scope.ts +125 -0
- package/packages/frame/src/kit/board/model.ts +337 -0
- package/packages/frame/src/kit/contract.ts +79 -0
- package/packages/frame/src/kit/contracts/accounts.ts +33 -0
- package/packages/frame/src/kit/contracts/ask.ts +120 -0
- package/packages/frame/src/kit/contracts/board.ts +6 -0
- package/packages/frame/src/kit/contracts/by.ts +22 -0
- package/packages/frame/src/kit/contracts/calls.ts +46 -0
- package/packages/frame/src/kit/contracts/events.ts +89 -0
- package/packages/frame/src/kit/contracts/mode.ts +26 -0
- package/packages/frame/src/kit/contracts/names.ts +12 -0
- package/packages/frame/src/kit/contracts/peers.ts +77 -0
- package/packages/frame/src/kit/contracts/prompts.ts +8 -0
- package/packages/frame/src/kit/contracts/screen.ts +75 -0
- package/packages/frame/src/kit/contracts/skills.ts +81 -0
- package/packages/frame/src/kit/contracts/sources.ts +81 -0
- package/packages/frame/src/kit/contracts/views.ts +42 -0
- package/packages/frame/src/kit/control.ts +59 -0
- package/packages/frame/src/kit/dark-theme.ts +41 -0
- package/packages/frame/src/kit/decode.ts +210 -0
- package/packages/frame/src/kit/events/log.ts +313 -0
- package/packages/frame/src/kit/format.ts +25 -0
- package/packages/frame/src/kit/host.ts +4 -0
- package/packages/frame/src/kit/index.ts +25 -0
- package/packages/frame/src/kit/legacy-tasks.ts +39 -0
- package/packages/frame/src/kit/overlay-images.ts +36 -0
- package/packages/frame/src/kit/paths.ts +16 -0
- package/packages/frame/src/kit/screen/client.ts +74 -0
- package/packages/frame/src/kit/screen/feature.ts +64 -0
- package/packages/frame/src/kit/screen/model.ts +182 -0
- package/packages/frame/src/kit/screen/view.ts +838 -0
- package/packages/frame/src/kit/settings/files.ts +90 -0
- package/packages/frame/src/kit/settings/setting.ts +57 -0
- package/packages/frame/src/kit/settings/store.ts +192 -0
- package/packages/frame/src/kit/sources/file.ts +129 -0
- package/packages/frame/src/kit/sources/follow.ts +155 -0
- package/packages/frame/src/kit/testing.ts +260 -0
- package/packages/frame/src/kit/totals.ts +72 -0
- package/packages/frame/src/kit/ui/entry.ts +75 -0
- package/packages/frame/src/kit/ui/frame.ts +212 -0
- package/packages/frame/src/kit/ui/glyphs.ts +132 -0
- package/packages/frame/src/kit/ui/scroll.ts +14 -0
- package/packages/frame/src/render/diff.ts +157 -0
- package/packages/frame/src/render/fit.ts +86 -0
- package/packages/frame/src/render/format.ts +108 -0
- package/packages/frame/src/render/frame.ts +106 -0
- package/packages/frame/src/render/glyphs.ts +189 -0
- package/packages/frame/src/render/keys.ts +43 -0
- package/packages/frame/src/render/lead.ts +37 -0
- package/packages/frame/src/render/paint.ts +135 -0
- package/packages/frame/src/render/safe.ts +19 -0
- package/packages/frame/src/render/segments.ts +735 -0
- package/packages/frame/src/render/streaming.ts +223 -0
- package/packages/frame/src/render/timing.ts +106 -0
- package/packages/frame/src/render/tools.ts +167 -0
- package/packages/frame/src/settings.ts +105 -0
- package/packages/frame/src/ui/editor.ts +272 -0
- package/packages/frame/src/ui/effort.ts +36 -0
- package/packages/frame/src/ui/footer.ts +165 -0
- package/packages/frame/src/ui/header.ts +362 -0
- package/packages/frame/src/ui/history-search.ts +23 -0
- package/packages/frame/src/ui/interrupt.ts +21 -0
- package/packages/frame/src/ui/outputs.ts +170 -0
- package/packages/frame/src/ui/pi-entries.ts +298 -0
- package/packages/frame/src/ui/previews.ts +72 -0
- package/packages/frame/src/ui/queue.ts +75 -0
- package/packages/frame/src/ui/screen.ts +59 -0
- package/packages/frame/src/ui/timings.ts +48 -0
- package/packages/frame/src/ui/todos.ts +205 -0
- package/packages/frame/src/ui/tool-frame.ts +595 -0
- package/packages/frame/src/ui/working.ts +25 -0
- package/packages/frame/src/view/state.ts +97 -0
- package/packages/frame/src/view/transcript.ts +175 -0
- package/packages/frame/src/view/view.ts +162 -0
- package/packages/memory/CONTRIBUTING.md +7 -0
- package/packages/memory/LICENSE +21 -0
- package/packages/memory/README.md +44 -0
- package/packages/memory/SECURITY.md +20 -0
- package/packages/memory/package.json +55 -0
- package/packages/memory/skills/review-memory/SKILL.md +48 -0
- package/packages/memory/src/index.ts +163 -0
- package/packages/memory/src/prompt.ts +25 -0
- package/packages/memory/src/review.ts +150 -0
- package/packages/memory/src/secrets.ts +22 -0
- package/packages/memory/src/store.ts +173 -0
- package/packages/modes/CHANGELOG.md +104 -0
- package/packages/modes/CONTRIBUTING.md +7 -0
- package/packages/modes/LICENSE +21 -0
- package/packages/modes/README.md +174 -0
- package/packages/modes/SECURITY.md +17 -0
- package/packages/modes/package.json +55 -0
- package/packages/modes/skills/review-permissions/SKILL.md +54 -0
- package/packages/modes/src/bash.ts +218 -0
- package/packages/modes/src/deny-off.ts +62 -0
- package/packages/modes/src/git-safety.ts +72 -0
- package/packages/modes/src/index.ts +338 -0
- package/packages/modes/src/interview.ts +206 -0
- package/packages/modes/src/judge-gate.ts +113 -0
- package/packages/modes/src/modes.ts +121 -0
- package/packages/modes/src/review/apply.mjs +156 -0
- package/packages/modes/src/review/collect.mjs +208 -0
- package/packages/modes/src/review/tools.ts +71 -0
- package/packages/modes/src/sandbox.ts +37 -0
- package/packages/router/CHANGELOG.md +8 -0
- package/packages/router/LICENSE +21 -0
- package/packages/router/README.md +90 -0
- package/packages/router/package.json +51 -0
- package/packages/router/src/config.ts +65 -0
- package/packages/router/src/index.ts +172 -0
- package/packages/router/src/judge.ts +51 -0
- package/packages/router/src/route.ts +67 -0
- package/packages/web/CHANGELOG.md +36 -0
- package/packages/web/LICENSE +21 -0
- package/packages/web/README.md +87 -0
- package/packages/web/SECURITY.md +13 -0
- package/packages/web/package.json +59 -0
- package/packages/web/src/config.ts +109 -0
- package/packages/web/src/dnsfilter.ts +97 -0
- package/packages/web/src/extract.ts +65 -0
- package/packages/web/src/index.ts +217 -0
- package/packages/web/src/markdown.ts +122 -0
- package/packages/web/src/net.ts +109 -0
- package/packages/web/src/reader.ts +73 -0
- package/packages/web/src/sanitize.ts +50 -0
- package/packages/web/src/search.ts +76 -0
- package/packages/web/src/secrets.ts +22 -0
- package/packages/web/src/validate.ts +186 -0
- package/src/index.ts +156 -0
- package/src/settings.ts +79 -0
|
@@ -0,0 +1,648 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Public-API child adapter for the attach frame witness (issue #222, step 23a).
|
|
3
|
+
*
|
|
4
|
+
* The adapter is inert until a runner explicitly enables it: it sends a `hello`
|
|
5
|
+
* carrying `clientId:"frame-reporter"` and `frameWitness:{protocol:1}`, and only a
|
|
6
|
+
* `{type:"frame_witness_enable", protocol:1, token}` reply turns the capability on.
|
|
7
|
+
* Old or silent runners therefore see byte-identical behaviour: no widget, no
|
|
8
|
+
* wrappers, no markers. Current runners may negotiate the capability explicitly;
|
|
9
|
+
* the adapter never enables itself merely because the extension loaded.
|
|
10
|
+
*
|
|
11
|
+
* ## Interface discipline (converged contract §2.1)
|
|
12
|
+
*
|
|
13
|
+
* - One zero-height belowEditor sentinel registered with the public `ctx.ui.setWidget`.
|
|
14
|
+
* It never changes focus, input, visible rows or editor content.
|
|
15
|
+
* - INSTANCE-OWN shadows only: `columns`/`rows` accessors, `write`/`hideCursor`/
|
|
16
|
+
* `showCursor` methods and the `start`/`stop` lifecycle. Prototype descriptors are
|
|
17
|
+
* recorded before shadowing, descriptor kinds are preserved, original `this`, args,
|
|
18
|
+
* return values and exceptions are passed through unchanged, and restore removes an
|
|
19
|
+
* own shadow ONLY while it is still this adapter's own function.
|
|
20
|
+
* - Focus comes from a runtime feature-detected `getFocusedComponent()`; when it is
|
|
21
|
+
* absent the capability reports `unsupported` instead of guessing from pixels.
|
|
22
|
+
* - Markers are written through the ORIGINAL (pre-shadow) terminal write, in order,
|
|
23
|
+
* with a reentrancy guard, and never enter candidate accounting. A marker failure
|
|
24
|
+
* turns the capability off and reports UNKNOWN; it never blocks or alters the
|
|
25
|
+
* original child output, and no later `F` is written as if the frame completed.
|
|
26
|
+
*
|
|
27
|
+
* Production wiring lives in `src/index.ts` and `runner/pty-runner.mjs`; the
|
|
28
|
+
* per-runner enable handshake remains mandatory on every path.
|
|
29
|
+
*/
|
|
30
|
+
import {
|
|
31
|
+
FRAME_REPORTER_CLIENT_ID,
|
|
32
|
+
FRAME_WITNESS_PROTOCOL,
|
|
33
|
+
FrameCandidateEvaluator,
|
|
34
|
+
MAX_FRAME_MARKER_BYTES,
|
|
35
|
+
encodeFrameMarkerA,
|
|
36
|
+
encodeFrameMarkerF,
|
|
37
|
+
encodeFrameMarkerS,
|
|
38
|
+
isValidFrameWitnessToken,
|
|
39
|
+
} from "./attach-frame-proof.mjs";
|
|
40
|
+
|
|
41
|
+
export const ATTACH_FRAME_WIDGET_KEY = "agent-board-frame-witness";
|
|
42
|
+
|
|
43
|
+
const SENTINEL_KEYS = ["columns", "rows", "write", "hideCursor", "showCursor", "start", "stop"];
|
|
44
|
+
const MODES = new Set(["fullscreen", "regular"]);
|
|
45
|
+
|
|
46
|
+
function ownDescriptor(target, key) {
|
|
47
|
+
return Object.getOwnPropertyDescriptor(target, key);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function protoDescriptorOf(target, key) {
|
|
51
|
+
const proto = Object.getPrototypeOf(target);
|
|
52
|
+
return proto === null ? undefined : Object.getOwnPropertyDescriptor(proto, key);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Restore an adapter-owned shadow. If the adapter's shadow is still current, the
|
|
57
|
+
* PRE-EXISTING own descriptor is reinstalled when there was one (never erased), and the
|
|
58
|
+
* property is deleted only when no own property existed before. A third party's later
|
|
59
|
+
* replacement is left untouched.
|
|
60
|
+
*/
|
|
61
|
+
function restoreOwnShadow(terminal, entry) {
|
|
62
|
+
const current = ownDescriptor(terminal, entry.key);
|
|
63
|
+
const installed = entry.installed;
|
|
64
|
+
const stillOurs = current !== undefined && (typeof installed === "function" ? current.get === installed || current.value === installed : current.value === installed);
|
|
65
|
+
if (!stillOurs) return current === undefined ? "already-absent" : "shadow-replaced-not-restored";
|
|
66
|
+
if (entry.ownBefore === undefined) delete terminal[entry.key];
|
|
67
|
+
else Object.defineProperty(terminal, entry.key, entry.ownBefore);
|
|
68
|
+
return "restored";
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Capability negotiation. Returns `{status, reason}`; only an exact well-formed enable
|
|
73
|
+
* message with a valid token turns the reporter on.
|
|
74
|
+
*/
|
|
75
|
+
export function negotiateFrameWitness(message) {
|
|
76
|
+
if (message === null || typeof message !== "object") return { status: "ignored", reason: "not-an-object" };
|
|
77
|
+
if (message.type !== "frame_witness_enable") return { status: "ignored", reason: "not-enable" };
|
|
78
|
+
if (message.protocol !== FRAME_WITNESS_PROTOCOL) return { status: "unsupported", reason: "protocol" };
|
|
79
|
+
if (!isValidFrameWitnessToken(message.token)) return { status: "unsupported", reason: "token" };
|
|
80
|
+
return { status: "enabled", reason: "ok", token: message.token };
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* The reporter. All I/O is injected so unit tests can drive it with public-shaped
|
|
85
|
+
* doubles while the real extension passes the live `ctx.ui` and reporters socket.
|
|
86
|
+
*/
|
|
87
|
+
export class AttachFrameReporter {
|
|
88
|
+
#ui;
|
|
89
|
+
#send;
|
|
90
|
+
#mode;
|
|
91
|
+
#rendererEpoch;
|
|
92
|
+
#sessionEpoch;
|
|
93
|
+
#now;
|
|
94
|
+
#state = "idle"; // idle | announced | enabled | unsupported | failed | disposed
|
|
95
|
+
#token = null;
|
|
96
|
+
#frameIdCounter = { value: 0 };
|
|
97
|
+
#evaluator;
|
|
98
|
+
#terminal = null;
|
|
99
|
+
#terminalWrite = null;
|
|
100
|
+
#shadows = [];
|
|
101
|
+
#sentinelTui = null;
|
|
102
|
+
#focusApi = "unknown";
|
|
103
|
+
#renderPasses = 0;
|
|
104
|
+
#markerCount = 0;
|
|
105
|
+
#markerFailure = null;
|
|
106
|
+
#unknown = [];
|
|
107
|
+
#inMarker = false;
|
|
108
|
+
#lastMetadata = null;
|
|
109
|
+
#suspensions = 0;
|
|
110
|
+
#latestFact = null;
|
|
111
|
+
#pendingAfterDispose = [];
|
|
112
|
+
#superseding = false;
|
|
113
|
+
#instanceId = null;
|
|
114
|
+
#intentRequestId = null;
|
|
115
|
+
#geometryRevision = -1;
|
|
116
|
+
#intentsRejected = 0;
|
|
117
|
+
#intentsApplied = 0;
|
|
118
|
+
#seenRequestIds = new Set();
|
|
119
|
+
#consumedRequestIds = new Set();
|
|
120
|
+
#intentId = 0;
|
|
121
|
+
|
|
122
|
+
constructor({ ui, send = (_message) => {}, mode = "fullscreen", rendererEpoch = 1, sessionEpoch = 0, now = () => Date.now() }) {
|
|
123
|
+
if (!ui || typeof ui.setWidget !== "function") throw new Error("AttachFrameReporter requires ctx.ui.setWidget");
|
|
124
|
+
if (!MODES.has(mode)) throw new Error(`unknown renderer mode: ${mode}`);
|
|
125
|
+
this.#ui = ui;
|
|
126
|
+
this.#send = send;
|
|
127
|
+
this.#mode = mode;
|
|
128
|
+
this.#rendererEpoch = rendererEpoch;
|
|
129
|
+
this.#sessionEpoch = sessionEpoch;
|
|
130
|
+
this.#now = now;
|
|
131
|
+
this.#evaluator = new FrameCandidateEvaluator({ mode, rendererEpoch, sessionEpoch, frameIdCounter: this.#frameIdCounter });
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
get state() {
|
|
135
|
+
return this.#state;
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
get capabilityEnabled() {
|
|
139
|
+
return this.#state === "enabled";
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
get markerCount() {
|
|
143
|
+
return this.#markerCount;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
get markerFailure() {
|
|
147
|
+
return this.#markerFailure;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
get instanceId() {
|
|
151
|
+
return this.#instanceId;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
get geometryRevision() {
|
|
155
|
+
return this.#geometryRevision;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
get intentStats() {
|
|
159
|
+
return {
|
|
160
|
+
applied: this.#intentsApplied,
|
|
161
|
+
rejected: this.#intentsRejected,
|
|
162
|
+
geometryRevision: this.#geometryRevision,
|
|
163
|
+
requestId: this.#intentRequestId,
|
|
164
|
+
intentId: this.#intentId,
|
|
165
|
+
consumedRequestIds: [...this.#consumedRequestIds],
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* First matching complete F consumes the current intent: later frames with the same
|
|
171
|
+
* nonce may update the view but never consume it again (and duplicate requestIds stay
|
|
172
|
+
* rejected). Applied is never completion.
|
|
173
|
+
*/
|
|
174
|
+
consumeIntentOnce() {
|
|
175
|
+
if (this.#intentRequestId === null || this.#consumedRequestIds.has(this.#intentRequestId)) return false;
|
|
176
|
+
this.#consumedRequestIds.add(this.#intentRequestId);
|
|
177
|
+
return true;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
get unknown() {
|
|
181
|
+
return this.#unknown.slice();
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
get decisions() {
|
|
185
|
+
return this.#evaluator.decisions;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
get frameIdCounter() {
|
|
189
|
+
return this.#frameIdCounter;
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
get installedKeys() {
|
|
193
|
+
return this.#shadows.map((entry) => entry.key);
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** Renderer-stops observed (a mode switch suspends and then re-arms the adapter). */
|
|
197
|
+
get suspensions() {
|
|
198
|
+
return this.#suspensions;
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Resident-socket greeting. Sent once; never retried automatically. */
|
|
202
|
+
announce() {
|
|
203
|
+
if (this.#state !== "idle") return this.#state;
|
|
204
|
+
this.#state = "announced";
|
|
205
|
+
this.#send({
|
|
206
|
+
type: "hello",
|
|
207
|
+
clientId: FRAME_REPORTER_CLIENT_ID,
|
|
208
|
+
frameWitness: { protocol: FRAME_WITNESS_PROTOCOL, sessionEpoch: this.#sessionEpoch },
|
|
209
|
+
});
|
|
210
|
+
return this.#state;
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* Handle one reporter message. Disabled/old runners leave the adapter in
|
|
215
|
+
* `unsupported` with NO widget, NO wrappers and NO markers.
|
|
216
|
+
*/
|
|
217
|
+
handleMessage(message) {
|
|
218
|
+
const negotiation = negotiateFrameWitness(message);
|
|
219
|
+
if (negotiation.status !== "enabled") {
|
|
220
|
+
if (this.#state !== "enabled") this.#state = negotiation.status === "unsupported" ? "unsupported" : this.#state;
|
|
221
|
+
return negotiation;
|
|
222
|
+
}
|
|
223
|
+
// P2: the enable must carry the reporter's ACTUAL session epoch; a missing,
|
|
224
|
+
// invalid or mismatched epoch is unsupported rather than silently accepted.
|
|
225
|
+
const epoch = message.sessionEpoch;
|
|
226
|
+
if (!Number.isInteger(epoch) || epoch < 0 || epoch !== this.#sessionEpoch) {
|
|
227
|
+
if (this.#state !== "enabled") this.#state = "unsupported";
|
|
228
|
+
return { status: "unsupported", reason: "sessionEpoch" };
|
|
229
|
+
}
|
|
230
|
+
this.#token = negotiation.token;
|
|
231
|
+
if (typeof message.instanceId === "string" && message.instanceId.length > 0) this.#instanceId = message.instanceId;
|
|
232
|
+
this.#state = "enabled";
|
|
233
|
+
return negotiation;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/** Register the belowEditor sentinel; the terminal is attached from its TuiRef. */
|
|
237
|
+
install() {
|
|
238
|
+
if (this.#state !== "enabled") return false;
|
|
239
|
+
if (this.#sentinelTui !== null) return false;
|
|
240
|
+
// Re-registration disposes the previous component synchronously; mark that window
|
|
241
|
+
// so a superseded sentinel does not tear the whole reporter down.
|
|
242
|
+
this.#superseding = true;
|
|
243
|
+
try {
|
|
244
|
+
this.#ui.setWidget(ATTACH_FRAME_WIDGET_KEY, (tui) => this.#createSentinel(tui), { placement: "belowEditor" });
|
|
245
|
+
} finally {
|
|
246
|
+
this.#superseding = false;
|
|
247
|
+
}
|
|
248
|
+
return this.#terminal !== null;
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Runner frame-witness intent (FW-04). Validated strictly: a wrong instance, a
|
|
253
|
+
* non-string requestId, a non-monotonic geometryRevision or non-positive dims are
|
|
254
|
+
* rejected without touching the renderer. A valid intent is stored and a NORMAL
|
|
255
|
+
* public `tui.requestRender()` is requested — never a private doRender, force reset
|
|
256
|
+
* or extra resize. `applied` from the runner is NOT completion; only the complete
|
|
257
|
+
* target-geometry frame can prove.
|
|
258
|
+
*/
|
|
259
|
+
applyIntent(message) {
|
|
260
|
+
if (this.#state !== "enabled") return { ok: false, reason: "capability-off" };
|
|
261
|
+
if (message === null || typeof message !== "object" || message.type !== "frame_witness_intent") return { ok: false, reason: "not-intent" };
|
|
262
|
+
if (this.#instanceId !== null && message.instanceId !== this.#instanceId) {
|
|
263
|
+
this.#intentsRejected += 1;
|
|
264
|
+
return { ok: false, reason: "instance" };
|
|
265
|
+
}
|
|
266
|
+
if (typeof message.requestId !== "string" || message.requestId.length === 0) {
|
|
267
|
+
this.#intentsRejected += 1;
|
|
268
|
+
return { ok: false, reason: "requestId" };
|
|
269
|
+
}
|
|
270
|
+
if (this.#seenRequestIds.has(message.requestId)) {
|
|
271
|
+
this.#intentsRejected += 1;
|
|
272
|
+
return { ok: false, reason: "duplicate-requestId" };
|
|
273
|
+
}
|
|
274
|
+
const match = /^fw-(\d+)$/.exec(message.requestId);
|
|
275
|
+
if (match === null || Number(match[1]) < 1) {
|
|
276
|
+
this.#intentsRejected += 1;
|
|
277
|
+
return { ok: false, reason: "requestId-format" };
|
|
278
|
+
}
|
|
279
|
+
if (!Number.isInteger(message.geometryRevision) || message.geometryRevision <= this.#geometryRevision) {
|
|
280
|
+
this.#intentsRejected += 1;
|
|
281
|
+
return { ok: false, reason: "geometryRevision" };
|
|
282
|
+
}
|
|
283
|
+
if (!Number.isInteger(message.cols) || message.cols < 1 || !Number.isInteger(message.rows) || message.rows < 1) {
|
|
284
|
+
this.#intentsRejected += 1;
|
|
285
|
+
return { ok: false, reason: "dims" };
|
|
286
|
+
}
|
|
287
|
+
this.#geometryRevision = message.geometryRevision;
|
|
288
|
+
this.#intentRequestId = message.requestId;
|
|
289
|
+
this.#intentId = Number(match[1]);
|
|
290
|
+
this.#seenRequestIds.add(message.requestId);
|
|
291
|
+
this.#intentsApplied += 1;
|
|
292
|
+
this.#evaluator.setIntent({ intentId: this.#intentId, geometryRevision: message.geometryRevision, cols: message.cols, rows: message.rows });
|
|
293
|
+
const requested = this.#requestRender();
|
|
294
|
+
return { ok: true, requested, geometryRevision: this.#geometryRevision };
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
#requestRender() {
|
|
298
|
+
const tui = this.#sentinelTui;
|
|
299
|
+
if (tui !== null && typeof tui.requestRender === "function") {
|
|
300
|
+
try {
|
|
301
|
+
tui.requestRender();
|
|
302
|
+
return true;
|
|
303
|
+
} catch {
|
|
304
|
+
return false;
|
|
305
|
+
}
|
|
306
|
+
}
|
|
307
|
+
return false;
|
|
308
|
+
}
|
|
309
|
+
|
|
310
|
+
/** Renderer mode change: carry the frameId counter, rebind the grammar. */
|
|
311
|
+
setMode(mode, { rendererEpoch = this.#rendererEpoch + 1 } = {}) {
|
|
312
|
+
if (!MODES.has(mode)) throw new Error(`unknown renderer mode: ${mode}`);
|
|
313
|
+
const produced = this.#evaluator.abort("mode-changed");
|
|
314
|
+
this.#mode = mode;
|
|
315
|
+
this.#rendererEpoch = rendererEpoch;
|
|
316
|
+
this.#evaluator = new FrameCandidateEvaluator({
|
|
317
|
+
mode,
|
|
318
|
+
rendererEpoch,
|
|
319
|
+
sessionEpoch: this.#sessionEpoch,
|
|
320
|
+
frameIdCounter: this.#frameIdCounter,
|
|
321
|
+
});
|
|
322
|
+
this.#sentinelTui = null;
|
|
323
|
+
this.#focusApi = "unknown";
|
|
324
|
+
return produced;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
/** Session-scoped teardown: stop proving and restore every own shadow. */
|
|
328
|
+
dispose(reason = "dispose") {
|
|
329
|
+
const produced = this.#evaluator.stop();
|
|
330
|
+
const restores = this.#restoreShadows(reason);
|
|
331
|
+
this.#state = "disposed";
|
|
332
|
+
return { decisions: produced, restores };
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
#createSentinel(tui) {
|
|
336
|
+
// F6: the renderer mode comes from the ACTUAL public `tui.mode`, never a hardcoded
|
|
337
|
+
// default. A mode change rebinds the grammar (carried frameId, same terminal); an
|
|
338
|
+
// unavailable/unknown mode is `unsupported`, not a fabricated value.
|
|
339
|
+
const observedMode = typeof tui?.mode === "string" ? tui.mode : null;
|
|
340
|
+
if (observedMode === null || !MODES.has(observedMode)) {
|
|
341
|
+
this.#unknown.push({ what: "mode", reason: "public tui.mode is unavailable or unknown; mode evidence unsupported" });
|
|
342
|
+
this.#state = "unsupported";
|
|
343
|
+
this.#sentinelTui = tui;
|
|
344
|
+
return { render: () => [], dispose: () => {} };
|
|
345
|
+
}
|
|
346
|
+
if (observedMode !== this.#mode) this.setMode(observedMode);
|
|
347
|
+
this.#sentinelTui = tui;
|
|
348
|
+
const maybe = tui?.getFocusedComponent;
|
|
349
|
+
this.#focusApi = typeof maybe === "function" ? "present" : "absent";
|
|
350
|
+
if (this.#focusApi === "absent") {
|
|
351
|
+
this.#unknown.push({ what: "focus", reason: "public TUI instance has no getFocusedComponent; focus evidence unsupported" });
|
|
352
|
+
if (this.#state === "enabled") this.#state = "unsupported";
|
|
353
|
+
}
|
|
354
|
+
this.#attachTerminal(tui);
|
|
355
|
+
return {
|
|
356
|
+
render: () => {
|
|
357
|
+
this.#renderPasses += 1;
|
|
358
|
+
if (this.#state !== "enabled") return [];
|
|
359
|
+
// Host 1.4.0 `switchTuiMode()` REUSES the existing widget component and never
|
|
360
|
+
// calls the factory again. A renderer stop suspends the binding, so the SAME
|
|
361
|
+
// component's next render is where the adapter must re-arm — no install() or
|
|
362
|
+
// setWidget() refresh, no test-only path.
|
|
363
|
+
if (this.#sentinelTui === null) this.#reactivate(tui);
|
|
364
|
+
if (this.#sentinelTui !== tui) return [];
|
|
365
|
+
const focus = this.#focusApi === "present" ? tui.getFocusedComponent?.() : undefined;
|
|
366
|
+
const overlayPresent = typeof tui?.hasOverlay === "function" ? Boolean(tui.hasOverlay()) : false;
|
|
367
|
+
this.#latestFact = { focusPresent: focus !== null && focus !== undefined, focusIdentity: focus, mode: this.#mode, overlayPresent };
|
|
368
|
+
this.#evaluator.sentinel(this.#latestFact);
|
|
369
|
+
return [];
|
|
370
|
+
},
|
|
371
|
+
dispose: () => {
|
|
372
|
+
if (this.#state !== "enabled") return;
|
|
373
|
+
// A disposed sentinel always aborts the open candidate. Only a REAL teardown
|
|
374
|
+
// (not the disposal that a re-registration triggers synchronously) stops
|
|
375
|
+
// proving and restores the wrappers.
|
|
376
|
+
this.#pendingAfterDispose.push(...this.#evaluator.abort("sentinel-dispose"));
|
|
377
|
+
if (this.#superseding) return;
|
|
378
|
+
this.#restoreShadows("sentinel-dispose");
|
|
379
|
+
this.#terminal = null;
|
|
380
|
+
this.#terminalWrite = null;
|
|
381
|
+
this.#sentinelTui = null;
|
|
382
|
+
this.#state = "disposed";
|
|
383
|
+
},
|
|
384
|
+
};
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
/**
|
|
388
|
+
* Post-stop reactivation from the EXISTING sentinel component: rebind from the actual
|
|
389
|
+
* current public `tui.mode`, advance rendererEpoch EXACTLY once (same-mode stop/start
|
|
390
|
+
* included), rebuild the grammar on the carried frameId counter and reinstall the own
|
|
391
|
+
* shadows on the same shared terminal. Never used by disposed/unsupported/failed states.
|
|
392
|
+
*/
|
|
393
|
+
#reactivate(tui) {
|
|
394
|
+
if (this.#state !== "enabled") return;
|
|
395
|
+
const observed = typeof tui?.mode === "string" ? tui.mode : null;
|
|
396
|
+
if (observed === null || !MODES.has(observed)) {
|
|
397
|
+
this.#unknown.push({ what: "mode", reason: "public tui.mode unavailable on reactivation" });
|
|
398
|
+
this.#state = "unsupported";
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
this.#rendererEpoch += 1;
|
|
402
|
+
this.#mode = observed;
|
|
403
|
+
this.#evaluator = new FrameCandidateEvaluator({
|
|
404
|
+
mode: observed,
|
|
405
|
+
rendererEpoch: this.#rendererEpoch,
|
|
406
|
+
sessionEpoch: this.#sessionEpoch,
|
|
407
|
+
frameIdCounter: this.#frameIdCounter,
|
|
408
|
+
});
|
|
409
|
+
this.#sentinelTui = tui;
|
|
410
|
+
this.#attachTerminal(tui);
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
#attachTerminal(tui) {
|
|
414
|
+
if (this.#terminal !== null) {
|
|
415
|
+
// Re-registration after a mode switch must keep the SAME shared terminal.
|
|
416
|
+
const same = tui?.terminal === this.#terminal;
|
|
417
|
+
if (!same) this.#unknown.push({ what: "terminal", reason: "re-registration reported a different terminal instance" });
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const terminal = tui?.terminal;
|
|
421
|
+
if (terminal === undefined || terminal === null) {
|
|
422
|
+
this.#unknown.push({ what: "terminal", reason: "public TUI instance does not expose the shared terminal" });
|
|
423
|
+
if (this.#state === "enabled") this.#state = "unsupported";
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
this.#terminal = terminal;
|
|
427
|
+
const proto = Object.getPrototypeOf(terminal);
|
|
428
|
+
// The raw, pre-shadow write is how markers reach the same ordered stream.
|
|
429
|
+
const protoWrite = proto === null ? undefined : protoDescriptorOf(terminal, "write")?.value;
|
|
430
|
+
this.#terminalWrite = typeof protoWrite === "function" ? protoWrite : null;
|
|
431
|
+
for (const key of ["columns", "rows"]) {
|
|
432
|
+
const descriptor = protoDescriptorOf(terminal, key);
|
|
433
|
+
const ownBefore = ownDescriptor(terminal, key);
|
|
434
|
+
if (ownBefore !== undefined && typeof ownBefore.get !== "function") {
|
|
435
|
+
// A pre-existing data property on the instance cannot be shadowed faithfully
|
|
436
|
+
// without erasing it on restore; refuse instead.
|
|
437
|
+
this.#unknown.push({ what: "shadow", reason: `pre-existing own data property ${key}` });
|
|
438
|
+
continue;
|
|
439
|
+
}
|
|
440
|
+
const getter = () => {
|
|
441
|
+
const value = ownBefore !== undefined ? ownBefore.get.call(terminal) : typeof descriptor?.get === "function" ? descriptor.get.call(terminal) : undefined;
|
|
442
|
+
this.#evaluator.sizeRead(key, value);
|
|
443
|
+
return value;
|
|
444
|
+
};
|
|
445
|
+
try {
|
|
446
|
+
Object.defineProperty(terminal, key, { configurable: true, enumerable: ownBefore?.enumerable ?? false, get: getter });
|
|
447
|
+
this.#shadows.push({ key, installed: getter, ownBefore });
|
|
448
|
+
} catch {
|
|
449
|
+
this.#unknown.push({ what: "shadow", reason: `cannot shadow ${key}` });
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
for (const key of ["write", "hideCursor", "showCursor"]) {
|
|
453
|
+
const ownBefore = ownDescriptor(terminal, key);
|
|
454
|
+
const original = typeof ownBefore?.value === "function" ? ownBefore.value : protoDescriptorOf(terminal, key)?.value;
|
|
455
|
+
if (typeof original !== "function") continue;
|
|
456
|
+
const wrapper = (...args) => {
|
|
457
|
+
const text = typeof args[0] === "string" ? args[0] : "";
|
|
458
|
+
if (this.#state !== "enabled") return original.apply(terminal, args);
|
|
459
|
+
const { pending, insertions } = this.#proveOnOperation(key, text);
|
|
460
|
+
let failed = false;
|
|
461
|
+
try {
|
|
462
|
+
if (insertions.length === 0) return original.apply(terminal, args);
|
|
463
|
+
// Split the write exactly at this frame's OPEN token: the prefix is written
|
|
464
|
+
// first, then S, then the remainder — so S never precedes a previous
|
|
465
|
+
// frame's CLOSE that shares the write. Only the LAST ordinary slice
|
|
466
|
+
// receives xterm's completion argument; intermediate slices take none.
|
|
467
|
+
const slices = [];
|
|
468
|
+
let sliceCursor = 0;
|
|
469
|
+
for (const insertion of insertions) {
|
|
470
|
+
if (insertion.offset > sliceCursor) {
|
|
471
|
+
slices.push(text.slice(sliceCursor, insertion.offset));
|
|
472
|
+
sliceCursor = insertion.offset;
|
|
473
|
+
}
|
|
474
|
+
}
|
|
475
|
+
if (sliceCursor < text.length) slices.push(text.slice(sliceCursor));
|
|
476
|
+
let sliceIndex = 0;
|
|
477
|
+
const writeSlice = (slice) => {
|
|
478
|
+
sliceIndex += 1;
|
|
479
|
+
return original.call(terminal, slice, ...(sliceIndex === slices.length ? args.slice(1) : []));
|
|
480
|
+
};
|
|
481
|
+
let cursor = 0;
|
|
482
|
+
let lastResult;
|
|
483
|
+
for (const insertion of insertions) {
|
|
484
|
+
if (insertion.offset > cursor) lastResult = writeSlice(text.slice(cursor, insertion.offset));
|
|
485
|
+
this.#emitS(insertion);
|
|
486
|
+
cursor = insertion.offset;
|
|
487
|
+
}
|
|
488
|
+
if (cursor < text.length) lastResult = writeSlice(text.slice(cursor));
|
|
489
|
+
return lastResult;
|
|
490
|
+
} catch (error) {
|
|
491
|
+
// A failed original write means the frame bytes were not delivered: abort
|
|
492
|
+
// instead of claiming completion (S, if any, must be closed by A), then
|
|
493
|
+
// rethrow the ORIGINAL error unchanged.
|
|
494
|
+
failed = true;
|
|
495
|
+
for (const decision of this.#evaluator.abort("write-failed")) {
|
|
496
|
+
if (decision.kind === "A") this.#emitA(decision.frameId, decision.reason);
|
|
497
|
+
}
|
|
498
|
+
throw error;
|
|
499
|
+
} finally {
|
|
500
|
+
// NB: no `return` here — a return inside `finally` would swallow the
|
|
501
|
+
// original write error rethrown above.
|
|
502
|
+
if (!failed) {
|
|
503
|
+
// Completion rechecks the LATEST focus identity/mode/overlay before any F.
|
|
504
|
+
const finished = this.#evaluator.finishWrite(this.#latestFact);
|
|
505
|
+
for (const decision of finished) {
|
|
506
|
+
if (decision.kind === "F") this.#emitF(decision);
|
|
507
|
+
else if (decision.kind === "A") this.#emitA(decision.frameId, decision.reason);
|
|
508
|
+
}
|
|
509
|
+
for (const decision of pending) {
|
|
510
|
+
if (decision.kind === "A") this.#emitA(decision.frameId, decision.reason);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
};
|
|
515
|
+
try {
|
|
516
|
+
Object.defineProperty(terminal, key, { configurable: true, enumerable: false, value: wrapper, writable: true });
|
|
517
|
+
this.#shadows.push({ key, installed: wrapper, ownBefore });
|
|
518
|
+
} catch {
|
|
519
|
+
this.#unknown.push({ what: "shadow", reason: `cannot shadow ${key}` });
|
|
520
|
+
}
|
|
521
|
+
}
|
|
522
|
+
for (const key of ["start", "stop"]) {
|
|
523
|
+
const original = protoDescriptorOf(terminal, key)?.value;
|
|
524
|
+
if (typeof original !== "function") continue;
|
|
525
|
+
const ownBefore = ownDescriptor(terminal, key);
|
|
526
|
+
const wrapper = (...args) => {
|
|
527
|
+
if (this.#state === "enabled") {
|
|
528
|
+
this.#evaluator.abort(`terminal-${key}`);
|
|
529
|
+
if (key === "stop") {
|
|
530
|
+
// A renderer stop (mode switch / teardown) aborts the candidate and
|
|
531
|
+
// detaches the adapter, but the capability stays armed: the next
|
|
532
|
+
// registration on the new renderer re-installs on the SAME terminal.
|
|
533
|
+
this.#restoreShadows("terminal-stop");
|
|
534
|
+
this.#terminal = null;
|
|
535
|
+
this.#terminalWrite = null;
|
|
536
|
+
this.#sentinelTui = null;
|
|
537
|
+
this.#suspensions += 1;
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
const result = original.apply(terminal, args);
|
|
541
|
+
// Epoch advances only AFTER a successful start (a throw preserves both the
|
|
542
|
+
// old epoch and the original exception).
|
|
543
|
+
if (key === "start" && this.#state === "enabled") {
|
|
544
|
+
this.#rendererEpoch += 1;
|
|
545
|
+
this.#evaluator.setEpoch({ rendererEpoch: this.#rendererEpoch });
|
|
546
|
+
}
|
|
547
|
+
return result;
|
|
548
|
+
};
|
|
549
|
+
try {
|
|
550
|
+
Object.defineProperty(terminal, key, { configurable: true, enumerable: false, value: wrapper, writable: true });
|
|
551
|
+
this.#shadows.push({ key, installed: wrapper, ownBefore });
|
|
552
|
+
} catch {
|
|
553
|
+
this.#unknown.push({ what: "shadow", reason: `cannot shadow ${key}` });
|
|
554
|
+
}
|
|
555
|
+
}
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
#restoreShadows(reason) {
|
|
559
|
+
const restores = [];
|
|
560
|
+
for (const entry of this.#shadows) {
|
|
561
|
+
if (this.#terminal === null) break;
|
|
562
|
+
restores.push({ key: entry.key, result: restoreOwnShadow(this.#terminal, entry), restoredOwnBefore: entry.ownBefore !== undefined });
|
|
563
|
+
}
|
|
564
|
+
if (restores.length > 0) this.#shadows = [];
|
|
565
|
+
this.#lastMetadata = this.#lastMetadata === null ? null : { ...this.#lastMetadata, restoreReason: reason };
|
|
566
|
+
return restores;
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
#proveOnOperation(kind, text) {
|
|
570
|
+
const { decisions, insertions } = this.#evaluator.op({ kind, text });
|
|
571
|
+
const pending = decisions.filter((decision) => decision.kind === "A");
|
|
572
|
+
return { pending, insertions };
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
#writeMarker(text) {
|
|
576
|
+
if (this.#inMarker) {
|
|
577
|
+
this.#failCapability("marker-recursion");
|
|
578
|
+
return false;
|
|
579
|
+
}
|
|
580
|
+
if (this.#state !== "enabled") return false;
|
|
581
|
+
if (Buffer.byteLength(text, "utf8") > MAX_FRAME_MARKER_BYTES) {
|
|
582
|
+
this.#failCapability("marker-oversize");
|
|
583
|
+
return false;
|
|
584
|
+
}
|
|
585
|
+
if (this.#terminalWrite === null || this.#terminal === null) {
|
|
586
|
+
this.#failCapability("marker-no-raw-write");
|
|
587
|
+
return false;
|
|
588
|
+
}
|
|
589
|
+
this.#inMarker = true;
|
|
590
|
+
try {
|
|
591
|
+
// Raw, pre-shadow reference: never re-enters candidate accounting.
|
|
592
|
+
this.#terminalWrite.call(this.#terminal, text);
|
|
593
|
+
this.#markerCount += 1;
|
|
594
|
+
return true;
|
|
595
|
+
} catch (error) {
|
|
596
|
+
this.#failCapability(`marker-write-failed:${error instanceof Error ? error.message : String(error)}`);
|
|
597
|
+
return false;
|
|
598
|
+
} finally {
|
|
599
|
+
this.#inMarker = false;
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
#emitS(insertion) {
|
|
604
|
+
if (this.#token === null) return;
|
|
605
|
+
const frameId = insertion.frameId;
|
|
606
|
+
const decision = this.#evaluator.decisions.filter((entry) => entry.kind === "S" && entry.frameId === frameId).at(-1) ?? null;
|
|
607
|
+
try {
|
|
608
|
+
this.#writeMarker(encodeFrameMarkerS({ token: this.#token, frameId, intentId: decision?.intentId ?? 0, sampledCols: decision?.cols }));
|
|
609
|
+
} catch (error) {
|
|
610
|
+
this.#failCapability(`marker-encode-failed:${error instanceof Error ? error.message : String(error)}`);
|
|
611
|
+
}
|
|
612
|
+
}
|
|
613
|
+
|
|
614
|
+
#emitF(decision) {
|
|
615
|
+
// Never write an F as if the frame completed once the capability failed.
|
|
616
|
+
if (this.#token === null || this.#state !== "enabled") return;
|
|
617
|
+
const frameId = decision.frameId;
|
|
618
|
+
const metadata = this.#evaluator.metadataFor(decision);
|
|
619
|
+
this.#lastMetadata = metadata;
|
|
620
|
+
try {
|
|
621
|
+
if (this.#writeMarker(encodeFrameMarkerF({ token: this.#token, frameId, metadata }))) {
|
|
622
|
+
// P2: consume only after the first matching F was actually written; later
|
|
623
|
+
// frames fall back to the ordinary intent 0 so they cannot masquerade as the
|
|
624
|
+
// completing frame. A failed write never consumes.
|
|
625
|
+
if (this.consumeIntentOnce()) this.#evaluator.setIntent(null);
|
|
626
|
+
}
|
|
627
|
+
} catch (error) {
|
|
628
|
+
this.#failCapability(`marker-encode-failed:${error instanceof Error ? error.message : String(error)}`);
|
|
629
|
+
}
|
|
630
|
+
}
|
|
631
|
+
|
|
632
|
+
#emitA(frameId, reason) {
|
|
633
|
+
if (this.#token === null || this.#state !== "enabled") return;
|
|
634
|
+
try {
|
|
635
|
+
this.#writeMarker(encodeFrameMarkerA({ token: this.#token, frameId, reason }));
|
|
636
|
+
} catch (error) {
|
|
637
|
+
this.#failCapability(`marker-encode-failed:${error instanceof Error ? error.message : String(error)}`);
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
|
|
641
|
+
/** Marker failure => capability OFF + UNKNOWN; original output keeps flowing. */
|
|
642
|
+
#failCapability(reason) {
|
|
643
|
+
if (this.#markerFailure === null) this.#markerFailure = { at: this.#now(), reason };
|
|
644
|
+
this.#unknown.push({ what: "marker", reason });
|
|
645
|
+
this.#state = "failed";
|
|
646
|
+
}
|
|
647
|
+
}
|
|
648
|
+
|