feat(desktop): scope tabs to windows (#34669)

This commit is contained in:
Brendan Allan
2026-07-01 07:02:10 +00:00
committed by GitHub
parent 55552c521f
commit af72cec799
14 changed files with 185 additions and 42 deletions
@@ -45,7 +45,7 @@ export async function installStressSessionTabs(page: Page, input?: { draftID?: s
}),
)
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([
...sessionIDs.map((sessionId) => ({
type: "session",
@@ -14,7 +14,7 @@ test("closing the active server's last tab opens the remaining server tab", asyn
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([
{ type: "session", server: "http://127.0.0.1:4096", sessionId: sessionA },
{ type: "session", server: serverB, sessionId: sessionB },
@@ -52,7 +52,7 @@ test("legacy session routes preserve an existing tab's server", async ({ page })
localStorage.setItem("settings.v3", JSON.stringify({ general: { newLayoutDesigns: true } }))
localStorage.setItem("opencode.global.dat:server", JSON.stringify({ list: [serverB] }))
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify([{ type: "session", server: serverB, sessionId: sessionB }]),
)
},
@@ -145,7 +145,7 @@ async function configurePage(page: Page) {
}),
)
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(sessionIDs.map((sessionId) => ({ type: "session", server, dirBase64, sessionId }))),
)
},
@@ -127,7 +127,7 @@ test.describe("smoke: session timeline", () => {
await page.addInitScript(
({ dirBase64, sourceID, targetID }) => {
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(
[sourceID, targetID].map((sessionId) => ({
type: "session",
@@ -253,7 +253,7 @@ test.describe("smoke: session timeline", () => {
await page.addInitScript(
({ dirBase64, sourceID, targetID }) => {
localStorage.setItem(
"opencode.global.dat:tabs",
"opencode.window.browser.dat:tabs",
JSON.stringify(
[sourceID, targetID].map((sessionId) => ({
type: "session",
+3
View File
@@ -64,6 +64,9 @@ type PlatformBase = {
/** Storage mechanism, defaults to localStorage */
storage?: (name?: string) => SyncStorage | AsyncStorage
/** Stable platform window identity for window-scoped persistence */
windowID?: string
/** Application-global desktop updater */
updater?: UpdaterPlatform
+2 -2
View File
@@ -51,7 +51,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
const fallback = server.key
const [store, setStore, _, ready] = persisted(
{
...Persist.global("tabs"),
...Persist.window("tabs"),
migrate: (value: unknown) => {
if (!Array.isArray(value)) return value
return value.map((tab) => {
@@ -62,7 +62,7 @@ export const { use: useTabs, provider: TabsProvider } = createSimpleContext({
},
createStore<Tab[]>([]),
)
const [recent, setRecent, , recentReady] = persisted(Persist.global("tabs.recent"), createStore<RecentTab>({}))
const [recent, setRecent, , recentReady] = persisted(Persist.window("tabs.recent"), createStore<RecentTab>({}))
const params = useParams()
const navigate = useNavigate()
+23 -1
View File
@@ -16,6 +16,7 @@ type PersistedWithReady<T> = [
type PersistTarget = {
storage?: string
scope?: "window"
legacyStorageNames?: string[]
key: string
legacy?: string[]
@@ -24,6 +25,7 @@ type PersistTarget = {
const LEGACY_STORAGE = "default.dat"
const GLOBAL_STORAGE = "opencode.global.dat"
const WINDOW_STORAGE = "opencode.window"
const LOCAL_PREFIX = "opencode."
const fallback = new Map<string, boolean>()
@@ -347,6 +349,11 @@ function draftStorage(draftID: string) {
return `opencode.draft.${head}.${sum}.dat`
}
function windowStorage(windowID: string) {
const safe = (windowID || "browser").replace(/[^a-zA-Z0-9._-]/g, "-")
return `${WINDOW_STORAGE}.${safe}.dat`
}
function legacyWorkspaceStorage(dir: string) {
const storage = workspaceStorage(pathKey(dir))
const result = new Set<string>()
@@ -467,6 +474,8 @@ export const PersistTesting = {
localStorageWithPrefix,
migrateLegacy,
normalize,
resolveTarget,
windowStorage,
workspaceStorage,
}
@@ -474,6 +483,9 @@ export const Persist = {
global(key: string, legacy?: string[]): PersistTarget {
return { storage: GLOBAL_STORAGE, key, legacy }
},
window(key: string, legacy?: string[]): PersistTarget {
return { scope: "window", key, legacy }
},
draft(draftID: string, key: string, legacy?: string[]): PersistTarget {
return { storage: draftStorage(draftID), key: `draft:${key}`, legacy }
},
@@ -503,6 +515,16 @@ export const Persist = {
},
}
function resolveTarget(target: PersistTarget, platform: Platform): PersistTarget {
if (target.scope !== "window") return target
if (platform.platform === "desktop" && !platform.windowID) return { ...target, storage: GLOBAL_STORAGE }
const windowID = platform.platform === "desktop" ? (platform.windowID ?? "browser") : "browser"
return {
...target,
storage: windowStorage(windowID),
}
}
export function removePersisted(
target: { storage?: string; legacyStorageNames?: string[]; key: string },
platform?: Platform,
@@ -533,7 +555,7 @@ export function persisted<T>(
store: [Store<T>, SetStoreFunction<T>],
): PersistedWithReady<T> {
const platform = usePlatform()
const config: PersistTarget = typeof target === "string" ? { key: target } : target
const config = resolveTarget(typeof target === "string" ? { key: target } : target, platform)
const defaults = snapshot(store[0])
const legacy = config.legacy ?? []