Skip to content
Merged
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
12 changes: 12 additions & 0 deletions apps/desktop/src/renderer/styles/composer.css
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,18 @@
max-width: var(--maka-reading-measure);
}

/* Reaching the transcript tail fades the scroll-to-bottom control while the
composer below it stays still. Both otherwise share the dock's filtered
compositing surface, so Chromium may re-raster the thin context-gauge paths
for that opacity transition: their DOM box does not move, but the icon
visibly hops by a device pixel. Keep the fading control in its own small
paint/compositing boundary. `astryx-chat-layout-scroll-button` is Astryx's
published theme class; the child is its fixed 32px transition container. */
.astryx-chat-layout-scroll-button > div {
contain: layout style paint;
will-change: opacity, transform;
}

.maka-composer-queue {
width: min(var(--maka-reading-measure), calc(100% - (2 * var(--space-6))));
max-width: var(--maka-reading-measure);
Expand Down
48 changes: 45 additions & 3 deletions apps/desktop/stories/app-shell.stories.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2071,15 +2071,21 @@ const HISTORY_BATCH = 4;
const HISTORY_BATCHES_AVAILABLE = 8;

/** A settled transcript with a turn the play function can make arrive. */
function SettledTranscriptHarness({ turns }: { turns: number }) {
function SettledTranscriptHarness({
turns,
composer,
}: {
turns: number;
composer?: Partial<ComposerProps>;
}) {
const [extra, setExtra] = useState(0);
useEffect(() => {
appendTurn = () => setExtra((count) => count + 1);
return () => {
appendTurn = undefined;
};
}, []);
return <ComposedShell chat={{ messages: transcriptTurns(0, turns + extra) }} />;
return <ComposedShell chat={{ messages: transcriptTurns(0, turns + extra) }} composer={composer} />;
}

/** The history seam is two props: `hasOlderHistory`, and a loader that prepends. */
Expand Down Expand Up @@ -2135,9 +2141,31 @@ export const TailFollowsGrowthOutsideTurns: Story = {
};

export const ReaderScrolledUpIsNotPulledBack: Story = {
render: () => <SettledTranscriptHarness turns={12} />,
render: () => (
<SettledTranscriptHarness
turns={12}
composer={{
contextUsage: {
usageTokens: 37_000,
declaredContextWindow: 100_000,
onOpen: noop,
},
}}
/>
),
play: async () => {
const root = tailScroller();
const contextGauge = document.querySelector<HTMLButtonElement>(
'button[aria-label="打开用量追踪"]',
);
const scrollButtonPaintBoundary = dockButton().parentElement;
if (!contextGauge?.querySelector('svg') || !scrollButtonPaintBoundary) {
throw new Error('the context gauge or scroll-button paint boundary is missing');
}
const boundaryStyle = getComputedStyle(scrollButtonPaintBoundary);
// Chromium canonicalizes `layout style paint` to the equivalent `content`.
expect(boundaryStyle.contain).toBe('content');
expect(boundaryStyle.willChange).toContain('opacity');
await waitFor(() => expect(tailMetrics().distance).toBeLessThanOrEqual(4));

root.scrollTop -= 500;
Expand Down Expand Up @@ -2169,6 +2197,20 @@ export const ReaderScrolledUpIsNotPulledBack: Story = {
JSON.stringify({ anchorTurnId, anchorTop, afterTop, ...tailMetrics() }),
).toBeLessThanOrEqual(4);
});

// Returning to the tail fades the dock button. That transition must not
// re-raster the context gauge on the same compositing surface (#4973).
// Geometry alone cannot see a device-pixel repaint, so the boundary above
// pins the causal contract; these samples separately ensure the fix never
// turns into a real footer movement.
const iconTops: number[] = [];
root.scrollTop = root.scrollHeight;
root.dispatchEvent(new Event('scroll'));
for (let frame = 0; frame < 16; frame += 1) {
await painted(1);
iconTops.push(contextGauge.querySelector('svg')!.getBoundingClientRect().top);
}
expect(Math.max(...iconTops) - Math.min(...iconTops)).toBeLessThanOrEqual(0.25);
},
};

Expand Down