Skip to content

Commit 6a17ccc

Browse files
Apply PR #24877: fix(core): run sessions with the same directory they were created with
2 parents e6e274d + 320f379 commit 6a17ccc

3 files changed

Lines changed: 145 additions & 20 deletions

File tree

packages/opencode/src/cli/cmd/tui/context/event.ts

Lines changed: 1 addition & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -12,22 +12,7 @@ export function useEvent() {
1212
return
1313
}
1414

15-
// Special hack for truly global events
16-
if (event.directory === "global") {
17-
handler(event.payload)
18-
}
19-
20-
if (project.workspace.current()) {
21-
if (event.workspace === project.workspace.current()) {
22-
handler(event.payload)
23-
}
24-
25-
return
26-
}
27-
28-
if (event.directory === project.instance.directory()) {
29-
handler(event.payload)
30-
}
15+
handler(event.payload)
3116
})
3217
}
3318

packages/opencode/src/server/workspace.ts

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -39,14 +39,14 @@ function getSessionID(url: URL) {
3939
return SessionID.make(id)
4040
}
4141

42-
async function getSessionWorkspace(url: URL) {
42+
async function getSession(url: URL) {
4343
const id = getSessionID(url)
4444
if (!id) return null
4545

4646
const session = await AppRuntime.runPromise(
4747
Session.Service.use((svc) => svc.get(id)).pipe(Effect.withSpan("WorkspaceRouter.lookup")),
4848
).catch(() => undefined)
49-
return session?.workspaceID
49+
return session
5050
}
5151

5252
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
@@ -55,10 +55,20 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
5555
return async (c, next) => {
5656
const url = new URL(c.req.url)
5757

58-
const sessionWorkspaceID = await getSessionWorkspace(url)
59-
const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace")
58+
const session = await getSession(url)
59+
const workspaceID = session?.workspaceID || url.searchParams.get("workspace")
6060

6161
if (!workspaceID || url.pathname.startsWith("/console") || Flag.OPENCODE_WORKSPACE_ID) {
62+
if (session) {
63+
return Instance.provide({
64+
directory: session.directory,
65+
init: () => AppRuntime.runPromise(InstanceBootstrap),
66+
async fn() {
67+
return next()
68+
},
69+
})
70+
}
71+
6272
return next()
6373
}
6474

Lines changed: 130 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,130 @@
1+
import { afterEach, describe, expect, test } from "bun:test"
2+
import { Flag } from "@opencode-ai/core/flag/flag"
3+
import path from "path"
4+
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
5+
import { Instance } from "../../src/project/instance"
6+
import { Server } from "../../src/server/server"
7+
import * as Log from "@opencode-ai/core/util/log"
8+
import { resetDatabase } from "../fixture/db"
9+
import { tmpdir } from "../fixture/fixture"
10+
11+
void Log.init({ print: false })
12+
13+
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
14+
type SyncTrace = { type: string; directory?: string }
15+
16+
function app() {
17+
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
18+
return Server.Default().app
19+
}
20+
21+
function route(pathname: string, directory: string, query?: Record<string, string>) {
22+
const url = new URL(pathname, "http://localhost")
23+
url.searchParams.set("directory", directory)
24+
for (const [key, value] of Object.entries(query ?? {})) {
25+
url.searchParams.set(key, value)
26+
}
27+
return url
28+
}
29+
30+
async function fetchJson<T>(
31+
pathname: string,
32+
directory: string,
33+
init?: RequestInit,
34+
query?: Record<string, string>,
35+
) {
36+
const response = await app().fetch(new Request(route(pathname, directory, query), init))
37+
if (response.status !== 200) throw new Error(await response.text())
38+
return (await response.json()) as T
39+
}
40+
41+
function pathFor(pathname: string, params: Record<string, string>) {
42+
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), pathname)
43+
}
44+
45+
afterEach(async () => {
46+
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
47+
await Instance.disposeAll()
48+
await resetDatabase()
49+
})
50+
51+
describe("Hono session routes", () => {
52+
test("use request directory for non-session routes and saved session directory for session routes", async () => {
53+
await using sessionDir = await tmpdir({
54+
git: true,
55+
config: { formatter: false, lsp: false },
56+
init: (dir) => Bun.write(path.join(dir, "marker.txt"), "session-directory"),
57+
})
58+
await using requestDir = await tmpdir({
59+
git: true,
60+
config: { formatter: false, lsp: false },
61+
init: (dir) => Bun.write(path.join(dir, "marker.txt"), "request-directory"),
62+
})
63+
64+
const json = { "content-type": "application/json" }
65+
const trace: SyncTrace[] = []
66+
const onEvent = (event: GlobalEvent) => {
67+
if (event.payload.type !== "sync") return
68+
if (!["session.created.1", "message.updated.1", "message.part.updated.1"].includes(event.payload.syncEvent.type)) return
69+
trace.push({ type: event.payload.syncEvent.type, directory: event.directory })
70+
}
71+
GlobalBus.on("event", onEvent)
72+
73+
const session = await fetchJson<{ id: string }>("/session", sessionDir.path, {
74+
method: "POST",
75+
headers: json,
76+
body: JSON.stringify({ title: "session-dir" }),
77+
})
78+
79+
const currentPath = await fetchJson<{ directory: string }>("/path", requestDir.path)
80+
expect(currentPath.directory).toBe(requestDir.path)
81+
82+
const marker = await fetchJson<{ type: string; content: string }>(
83+
"/file/content",
84+
requestDir.path,
85+
undefined,
86+
{
87+
path: "marker.txt",
88+
},
89+
)
90+
expect(marker).toMatchObject({ type: "text", content: "request-directory" })
91+
92+
await fetchJson<unknown>(pathFor("/session/:sessionID", { sessionID: session.id }), requestDir.path)
93+
94+
await fetchJson<unknown>(
95+
pathFor("/session/:sessionID/fork", { sessionID: session.id }),
96+
requestDir.path,
97+
{
98+
method: "POST",
99+
headers: json,
100+
body: JSON.stringify({}),
101+
},
102+
)
103+
104+
await fetchJson<{ info: { path: { cwd: string; root: string } }; parts: unknown[] }>(
105+
pathFor("/session/:sessionID/shell", { sessionID: session.id }),
106+
requestDir.path,
107+
{
108+
method: "POST",
109+
headers: json,
110+
body: JSON.stringify({
111+
agent: "build",
112+
model: { providerID: "test", modelID: "test" },
113+
command: "pwd",
114+
}),
115+
},
116+
)
117+
GlobalBus.off("event", onEvent)
118+
119+
expect(trace).toContainEqual({ type: "session.created.1", directory: sessionDir.path })
120+
expect(trace.filter((event) => event.type === "session.created.1")).toEqual([
121+
{ type: "session.created.1", directory: sessionDir.path },
122+
{ type: "session.created.1", directory: sessionDir.path },
123+
])
124+
expect(trace.filter((event) => event.type === "message.updated.1").map((event) => event.directory)).toEqual(
125+
expect.arrayContaining([sessionDir.path]),
126+
)
127+
expect(trace.filter((event) => event.type === "message.updated.1").every((event) => event.directory === sessionDir.path))
128+
.toBe(true)
129+
})
130+
})

0 commit comments

Comments
 (0)