Skip to content

Commit 275770b

Browse files
JDis03claudepascalandr
authored
fix(ui): remove scope=project from session list requests (NeuralNomadsAI#572)
## Problem PR NeuralNomadsAI#565 (commit e29c3a0) added `scope=project` to session list requests so worktree/project sessions remain visible. With OpenCode 1.17.x, the `/session` endpoint can ignore the `directory` filter when `scope=project` is present and return sessions from unrelated folders that share the same project/global bucket. This can make sessions appear in the wrong project tab. Deleting one of those sessions still deletes it from the shared OpenCode database, so the UI must avoid showing unrelated sessions in the wrong tab. ## Reproduction ```bash PORT=4096 curl -s -u "codenomad:$PASSWORD" \ "http://127.0.0.1:$PORT/session?directory=/home/user/project-a&scope=project" \ | jq 'length' # Can include sessions from other folders too. curl -s -u "codenomad:$PASSWORD" \ "http://127.0.0.1:$PORT/session?directory=/home/user/project-a" \ | jq 'length' # Filters to the requested folder, but may miss project/worktree sessions. Related: anomalyco/opencode#33113 Fix Keep scope: "project" so OpenCode still returns project/worktree sessions, then filter the returned sessions client-side to the current root directory plus known CodeNomad worktree directories. This preserves the worktree-session visibility behavior from NeuralNomadsAI#565 while avoiding unrelated cross-directory leakage when OpenCode ignores directory alongside scope=project. Validation Focused session-pagination tests pass. UI typecheck passes. UI build passes. Tauri release build passes. Manual exe tested for validation. --------- Co-authored-by: Claude <noreply@anthropic.com> Co-authored-by: Pascal André <pascalandr@gmail.com>
1 parent e8623d3 commit 275770b

3 files changed

Lines changed: 72 additions & 4 deletions

File tree

packages/ui/src/stores/session-api.ts

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -53,10 +53,17 @@ import { clearCacheForSession } from "../lib/global-cache"
5353
import { getLogger } from "../lib/logger"
5454
import { requestData } from "../lib/opencode-api"
5555
import { getRootClient } from "./opencode-client"
56-
import { getWorktreeSlugForSession, migrateLegacyWorktreeMapToSessionMetadata, pruneStaleLegacyWorktreeMapEntries, removeLegacyParentSessionMapping, setWorktreeSlugForParentSession } from "./worktrees"
56+
import {
57+
getWorktreeSlugForSession,
58+
getWorktrees,
59+
migrateLegacyWorktreeMapToSessionMetadata,
60+
pruneStaleLegacyWorktreeMapEntries,
61+
removeLegacyParentSessionMapping,
62+
setWorktreeSlugForParentSession,
63+
} from "./worktrees"
5764
import { getOpenCodeWorkspaceIdForSession } from "./opencode-workspaces"
5865
import { hydrateSessionMetadataWithClient } from "./session-metadata"
59-
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions } from "./session-list-options"
66+
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions, filterProjectScopedSessions } from "./session-list-options"
6067

6168
const log = getLogger("api")
6269

@@ -142,7 +149,11 @@ async function fetchV2Sessions(instanceId: string, options: V2SessionListOptions
142149
const client = getRootClient(instanceId)
143150
const listOptions = buildProjectSessionListOptions(options)
144151
const data = await requestData<SessionListResponse>(client.session.list(listOptions), "session.list")
145-
return { data }
152+
const allowedDirectories = [options.directory, ...getWorktrees(instanceId).map((worktree) => worktree.directory)]
153+
154+
return {
155+
data: filterProjectScopedSessions(data, allowedDirectories),
156+
}
146157
}
147158

148159
function getV2SessionItems(response: ProjectSessionListResponse): SDKSession[] {

packages/ui/src/stores/session-list-options.ts

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,21 @@ export type ProjectSessionListOptions = ProjectSessionListInput & {
1010
scope: "project"
1111
}
1212

13+
type SessionDirectorySource = {
14+
directory?: string | null
15+
}
16+
17+
function normalizeSessionDirectory(directory: string | null | undefined): string {
18+
const trimmed = directory?.trim()
19+
if (!trimmed) return ""
20+
21+
const slashNormalized = trimmed.replace(/\\/g, "/").replace(/\/+$/, "")
22+
const comparable = slashNormalized || "/"
23+
const isWindowsPath = /^[A-Za-z]:\//.test(comparable) || comparable.startsWith("//") || trimmed.includes("\\")
24+
25+
return isWindowsPath ? comparable.toLowerCase() : comparable
26+
}
27+
1328
export function buildProjectSessionListOptions(options: ProjectSessionListInput): ProjectSessionListOptions {
1429
return {
1530
...(options.directory ? { directory: options.directory } : {}),
@@ -18,3 +33,16 @@ export function buildProjectSessionListOptions(options: ProjectSessionListInput)
1833
scope: "project",
1934
}
2035
}
36+
37+
export function filterProjectScopedSessions<T extends SessionDirectorySource>(
38+
sessions: T[],
39+
allowedDirectories: Array<string | null | undefined>,
40+
): T[] {
41+
const allowed = new Set(allowedDirectories.map(normalizeSessionDirectory).filter(Boolean))
42+
if (allowed.size === 0) return sessions
43+
44+
return sessions.filter((session) => {
45+
const directory = normalizeSessionDirectory(session.directory)
46+
return !directory || allowed.has(directory)
47+
})
48+
}

packages/ui/src/stores/session-pagination.test.ts

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@ import assert from "node:assert/strict"
22
import { describe, it } from "node:test"
33

44
import { applySessionPage, getDefaultSessionPaginationState } from "./session-pagination-model.ts"
5-
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions } from "./session-list-options.ts"
5+
import { PROJECT_SESSION_LIST_LIMIT, buildProjectSessionListOptions, filterProjectScopedSessions } from "./session-list-options.ts"
66

77
describe("project session list loading", () => {
88
it("builds a one-shot project-scoped request without pagination params", () => {
@@ -18,6 +18,35 @@ describe("project session list loading", () => {
1818
assert.equal("cursor" in options, false)
1919
})
2020

21+
it("filters project-scoped results to the root and known worktree directories", () => {
22+
const sessions = [
23+
{ id: "root", directory: "/repo" },
24+
{ id: "worktree", directory: "/repo/.codenomad/worktrees/feature" },
25+
{ id: "sibling", directory: "/other" },
26+
{ id: "unknown" },
27+
]
28+
29+
assert.deepEqual(
30+
filterProjectScopedSessions(sessions, ["/repo", "/repo/.codenomad/worktrees/feature"]).map((session) => session.id),
31+
["root", "worktree", "unknown"],
32+
)
33+
})
34+
35+
it("normalizes Windows paths when filtering project-scoped results", () => {
36+
const sessions = [
37+
{ id: "root", directory: String.raw`C:\Repo` },
38+
{ id: "worktree", directory: "c:/repo/.codenomad/worktrees/feature/" },
39+
{ id: "other", directory: String.raw`C:\Other` },
40+
]
41+
42+
assert.deepEqual(
43+
filterProjectScopedSessions(sessions, ["c:/repo/", String.raw`C:\Repo\.codenomad\worktrees\feature`]).map(
44+
(session) => session.id,
45+
),
46+
["root", "worktree"],
47+
)
48+
})
49+
2150
it("marks the loaded session list complete because the API does not paginate", () => {
2251
const state = applySessionPage(getDefaultSessionPaginationState(), ["root-1", "root-2"], false, true)
2352

0 commit comments

Comments
 (0)