mirror of
https://github.com/anomalyco/opencode.git
synced 2026-08-20 23:43:26 +00:00
fix(app): stabilize session navigation (#42477)
This commit is contained in:
parent
72d37cda81
commit
d698746951
6 changed files with 97 additions and 24 deletions
|
|
@ -27,6 +27,30 @@ test("navigates to a subagent child session missing from the session list", asyn
|
|||
await expect(titlebarRight.getByRole("button", { name: "Toggle review" })).toHaveCount(1)
|
||||
})
|
||||
|
||||
test("keeps the parent visible while child lineage resolves", async ({ page }) => {
|
||||
await setup(page)
|
||||
const requested = Promise.withResolvers<void>()
|
||||
const release = Promise.withResolvers<void>()
|
||||
await page.route(
|
||||
(url) => url.pathname === `/api/session/${childID}` && url.port === (process.env.PLAYWRIGHT_SERVER_PORT ?? "4096"),
|
||||
async (route) => {
|
||||
requested.resolve()
|
||||
await release.promise
|
||||
await route.fallback()
|
||||
},
|
||||
)
|
||||
await page.goto(sessionHref(parentID))
|
||||
await expectSessionTitle(page, parentTitle)
|
||||
|
||||
await page.locator(`a[href="${sessionHref(childID)}"]`).click()
|
||||
await requested.promise
|
||||
await Promise.all([expect(page).toHaveURL(sessionHref(parentID)), expectSessionTitle(page, parentTitle)]).finally(
|
||||
() => release.resolve(),
|
||||
)
|
||||
|
||||
await expectSessionTitle(page, taskDescription)
|
||||
})
|
||||
|
||||
test("shows the not found fallback when the viewed session is deleted", async ({ page }) => {
|
||||
const events: EventPayload[] = []
|
||||
await setup(page, () => events.splice(0, 1))
|
||||
|
|
|
|||
|
|
@ -32,6 +32,11 @@ export function DirectoryDataProvider(
|
|||
if (props.server) return sessionHref(props.server, sessionID)
|
||||
return `/${slug()}/session/${sessionID}`
|
||||
}
|
||||
const navigateToSession = async (sessionID: string) => {
|
||||
const session = serverSync.session
|
||||
await Promise.allSettled([session.lineage.resolve(sessionID), session.sync(sessionID)])
|
||||
navigate(href(sessionID))
|
||||
}
|
||||
|
||||
createEffect(() => {
|
||||
// A draft lives at /new-session?draftId=… and has no directory segment to normalize.
|
||||
|
|
@ -61,7 +66,7 @@ export function DirectoryDataProvider(
|
|||
data={sync().data}
|
||||
directory={directory}
|
||||
sessionID={params.id}
|
||||
onNavigateToSession={(sessionID: string) => navigate(href(sessionID))}
|
||||
onNavigateToSession={navigateToSession}
|
||||
onSessionHref={href}
|
||||
>
|
||||
<LocalProvider>{props.children}</LocalProvider>
|
||||
|
|
|
|||
|
|
@ -1,9 +1,8 @@
|
|||
import type { SessionInfo } from "@opencode-ai/client/promise"
|
||||
import { preloadMarkdown } from "@opencode-ai/session-ui/markdown-cache"
|
||||
import { useDialog } from "@opencode-ai/ui/context/dialog"
|
||||
import { skipToken, useQuery } from "@tanstack/solid-query"
|
||||
import { DateTime } from "luxon"
|
||||
import { type Accessor, createEffect, createMemo, createRoot, type JSX, startTransition } from "solid-js"
|
||||
import { type Accessor, createEffect, createMemo, type JSX, startTransition, untrack } from "solid-js"
|
||||
import { produce } from "solid-js/store"
|
||||
import { useCommand } from "@/context/command"
|
||||
import {
|
||||
|
|
@ -113,26 +112,7 @@ export function createHomeSessionsController(home: HomeController) {
|
|||
const key = `${ServerConnection.key(conn)}\0${record.session.id}`
|
||||
if (prefetched.has(key)) return
|
||||
prefetched.add(key)
|
||||
createRoot((dispose) => {
|
||||
try {
|
||||
void ctx.sync.session
|
||||
.sync(record.session.id)
|
||||
.then(() =>
|
||||
Promise.all(
|
||||
(ctx.sync.session.data.message[record.session.id] ?? []).flatMap((message) =>
|
||||
(ctx.sync.session.data.part[message.id] ?? []).flatMap((part) => {
|
||||
if (part.type !== "text" || !part.text) return []
|
||||
return preloadMarkdown(part.text, part.id)
|
||||
}),
|
||||
),
|
||||
),
|
||||
)
|
||||
.catch(() => {})
|
||||
.finally(dispose)
|
||||
} catch {
|
||||
dispose()
|
||||
}
|
||||
})
|
||||
void untrack(() => ctx.sync.session.sync(record.session.id)).catch(() => {})
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -199,6 +179,7 @@ export function createHomeSessionsController(home: HomeController) {
|
|||
const directory = project?.worktree ?? session.location.directory
|
||||
const ctx = home.server.focusedContext()
|
||||
if (!ctx) return
|
||||
ctx.sync.session.remember(session)
|
||||
ctx.projects.open(directory)
|
||||
if (options?.background) {
|
||||
tabs.addSessionTab({ server: connKey, sessionId: session.id })
|
||||
|
|
|
|||
43
packages/app/test-browser/auto-scroll.test.ts
Normal file
43
packages/app/test-browser/auto-scroll.test.ts
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
import { expect, test } from "bun:test"
|
||||
import { createAutoScroll } from "@opencode-ai/ui/hooks"
|
||||
import { createRoot } from "solid-js"
|
||||
|
||||
test("restores bottom anchoring when Suspense reattaches the scroll viewport", async () => {
|
||||
const main = document.createElement("main")
|
||||
const surface = document.createElement("div")
|
||||
const viewport = document.createElement("div")
|
||||
const content = document.createElement("div")
|
||||
surface.append(viewport)
|
||||
viewport.append(content)
|
||||
main.append(surface)
|
||||
document.body.append(main)
|
||||
|
||||
let scrollTop = 200
|
||||
Object.defineProperties(viewport, {
|
||||
clientHeight: { value: 100 },
|
||||
scrollHeight: { value: 300 },
|
||||
scrollTop: {
|
||||
get: () => scrollTop,
|
||||
set: (value: number) => {
|
||||
scrollTop = Math.min(value, 200)
|
||||
},
|
||||
},
|
||||
})
|
||||
|
||||
const dispose = createRoot((dispose) => {
|
||||
const scroll = createAutoScroll({ working: () => true })
|
||||
scroll.scrollRef(viewport)
|
||||
scroll.contentRef(content)
|
||||
return dispose
|
||||
})
|
||||
|
||||
surface.remove()
|
||||
viewport.scrollTop = 0
|
||||
main.append(surface)
|
||||
await Promise.resolve()
|
||||
|
||||
expect(viewport.scrollTop).toBe(200)
|
||||
|
||||
dispose()
|
||||
main.remove()
|
||||
})
|
||||
|
|
@ -162,7 +162,7 @@ export function PromptInputV2(props: PromptInputV2Props) {
|
|||
spellcheck={state.mode === "normal"}
|
||||
// @ts-expect-error
|
||||
autocomplete="off"
|
||||
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none empty:before:content-['\\200B'] [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
|
||||
class="relative z-10 block min-h-[60px] max-h-[180px] w-full overflow-y-auto whitespace-pre-wrap bg-transparent px-4 pt-4 pb-2 text-[13px] font-[440] leading-5 text-v2-text-text-base focus:outline-none empty:before:content-['\200B'] [&_[data-mention=file]]:text-syntax-property [&_[data-mention=agent]]:text-syntax-type [&_[data-mention=reference]]:text-syntax-keyword"
|
||||
classList={{ "font-mono!": state.mode === "shell", "opacity-50": props.disabled }}
|
||||
style={{
|
||||
"unicode-bidi": state.mode === "normal" ? "plaintext" : undefined,
|
||||
|
|
|
|||
|
|
@ -186,6 +186,26 @@ export function createAutoScroll(options: AutoScrollOptions) {
|
|||
},
|
||||
)
|
||||
|
||||
createEffect(() => {
|
||||
const el = store.scrollRef
|
||||
if (!el) return
|
||||
const root = el.closest("main")
|
||||
if (!root) return
|
||||
let connected = el.isConnected
|
||||
const contains = (node: Node) => node === el || (node instanceof Element && node.contains(el))
|
||||
const observer = new MutationObserver((records) => {
|
||||
records.forEach((record) => {
|
||||
if ([...record.removedNodes].some(contains)) connected = false
|
||||
if (!record.addedNodes.length || ![...record.addedNodes].some(contains)) return
|
||||
const reattached = !connected
|
||||
connected = true
|
||||
if (reattached) scrollToBottom(false)
|
||||
})
|
||||
})
|
||||
observer.observe(root, { childList: true })
|
||||
onCleanup(() => observer.disconnect())
|
||||
})
|
||||
|
||||
createEffect(
|
||||
on(options.working, (working: boolean) => {
|
||||
settling = false
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue