返回 DeepSeek-Reasonix
transcriptScrollProbe.ts
根目录 / desktop / frontend / src / lib / transcriptScrollProbe.ts
1 /**
2 * Observability probe for every imperative scroll write against the
3 * transcript viewport. The single-writer contract is enforced statically by
4 * scripts/check-single-scroll-writer.mjs; this probe is the runtime mirror:
5 * tests and diagnostics can observe who wrote, what kind of write, and where
6 * it landed, without intercepting the DOM.
7 */
8 import { isFrontendDiagnosticsBuild } from "./frontendDiagnosticsBuild";
9 import { recordFrontendDiagnostic } from "./frontendDiagnosticBridge";
10
11 export type TranscriptScrollWriteRecord = {
12 session?: string;
13 transaction?: number;
14 owner: string;
15 intent?: string;
16 requestedOffset?: number;
17 acceptedOffset?: number;
18 outcome?: string;
19 kind: "scrollTo" | "scrollBy" | "scrollToIndex" | "pinTail";
20 top?: number;
21 index?: number | "LAST";
22 source?: string;
23 phase?: "mount-anchor" | "correct-offset" | "initial" | "settle";
24 scrollTop?: number;
25 scrollHeight?: number;
26 clientHeight?: number;
27 bottomDistance?: number;
28 mode?: string;
29 sequence?: number;
30 generation?: number;
31 ownershipEpoch?: number;
32 geometryRevision?: number;
33 transactionId?: number;
34 rejectedReason?: string;
35 settleFrame?: number;
36 offBottomFrames?: number;
37 stagnantFrames?: number;
38 };
39
40 type DiagnosticSink = (type: string, fields: Record<string, unknown>) => void;
41 let diagnosticSink: DiagnosticSink | undefined;
42 const CAPTURE_SCROLL_DIAGNOSTIC_DETAILS = isFrontendDiagnosticsBuild(
43 typeof __BUILD_CHANNEL__ === "string" ? __BUILD_CHANNEL__ : "development",
44 Boolean(import.meta.env?.DEV),
45 );
46
47 export function isTranscriptScrollDiagnosticsBuild(channel: string, development: boolean): boolean {
48 return isFrontendDiagnosticsBuild(channel, development);
49 }
50
51 export function setTranscriptScrollDiagnosticSink(sink: DiagnosticSink): void {
52 diagnosticSink = sink;
53 }
54
55 export function recordTranscriptScrollDiagnostic(type: string, fields: Record<string, unknown> = {}): void {
56 diagnosticSink?.(type, fields);
57 // Forward the same content-free geometry into the broader frontend timeline.
58 recordFrontendDiagnostic("transcript", `transcript.${type}`, fields);
59 // The bench harness (desktop/frontend/bench) installs this page-side hook to
60 // attach the diagnostic stream to replay failure output.
61 if (typeof window !== "undefined") window.__REASONIX_TRANSCRIPT_SCROLL_DIAGNOSTIC__?.(type, fields);
62 }
63
64 declare global {
65 interface Window {
66 __REASONIX_TRANSCRIPT_SCROLL_WRITE__?: (write: TranscriptScrollWriteRecord) => void;
67 __REASONIX_TRANSCRIPT_SCROLL_DIAGNOSTIC__?: (type: string, fields: Record<string, unknown>) => void;
68 }
69 }
70
71 export function noteTranscriptScrollWrite(write: TranscriptScrollWriteRecord): void {
72 if (CAPTURE_SCROLL_DIAGNOSTIC_DETAILS) {
73 recordTranscriptScrollDiagnostic("scroll-write", {
74 session: write.session,
75 transaction: write.transaction,
76 owner: write.owner,
77 intent: write.intent,
78 requestedOffset: write.requestedOffset,
79 acceptedOffset: write.acceptedOffset,
80 outcome: write.outcome,
81 writeKind: write.kind,
82 targetTop: write.top,
83 targetIndex: write.index,
84 source: write.source,
85 phase: write.phase,
86 scrollTop: write.scrollTop,
87 scrollHeight: write.scrollHeight,
88 clientHeight: write.clientHeight,
89 bottomDistance: write.bottomDistance,
90 mode: write.mode,
91 sequence: write.sequence,
92 generation: write.generation,
93 ownershipEpoch: write.ownershipEpoch,
94 geometryRevision: write.geometryRevision,
95 transactionId: write.transactionId,
96 rejectedReason: write.rejectedReason,
97 settleFrame: write.settleFrame,
98 offBottomFrames: write.offBottomFrames,
99 stagnantFrames: write.stagnantFrames,
100 });
101 }
102 window.__REASONIX_TRANSCRIPT_SCROLL_WRITE__?.(write);
103 }
104
104 lines TYPESCRIPT