Compare commits

...

2 Commits

Author SHA1 Message Date
James Long
fdfe599cb5 handle all events 2026-04-30 10:43:06 -04:00
James Long
6df566161e fix(core): run sessions with the same directory they were created with 2026-04-30 10:43:05 -04:00
3 changed files with 145 additions and 20 deletions

View File

@@ -12,22 +12,7 @@ export function useEvent() {
return
}
// Special hack for truly global events
if (event.directory === "global") {
handler(event.payload)
}
if (project.workspace.current()) {
if (event.workspace === project.workspace.current()) {
handler(event.payload)
}
return
}
if (event.directory === project.instance.directory()) {
handler(event.payload)
}
handler(event.payload)
})
}

View File

@@ -48,14 +48,14 @@ export function workspaceProxyURL(target: string | URL, requestURL: URL) {
return proxyURL
}
async function getSessionWorkspace(url: URL) {
async function getSession(url: URL) {
const id = getWorkspaceRouteSessionID(url)
if (!id) return null
const session = await AppRuntime.runPromise(
Session.Service.use((svc) => svc.get(id)).pipe(Effect.withSpan("WorkspaceRouter.lookup")),
).catch(() => undefined)
return session?.workspaceID
return session
}
export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): MiddlewareHandler {
@@ -64,10 +64,20 @@ export function WorkspaceRouterMiddleware(upgrade: UpgradeWebSocket): Middleware
return async (c, next) => {
const url = new URL(c.req.url)
const sessionWorkspaceID = await getSessionWorkspace(url)
const workspaceID = sessionWorkspaceID || url.searchParams.get("workspace")
const session = await getSession(url)
const workspaceID = session?.workspaceID || url.searchParams.get("workspace")
if (!workspaceID || url.pathname.startsWith("/console") || Flag.OPENCODE_WORKSPACE_ID) {
if (session) {
return Instance.provide({
directory: session.directory,
init: () => AppRuntime.runPromise(InstanceBootstrap),
async fn() {
return next()
},
})
}
return next()
}

View File

@@ -0,0 +1,130 @@
import { afterEach, describe, expect, test } from "bun:test"
import { Flag } from "@opencode-ai/core/flag/flag"
import path from "path"
import { GlobalBus, type GlobalEvent } from "../../src/bus/global"
import { Instance } from "../../src/project/instance"
import { Server } from "../../src/server/server"
import * as Log from "@opencode-ai/core/util/log"
import { resetDatabase } from "../fixture/db"
import { tmpdir } from "../fixture/fixture"
void Log.init({ print: false })
const originalHttpApi = Flag.OPENCODE_EXPERIMENTAL_HTTPAPI
type SyncTrace = { type: string; directory?: string }
function app() {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = false
return Server.Default().app
}
function route(pathname: string, directory: string, query?: Record<string, string>) {
const url = new URL(pathname, "http://localhost")
url.searchParams.set("directory", directory)
for (const [key, value] of Object.entries(query ?? {})) {
url.searchParams.set(key, value)
}
return url
}
async function fetchJson<T>(
pathname: string,
directory: string,
init?: RequestInit,
query?: Record<string, string>,
) {
const response = await app().fetch(new Request(route(pathname, directory, query), init))
if (response.status !== 200) throw new Error(await response.text())
return (await response.json()) as T
}
function pathFor(pathname: string, params: Record<string, string>) {
return Object.entries(params).reduce((result, [key, value]) => result.replace(`:${key}`, value), pathname)
}
afterEach(async () => {
Flag.OPENCODE_EXPERIMENTAL_HTTPAPI = originalHttpApi
await Instance.disposeAll()
await resetDatabase()
})
describe("Hono session routes", () => {
test("use request directory for non-session routes and saved session directory for session routes", async () => {
await using sessionDir = await tmpdir({
git: true,
config: { formatter: false, lsp: false },
init: (dir) => Bun.write(path.join(dir, "marker.txt"), "session-directory"),
})
await using requestDir = await tmpdir({
git: true,
config: { formatter: false, lsp: false },
init: (dir) => Bun.write(path.join(dir, "marker.txt"), "request-directory"),
})
const json = { "content-type": "application/json" }
const trace: SyncTrace[] = []
const onEvent = (event: GlobalEvent) => {
if (event.payload.type !== "sync") return
if (!["session.created.1", "message.updated.1", "message.part.updated.1"].includes(event.payload.syncEvent.type)) return
trace.push({ type: event.payload.syncEvent.type, directory: event.directory })
}
GlobalBus.on("event", onEvent)
const session = await fetchJson<{ id: string }>("/session", sessionDir.path, {
method: "POST",
headers: json,
body: JSON.stringify({ title: "session-dir" }),
})
const currentPath = await fetchJson<{ directory: string }>("/path", requestDir.path)
expect(currentPath.directory).toBe(requestDir.path)
const marker = await fetchJson<{ type: string; content: string }>(
"/file/content",
requestDir.path,
undefined,
{
path: "marker.txt",
},
)
expect(marker).toMatchObject({ type: "text", content: "request-directory" })
await fetchJson<unknown>(pathFor("/session/:sessionID", { sessionID: session.id }), requestDir.path)
await fetchJson<unknown>(
pathFor("/session/:sessionID/fork", { sessionID: session.id }),
requestDir.path,
{
method: "POST",
headers: json,
body: JSON.stringify({}),
},
)
await fetchJson<{ info: { path: { cwd: string; root: string } }; parts: unknown[] }>(
pathFor("/session/:sessionID/shell", { sessionID: session.id }),
requestDir.path,
{
method: "POST",
headers: json,
body: JSON.stringify({
agent: "build",
model: { providerID: "test", modelID: "test" },
command: "pwd",
}),
},
)
GlobalBus.off("event", onEvent)
expect(trace).toContainEqual({ type: "session.created.1", directory: sessionDir.path })
expect(trace.filter((event) => event.type === "session.created.1")).toEqual([
{ type: "session.created.1", directory: sessionDir.path },
{ type: "session.created.1", directory: sessionDir.path },
])
expect(trace.filter((event) => event.type === "message.updated.1").map((event) => event.directory)).toEqual(
expect.arrayContaining([sessionDir.path]),
)
expect(trace.filter((event) => event.type === "message.updated.1").every((event) => event.directory === sessionDir.path))
.toBe(true)
})
})