chore: cleanup

This commit is contained in:
Adam
2026-02-19 11:36:32 -06:00
parent 3c21735b35
commit f2858a42ba
4 changed files with 138 additions and 74 deletions

View File

@@ -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)
}
})

View File

@@ -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) {

View File

@@ -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" })
})
})

View File

@@ -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))