refactor(sessions): remove dead serverHome/path.get() plumbing

sessionScopeDirectory() always returned null and ignored its home arg, so
the scope plumbing in loadSessions/createSession was dead: scopeDir was
always null, listClient always the default client, and the lazy path.get()
fetch fed only that dead branch. Collapse both call sites to use the
connection's default client directly and delete the now-orphaned
sessionScope.ts helper and its test.

serverHome is intentionally KEPT in connections.ts: it is still consumed by
the directory switcher UI (DirectorySwitcher.tsx, app/(tabs)/index.tsx) for
~ path expansion, so it is not dead code.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
Dennis V
2026-06-23 05:20:41 +00:00
parent 9498b19457
commit 472ff8d27d
3 changed files with 12 additions and 105 deletions

View File

@@ -1,40 +0,0 @@
import { test } from "node:test"
import assert from "node:assert/strict"
import { sessionScopeDirectory } from "./sessionScope.ts"
// Bug #10 regression guard: list and create must resolve to the SAME scope.
// Bug #32 regression guard: the default scope must be "no header" (null), not
// the server's home directory. The home dir resolves to the "global" project on
// the opencode server and hides real workspace sessions.
test("no explicit directory + known home -> default client (null), home ignored (#32)", () => {
assert.equal(sessionScopeDirectory(false, "/home/user"), null)
})
test("no explicit directory + unknown home -> default client (null)", () => {
assert.equal(sessionScopeDirectory(false, null), null)
assert.equal(sessionScopeDirectory(false, undefined), null)
assert.equal(sessionScopeDirectory(false, ""), null)
})
test("explicit directory -> default client (null), home ignored", () => {
assert.equal(sessionScopeDirectory(true, "/home/user"), null)
assert.equal(sessionScopeDirectory(true, null), null)
})
test("create and list resolve identically across all inputs (no drift, #10)", () => {
for (const hasDir of [true, false]) {
for (const home of ["/home/user", null, undefined, ""]) {
const listScope = sessionScopeDirectory(hasDir, home)
const createScope = sessionScopeDirectory(hasDir, home)
assert.equal(listScope, createScope)
}
}
})
test("server directory != home: rule returns null so server uses its CWD project (#32)", () => {
// Reproduces the real-world setup: server launched in ~/workspace/opencode
// while $HOME is /home/azureuser. Old rule returned "/home/azureuser" (global
// project, empty). New rule returns null so server uses its CWD project.
assert.equal(sessionScopeDirectory(false, "/home/azureuser"), null)
})

View File

@@ -1,26 +0,0 @@
// Single source of truth for which directory scope session reads/writes use.
//
// Bug #10 ("sessions tab empty after connect / create") happened because the
// list path and the create path each computed this rule independently and drifted:
// loadSessions listed sessions home-scoped while createSession wrote CWD-scoped, so
// a freshly created session was invisible. Both call sites now derive their client
// from this one function, so they cannot disagree again.
//
// Bug #32 ("recent sessions missing"): the previous rule scoped to the server's
// home directory when no explicit directory was set. opencode-server resolves the
// x-opencode-directory header to a project id by exact-match, NOT subtree prefix.
// $HOME typically maps to the synthetic "global" project, which never contains
// the user's real workspace sessions (those live under e.g. ~/workspace/foo, a
// different project id). The list looked empty even when sessions existed.
//
// Rule: when the active connection pins an explicit directory, use the default
// client (it is already scoped to that directory). Otherwise, send no scope at
// all so the server uses its own CWD — i.e. the project where opencode serve was
// launched, which is the project the user actually works in. The `home` argument
// is accepted for backward compatibility but intentionally ignored.
export function sessionScopeDirectory(hasExplicitDirectory: boolean, _home?: string | null | undefined): string | null {
void _home
if (hasExplicitDirectory) return null
return null
}

View File

@@ -2,7 +2,6 @@ import { create } from "zustand"
import type { Session, Message, Part, Event, MessageWithParts, Client } from "../lib/sdk"
import { useConnections } from "./connections"
import { useSettings } from "./settings"
import { sessionScopeDirectory } from "./sessionScope"
import { addBreadcrumb } from "../lib/sentry"
// Helper to convert API response to our internal format
@@ -75,36 +74,20 @@ export const useSessions = create<SessionsState>((set, get) => ({
loadSessions: async () => {
const connState = useConnections.getState()
if (!connState.client) {
const client = connState.client
if (!client) {
set({ error: "No active connection" })
return
}
try {
set({ isLoading: true, error: null })
const initialConnectionID = connState.activeConnection?.id || null
const hasExplicitDirectory = Boolean(connState.activeConnection?.directory)
let home = connState.serverHome
if (!hasExplicitDirectory && !home) {
const paths = await connState.client.path.get().catch(() => null)
home = paths?.home || null
if (home) {
useConnections.setState({ serverHome: home })
}
}
const latestConnState = useConnections.getState()
const latestConnectionID = latestConnState.activeConnection?.id || null
if (latestConnectionID !== initialConnectionID) {
set({ isLoading: false })
return
}
const scopeDir = sessionScopeDirectory(Boolean(latestConnState.activeConnection?.directory), home)
const listClient = scopeDir ? latestConnState.clientForDirectory(scopeDir) : latestConnState.client
const sessions = await (listClient || latestConnState.client!).session.list({ roots: true, limit: 50 })
// List with the connection's default client and send no directory scope:
// opencode-server resolves the x-opencode-directory header to a project by
// exact match, and $HOME maps to the empty "global" project, which hides the
// user's real workspace sessions (bug #32). The default client already carries
// the connection's explicit directory when one is configured.
const sessions = await client.session.list({ roots: true, limit: 50 })
set({ sessions, isLoading: false })
} catch (error) {
set({ error: "Failed to load sessions", isLoading: false })
@@ -185,34 +168,24 @@ export const useSessions = create<SessionsState>((set, get) => ({
createSession: async (title) => {
const connState = useConnections.getState()
if (!connState.client) {
const client = connState.client
if (!client) {
set({ error: "No active connection" })
return null
}
// Create the session in the SAME directory scope that loadSessions reads from
// (see sessionScopeDirectory / bug #10). Both paths derive scope from the one
// helper so a freshly created session is always visible to the list.
const scopeDir = sessionScopeDirectory(Boolean(connState.activeConnection?.directory), connState.serverHome)
const client = scopeDir ? connState.clientForDirectory(scopeDir) || connState.client : connState.client
try {
const created = await client.session.create({ title })
// Stamp the scope we created in onto the session so every downstream path
// (navigation params, selectSession, sendMessage's clientFor) addresses it
// in the SAME scope. Without this, opening/sending to a freshly created
// home-scoped session via the default client would hit the wrong scope (#10).
const session = scopeDir && !created.directory ? { ...created, directory: scopeDir } : created
// Don't optimistically add to sessions list — let loadSessions() handle it
// to avoid duplicate key errors from race conditions
set({
currentSession: session,
currentSession: created,
messages: [],
parts: {},
hasMore: false,
loadingMore: false,
})
return session
return created
} catch (error) {
set({ error: "Failed to create session" })
return null