Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion desktop/frontend/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,7 @@
"test:motion-browser": "PLAYWRIGHT_BROWSERS_PATH=.pw-browsers node bench/approval-animation.mjs",
"test:theme-browser": "PLAYWRIGHT_BROWSERS_PATH=.pw-browsers node bench/theme-surface-contract.mjs",
"test:diagnostics": "tsx src/__tests__/diagnostics-settings.test.tsx",
"test:transcript": "tsx src/__tests__/transcript-geometry-replay.test.ts && tsx src/__tests__/transcript-virtuoso-index.test.ts && tsx src/__tests__/transcript-row-geometry.test.ts && tsx src/__tests__/transcript-live-turn-stability.test.tsx && tsx src/__tests__/transcript-geometry-environment.test.ts && tsx src/__tests__/transcript-measured-sizes.test.ts && tsx src/__tests__/transcript-state-snapshot.test.ts && tsx src/__tests__/transcript-layout-recovery.test.ts && tsx src/__tests__/transcript-reader-extent-stability.test.ts && tsx src/__tests__/transcript-reader-extent-race.test.tsx && tsx src/__tests__/transcript-recovery-race.test.tsx && tsx src/__tests__/transcript-same-tab-tail-race.test.tsx && tsx src/__tests__/transcript-history-prepend-race.test.tsx && tsx src/__tests__/transcript-anchor-compensation-race.test.tsx && tsx src/__tests__/transcript-scroll-release.test.ts && tsx src/__tests__/transcript-tail-clamp-race.test.ts && tsx src/__tests__/transcript-scroll-writer.test.ts && tsx src/__tests__/transcript-scroll-diagnostics.test.ts && tsx src/__tests__/frontend-diagnostics.test.ts && tsx src/__tests__/project-tree-diagnostics.test.ts && tsx src/__tests__/transcript-native-scrollbar.test.ts && tsx src/__tests__/nested-scroll-handoff.test.ts && tsx src/__tests__/reasoning-scroll-follow.test.tsx && tsx src/__tests__/creation-transcript-scrollbar.test.ts && tsx src/__tests__/question-jump-bar.test.tsx && tsx src/__tests__/transcript-question-nav.test.ts && tsx src/__tests__/transcript-question-nav-integration.test.ts && tsx src/__tests__/markdown-table-virtual.test.tsx && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/transcript-selection-retention.test.tsx && tsx src/__tests__/transcript-logical-selection.test.ts && tsx src/__tests__/transcript-selection-overlay.test.tsx && tsx src/__tests__/markdown-pipeline.test.tsx && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/transcript-selection-menu.test.tsx && tsx src/__tests__/transcript-selection-rendering.test.ts && tsx src/__tests__/transcript-store.test.ts && tsx src/__tests__/transcript-virtualization.test.tsx && tsx src/__tests__/transcript-question-jump.test.tsx",
"test:transcript": "tsx src/__tests__/transcript-geometry-replay.test.ts && tsx src/__tests__/transcript-virtuoso-index.test.ts && tsx src/__tests__/transcript-row-geometry.test.ts && tsx src/__tests__/transcript-live-turn-stability.test.tsx && tsx src/__tests__/transcript-geometry-environment.test.ts && tsx src/__tests__/transcript-measured-sizes.test.ts && tsx src/__tests__/transcript-state-snapshot.test.ts && tsx src/__tests__/transcript-layout-recovery.test.ts && tsx src/__tests__/transcript-reader-extent-stability.test.ts && tsx src/__tests__/transcript-reader-extent-race.test.tsx && tsx src/__tests__/transcript-recovery-race.test.tsx && tsx src/__tests__/transcript-same-tab-tail-race.test.tsx && tsx src/__tests__/transcript-history-prepend-race.test.tsx && tsx src/__tests__/transcript-anchor-compensation-race.test.tsx && tsx src/__tests__/transcript-scroll-release.test.ts && tsx src/__tests__/transcript-tail-clamp-race.test.ts && tsx src/__tests__/transcript-scroll-writer.test.ts && tsx src/__tests__/transcript-scroll-diagnostics.test.ts && tsx src/__tests__/frontend-diagnostics.test.ts && tsx src/__tests__/transcript-surface-transaction.test.ts && tsx src/__tests__/project-tree-diagnostics.test.ts && tsx src/__tests__/transcript-native-scrollbar.test.ts && tsx src/__tests__/nested-scroll-handoff.test.ts && tsx src/__tests__/reasoning-scroll-follow.test.tsx && tsx src/__tests__/creation-transcript-scrollbar.test.ts && tsx src/__tests__/question-jump-bar.test.tsx && tsx src/__tests__/transcript-question-nav.test.ts && tsx src/__tests__/transcript-question-nav-integration.test.ts && tsx src/__tests__/markdown-table-virtual.test.tsx && tsx src/__tests__/typography-overflow-contract.test.ts && tsx src/__tests__/transcript-selection-retention.test.tsx && tsx src/__tests__/transcript-logical-selection.test.ts && tsx src/__tests__/transcript-selection-overlay.test.tsx && tsx src/__tests__/markdown-pipeline.test.tsx && tsx src/__tests__/message-selection-copy.test.ts && tsx src/__tests__/transcript-selection-menu.test.tsx && tsx src/__tests__/transcript-selection-rendering.test.ts && tsx src/__tests__/transcript-store.test.ts && tsx src/__tests__/transcript-virtualization.test.tsx && tsx src/__tests__/transcript-question-jump.test.tsx",
"test:transcript-browser": "node bench/transcript-selection.mjs && node bench/transcript-scroll-stability.mjs && node bench/composer-transcript-stability.mjs",
"test:transcript-reader-browser": "node bench/transcript-reader-transaction.mjs",
"pretest": "pnpm test:terminal && pnpm test:task-monitor && pnpm test:composer && tsx src/__tests__/context-center-contract.test.ts && tsx src/__tests__/provider-model-cache.test.ts && tsx src/__tests__/format-tokens.test.ts && pnpm test:usage-stats && pnpm test:settings-responsive && pnpm test:composer-menu-viewport && pnpm test:diagnostics && pnpm test:transcript",
Expand Down
19 changes: 11 additions & 8 deletions desktop/frontend/scripts/check-bundle-budget.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -180,9 +180,11 @@ console.log("\nbundle budgets");
// move the combined path to 462.2 KiB. Local spectator reclaim adds the
// desktop-vs-remote command branch. Sticky Context's session-scoped file chips
// bring the merged stable path to 462.587 KiB. Windows' embedded build metadata
// lands just above the rounded 462.6 KiB boundary; retain one cross-platform
// decimal step without widening any chunk or raw gate.
const initialJSBudgetKiB = 462.7;
// lands just above the rounded 462.6 KiB boundary. The generation-bound
// surface transaction and content-free reversal diagnostics add 0.7 KiB to
// the initial path; retain the smallest one-decimal ratchet without widening
// any chunk or raw gate.
const initialJSBudgetKiB = 463.7;
assertBudget("initial JavaScript gzip", initialJSGzip, initialJSBudgetKiB * 1024);
assertBudget("largest initial JavaScript chunk gzip", largestInitialJS, 280 * 1024);
// Render-blocking CSS is intentionally absent: styles.css loads deferred via
Expand Down Expand Up @@ -324,10 +326,11 @@ const rawInitialBytes = [...initialJS, ...initialCSS, ...appShellCSS]
// path 2465.105 KiB raw; the merged test channel measures 2464.979 KiB.
// Session takeover banners and #9703/#9711's provisional-selection handoff
// combine with Sticky Context's pinned-file state at 2469.125 KiB raw on the
// merged stable path. Retain only the next one-decimal ceiling.
// The passive reader-anchor lease for delayed WebView2 range commits measures
// 2469.347 KiB raw (+0.222 KiB, +0.009%). Retain only the next one-decimal
// ceiling; gzip and largest-chunk budgets remain unchanged.
const rawInitialBudgetKiB = 2_469.4;
// merged stable path. The bounded surface transaction, scrollbar rebase, and
// Markdown empty-block guard add 3.1 KiB raw; the native-thumb generation
// fence adds the final measured 0.3 KiB. Retain the smallest one-decimal
// ceiling for the measured 2473.1 KiB path after adding the reader-lease
// writer fence on top of the latest main-v2 anchor-compensation baseline.
const rawInitialBudgetKiB = 2_473.5;
assertBudget("initial raw JavaScript and CSS", rawInitialBytes, rawInitialBudgetKiB * 1024);
assertBudget("largest initial JavaScript chunk raw", largestInitialJSRaw, 1_000 * 1024);
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@

import {
mapFrozenScrollbarDrag,
rebaseFrozenScrollbarDrag,
readCreationScrollbarGeometry,
} from "../lib/useCreationTranscriptScrollbar";

Expand Down Expand Up @@ -36,12 +37,16 @@ const moved = mapFrozenScrollbarDrag(drag, 200);
eq(moved.thumbTop, 420, "drag follows pointer pixels on the frozen track");
eq(moved.scrollTop, 2_100, "drag maps through the frozen pointerdown overflow");

// Content can grow while virtual rows mount. The active drag must keep using
// the pointerdown geometry rather than changing sensitivity under the pointer.
// Content can grow while virtual rows mount. Rebase the active drag at the
// current physical position so the next pointer move cannot write through a
// stale extent while preserving the thumb's visible position.
const grown = readCreationScrollbarGeometry(800, 8_000);
if (!grown) throw new Error("expected grown scrollable geometry");
eq(grown.overflow, 7_200, "content growth would otherwise change the live mapping");
eq(mapFrozenScrollbarDrag(drag, 200).scrollTop, 2_100, "content growth cannot remap an active drag");
const rebased = rebaseFrozenScrollbarDrag(drag, grown, 2_100, 200);
eq(rebased.startY, 200, "geometry rebase pins the drag to the latest pointer sample");
eq(rebased.startThumbTop, 210, "geometry rebase preserves the current physical scroll ratio");
eq(mapFrozenScrollbarDrag(rebased, 300).scrollTop, 3_100, "subsequent drag movement uses the current extent");

eq(mapFrozenScrollbarDrag(drag, -1_000).thumbTop, 0, "drag clamps at the top");
eq(mapFrozenScrollbarDrag(drag, 2_000).thumbTop, 640, "drag clamps at the bottom");
Expand Down
6 changes: 6 additions & 0 deletions desktop/frontend/src/__tests__/frontend-diagnostics.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,5 +154,11 @@ assert.deepEqual(analyzeFrontendDiagnosticAnomalies([
{ t: 0, type: "navigation.begin", intent: 9 },
{ t: 1, type: "navigation.settle", intent: 9, outcome: "failed" },
]), [], "a failed data terminal may release its mask without a paint-ready false positive");
assert.deepEqual(analyzeFrontendDiagnosticAnomalies([
{ t: 0, type: "transcript.scroll-anomaly", transactionId: 40, result: "restore-anchor", reverseDisplacement: 2.01 },
{ t: 1, type: "transcript.scroll-anomaly", transactionId: 40, result: "restore-anchor", reverseDisplacement: 27_104.41 },
]), [
{ code: "unauthorized-scroll-reversal", transactionId: 40, maxReverseDisplacement: 27_104.41 },
], "large reader reversals are surfaced in the diagnostic summary");

console.log("frontend diagnostics tests passed");
15 changes: 15 additions & 0 deletions desktop/frontend/src/__tests__/markdown-pipeline.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,21 @@ for (const [name, text] of Object.entries(fixtures)) {
eq(sliced, expected, `${name}: sliced blocks render identically (${blocks.length} blocks)`);
}

// Empty fenced blocks are formatting placeholders. They must not create a
// bordered CodeViewer, while comment-only and whitespace-adjacent blocks stay
// visible as real code content.
{
const emptyFences = "Before\n\n```\n\n```\n\nAfter\n\n```ts\n \n```";
const html = renderCurrent(emptyFences);
ok(!html.includes("code-block"), "empty fenced blocks do not render phantom code cards");
ok(html.includes("Before") && html.includes("After"), "text around empty fenced blocks remains visible");

const comments = "```ts\n// keep this comment\nconst stable = true;\n```";
const commentHtml = renderCurrent(comments);
ok(commentHtml.includes("keep this comment"), "non-empty comment code blocks remain visible");
ok(commentHtml.includes("code-block"), "non-empty comment code keeps its code-block surface");
}

// Block keys are stable top-level indexes.
{
const blocks = parseMarkdownToBlocks("one\n\ntwo\n\nthree");
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,25 @@ export const unloadedQuestionJumpReplay = {
{ firstTurn: 1, lastTurn: 994, hasOlderHistory: false, rowCount: 994 },
] as const,
} as const;

// Anonymous scroll evidence distilled from the Windows field report
// `reasonix-frontend-diagnostics-8a5de879.json`. Text, paths and stable row IDs
// are intentionally omitted; only the geometry needed to prevent regression is
// retained.
export const field9711ScrollReplay = {
buildCommit: "d9cd713",
viewport: 555,
direction: -1,
result: "restore-anchor",
transactions: [
{ id: 40, maxReverse: 2_593.02, extentDelta: 5_185 },
{ id: 54, maxReverse: 299.68, extentDelta: 589 },
{ id: 59, maxReverse: 7_252.06, extentDelta: 14_492 },
{ id: 62, maxReverse: 4_586.03, extentDelta: 9_163 },
{ id: 64, maxReverse: 5_685.71, extentDelta: 11_371 },
{ id: 66, maxReverse: 6_836.19, extentDelta: 13_700 },
{ id: 71, maxReverse: 5_342.22, extentDelta: 10_692 },
{ id: 75, maxReverse: 5_217.14, extentDelta: 10_497 },
{ id: 81, maxReverse: 27_104.41, extentDelta: 51_140 },
],
} as const;
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import { readFileSync } from "node:fs";
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";
import { isSupportedFrontendDiagnosticSchemaVersion } from "../lib/frontendDiagnostics";
import { field9711ScrollReplay } from "./transcript-diagnostic-replay.fixtures";

type Fixture = {
schemaVersion: number;
Expand All @@ -30,3 +31,8 @@ for (const name of names) {
}

console.log("transcript anonymous geometry replay fixtures passed");

assert.equal(field9711ScrollReplay.buildCommit, "d9cd713", "field replay is tied to the reported main-v2 build");
assert.equal(field9711ScrollReplay.transactions.length, 9, "field replay retains every unauthorized reversal transaction");
assert.equal(Math.max(...field9711ScrollReplay.transactions.map((entry) => entry.maxReverse)), 27_104.41, "field replay retains the largest reported reversal");
assert.ok(field9711ScrollReplay.transactions.every((entry) => entry.maxReverse > 2 && entry.extentDelta > 0), "field replay keeps only geometry reversals beyond tolerance");
Original file line number Diff line number Diff line change
Expand Up @@ -139,6 +139,10 @@ ok(newestSurface === null, "only C's own terminal releases the latest surface");
rowTop >= el.scrollTop - 100 && rowTop <= el.scrollTop + el.clientHeight,
`jump to question ${targetIndex + 1} lands its row inside the viewport (rowTop ${rowTop}, scrollTop ${el.scrollTop})`,
);
if (targetIndex > 0 && targetIndex < 39 && rowTop !== null) {
ok(Math.abs(rowTop - el.scrollTop - 16) <= 1,
`jump to question ${targetIndex + 1} keeps the selected row near the 16px top margin`);
}
const expectedText = `question ${targetIndex}`;
ok(anchor.textContent?.includes(expectedText) ?? false, `jump to question ${targetIndex + 1} mounts the selected question content`);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -767,6 +767,35 @@ check(scrollElement.dataset.nativeScrollbarDrag === undefined,
check(String(arbiter?.modeRef.current) === "tail-follow",
"a generation reset cannot retain native-thumb ownership");

// A delayed WebView2 range replacement can unmount the logical anchor for a
// paint while the native extent is collapsed. Keep the accepted viewport
// visually held from the native scroll delta even though there is no anchor
// rect to measure yet; otherwise the replacement flashes stale/duplicate
// transcript pixels until the task settles.
await act(async () => arbiter?.reset());
scrollExtent = 5_000;
scrollElement.scrollTop = 2_000;
rowElement.getBoundingClientRect = () => rectAt(20);
await act(async () => arbiter?.deliverScroll());
await act(async () => arbiter?.releaseTailFollow());
await act(async () => arbiter?.onWheelIntent({
ctrlKey: false,
deltaMode: 0,
deltaX: 0,
deltaY: 24,
target: scrollElement,
} as React.WheelEvent<HTMLElement>));
scrollExtent = 4_000;
scrollElement.scrollTop = 1_000;
rowElement.remove();
reboundCoverageRow.getBoundingClientRect = () => rectAt(2_000);
scrollElement.append(reboundCoverageRow);
await act(async () => arbiter?.deliverScroll());
check(scrollElement.dataset.transcriptReaderVisualGuard === "true",
"a blank range replacement keeps a visual hold while the logical anchor is unmounted");
reboundCoverageRow.remove();
scrollElement.append(rowElement);

await act(async () => root.unmount());
dom.window.close();

Expand Down
13 changes: 13 additions & 0 deletions desktop/frontend/src/__tests__/transcript-scroll-writer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -106,6 +106,19 @@ equal(writer.write({
equal(records[4]?.rejectedReason, "duplicate-revision-phase", "duplicate phases are diagnosable");

geometryRevisionRef.current = 10;
element.dataset.transcriptReaderIntent = "true";
geometryRevisionRef.current = 12;
equal(writer.write({
owner: "anchor-compensation",
operation: "scrollTo",
top: 300,
reason: "reader-intent-guard",
expectedSurfaceGeneration: 4,
expectedOwnershipEpoch: 7,
expectedGeometryRevision: 12,
}), false, "anchor compensation cannot write while reader intent owns the viewport");
equal(records[5]?.rejectedReason, "reader-intent-owner", "reader-intent suppression is diagnosable");
delete element.dataset.transcriptReaderIntent;

modeRef.current = "native-thumb";
equal(writer.write({
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import assert from "node:assert/strict";
import { createTranscriptSurfaceTransactions } from "../lib/transcriptSurfaceTransaction";

const events: Array<{ type: string; fields: Record<string, unknown> }> = [];
const previous = (globalThis as { window?: Window }).window;
const windowStub = {
__REASONIX_TRANSCRIPT_SCROLL_DIAGNOSTIC__: (type: string, fields: Record<string, unknown>) => events.push({ type, fields }),
} as unknown as Window;
(globalThis as { window?: Window }).window = windowStub;

const transactions = createTranscriptSurfaceTransactions();
const first = transactions.begin({
kind: "reader-prepend",
surfaceGeneration: 3,
ownershipEpoch: 8,
geometryRevision: 13,
mutationSeq: 21,
anchor: { rowKey: "row-a", logicalIndex: 44, viewportOffset: 16 },
});
assert.equal(first.token, 1);
assert.equal(transactions.isCurrent(first.token), true);
assert.equal(transactions.update(first.token, { phase: "mutating", mutationSeq: 22 }, "prepend"), true);
assert.equal(transactions.update(first.token - 1, { phase: "settling" }), false);
assert.equal(transactions.finish(first.token - 1, "committed"), false);
assert.equal(transactions.finish(first.token, "committed"), true);
assert.equal(transactions.current(), null);
assert.deepEqual(events.map((event) => event.fields.result), ["begin", "prepend", "committed"]);
assert.equal(events[0]?.fields.anchorIndex, 44);
assert.equal(events[0]?.fields.anchorOffset, 16);

(globalThis as { window?: Window }).window = previous;
console.log("transcript surface transaction tests passed");
Loading