From bac474aaa0e2d1b48217c6c542b37469e827f61e Mon Sep 17 00:00:00 2001 From: "opencode-agent[bot]" <219766164+opencode-agent[bot]@users.noreply.github.com> Date: Tue, 18 Aug 2026 17:01:15 +0000 Subject: [PATCH] fix(tui): scope session picker to active location (#43264) Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com> --- .../tui/src/component/dialog-session-list.tsx | 22 ++- .../test/cli/tui/dialog-session-list.test.tsx | 127 ++++++++++++++++++ 2 files changed, 143 insertions(+), 6 deletions(-) create mode 100644 packages/tui/test/cli/tui/dialog-session-list.test.tsx diff --git a/packages/tui/src/component/dialog-session-list.tsx b/packages/tui/src/component/dialog-session-list.tsx index cc0344dc6ab..a045a46fb35 100644 --- a/packages/tui/src/component/dialog-session-list.tsx +++ b/packages/tui/src/component/dialog-session-list.tsx @@ -23,6 +23,7 @@ import { useStorage } from "../context/storage" import { useConfig } from "../config" import { withTimestampedFallback } from "@opencode-ai/util/session-title-fallback" import { projectName } from "../util/project" +import { useLocation } from "../context/location" export function DialogSessionList() { const dialog = useDialog() @@ -36,6 +37,7 @@ export function DialogSessionList() { const sessionTabs = useSessionTabs() const config = useConfig().data const toast = useToast() + const activeLocation = useLocation() const [filter, setFilter] = createSignal("") const shortcuts = Keymap.useShortcuts() const [search, setSearch] = createDebouncedSignal("", 150) @@ -44,13 +46,21 @@ export function DialogSessionList() { initial: { allProjects: config.tabs?.scope !== "cwd" }, }) const allProjects = () => prefs.allProjects + const pickerLocation = () => + (route.data.type === "session" ? data.session.get(route.data.sessionID)?.location : undefined) ?? + activeLocation.ref ?? + data.location.default() const [searchResults, { mutate: setSearchResults }] = createResource( - () => ({ query: search().trim(), allProjects: allProjects() }), - async ({ query, allProjects }) => { + () => ({ + query: search().trim(), + allProjects: allProjects(), + location: pickerLocation(), + }), + async ({ query, allProjects, location }) => { try { - if (!data.location.info()) await data.location.sync() - const current = data.location.info() + if (!data.location.info(location)) await data.location.sync(location) + const current = data.location.info(location) if (!current) throw new Error("Location unavailable") const response = await client.api.session.list({ ...(allProjects @@ -78,7 +88,7 @@ export function DialogSessionList() { const currentSessionID = createMemo(() => (route.data.type === "session" ? route.data.sessionID : undefined)) const localSessions = createMemo(() => { const query = filter().trim().toLowerCase() - const current = data.location.info() + const current = data.location.info(pickerLocation()) const sessions = data.session .list() .filter( @@ -125,7 +135,7 @@ export function DialogSessionList() { return hint && local.session.slots().length > 0 ? [{ title: "switch", label: hint }] : [] }) const currentProjectName = createMemo(() => { - const current = data.location.info() + const current = data.location.info(pickerLocation()) if (!current) return "" const project = data.project.get(current.project.id) return projectName(project) ?? "" diff --git a/packages/tui/test/cli/tui/dialog-session-list.test.tsx b/packages/tui/test/cli/tui/dialog-session-list.test.tsx new file mode 100644 index 00000000000..9722416ad6e --- /dev/null +++ b/packages/tui/test/cli/tui/dialog-session-list.test.tsx @@ -0,0 +1,127 @@ +/** @jsxImportSource @opentui/solid */ +import { expect, test } from "bun:test" +import { testRender } from "@opentui/solid" +import { onMount } from "solid-js" +import { DialogSessionList } from "../../../src/component/dialog-session-list" +import { ConfigProvider } from "../../../src/config" +import { ArgsProvider } from "../../../src/context/args" +import { ClientProvider } from "../../../src/context/client" +import { DataProvider, useData } from "../../../src/context/data" +import { Keymap } from "../../../src/context/keymap" +import { LocalProvider } from "../../../src/context/local" +import { LocationProvider } from "../../../src/context/location" +import { PermissionProvider } from "../../../src/context/permission" +import { RouteProvider, useRoute } from "../../../src/context/route" +import { TuiAppProvider } from "../../../src/context/runtime" +import { SessionTabsProvider } from "../../../src/context/session-tabs" +import { StorageProvider, useStorage } from "../../../src/context/storage" +import { ThemeProvider } from "../../../src/context/theme" +import { DialogProvider, useDialog } from "../../../src/ui/dialog" +import { ToastProvider } from "../../../src/ui/toast" +import { createApi, createEventStream, createFetch, json } from "../../fixture/tui-client" +import { emptyThemeSource, tmpdir } from "../../fixture/fixture" +import { TestTuiContexts } from "../../fixture/tui-environment" +import { createTuiResolvedConfig } from "../../fixture/tui-runtime" + +test("scopes sessions to the active session location", async () => { + const active = "/tmp/opencode/project-b" + const events = createEventStream() + const requestedProjects: string[] = [] + const calls = createFetch((url) => { + if (url.pathname === "/api/location") { + const directory = url.searchParams.get("location[directory]") ?? process.cwd() + const project = directory === active ? "proj_b" : "proj_a" + return json({ directory, project: { id: project, directory, canonical: directory } }) + } + if (url.pathname !== "/api/session") return undefined + const project = url.searchParams.get("project") ?? "" + requestedProjects.push(project) + return json({ + data: [ + { + id: project === "proj_b" ? "ses_b" : "ses_a", + projectID: project, + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 2 }, + title: project === "proj_b" ? "Project B session" : "Project A session", + location: { directory: project === "proj_b" ? active : process.cwd() }, + }, + ], + cursor: {}, + }) + }, events) + const temporary = await tmpdir() + let storage!: ReturnType + + function Probe() { + const data = useData() + const dialog = useDialog() + const route = useRoute() + storage = useStorage() + onMount(() => { + data.session.remember({ + id: "ses_active", + projectID: "proj_b", + cost: 0, + tokens: { input: 0, output: 0, reasoning: 0, cache: { read: 0, write: 0 } }, + time: { created: 1, updated: 3 }, + title: "Active session", + location: { directory: active }, + }) + route.navigate({ type: "session", sessionID: "ses_active" }) + dialog.replace(() => ) + }) + return null + } + + const app = await testRender( + () => ( + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + ), + { width: 100, height: 30, kittyKeyboard: true }, + ) + app.renderer.start() + + try { + const frame = await app.waitForFrame((value) => value.includes("Project B session")) + expect(frame).not.toContain("Project A session") + expect(requestedProjects.at(-1)).toBe("proj_b") + } finally { + app.renderer.destroy() + await storage.flush() + await temporary[Symbol.asyncDispose]() + } +})