From dc3ff4b7aa795c99e24bc4741848798330480112 Mon Sep 17 00:00:00 2001 From: Aarav Sareen <96787824+arvsrn@users.noreply.github.com> Date: Thu, 16 Jul 2026 15:21:42 +0530 Subject: [PATCH] fix(app): home page scroll target (#36664) Co-authored-by: Jay V Co-authored-by: Brendan Allan --- packages/app/src/pages/home.tsx | 301 +++++++++++------- packages/ui/src/components/scroll-view.css | 1 + .../ui/src/components/scroll-view.test.ts | 15 + packages/ui/src/components/scroll-view.tsx | 128 ++++++-- 4 files changed, 302 insertions(+), 143 deletions(-) diff --git a/packages/app/src/pages/home.tsx b/packages/app/src/pages/home.tsx index 7daf8f41bff..215696d7de6 100644 --- a/packages/app/src/pages/home.tsx +++ b/packages/app/src/pages/home.tsx @@ -5,6 +5,7 @@ import { createMemo, createResource, createRoot, + createSignal, For, Match, on, @@ -78,6 +79,23 @@ const HOME_SESSION_LIMIT = 64 const HOME_SESSION_HEADER_STICKY_TOP = 12 const HOME_SESSION_HEADER_TEXT_HEIGHT = 16 const HOME_SESSION_HEADER_FADE_DISTANCE = 16 + +function containHomeWheel(event: WheelEvent, viewport: HTMLElement) { + if (event.defaultPrevented || event.ctrlKey || !event.deltaY) return + if (!(event.target instanceof Element)) return + + const scrollable = event.target.closest("[data-scrollable]") + if ( + scrollable !== viewport && + scrollable && + (event.deltaY < 0 + ? scrollable.scrollTop > 0 + : scrollable.scrollTop < scrollable.scrollHeight - scrollable.clientHeight) + ) + return + + event.preventDefault() +} const SHOW_HOME_SESSION_ARCHIVE = false const HOME_ROW_LAYOUT = "flex min-w-0 w-full shrink-0 cursor-default items-center rounded-[6px] bg-transparent text-left transition-[background-color,color,box-shadow] duration-[120ms] ease-in-out focus-visible:outline-none" @@ -150,6 +168,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { let content: HTMLDivElement | undefined let positionFrame: number | undefined let resizeObserver: ResizeObserver | undefined + let stickyTop = HOME_SESSION_HEADER_STICKY_TOP const headerRefs = new Map() const headerOffsets = new Map() const [state, setState] = createStore({ @@ -208,6 +227,13 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { function updatePositionCache() { if (!viewport) return + const header = groups() + .map((group) => headerRefs.get(group.id)) + .find((el) => el !== undefined) + if (header && typeof getComputedStyle === "function") { + const top = Number.parseFloat(getComputedStyle(header).top) + if (Number.isFinite(top)) stickyTop = top + } groups().forEach((group) => { const el = headerRefs.get(group.id) if (!el) return @@ -223,7 +249,7 @@ function useHomeSessionHeaderOpacity(groups: () => HomeSessionGroup[]) { .slice(index + 1) .map((item) => headerOffsets.get(item.id)) .find((offset) => offset !== undefined) - const fadeEnd = HOME_SESSION_HEADER_STICKY_TOP + HOME_SESSION_HEADER_TEXT_HEIGHT + const fadeEnd = stickyTop + HOME_SESSION_HEADER_TEXT_HEIGHT const nextTop = nextOffset === undefined ? undefined : nextOffset - scrollTop const opacity = nextTop === undefined ? 1 : Math.max(0, Math.min(1, (nextTop - fadeEnd) / HOME_SESSION_HEADER_FADE_DISTANCE)) @@ -277,6 +303,9 @@ export function NewHome() { const marked = useMarked() const openSettings = useSettingsCommand() let focusSessionSearch: (() => void) | undefined + let sessionViewport: HTMLDivElement | undefined + const [sessionThumbTrack, setSessionThumbTrack] = createSignal() + const [sessionHoverTarget, setSessionHoverTarget] = createSignal() const [state, setState] = createStore({ search: "", searchFocused: false, @@ -604,127 +633,158 @@ export function NewHome() { } return ( -
-
- addProjects(conn, [directory])} - chooseProject={(conn) => void chooseProject(conn)} - editProject={editProject} - closeProject={(conn, directory) => { - const next = closeHomeProject( - selection(), - ServerConnection.key(conn), - global.ensureServerCtx(conn).projects, - directory, - ) - if (next) setSelection(next) - }} - clearNotifications={clearNotifications} - unseenCount={unseenCount} - openSettings={openSettings} - openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} - language={language} - /> - -
- { - focusSessionSearch = focus +
+ { + sessionViewport = el + sessionHeaderOpacity.setViewport(el) + }} + onScroll={(event) => sessionHeaderOpacity.update(event.currentTarget.scrollTop)} + onWheel={(event) => { + if (!sessionViewport) return + if (event.target instanceof Node && sessionViewport.contains(event.target)) return + containHomeWheel(event, sessionViewport) + }} + > +
+ addProjects(conn, [directory])} + chooseProject={(conn) => void chooseProject(conn)} + editProject={editProject} + closeProject={(conn, directory) => { + const next = closeHomeProject( + selection(), + ServerConnection.key(conn), + global.ensureServerCtx(conn).projects, + directory, + ) + if (next) setSelection(next) + }} + clearNotifications={clearNotifications} + unseenCount={unseenCount} + openSettings={openSettings} + openHelp={() => platform.openLink("https://opencode.ai/desktop-feedback")} + language={language} + onWheel={(event) => { + if (sessionViewport) containHomeWheel(event, sessionViewport) }} - onInput={(value) => setState("search", value)} - onFocus={() => setState("searchFocused", true)} - onClose={closeSearch} - onSelect={selectSearchSession} /> - sessionHeaderOpacity.update(event.currentTarget.scrollTop)} + +
- 0 && newSessionProject()}> -
- - {language.t("command.session.new")} - -
-
- - -
- } +
{ + if (sessionViewport) containHomeWheel(event, sessionViewport) + }} > - 0} - fallback={} - > -
- - {(group, index) => ( - <> - sessionHeaderOpacity.setHeaderRef(group.id, el)} - elevated={index() === 0} - /> -
- - {(record) => ( - - )} - -
- - )} -
+ { + focusSessionSearch = focus + }} + onInput={(value) => setState("search", value)} + onFocus={() => setState("searchFocused", true)} + onClose={closeSearch} + onSelect={selectSearchSession} + /> + 0 && newSessionProject()}> +
+ + {language.t("command.session.new")} +
- - -
- platform.openLink("https://opencode.ai/desktop-feedback")} - language={language} - /> -
+
+ {/* Sticky chrome for the portaled session scrollbar — matches old sessions ScrollView bounds */} + + + platform.openLink("https://opencode.ai/desktop-feedback")} + language={language} + /> + + ) } @@ -746,6 +806,7 @@ function HomeProjectColumn(props: { openSettings: () => void openHelp: () => void language: ReturnType + onWheel: (event: WheelEvent) => void }) { const global = useGlobal() const dialog = useDialog() @@ -762,8 +823,12 @@ function HomeProjectColumn(props: { return (