Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Add copy buttons to all
 blocks
(function() {
function addCopyButtons() {
document.querySelectorAll('pre code').forEach(function(codeBlock) {
if (codeBlock.parentElement.hasAttribute('data-copy-added')) return;
codeBlock.parentElement.setAttribute('data-copy-added', 'true');
var btn = document.createElement('button');
btn.textContent = 'Copy';
btn.style.cssText = 'position:absolute;top:4px;right:4px;padding:2px 8px;font-size:11px;background:#4ecdc4;border:none;border-radius:4px;color:#1a1a2e;cursor:pointer;opacity:0.7;transition:opacity 0.2s;';
btn.onmouseover = function() { this.style.opacity = '1'; };
btn.onmouseout = function() { this.style.opacity = '0.7'; };
btn.onclick = function() {
navigator.clipboard.writeText(codeBlock.textContent).then(function() {
btn.textContent = 'Copied!';
setTimeout(function() { btn.textContent = 'Copy'; }, 1500);
});
};
codeBlock.parentElement.style.position = 'relative';
codeBlock.parentElement.appendChild(btn);
});
}
addCopyButtons();
// Re-run on dynamic content
var observer = new MutationObserver(addCopyButtons);
observer.observe(document.body, { childList: true, subtree: true });
})();
}
} catch(__e) { console.warn('[Userscript:Add Copy Buttons to Code Blocks]', __e); }
})();
(function(){
try {
var __m = "github.com";
var __re = new RegExp('^' + "github\\.com" + '
fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Force GitHub README to respect dark mode (function() { var style = document.createElement('style'); style.textContent = ' .markdown-body { color-scheme: dark light; } .markdown-body pre { background: #161b22 !important; } .markdown-body code { background: rgba(110, 118, 129, 0.4) !important; } .markdown-body table th, .markdown-body table td { border-color: #30363d !important; } .markdown-body img { background: #0d1117; } .markdown-body blockquote { border-left-color: #8b949e; } .markdown-body hr { border-color: #30363d; } '; document.head.appendChild(style); })(); } } catch(__e) { console.warn('[Userscript:GitHub Dark Mode README Fix]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Highlight search terms from Google/DuckDuckGo/Bing referrer (function() { var ref = document.referrer; var terms = []; if (ref.includes('google.com') || ref.includes('duckduckgo.com') || ref.includes('bing.com')) { var url = new URL(ref); var q = url.searchParams.get('q') || url.searchParams.get('p'); if (q) { terms = q.split(/\s+/).filter(function(t) { return t.length > 2; }); } } if (terms.length === 0) return; var style = document.createElement('style'); style.textContent = '.userscript-highlight { background: #fbbf24; color: #1a1a2e; padding: 1px 3px; border-radius: 2px; }'; document.head.appendChild(style); function highlight(node) { if (node.nodeType === 3) { // text node var text = node.textContent; var found = false; terms.forEach(function(term) { var regex = new RegExp('(' + term.replace(/[.*+?^${}()|[\]\\]/g, '\\') + ')', 'gi'); if (regex.test(text)) { found = true; var frag = document.createDocumentFragment(); var parts = text.split(regex); parts.forEach(function(part, i) { if (i % 2 === 0) { frag.appendChild(document.createTextNode(part)); } else { var span = document.createElement('span'); span.className = 'userscript-highlight'; span.textContent = part; frag.appendChild(span); } }); node.parentNode.replaceChild(frag, node); } }); } else if (node.nodeType === 1 && node.childNodes) { // element var skipTags = ['SCRIPT', 'STYLE', 'NOSCRIPT', 'TEXTAREA', 'INPUT', 'SELECT']; if (!skipTags.includes(node.tagName)) { Array.from(node.childNodes).forEach(highlight); } } } highlight(document.body); // Re-highlight on dynamic content var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1 || node.nodeType === 3) highlight(node); }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Highlight Search Terms]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Strip utm_, fbclid, gclid, etc. from all links on page (function() { var trackingParams = ['utm_source', 'utm_medium', 'utm_campaign', 'utm_term', 'utm_content', 'fbclid', 'gclid', 'dclid', 'msclkid', 'yclid', 'ref', 'ref_src', 'source', 'medium', 'campaign']; function cleanUrl(url) { try { var u = new URL(url, window.location.origin); var changed = false; trackingParams.forEach(function(p) { if (u.searchParams.has(p)) { u.searchParams.delete(p); changed = true; } }); return changed ? u.toString() : url; } catch (e) { return url; } } function cleanLinks() { document.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } cleanLinks(); var observer = new MutationObserver(function(mutations) { mutations.forEach(function(m) { m.addedNodes.forEach(function(node) { if (node.nodeType === 1) { if (node.tagName === 'A') cleanLinks(); node.querySelectorAll('a[href]').forEach(function(a) { var clean = cleanUrl(a.href); if (clean !== a.href) a.href = clean; }); } }); }); }); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:Remove Tracking Parameters from Links]', __e); } })(); (function(){ try { var __m = "youtube.com"; var __re = new RegExp('^' + "youtube\\.com" + ' fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Auto-enable theater mode on YouTube (function() { function tryTheater() { var btn = document.querySelector('button[aria-label="Theater mode"], ytd-player #player button[title="Theater mode"]'); if (btn && !btn.classList.contains('activated')) { btn.click(); } } // Try immediately tryTheater(); // Try after navigation (SPA) var lastUrl = location.href; setInterval(function() { if (location.href !== lastUrl) { lastUrl = location.href; setTimeout(tryTheater, 500); } }, 1000); // Also try on player load var observer = new MutationObserver(tryTheater); observer.observe(document.body, { childList: true, subtree: true }); })(); } } catch(__e) { console.warn('[Userscript:YouTube Theater Mode Default]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Remove or un-stick sticky/fixed headers that block content (function() { function unstick() { document.querySelectorAll('header, nav, [role="banner"], .header, .navbar, .sticky, .fixed-top, [style*="position: fixed"], [style*="position:sticky"]').forEach(function(el) { if (el.style.position === 'fixed' || el.style.position === 'sticky' || getComputedStyle(el).position === 'fixed' || getComputedStyle(el).position === 'sticky') { el.style.position = 'static'; el.style.top = 'auto'; el.style.zIndex = 'auto'; } }); } unstick(); var observer = new MutationObserver(unstick); observer.observe(document.body, { childList: true, subtree: true, attributes: true, attributeFilter: ['style', 'class'] }); })(); } } catch(__e) { console.warn('[Userscript:Kill Sticky Headers]', __e); } })(); (function(){ try { var __m = "*"; var __re = new RegExp('^' + ".*" + ' fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han
, 'i'); if (__m === '*' || __re.test(location.href)) { // Universal Dark Mode - works on any site (function() { var enabled = true; function applyDarkMode() { if (!enabled) return; // Create style element if it doesn't exist var style = document.getElementById('universal-dark-mode-style'); if (!style) { style = document.createElement('style'); style.id = 'universal-dark-mode-style'; document.head.appendChild(style); } // Dark mode CSS - inverts colors but preserves images/video style.textContent = ' /* Invert everything except media */ html { filter: invert(1) hue-rotate(180deg) !important; background: #1a1a2e !important; } /* Restore images, videos, iframes, canvas */ img, video, iframe, canvas, svg, picture, [style*="background-image"] { filter: invert(1) hue-rotate(180deg) !important; } /* Preserve specific elements that should not be inverted */ .no-dark-mode, .no-dark-mode *, [data-theme="light"], [data-theme="light"], .ace_editor, .ace_editor *, .CodeMirror, .CodeMirror *, .monaco-editor, .monaco-editor *, .markdown-body pre, .markdown-body pre *, .highlight, .highlight *, pre code, pre code * { filter: none !important; } /* Fix common UI elements */ .modal, .popup, .dropdown-menu, .tooltip, .popover { filter: invert(1) hue-rotate(180deg) !important; background: #2d2d44 !important; border-color: #444 !important; } /* Scrollbars */ ::-webkit-scrollbar { background: #1a1a2e !important; } ::-webkit-scrollbar-thumb { background: #444 !important; } ::-webkit-scrollbar-thumb:hover { background: #555 !important; } /* Selection */ ::selection { background: #4ecdc4 !important; color: #1a1a2e !important; } ::-moz-selection { background: #4ecdc4 !important; color: #1a1a2e !important; } '; } function removeDarkMode() { var style = document.getElementById('universal-dark-mode-style'); if (style) style.remove(); } // Toggle with Alt+Shift+D document.addEventListener('keydown', function(e) { if (e.altKey && e.shiftKey && e.key === 'D') { e.preventDefault(); enabled = !enabled; if (enabled) { applyDarkMode(); console.log('[Universal Dark Mode] Enabled'); } else { removeDarkMode(); console.log('[Universal Dark Mode] Disabled'); } } }); // Apply on load applyDarkMode(); // Re-apply on dynamic content var observer = new MutationObserver(function(mutations) { if (enabled && !document.getElementById('universal-dark-mode-style')) { applyDarkMode(); } }); observer.observe(document.head, { childList: true }); console.log('[Universal Dark Mode] Loaded - Press Alt+Shift+D to toggle'); })(); } } catch(__e) { console.warn('[Userscript:Universal Dark Mode]', __e); } })(); })(); fix(ui): keep the prompt rail clickable on macOS by Astro-Han · Pull Request #2338 · apache/maka · GitHub
Skip to content

fix(ui): keep the prompt rail clickable on macOS - #2338

Merged
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test
Aug 6, 2026
Merged

fix(ui): keep the prompt rail clickable on macOS#2338
Astro-Han merged 1 commit into
mainfrom
fix/prompt-rail-macos-hit-test

Conversation

@Astro-Han

@Astro-HanAstro-Han commented Aug 6, 2026

Copy link
Copy Markdown
Contributor

问题

Prompt Rail 在新 main 上"消失"了:rail 正常渲染,但所有 tick 点击/hover 无响应。本地 macOS 上 prompt-rail E2E 3 挂 6 过,CI(Linux)93 个测试全绿。

根因(经对抗性审查与实测修正)

rail 贴死在 chat scroller 右缘(right: 4px + translateX(3px))。macOS 的 scrollbar 是 overlay——不占布局空间,rail 画在它上面,但scrollbar 命中区域依然拦截指针:

修复(2 个文件,+51/-18,组件零改动)

  • prompt-rail.css:rail 停靠 right: calc(space-1 + space-2)(12px,实测死区外,余量 8px);hover 向内微移 3px 改用 right(不再用 translateX 推入死区);保留 translateY(-50%) 纯 CSS 垂直居中(垂直位移与水平命中无关,JS 测高方案已废弃);隐藏 rail 自身 scrollbar(22px 宽的 rail 不需要可见滚动条,wheel 滚动保留)。
  • prompt-rail.spec.ts:平台注记如实描述机制;"stays inside the scrollport" 测试新增平台无关几何回归锁——tick bar 距 scroller 右缘 ≥ 14px(修复前实测 ~5px,会失败;已在旧几何下验证断言确实变红)。

验证

  • prompt-rail.spec.ts macOS 单 worker(CI 配置)9/9 通过(修复前 3 挂 6 过);
  • 全量 desktop e2e 93 个通过(并行);format/lint 干净;
  • 回归锁有效性:临时改回旧几何(right 4px)→ barInset 8px < 14 断言失败 ✓;
  • 本地 4 workers 并行下 "clicking" 偶发超时是后台窗口节流(配置注释已声明该本地并行问题),CI 单 worker 不触发。

对抗性审查结论(独立子代理)

  • 初版含 ~35 行 JS 测高(替代 translateY),经 4 路独立审查确认是过度设计——translateY 从未参与水平命中问题,已重构移除;
  • 初版注释"stays clear of the edge"/"无 transform"与几何不符,已按实测重写;
  • 余量从 1px(压线)提升到 8px,并把几何不变量写成 CI 可断言的回归锁。

@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 3d81dc5 to 6fb561cCompareAugust 6, 2026 14:49
The rail rested flush against the chat scroller's right edge. On macOS the
scroller's vertical scrollbar is overlay: it takes no layout space, so the
rail drew on top of it, but the scrollbar's hit region still intercepted
every pointer — ticks rendered yet never received a click or hover, and
the rail read as gone. #2215's translateX(3px) had pushed the tick centres
into that band; before it, the ticks were merely grazed. Linux's in-flow
scrollbar shifts the content column left instead, which is why the
regression sailed through CI green.
The rail now rests at right: space-1 + space-2 (12px) — clear of the
measured ~14px dead band — with the hover settle-in moved to the `right`
property (3px further inward); the vertical centring translateY(-50%)
stays, as it only centres vertically and has no part in the hit-region
problem. The rail's own scrollbar is hidden: at 22px wide the overlay
bar's hit region swallowed tick halves right after the rail scrolled
itself (wheel scrolling still works).
prompt-rail.spec.ts documents why the reachability assertions are
load-bearing on macOS, and the "stays inside the scrollport" test now
asserts the platform-neutral geometry the fix rests on (tick bar at
least 14px clear of the scroller's right edge — pre-fix it read ~5px).
The suite passes 9/9 on macOS under CI's single worker.
@Astro-Han
Astro-Hanforce-pushed the fix/prompt-rail-macos-hit-test branch from 6fb561c to 0608200CompareAugust 6, 2026 14:49
@Astro-Han
Astro-Han marked this pull request as ready for review August 6, 2026 15:03
@Astro-Han
Astro-Han merged commit 135c295 into mainAug 6, 2026
12 checks passed
@Astro-Han
Astro-Han deleted the fix/prompt-rail-macos-hit-test branch August 6, 2026 15:06
ARE404 added a commit to ARE404/maka-agent that referenced this pull request Aug 13, 2026
apache#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— apache#2161 pinned it against a containing block as tall as the conversation,
apache#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in apache#2462, and the multi-prompt fixtures it ran on in apache#2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
Astro-Han pushed a commit that referenced this pull request Aug 13, 2026
…2923)
* fix(ui): give the prompt rail's tick bars a box again
#2580 moved the rail's tick onto Astryx's Button. The bar the tick draws
was a direct child of the flex tick and got blockified; the Button wraps
its children in a label span, so the bar went back to normal flow as an
inline box. An inline box takes no width or height, so every bar computed
to 0x0 and the rail shipped invisible in 0.1.9 and 0.1.10 — present in the
DOM, painting nothing.
`display: block` on the bar restores it. Measured on the new fixture at
1280x800: the rail's box goes from 8px wide (its own padding, ticks
contributing nothing) back to the designed 22px.
This is the third time the rail has failed by rendering and not painting
— #2161 pinned it against a containing block as tall as the conversation,
#2338 parked it under macOS's overlay scrollbar — and the second time it
reached a release. The e2e coverage that would have caught all three was
deleted in #2462, and the multi-prompt fixtures it ran on in #2656, so
this adds back the smallest thing that closes the gap:
- `chat-prompt-rail`, a plain 8-prompt conversation. The rail hides itself
below three prompts, so the shipped single-prompt fixture cannot show it
at all.
- `prompt-rail.spec.ts` with one test per past failure: bars have a real
box, the rail stays inside the scrollport at both scroll extremes, and a
tick is what the pointer lands on. Three tests where the deleted suite
had nine.
Verified the first test fails on the unfixed renderer with "Expected: > 0,
Received: 0" and passes with the fix. Neither a static CSS read nor a
jsdom unit test can see any of this: jsdom has no layout engine.
* fix(ui): make the prompt rail's hover and jump behave
Four things the rail got wrong once it was visible again, found by using it:
- A 4px gap between ticks was a band where the pointer was over the rail
and over no tick, so the dock-style hover falloff dropped out and picked
up again every few pixels of travel. The rail's `gap` moves into the
ticks' own `padding-block`: same pitch, hit boxes now tile.
- The hover preview waited 300ms before opening — Astryx's HoverCard
default, meant for a pointer crossing a wide row on its way somewhere
else. A tick is 22px of rail that nothing is on the way to, and the wait
is the one part of this hover with no motion in it. Now 120ms.
- The highlight glided 280ms to wherever a click landed, so crossing
twenty prompts read as the bar flying off across the rail. A click now
owns the highlight until its scroll settles: no glide, and the scroll
no longer walks the highlight through every prompt it passes.
- The first click into a session did nothing until the reader scrolled by
hand. See below.
That last one is a collision between Astryx's auto-follow lock and the
progressive transcript mount, and neither side is wrong on its own.
`useChatStreamScroll` unlocks on a scroll up, detected by comparing
scrollTop between events — but it ignores any scroll event that arrives
with a changed scrollHeight or offsetHeight, because Chrome fires those
when content resizes and they are not the reader moving. A jump into an
unmounted turn mounts it and the fill that follows changes scrollHeight
for several frames, so the jump's own scroll is invisible to the lock: it
stays on, and `scrollIfLocked` pulls the transcript back to the bottom.
Only a wheel gesture broke it, which takes a separate path in Astryx.
`holdJumpDestination` re-aims at the target on each height change until
the fill stops. The last of those scrolls lands with a stable height,
which is the one the lock finally reads as a scroll up. Measured on the
30-prompt fixture: clicking the first tick went to scrollTop 7042 (the
bottom) and now goes to 24 and holds.
The fixture grows from 8 prompts to 30 because the progressive mount's
initial window is 10 — at 8 the head of the transcript is already mounted
and the jump-into-unmounted-turns path never runs at all.
Coverage note: the e2e case for the first click is an end-to-end check,
not a guard. Whether the lock wins depends on which frame the fill lands
on relative to a smooth scroll still in flight, and it goes green against
the unfixed renderer often enough to be worthless as one. The guard is
the `holdJumpDestination` unit test, which drives the frames itself.
* fix(ui): own a rail jump through the mount instead of racing it
Review of #2923 found the jump's ownership bound to a clock rather than to
the navigation, and the e2e case that was supposed to guard it asserting
almost nothing. Both hold.
Jump ownership:
- A second click during a jump only replaced the target; the first click's
700ms timer still governed, and could clear the second jump mid-flight.
Each click now carries its own sequence and starts its own hold.
- The fixed window is gone. A hold runs until the progressive mount reports
the transcript filled AND nothing has moved for a few frames, so a long
transcript is never released mid-fill, and it ends the moment the reader
touches the transcript (wheel, touch, pointer, key) rather than outliving
their interest in it.
Chasing the "just release auto-follow" direction the review preferred found
that ChatLayout publishes no such seam, so this adds one — `unlockAutoFollow`
on `ChatLayoutContextValue`, exposing the scroll hook's existing `unlock`
(patch hunk + patches/README entry). It is necessary and it is not
sufficient, which the earlier framing got wrong:
- Astryx re-locks on any `scrollend` that settles near the bottom, and a
session that opens at the bottom produces exactly that while the mount is
still catching up. Releasing once at the click is undone before the jump
goes anywhere — traced: released at the click, landed at 154ms, dragged
back to the bottom by 166ms. The release is now re-asserted for the life
of the hold.
- Auto-follow is not the only thing moving the transcript. The progressive
mount's own scroll compensation holds the reader's position across each
fill step, and mounting the turn a jump asked for IS a fill step, so it
lands after the jump and restores the position the jump just left. That
one no seam can fix; it is what the hold is for.
Jumps also scroll instantly now, whatever the app's scroll-motion policy
says. A jump is a teleport the reader asked for, and an animated one does
not survive this surface: traced on the 30-prompt fixture, the smooth scroll
was cancelled by the mount's compensation and by the follow spring and
stalled two pixels from where it started.
Coverage:
- The first-click e2e case named the wrong turn (`[data-turn-id]` is the
first MOUNTED turn, whose top is already negative at the opening scroll
position, so an upper-bound-only check passed without the jump doing
anything). It now names `turn-prompt-rail-1`, bounds it on both sides, and
asserts that tick's `aria-current`.
- `emulateMedia` could not put that case on the production scroll path:
`resolveScrollMotionBehavior` collapses motion for ANY fixture, keyed on
`data-maka-e2e-fixture` rather than on the media query. Fixtures can now
ask for a behavior back (`scrollMotion`, per launch — it costs seconds of
settling per window, so only the case that needs it pays), with unit
coverage for the precedence: a fixture request never outranks a stated
preference for less motion.
- `holdJumpDestination`'s unit tests grew the two cases its rewrite is
about: it must not settle while the transcript is still filling, and it
must hand the transcript back the moment the reader touches it.
Verified 5/5 on the smooth-scroll fixture, where the previous revision lost
1 in 4. `quote-selection.spec.ts` flakes on this machine (1 in 4) at
upstream/main as well, unchanged by this branch.
Sign up for freeto join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant

@Astro-Han