fix(app): keep inbox scroll position when opening a session (#60356)

This commit is contained in:
Dax
2026-10-29 16:23:18 -04:00
committed by GitHub
Unverified
parent c24760c70a
commit 51625f7709
4 changed files with 56 additions and 6 deletions
+41
View File
@@ -4,6 +4,7 @@ import {
NO_PROVIDER,
REMOTE_SERVER,
SERVER,
T0,
draftHref,
expectPath,
holdRoute,
@@ -1356,3 +1357,43 @@ async function emptySidePanel(page: Page) {
return { beta, alphaTab: page.locator(`[data-titlebar-tab-slot]:has(a[href="${sessionHref(alpha.id)}"])`) }
}
test("opening a session from the inbox keeps the list's scroll position and rows", async ({ page }) => {
await page.setViewportSize({ width: 1280, height: 480 })
const sessions = Array.from({ length: 24 }, (_, index) => ({
id: `ses_inbox_${String(index).padStart(2, "0")}`,
title: `Inbox session ${index}`,
time: { created: T0, updated: T0 + index, idle: T0 + index },
}))
await mockWorkspace(page, {
name: "Inbox",
sessions,
seed: {
settings: { appearance: { tabLayout: "inbox" } },
tabs: [],
// Every seeded session finished a run after the inbox was first used, so Recent lists them all.
storage: { "opencode.global.dat:sidebar.inbox": { workingExpanded: true, since: 0 } },
},
})
await page.goto("/")
const list = page.locator('[data-slot="sidebar-inbox-scroll"]')
const items = list.locator('[data-slot="sidebar-inbox-item"]')
const target = items.filter({ hasText: "Inbox session 0" })
await expect(items).toHaveCount(sessions.length)
await list.evaluate((element) => element.scrollTo(0, element.scrollHeight))
await expect(target).toBeInViewport({ ratio: 1 })
const offset = await list.evaluate((element) => element.scrollTop)
expect(offset).toBeGreaterThan(0)
const row = await target.elementHandle()
await target.click()
await expectPath(page, sessionHref("ses_inbox_00"))
await expect(target).toHaveAttribute("data-state", "active")
// Opening a session loads it into the client, which updates every section.
await expect(page.locator('[data-component="composer-editor"]')).toBeEditable()
expect(await target.evaluate((element, before) => element === before, row)).toBe(true)
expect(await list.evaluate((element) => element.scrollTop)).toBe(offset)
})
+6
View File
@@ -321,6 +321,12 @@ const Group = HttpApiGroup.make("mock")
success: NoContent,
}),
)
.add(
HttpApiEndpoint.post("sessionView", "/api/session/:sessionID/view", {
params: SessionParams,
success: NoContent,
}),
)
.add(
HttpApiEndpoint.post("sessionShell", "/api/session/:sessionID/shell", {
params: SessionParams,
+1
View File
@@ -1315,6 +1315,7 @@ function mockHandlers(
}),
sessionRemove: () => noContent,
sessionShell: () => noContent,
sessionView: () => noContent,
sessionForm: (ctx) =>
Effect.succeed({
data: resolve(config.forms ?? []).filter(
+8 -6
View File
@@ -103,8 +103,10 @@ export function SidebarInbox() {
)
}
const rows = (list: InboxEntry[]) => (
<For each={list.map((entry) => entry.key)}>
// Takes an accessor so only the For tracks the section. Reading it here would recreate the For, remounting every
// row on any session change and resetting the list's scroll.
const rows = (list: () => InboxEntry[]) => (
<For each={list().map((entry) => entry.key)}>
{(key) => (
<Show when={inbox.byKey().get(key)}>
{(entry) => (
@@ -133,7 +135,7 @@ export function SidebarInbox() {
<Show when={sections().needs.length > 0}>
<div class="flex shrink-0 flex-col gap-px pb-3">
<SectionHeader label={language.t("sidebar.inbox.section.needsAttention")} count={sections().needs.length} />
{rows(sections().needs)}
{rows(() => sections().needs)}
</div>
</Show>
<Show
@@ -153,12 +155,12 @@ export function SidebarInbox() {
<Show when={sections().needs.length > 0}>
<SectionHeader label={language.t("sidebar.inbox.section.recent")} />
</Show>
{rows(sections().inbox)}
{rows(() => sections().inbox)}
</Show>
<Show when={sections().archived.length > 0}>
<div class="flex shrink-0 flex-col gap-px pt-3">
<SectionHeader label={language.t("sidebar.inbox.section.archived")} />
{rows(sections().archived)}
{rows(() => sections().archived)}
</div>
</Show>
</div>
@@ -176,7 +178,7 @@ export function SidebarInbox() {
/>
<div class="flex min-h-0 flex-col gap-px overflow-y-auto">
{/* A collapsed shelf still shows the session that is open. */}
{rows(sections().working.filter((entry) => state.workingExpanded || inbox.selected(entry)))}
{rows(() => sections().working.filter((entry) => state.workingExpanded || inbox.selected(entry)))}
</div>
</div>
</Show>