diff --git a/packages/app/src/pages/layout.tsx b/packages/app/src/pages/layout.tsx index 29ba142e5..1e46b3085 100644 --- a/packages/app/src/pages/layout.tsx +++ b/packages/app/src/pages/layout.tsx @@ -61,6 +61,7 @@ import { displayName, errorMessage, getDraggableId, + projectSessionTarget, sortedRootSessions, syncWorkspaceOrder, workspaceKey, @@ -82,6 +83,7 @@ export default function Layout(props: ParentProps) { Persist.global("layout.page", ["layout.page.v1"]), createStore({ lastSession: {} as { [directory: string]: string }, + lastSessionAt: {} as { [directory: string]: number }, activeProject: undefined as string | undefined, activeWorkspace: undefined as string | undefined, workspaceOrder: {} as Record, @@ -1077,8 +1079,16 @@ export default function Layout(props: ParentProps) { function navigateToProject(directory: string | undefined) { if (!directory) return server.projects.touch(directory) - const lastSession = store.lastSession[directory] - navigateWithSidebarReset(`/${base64Encode(directory)}${lastSession ? `/session/${lastSession}` : ""}`) + 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}` : ""}`) } function navigateToSession(session: Session | undefined) { @@ -1433,6 +1443,7 @@ export default function Layout(props: ParentProps) { const directory = decode64(dir) if (!directory) return setStore("lastSession", directory, id) + setStore("lastSessionAt", directory, Date.now()) 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 83d8f4748..6f868ab69 100644 --- a/packages/app/src/pages/layout/helpers.test.ts +++ b/packages/app/src/pages/layout/helpers.test.ts @@ -1,6 +1,13 @@ import { describe, expect, test } from "bun:test" import { collectOpenProjectDeepLinks, drainPendingDeepLinks, parseDeepLink } from "./deep-links" -import { displayName, errorMessage, getDraggableId, syncWorkspaceOrder, workspaceKey } from "./helpers" +import { + displayName, + errorMessage, + getDraggableId, + projectSessionTarget, + syncWorkspaceOrder, + workspaceKey, +} from "./helpers" describe("layout deep links", () => { test("parses open-project deep links", () => { @@ -89,4 +96,34 @@ 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 6a1e7c012..88066cfb8 100644 --- a/packages/app/src/pages/layout/helpers.ts +++ b/packages/app/src/pages/layout/helpers.ts @@ -62,6 +62,24 @@ 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))