summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorAdam <[email protected]>2026-02-19 11:36:32 -0600
committerAdam <[email protected]>2026-02-19 11:36:37 -0600
commitf2858a42ba17fba1e3376440e8f3aae2aa64ca61 (patch)
tree3fbc753ccb26a79f015aa7a6dd71f384348e383a
parent3c21735b35f779d69a5458b1fa5fada49fb7decb (diff)
downloadopencode-f2858a42ba17fba1e3376440e8f3aae2aa64ca61.tar.gz
opencode-f2858a42ba17fba1e3376440e8f3aae2aa64ca61.zip
chore: cleanup
-rw-r--r--packages/app/e2e/projects/projects-switch.spec.ts109
-rw-r--r--packages/app/src/pages/layout.tsx46
-rw-r--r--packages/app/src/pages/layout/helpers.test.ts39
-rw-r--r--packages/app/src/pages/layout/helpers.ts18
4 files changed, 138 insertions, 74 deletions
diff --git a/packages/app/e2e/projects/projects-switch.spec.ts b/packages/app/e2e/projects/projects-switch.spec.ts
index a817412cd..f17557a80 100644
--- a/packages/app/e2e/projects/projects-switch.spec.ts
+++ b/packages/app/e2e/projects/projects-switch.spec.ts
@@ -1,7 +1,19 @@
+import { base64Decode } from "@opencode-ai/util/encode"
import { test, expect } from "../fixtures"
-import { defocus, createTestProject, cleanupTestProject } from "../actions"
-import { projectSwitchSelector } from "../selectors"
-import { dirSlug } from "../utils"
+import {
+ defocus,
+ createTestProject,
+ cleanupTestProject,
+ openSidebar,
+ setWorkspacesEnabled,
+ sessionIDFromUrl,
+} from "../actions"
+import { projectSwitchSelector, promptSelector, workspaceItemSelector, workspaceNewSessionSelector } from "../selectors"
+import { createSdk, dirSlug } from "../utils"
+
+function slugFromUrl(url: string) {
+ return /\/([^/]+)\/session(?:\/|$)/.exec(url)?.[1] ?? ""
+}
test("can switch between projects from sidebar", async ({ page, withProject }) => {
await page.setViewportSize({ width: 1400, height: 800 })
@@ -33,3 +45,94 @@ test("can switch between projects from sidebar", async ({ page, withProject }) =
await cleanupTestProject(other)
}
})
+
+test("switching back to a project opens the latest workspace session", async ({ page, withProject }) => {
+ await page.setViewportSize({ width: 1400, height: 800 })
+
+ const other = await createTestProject()
+ const otherSlug = dirSlug(other)
+ const stamp = Date.now()
+ let rootDir: string | undefined
+ let workspaceDir: string | undefined
+ let sessionID: string | undefined
+
+ try {
+ await withProject(
+ async ({ directory, slug }) => {
+ rootDir = directory
+ await defocus(page)
+ await openSidebar(page)
+ await setWorkspacesEnabled(page, slug, true)
+
+ await page.getByRole("button", { name: "New workspace" }).first().click()
+
+ await expect
+ .poll(
+ () => {
+ const next = slugFromUrl(page.url())
+ if (!next) return ""
+ if (next === slug) return ""
+ return next
+ },
+ { timeout: 45_000 },
+ )
+ .not.toBe("")
+
+ const workspaceSlug = slugFromUrl(page.url())
+ workspaceDir = base64Decode(workspaceSlug)
+ await openSidebar(page)
+
+ const workspace = page.locator(workspaceItemSelector(workspaceSlug)).first()
+ await expect(workspace).toBeVisible()
+ await workspace.hover()
+
+ const newSession = page.locator(workspaceNewSessionSelector(workspaceSlug)).first()
+ await expect(newSession).toBeVisible()
+ await newSession.click({ force: true })
+
+ 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()}`)
+ sessionID = created
+ await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
+
+ await openSidebar(page)
+
+ const otherButton = page.locator(projectSwitchSelector(otherSlug)).first()
+ await expect(otherButton).toBeVisible()
+ await otherButton.click()
+ await expect(page).toHaveURL(new RegExp(`/${otherSlug}/session`))
+
+ const rootButton = page.locator(projectSwitchSelector(slug)).first()
+ await expect(rootButton).toBeVisible()
+ await rootButton.click()
+
+ await expect(page).toHaveURL(new RegExp(`/${workspaceSlug}/session/${created}(?:[/?#]|$)`))
+ },
+ { extra: [other] },
+ )
+ } finally {
+ if (sessionID) {
+ const id = sessionID
+ const dirs = [rootDir, workspaceDir].filter((x): x is string => !!x)
+ await Promise.all(
+ dirs.map((directory) =>
+ createSdk(directory)
+ .session.delete({ sessionID: id })
+ .catch(() => undefined),
+ ),
+ )
+ }
+ if (workspaceDir) {
+ await cleanupTestProject(workspaceDir)
+ }
+ await cleanupTestProject(other)
+ }
+})
diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx
index 1e46b3085..62094a6e4 100644
--- a/packages/app/src/pages/layout.tsx
+++ b/packages/app/src/pages/layout.tsx
@@ -61,7 +61,6 @@ import {
displayName,
errorMessage,
getDraggableId,
- projectSessionTarget,
sortedRootSessions,
syncWorkspaceOrder,
workspaceKey,
@@ -82,8 +81,7 @@ export default function Layout(props: ParentProps) {
const [store, setStore, , ready] = persisted(
Persist.global("layout.page", ["layout.page.v1"]),
createStore({
- lastSession: {} as { [directory: string]: string },
- lastSessionAt: {} as { [directory: string]: number },
+ lastProjectSession: {} as { [directory: string]: { directory: string; id: string; at: number } },
activeProject: undefined as string | undefined,
activeWorkspace: undefined as string | undefined,
workspaceOrder: {} as Record<string, string[]>,
@@ -1076,19 +1074,37 @@ export default function Layout(props: ParentProps) {
dialog.show(() => <DialogSettings />)
}
- function navigateToProject(directory: string | undefined) {
- if (!directory) return
- server.projects.touch(directory)
+ function projectRoot(directory: string) {
const project = layout.projects
.list()
.find((item) => item.worktree === directory || item.sandboxes?.includes(directory))
- const target = projectSessionTarget({
- directory,
- project,
- lastSession: store.lastSession,
- lastSessionAt: store.lastSessionAt,
- })
- navigateWithSidebarReset(`/${base64Encode(target.directory)}${target.id ? `/session/${target.id}` : ""}`)
+ if (project) return project.worktree
+
+ const known = Object.entries(store.workspaceOrder).find(
+ ([root, dirs]) => root === directory || dirs.includes(directory),
+ )
+ if (known) return known[0]
+
+ const [child] = globalSync.child(directory, { bootstrap: false })
+ const id = child.project
+ if (!id) return directory
+
+ const meta = globalSync.data.project.find((item) => item.id === id)
+ return meta?.worktree ?? directory
+ }
+
+ function navigateToProject(directory: string | undefined) {
+ if (!directory) return
+ const root = projectRoot(directory)
+ server.projects.touch(root)
+
+ const projectSession = store.lastProjectSession[root]
+ if (projectSession?.id) {
+ navigateWithSidebarReset(`/${base64Encode(projectSession.directory)}/session/${projectSession.id}`)
+ return
+ }
+
+ navigateWithSidebarReset(`/${base64Encode(root)}/session`)
}
function navigateToSession(session: Session | undefined) {
@@ -1442,8 +1458,8 @@ export default function Layout(props: ParentProps) {
if (!dir || !id) return
const directory = decode64(dir)
if (!directory) return
- setStore("lastSession", directory, id)
- setStore("lastSessionAt", directory, Date.now())
+ const at = Date.now()
+ setStore("lastProjectSession", projectRoot(directory), { directory, id, at })
notification.session.markViewed(id)
const expanded = untrack(() => store.workspaceExpanded[directory])
if (expanded === false) {
diff --git a/packages/app/src/pages/layout/helpers.test.ts b/packages/app/src/pages/layout/helpers.test.ts
index 6f868ab69..83d8f4748 100644
--- a/packages/app/src/pages/layout/helpers.test.ts
+++ b/packages/app/src/pages/layout/helpers.test.ts
@@ -1,13 +1,6 @@
import { describe, expect, test } from "bun:test"
import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links"
-import {
- displayName,
- errorMessage,
- getDraggableId,
- projectSessionTarget,
- syncWorkspaceOrder,
- workspaceKey,
-} from "./helpers"
+import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers"
describe("layout deep links", () => {
test("parses open-project deep links", () => {
@@ -96,34 +89,4 @@ describe("layout workspace helpers", () => {
expect(errorMessage(new Error("broken"), "fallback")).toBe("broken")
expect(errorMessage("unknown", "fallback")).toBe("fallback")
})
-
- test("picks newest session across project workspaces", () => {
- const result = projectSessionTarget({
- directory: "/root",
- project: { worktree: "/root", sandboxes: ["/root/a", "/root/b"] },
- lastSession: {
- "/root": "root-session",
- "/root/a": "sandbox-a",
- "/root/b": "sandbox-b",
- },
- lastSessionAt: {
- "/root": 1,
- "/root/a": 3,
- "/root/b": 2,
- },
- })
-
- expect(result).toEqual({ directory: "/root/a", id: "sandbox-a", at: 3 })
- })
-
- test("falls back to project route when no session exists", () => {
- const result = projectSessionTarget({
- directory: "/root",
- project: { worktree: "/root", sandboxes: ["/root/a"] },
- lastSession: {},
- lastSessionAt: {},
- })
-
- expect(result).toEqual({ directory: "/root" })
- })
})
diff --git a/packages/app/src/pages/layout/helpers.ts b/packages/app/src/pages/layout/helpers.ts
index 88066cfb8..6a1e7c012 100644
--- a/packages/app/src/pages/layout/helpers.ts
+++ b/packages/app/src/pages/layout/helpers.ts
@@ -62,24 +62,6 @@ export const errorMessage = (err: unknown, fallback: string) => {
return fallback
}
-export function projectSessionTarget(input: {
- directory: string
- project?: { worktree: string; sandboxes?: string[] }
- lastSession: Record<string, string>
- lastSessionAt: Record<string, number>
-}): { directory: string; id?: string; at?: number } {
- const dirs = input.project ? [input.project.worktree, ...(input.project.sandboxes ?? [])] : [input.directory]
- const best = dirs.reduce<{ directory: string; id: string; at: number } | undefined>((result, directory) => {
- const id = input.lastSession[directory]
- if (!id) return result
- const at = input.lastSessionAt[directory] ?? 0
- if (result && result.at >= at) return result
- return { directory, id, at }
- }, undefined)
- if (best) return best
- return { directory: input.directory }
-}
-
export const syncWorkspaceOrder = (local: string, dirs: string[], existing?: string[]) => {
if (!existing) return dirs
const keep = existing.filter((d) => d !== local && dirs.includes(d))