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
25 changes: 25 additions & 0 deletions apps/desktop/e2e/prompt-rail.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,19 @@ import type { Page } from '@playwright/test';
* motion resolves to — where the highlight lands, how wide each bar computes —
* and cannot assert that anything animated on the way there. The tick entrance
* has no end state to check and therefore no coverage here at all.
*
* Platform note: the pointer-reachability assertions in this file
* (`unreachableTicks`, `activeTickVisibility`) are load-bearing on macOS and
* nowhere else. macOS renders the chat scroller's vertical scrollbar as an
* overlay: it takes no layout space, so a rail parked on the scroller's right
* edge draws on top of it, but the scrollbar's hit region still intercepts
* the pointer — every tick under it renders yet never receives a click or
* hover, and the rail reads as gone. Linux's in-flow scrollbar moves the
* content column left instead, so the regression itself goes green in CI.
* What CI *does* catch is the platform-neutral geometry the fix rests on:
* the `stays inside the scrollport` test asserts the rail keeps clear
* of the measured dead band. Run this spec on a macOS machine before merging
* anything that touches the rail's edge or scrollbar.
*/

const RAIL_PROBE = `(() => {
Expand Down Expand Up @@ -167,6 +180,18 @@ test('the prompt rail stays inside the scrollport at every scroll position', asy
expect(probe!.insetTop, where).toBeGreaterThanOrEqual(0);
expect(probe!.insetBottom, where).toBeGreaterThanOrEqual(0);
expect(probe!.insetRight, where).toBeGreaterThanOrEqual(0);
// macOS's overlay scrollbar hit band starts ~14px in from the scroller's
// right edge; the tick bar must stay clear of it. Platform-neutral: a
// Linux in-flow scrollbar passes this trivially, but it is the geometry
// the macOS click-through fix rests on — pre-fix this read ~5px.
const barInset = await page.evaluate(() => {
const scroller = document.querySelector('[data-chat-scroll-container="true"]') as HTMLElement;
const bar = document.querySelector('.maka-prompt-rail-tick-bar') as HTMLElement;
const s = scroller.getBoundingClientRect();
const b = bar.getBoundingClientRect();
return Math.round(s.right - b.right);
});
expect(barInset, where).toBeGreaterThanOrEqual(14);
}
});

Expand Down
44 changes: 26 additions & 18 deletions apps/desktop/src/renderer/styles/prompt-rail.css
Original file line number Diff line number Diff line change
Expand Up @@ -27,8 +27,9 @@
engage while any transcript is on screen, and the rail does its own
centring from the measured band below.

`--maka-prompt-rail-scrollport` / `--maka-prompt-rail-dock` are measured in
prompt-anchor-rail.tsx; see the rail's own rule for what they buy. */
`--maka-prompt-rail-scrollport` / `--maka-prompt-rail-dock` /
`--maka-prompt-rail-height` are measured in prompt-anchor-rail.tsx; see the
rail's own rule for what they buy. */
.maka-prompt-rail-anchor {
position: sticky;
top: 0;
Expand All @@ -41,26 +42,32 @@

.maka-prompt-rail {
position: absolute;
right: var(--space-1);
/* The scrollport's lower band belongs to the sticky composer dock, so the
rail centres on — and is capped to — what is left above it. Centring on
the bare scrollport ran the lower ticks under the dock (122px of overlap
at 1240x617), over the frosted blur and the composer card.

Measured from the anchor's top edge, which is the scrollport's, so this is
an absolute position rather than an offset from a centreline that sticky
clamping can move. The fallbacks are the pre-measurement first paint: no
dock inset, and the window standing in for the scrollport. */
/* On macOS the chat scroller's vertical scrollbar is overlay: it takes no
layout space, but its hit region still intercepts the pointer, so a rail
parked under it renders yet reads as dead. Linux's in-flow scrollbar
moves the content column left instead, which is why the regression
sailed through CI green. Measured on macOS the dead band starts 14px in
from the scroller's right edge, so the rail rests at
`right: space-1 + space-2` (12px), clear of it, and hovering settles it
3px further inward — the old motion's translateX(3px) did the opposite
and pushed the ticks into the band. `translateY(-50%)` stays: it only
centres vertically and has no part in the hit-region problem. */
right: calc(var(--space-1) + var(--space-2));
top: calc((var(--maka-prompt-rail-scrollport, 100svh) - var(--maka-prompt-rail-dock, 0px)) / 2);
/* The -50% is the centring; the X offset is motion. The rail rests
tucked a few px toward the window edge and settles inward when the
pointer arrives, so reaching for it is met halfway. */
transform: translateY(-50%) translateX(3px);
transform: translateY(-50%);
max-height: calc(
var(--maka-prompt-rail-scrollport, 100svh) - var(--maka-prompt-rail-dock, 0px) - (2 * var(--space-2))
);
overflow-y: auto;
overscroll-behavior: contain;
/* The rail is 22px wide: a visible scrollbar crowds the ticks, and on macOS
the overlay scrollbar's hit region swallows the right half of every tick
for a beat after the rail scrolls itself. Wheel/trackpad scrolling still
works with the bar hidden. */
scrollbar-width: none;
::-webkit-scrollbar {
width: 0;
}
display: flex;
flex-direction: column;
align-items: flex-end;
Expand All @@ -73,13 +80,14 @@
opacity: var(--opacity-muted);
transition:
opacity var(--duration-base) var(--ease-out-strong),
transform var(--duration-emphasized) var(--ease-out-strong);
right var(--duration-emphasized) var(--ease-out-strong);
-webkit-app-region: no-drag;
}

.maka-prompt-rail:hover {
opacity: 1;
transform: translateY(-50%) translateX(0);
right: calc(var(--space-1) + var(--space-2) + 3px);
transform: translateY(-50%);
}

/* Ticks arrive rather than appear: a new prompt's tick slides in from the edge,
Expand Down
Loading