From f2858a42ba17fba1e3376440e8f3aae2aa64ca61 Mon Sep 17 00:00:00 2001 From: Adam <2363879+adamdotdevin@users.noreply.github.com> Date: Thu, 19 Feb 2026 11:36:32 -0600 Subject: [PATCH] chore: cleanup --- .../app/e2e/projects/projects-switch.spec.ts | 109 +++++++++++++++++- packages/app/src/pages/layout.tsx | 46 +++++--- packages/app/src/pages/layout/helpers.test.ts | 39 +------ packages/app/src/pages/layout/helpers.ts | 18 --- 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, @@ -1076,19 +1074,37 @@ export default function Layout(props: ParentProps) { dialog.show(() => ) } - 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 - lastSessionAt: Record -}): { 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))