summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
-rw-r--r--packages/app/e2e/projects/projects-switch.spec.ts22
-rw-r--r--packages/app/src/pages/layout.tsx42
-rw-r--r--packages/app/src/pages/layout/helpers.test.ts76
-rw-r--r--packages/app/src/pages/layout/helpers.ts5
4 files changed, 131 insertions, 14 deletions
diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts
index f17557a80..74b389088 100644
--- a/packages/app/e2e/projects/projects-switch.spec.ts
+++ b/packages/app/e2e/projects/projects-switch.spec.ts
@@ -9,7 +9,7 @@ import {
sessionIDFromUrl,
} from "../actions"
import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
-import { createSdk, dirSlug } from "../utils"
+import { createSdk, dirSlug, sessionPath } from "../utils"
function slugFromUrl(url: string) {
return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
@@ -51,7 +51,6 @@ test("switching back to a project opens the latest workspace session", async ({
const other = await createTestProject()
const otherSlug = dirSlug(other)
- const stamp = Date.now()
let rootDir: string | undefined
let workspaceDir: string | undefined
let sessionID: string | undefined
@@ -80,6 +79,7 @@ test("switching back to a project opens the latest workspace session", async ({
const workspaceSlug = slugFromUrl(page.url())
workspaceDir = base64Decode(workspaceSlug)
+ if (!workspaceDir) throw new Error(`Failed to decode workspace slug: ${workspaceSlug}`)
await openSidebar(page)
const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
@@ -92,15 +92,14 @@ test("switching back to a project opens the latest workspace session", async ({
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session(?:[/?#]|$)`))
- const prompt = page.locator(promptSelector)
- await expect(prompt).toBeVisible()
- await prompt.fill(`project switch remembers workspace ${stamp}`)
- await prompt.press("Enter")
-
- await expect.poll(() => sessionIDFromUrl(page.url()) ?? "", { timeout: 30_000 }).not.toBe("")
- const created = sessionIDFromUrl(page.url())
- if (!created) throw new Error(`Failed to parse session id from URL: ${page.url()}`)
+ const created = await createSdk(workspaceDir)
+ .session.create()
+ .then((x) => x.data?.id)
+ if (!created) throw new Error(`Failed to create session for workspace: ${workspaceDir}`)
sessionID = created
+
+ await page.goto(sessionPath(workspaceDir, created))
+ await expect(page.locator(promptSelector)).toBeVisible()
await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
await openSidebar(page)
@@ -114,7 +113,8 @@ test("switching back to a project opens the latest workspace session", async ({
await expect(rootButton).toBeVisible()
await rootButton.click()
- await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
+ await expect.poll(() => sessionIDFromUrl(page.url()) ?? "").toBe(created)
+ await expect(page).toHaveURL(new RegExp(`/session/${created}(?:[/?#]|$)`))
},
{ extra: [other] },
)
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 62094a6e4..cb194052d 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -61,6 +61,7 @@ import {
displayName,
errorMessage,
getDraggableId,
+ latestRootSession,
sortedRootSessions,
syncWorkspaceOrder,
workspaceKey,
@@ -1093,14 +1094,51 @@ export default function Layout(props: ParentProps) {
return meta?.worktree ?? directory
}
- function navigateToProject(directory: string | undefined) {
+ async function navigateToProject(directory: string | undefined) {
if (!directory) return
const root = projectRoot(directory)
server.projects.touch(root)
+ const project = layout.projects.list().find((item) => item.worktree === root)
+ const dirs = Array.from(new Set([root, ...(store.workspaceOrder[root] ?? []), ...(project?.sandboxes ?? [])]))
+ const openSession = async (target: { directory: string; id: string }) => {
+ const resolved = await globalSDK.client.session
+ .get({ sessionID: target.id })
+ .then((x) => x.data)
+ .catch(() => undefined)
+ const next = resolved?.directory ? resolved : target
+ setStore("lastProjectSession", root, { directory: next.directory, id: next.id, at: Date.now() })
+ navigateWithSidebarReset(`/${base64Encode(next.directory)}/session/${next.id}`)
+ }
const projectSession = store.lastProjectSession[root]
if (projectSession?.id) {
- navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`)
+ await openSession(projectSession)
+ return
+ }
+
+ const latest = latestRootSession(
+ dirs.map((item) => globalSync.child(item, { bootstrap: false })[0]),
+ Date.now(),
+ )
+ if (latest) {
+ await openSession(latest)
+ return
+ }
+
+ const fetched = latestRootSession(
+ await Promise.all(
+ dirs.map(async (item) => ({
+ path: { directory: item },
+ session: await globalSDK.client.session
+ .list({ directory: item })
+ .then((x) => x.data ?? [])
+ .catch(() => []),
+ })),
+ ),
+ Date.now(),
+ )
+ if (fetched) {
+ await openSession(fetched)
return
}
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index 83d8f4748..7627d9ba1 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -1,6 +1,25 @@
import { describe, expect, test } from "bun:test"
+import { type Session } from "@opencode-ai/sdk/v2/client"
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
-import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
+import {
+ displayName,
+ errorMessage,
+ getDraggableId,
+ latestRootSession,
+ syncWorkspaceOrder,
+ workspaceKey,
+} from "./helpers"
+
+const session = (input: Partial<Session> & Pick<Session, "id" | "directory">) =>
+ ({
+ title: "",
+ version: "v2",
+ parentID: undefined,
+ messageCount: 0,
+ permissions: { session: {}, share: {} },
+ time: { created: 0, updated: 0, archived: undefined },
+ ...input,
+ }) as Session
describe("layout deep links", () => {
test("parses open-project deep links", () => {
@@ -73,6 +92,61 @@ describe("layout workspace helpers", () => {
expect(result).toEqual(["/root", "/c", "/b"])
})
+ test("finds the latest root session across workspaces", () => {
+ const result = latestRootSession(
+ [
+ {
+ path: { directory: "/root" },
+ session: [session({ id: "root", directory: "/root", time: { created: 1, updated: 1, archived: undefined } })],
+ },
+ {
+ path: { directory: "/workspace" },
+ session: [
+ session({
+ id: "workspace",
+ directory: "/workspace",
+ time: { created: 2, updated: 2, archived: undefined },
+ }),
+ ],
+ },
+ ],
+ 120_000,
+ )
+
+ expect(result?.id).toBe("workspace")
+ })
+
+ test("ignores archived and child sessions when finding latest root session", () => {
+ const result = latestRootSession(
+ [
+ {
+ path: { directory: "/workspace" },
+ session: [
+ session({
+ id: "archived",
+ directory: "/workspace",
+ time: { created: 10, updated: 10, archived: 10 },
+ }),
+ session({
+ id: "child",
+ directory: "/workspace",
+ parentID: "parent",
+ time: { created: 20, updated: 20, archived: undefined },
+ }),
+ session({
+ id: "root",
+ directory: "/workspace",
+ time: { created: 30, updated: 30, archived: undefined },
+ }),
+ ],
+ },
+ ],
+ 120_000,
+ )
+
+ expect(result?.id).toBe("root")
+ })
+
test("extracts draggable id safely", () => {
expect(getDraggableId({ draggable: { id: "x" } })).toBe("x")
expect(getDraggableId({ draggable: { id: 42 } })).toBeUndefined()
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 6a1e7c012..be4297fbe 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -28,6 +28,11 @@ export const isRootVisibleSession = (session: Session, directory: string) =>
export const sortedRootSessions = (store: { session: Session[]; path: { directory: string } }, now: number) =>
store.session.filter((session) => isRootVisibleSession(session, store.path.directory)).sort(sortSessions(now))
+export const latestRootSession = (stores: { session: Session[]; path: { directory: string } }[], now: number) =>
+ stores
+ .flatMap((store) => store.session.filter((session) => isRootVisibleSession(session, store.path.directory)))
+ .sort(sortSessions(now))[0]
+
export const childMapByParent = (sessions: Session[]) => {
const map = new Map<string, string[]>()
for (const session of sessions) {