fix(tui): scope session picker to active location (#43264)

Co-authored-by: neriousy <34747899+neriousy@users.noreply.github.com>
This commit is contained in:
opencode-agent[bot]
2026-08-18 17:01:15 +00:00
committed by GitHub
Unverified
parent 98c717cb5b
commit bac474aaa0
2 changed files with 143 additions and 6 deletions
@@ -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) ?? ""
@@ -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<typeof useStorage>
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(() => <DialogSessionList />)
})
return null
}
const app = await testRender(
() => (
<TestTuiContexts paths={{ state: temporary.path }}>
<TuiAppProvider value={{ name: "test", version: "test", channel: "test" }}>
<StorageProvider>
<ArgsProvider>
<ConfigProvider config={createTuiResolvedConfig()}>
<Keymap.Provider>
<ToastProvider>
<RouteProvider>
<ClientProvider api={createApi(calls.fetch)}>
<PermissionProvider>
<DataProvider>
<LocationProvider>
<SessionTabsProvider>
<ThemeProvider mode="dark" source={emptyThemeSource}>
<LocalProvider>
<DialogProvider>
<Probe />
</DialogProvider>
</LocalProvider>
</ThemeProvider>
</SessionTabsProvider>
</LocationProvider>
</DataProvider>
</PermissionProvider>
</ClientProvider>
</RouteProvider>
</ToastProvider>
</Keymap.Provider>
</ConfigProvider>
</ArgsProvider>
</StorageProvider>
</TuiAppProvider>
</TestTuiContexts>
),
{ 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]()
}
})