import { scopeThreadRef } from "@t3tools/client-runtime/environment";import { type EnvironmentId, ThreadId } from "@t3tools/contracts";import { beforeEach, describe, expect, it } from "vite-plus/test"; import { migratePersistedRightPanelState, pullRequestSurface, pullRequestSurfaceId, selectActiveRightPanel, selectActiveRightPanelSurface, selectSelectedRightPanelSurface, selectThreadRightPanelState, useRightPanelStore,} from "./rightPanelStore"; const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A"));const refB = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-B")); beforeEach(() => { useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} });}); describe("rightPanelStore", () => { it("gives each host/device its own tab and preserves renamed tabs", () => { const store = useRightPanelStore.getState(); const android = { hostId: "nucbox", deviceId: "emulator-5580", name: "Pixel", platform: "android", } as const; const ios = { hostId: "macmini", deviceId: "ios-1", name: "iPhone", platform: "ios" } as const; store.open(refA, "device"); store.openDevice(refA, android); store.open(refA, "device"); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); store.openDevice(refA, ios); let state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces.map((surface) => surface.id)).toEqual([ "device:nucbox:emulator-5580", "device:macmini:ios-1", ]); store.renameDevice(refA, "device:nucbox:emulator-5580", "Android test"); store.openDevice(refA, android); state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces).toHaveLength(2); expect(state.surfaces[0]).toMatchObject({ title: "Android test", target: android }); expect(state.activeSurfaceId).toBe("device:nucbox:emulator-5580"); store.closeSurface(refA, state.activeSurfaceId!); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toEqual([expect.objectContaining({ target: ios })]); }); it("does not collide when two hosts expose the same device id", () => { const store = useRightPanelStore.getState(); const device = { deviceId: "emulator-5554", name: "Pixel", platform: "android" } as const; store.openDevice(refA, { ...device, hostId: "a:b" }); store.openDevice(refA, { ...device, hostId: "a" }); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refB).surfaces, ).toHaveLength(0); }); it.each(["one", "all", "others", "right"])( "keeps device tabs dismissed across reload after closing %s", (mode) => { const store = useRightPanelStore.getState(); const target = { hostId: "nucbox", deviceId: "emulator-5580", name: "Pixel", platform: "android", } as const; store.open(refA, "files"); store.openDevice(refA, target); if (mode === "one") store.closeSurface(refA, "device:nucbox:emulator-5580"); if (mode === "all") store.closeAllSurfaces(refA); if (mode === "others") store.closeOtherSurfaces(refA, "files"); if (mode === "right") store.closeSurfacesToRight(refA, "files"); const persisted = JSON.parse( JSON.stringify({ byThreadKey: useRightPanelStore.getState().byThreadKey }), ); useRightPanelStore.setState(migratePersistedRightPanelState(persisted)); store.openDevice(refA, target, true); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.some( (surface) => surface.kind === "device", ), ).toBe(false); store.openDevice(refA, target); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.some( (surface) => surface.kind === "device", ), ).toBe(true); }, ); const completedDiff = { id: "diff", kind: "diff" } as const; const linkedPullRequest = pullRequestSurface({ projectId: "project-a", repository: "pingdotgg/t3code", number: 42, }); it.each(["diff-first", "pull-request-first"])( "prioritizes the linked pull request over browser and diff with %s delivery", (order) => { const store = useRightPanelStore.getState(); store.openBrowser(refA, "existing-browser"); const revision = store.getUserActionRevision(refA); const requests = order === "diff-first" ? [completedDiff, linkedPullRequest] : [linkedPullRequest, completedDiff]; for (const surface of requests) store.openProactive(refA, surface, revision); store.reconcileBrowserSurfaces(refA, ["existing-browser", "agent-browser"]); expect( selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA), ).toEqual(linkedPullRequest); store.open(refA, "diff"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }, ); it.each([ { choice: "file", choose: () => useRightPanelStore.getState().openFile(refA, "src/app.ts") }, { choice: "pull request", choose: () => useRightPanelStore.getState().openPullRequest(refA, { ...linkedPullRequest, number: 41 }), }, { choice: "browser", choose: () => useRightPanelStore.getState().openBrowser(refA, "tab-a") }, { choice: "terminal", choose: () => useRightPanelStore.getState().openTerminal(refA, "term-1"), }, { choice: "same tab", choose: () => useRightPanelStore.getState().activateSurface(refA, "diff"), }, { choice: "hide", choose: () => useRightPanelStore.getState().close(refA) }, { choice: "toggle", choose: () => useRightPanelStore.getState().toggle(refA, "diff") }, { choice: "close all", choose: () => useRightPanelStore.getState().closeAllSurfaces(refA) }, { choice: "terminal close", choose: () => { const store = useRightPanelStore.getState(); store.openTerminal(refA, "term-1"); store.closeTerminal(refA, "terminal:term-1", "term-1"); }, }, ])("keeps a later $choice choice when automatic requests arrive", ({ choose }) => { const store = useRightPanelStore.getState(); store.open(refA, "diff"); const revision = store.getUserActionRevision(refA); choose(); const chosen = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(store.openProactive(refA, completedDiff, revision)).toBe(false); expect(store.openProactive(refA, linkedPullRequest, revision)).toBe(false); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toBe( chosen, ); }); it("allows automatic panels for a later turn after a manual choice", () => { const store = useRightPanelStore.getState(); const firstTurnRevision = store.getUserActionRevision(refA); store.openFile(refA, "src/app.ts"); expect(store.openProactive(refA, completedDiff, firstTurnRevision)).toBe(false); const nextTurnRevision = store.getUserActionRevision(refA); expect(store.openProactive(refA, completedDiff, nextTurnRevision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }); it("keeps manual choices scoped to their thread and environment", () => { const otherEnvironment = scopeThreadRef("env-2" as EnvironmentId, refA.threadId); const store = useRightPanelStore.getState(); const revision = store.getUserActionRevision(refA); store.openFile(refB, "src/app.ts"); store.openFile(otherEnvironment, "src/app.ts"); expect(store.openProactive(refA, completedDiff, revision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refB)).toBe("file"); expect( selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, otherEnvironment), ).toBe("file"); }); it("does not treat resource reconciliation as a manual choice", () => { const store = useRightPanelStore.getState(); store.openFile(refA, "src/app.ts"); const revision = store.getUserActionRevision(refA); store.reconcileBrowserSurfaces(refA, ["agent-browser"]); store.reconcileFileSurfaces(refA, false); expect(store.openProactive(refA, completedDiff, revision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }); it("drops the legacy singleton terminal surface during migration", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { activeSurfaceId: "terminal", surfaces: [ { id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }, { id: "terminal", kind: "terminal" }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: false, activeSurfaceId: null, surfaces: [{ id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }], }, }, }); }); it("upgrades saved single-session terminal surfaces to split-capable surfaces", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [{ id: "terminal:term-1", kind: "terminal", resourceId: "term-1" }], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [ { id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1"], activeTerminalId: "term-1", }, ], }, }, }); }); it("upgrades saved file surfaces with neutral reveal state", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "file:src/index.ts", surfaces: [{ id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "file:src/index.ts", surfaces: [ { id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts", revealLine: null, revealRequestId: 0, }, ], }, }, }); }); it("upgrades the legacy singleton pull request surface to a reference-keyed tab", () => { const id = pullRequestSurfaceId({ projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }); expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "pull-request", surfaces: [ { id: "pull-request", kind: "pull-request", projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: id, surfaces: [ { id, kind: "pull-request", projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }, }, }); }); it("drops the pull-request list's shared panel so a restart opens the page fresh", () => { const id = pullRequestSurfaceId({ projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }); const panelState = { isOpen: true, activeSurfaceId: id, surfaces: [ { id, kind: "pull-request" as const, projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }; expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:pull-requests-panel": panelState, "env-1:thread-A": panelState, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": panelState } }); }); it("drops persisted plan surfaces and does not reopen an empty panel", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "plan", surfaces: [{ id: "plan", kind: "plan" }], }, "env-1:thread-B": { isOpen: true, activeSurfaceId: "plan", surfaces: [ { id: "plan", kind: "plan" }, { id: "diff", kind: "diff" }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: false, activeSurfaceId: null, surfaces: [], }, "env-1:thread-B": { isOpen: true, activeSurfaceId: "diff", surfaces: [{ id: "diff", kind: "diff" }], }, }, }); }); it("open sets the active panel for a thread", () => { useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refB)).toBeNull(); }); it("opening a different kind keeps both surfaces and activates the new one", () => { useRightPanelStore.getState().open(refA, "agents"); useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); }); it("reopening an inactive singleton activates its existing surface", () => { useRightPanelStore.getState().open(refA, "diff"); useRightPanelStore.getState().open(refA, "agents"); useRightPanelStore.getState().open(refA, "diff"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "diff", surfaces: [ { id: "diff", kind: "diff" }, { id: "agents", kind: "agents" }, ], }); }); it("keeps files as a singleton surface", () => { useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().open(refA, "files"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "files", surfaces: [{ id: "files", kind: "files" }], }); }); it("replaces the standalone explorer with peer file surfaces", () => { useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().openFile(refA, "src/index.ts"); useRightPanelStore.getState().openFile(refA, "src/index.ts"); useRightPanelStore.getState().openFile(refA, "README.md"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "file:README.md", surfaces: [ { id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts", revealLine: null, revealRequestId: 2, }, { id: "file:README.md", kind: "file", relativePath: "README.md", revealLine: null, revealRequestId: 1, }, ], }); }); it("opens an attachment as a file surface without the standalone explorer", () => { const attachment = { type: "file" as const, id: "thread-A-attachment-pdf", name: "report.pdf", mimeType: "application/pdf", sizeBytes: 42, }; useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().openAttachment(refA, attachment); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "attachment:thread-A-attachment-pdf", surfaces: [ { id: "attachment:thread-A-attachment-pdf", kind: "file", relativePath: "report.pdf", revealLine: null, revealRequestId: 0, attachment, }, ], }); }); it("keeps attachment and workspace file ids disjoint", () => { useRightPanelStore.getState().openFile(refA, "attachment:shared-id"); useRightPanelStore.getState().openAttachment(refA, { type: "file",Lines 1–500 of 920. Load more
import { scopeThreadRef } from "@t3tools/client-runtime/environment"; import { type EnvironmentId, ThreadId } from "@t3tools/contracts"; import { beforeEach, describe, expect, it } from "vite-plus/test"; import { migratePersistedRightPanelState, pullRequestSurface, pullRequestSurfaceId, selectActiveRightPanel, selectActiveRightPanelSurface, selectSelectedRightPanelSurface, selectThreadRightPanelState, useRightPanelStore, } from "./rightPanelStore"; const refA = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-A")); const refB = scopeThreadRef("env-1" as EnvironmentId, ThreadId.make("thread-B")); beforeEach(() => { useRightPanelStore.setState({ byThreadKey: {}, userActionRevisionByThreadKey: {} }); }); describe("rightPanelStore", () => { it("gives each host/device its own tab and preserves renamed tabs", () => { const store = useRightPanelStore.getState(); const android = { hostId: "nucbox", deviceId: "emulator-5580", name: "Pixel", platform: "android", } as const; const ios = { hostId: "macmini", deviceId: "ios-1", name: "iPhone", platform: "ios" } as const; store.open(refA, "device"); store.openDevice(refA, android); store.open(refA, "device"); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); store.openDevice(refA, ios); let state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces.map((surface) => surface.id)).toEqual([ "device:nucbox:emulator-5580", "device:macmini:ios-1", ]); store.renameDevice(refA, "device:nucbox:emulator-5580", "Android test"); store.openDevice(refA, android); state = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(state.surfaces).toHaveLength(2); expect(state.surfaces[0]).toMatchObject({ title: "Android test", target: android }); expect(state.activeSurfaceId).toBe("device:nucbox:emulator-5580"); store.closeSurface(refA, state.activeSurfaceId!); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toEqual([expect.objectContaining({ target: ios })]); }); it("does not collide when two hosts expose the same device id", () => { const store = useRightPanelStore.getState(); const device = { deviceId: "emulator-5554", name: "Pixel", platform: "android" } as const; store.openDevice(refA, { ...device, hostId: "a:b" }); store.openDevice(refA, { ...device, hostId: "a" }); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refB).surfaces, ).toHaveLength(0); }); it.each(["one", "all", "others", "right"])( "keeps device tabs dismissed across reload after closing %s", (mode) => { const store = useRightPanelStore.getState(); const target = { hostId: "nucbox", deviceId: "emulator-5580", name: "Pixel", platform: "android", } as const; store.open(refA, "files"); store.openDevice(refA, target); if (mode === "one") store.closeSurface(refA, "device:nucbox:emulator-5580"); if (mode === "all") store.closeAllSurfaces(refA); if (mode === "others") store.closeOtherSurfaces(refA, "files"); if (mode === "right") store.closeSurfacesToRight(refA, "files"); const persisted = JSON.parse( JSON.stringify({ byThreadKey: useRightPanelStore.getState().byThreadKey }), ); useRightPanelStore.setState(migratePersistedRightPanelState(persisted)); store.openDevice(refA, target, true); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.some( (surface) => surface.kind === "device", ), ).toBe(false); store.openDevice(refA, target); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces.some( (surface) => surface.kind === "device", ), ).toBe(true); }, ); const completedDiff = { id: "diff", kind: "diff" } as const; const linkedPullRequest = pullRequestSurface({ projectId: "project-a", repository: "pingdotgg/t3code", number: 42, }); it.each(["diff-first", "pull-request-first"])( "prioritizes the linked pull request over browser and diff with %s delivery", (order) => { const store = useRightPanelStore.getState(); store.openBrowser(refA, "existing-browser"); const revision = store.getUserActionRevision(refA); const requests = order === "diff-first" ? [completedDiff, linkedPullRequest] : [linkedPullRequest, completedDiff]; for (const surface of requests) store.openProactive(refA, surface, revision); store.reconcileBrowserSurfaces(refA, ["existing-browser", "agent-browser"]); expect( selectActiveRightPanelSurface(useRightPanelStore.getState().byThreadKey, refA), ).toEqual(linkedPullRequest); store.open(refA, "diff"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }, ); it.each([ { choice: "file", choose: () => useRightPanelStore.getState().openFile(refA, "src/app.ts") }, { choice: "pull request", choose: () => useRightPanelStore.getState().openPullRequest(refA, { ...linkedPullRequest, number: 41 }), }, { choice: "browser", choose: () => useRightPanelStore.getState().openBrowser(refA, "tab-a") }, { choice: "terminal", choose: () => useRightPanelStore.getState().openTerminal(refA, "term-1"), }, { choice: "same tab", choose: () => useRightPanelStore.getState().activateSurface(refA, "diff"), }, { choice: "hide", choose: () => useRightPanelStore.getState().close(refA) }, { choice: "toggle", choose: () => useRightPanelStore.getState().toggle(refA, "diff") }, { choice: "close all", choose: () => useRightPanelStore.getState().closeAllSurfaces(refA) }, { choice: "terminal close", choose: () => { const store = useRightPanelStore.getState(); store.openTerminal(refA, "term-1"); store.closeTerminal(refA, "terminal:term-1", "term-1"); }, }, ])("keeps a later $choice choice when automatic requests arrive", ({ choose }) => { const store = useRightPanelStore.getState(); store.open(refA, "diff"); const revision = store.getUserActionRevision(refA); choose(); const chosen = selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA); expect(store.openProactive(refA, completedDiff, revision)).toBe(false); expect(store.openProactive(refA, linkedPullRequest, revision)).toBe(false); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toBe( chosen, ); }); it("allows automatic panels for a later turn after a manual choice", () => { const store = useRightPanelStore.getState(); const firstTurnRevision = store.getUserActionRevision(refA); store.openFile(refA, "src/app.ts"); expect(store.openProactive(refA, completedDiff, firstTurnRevision)).toBe(false); const nextTurnRevision = store.getUserActionRevision(refA); expect(store.openProactive(refA, completedDiff, nextTurnRevision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }); it("keeps manual choices scoped to their thread and environment", () => { const otherEnvironment = scopeThreadRef("env-2" as EnvironmentId, refA.threadId); const store = useRightPanelStore.getState(); const revision = store.getUserActionRevision(refA); store.openFile(refB, "src/app.ts"); store.openFile(otherEnvironment, "src/app.ts"); expect(store.openProactive(refA, completedDiff, revision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refB)).toBe("file"); expect( selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, otherEnvironment), ).toBe("file"); }); it("does not treat resource reconciliation as a manual choice", () => { const store = useRightPanelStore.getState(); store.openFile(refA, "src/app.ts"); const revision = store.getUserActionRevision(refA); store.reconcileBrowserSurfaces(refA, ["agent-browser"]); store.reconcileFileSurfaces(refA, false); expect(store.openProactive(refA, completedDiff, revision)).toBe(true); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("diff"); }); it("drops the legacy singleton terminal surface during migration", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { activeSurfaceId: "terminal", surfaces: [ { id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }, { id: "terminal", kind: "terminal" }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: false, activeSurfaceId: null, surfaces: [{ id: "browser:tab-a", kind: "preview", resourceId: "tab-a" }], }, }, }); }); it("upgrades saved single-session terminal surfaces to split-capable surfaces", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [{ id: "terminal:term-1", kind: "terminal", resourceId: "term-1" }], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "terminal:term-1", surfaces: [ { id: "terminal:term-1", kind: "terminal", resourceId: "term-1", terminalIds: ["term-1"], activeTerminalId: "term-1", }, ], }, }, }); }); it("upgrades saved file surfaces with neutral reveal state", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "file:src/index.ts", surfaces: [{ id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts" }], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "file:src/index.ts", surfaces: [ { id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts", revealLine: null, revealRequestId: 0, }, ], }, }, }); }); it("upgrades the legacy singleton pull request surface to a reference-keyed tab", () => { const id = pullRequestSurfaceId({ projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }); expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "pull-request", surfaces: [ { id: "pull-request", kind: "pull-request", projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: id, surfaces: [ { id, kind: "pull-request", projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }, }, }); }); it("drops the pull-request list's shared panel so a restart opens the page fresh", () => { const id = pullRequestSurfaceId({ projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }); const panelState = { isOpen: true, activeSurfaceId: id, surfaces: [ { id, kind: "pull-request" as const, projectId: "project-a", repository: "pingdotgg/t3code", number: 4909, }, ], }; expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:pull-requests-panel": panelState, "env-1:thread-A": panelState, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": panelState } }); }); it("drops persisted plan surfaces and does not reopen an empty panel", () => { expect( migratePersistedRightPanelState({ byThreadKey: { "env-1:thread-A": { isOpen: true, activeSurfaceId: "plan", surfaces: [{ id: "plan", kind: "plan" }], }, "env-1:thread-B": { isOpen: true, activeSurfaceId: "plan", surfaces: [ { id: "plan", kind: "plan" }, { id: "diff", kind: "diff" }, ], }, }, }), ).toEqual({ byThreadKey: { "env-1:thread-A": { isOpen: false, activeSurfaceId: null, surfaces: [], }, "env-1:thread-B": { isOpen: true, activeSurfaceId: "diff", surfaces: [{ id: "diff", kind: "diff" }], }, }, }); }); it("open sets the active panel for a thread", () => { useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refB)).toBeNull(); }); it("opening a different kind keeps both surfaces and activates the new one", () => { useRightPanelStore.getState().open(refA, "agents"); useRightPanelStore.getState().open(refA, "preview"); expect(selectActiveRightPanel(useRightPanelStore.getState().byThreadKey, refA)).toBe("preview"); expect( selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA).surfaces, ).toHaveLength(2); }); it("reopening an inactive singleton activates its existing surface", () => { useRightPanelStore.getState().open(refA, "diff"); useRightPanelStore.getState().open(refA, "agents"); useRightPanelStore.getState().open(refA, "diff"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "diff", surfaces: [ { id: "diff", kind: "diff" }, { id: "agents", kind: "agents" }, ], }); }); it("keeps files as a singleton surface", () => { useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().open(refA, "files"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "files", surfaces: [{ id: "files", kind: "files" }], }); }); it("replaces the standalone explorer with peer file surfaces", () => { useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().openFile(refA, "src/index.ts"); useRightPanelStore.getState().openFile(refA, "src/index.ts"); useRightPanelStore.getState().openFile(refA, "README.md"); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "file:README.md", surfaces: [ { id: "file:src/index.ts", kind: "file", relativePath: "src/index.ts", revealLine: null, revealRequestId: 2, }, { id: "file:README.md", kind: "file", relativePath: "README.md", revealLine: null, revealRequestId: 1, }, ], }); }); it("opens an attachment as a file surface without the standalone explorer", () => { const attachment = { type: "file" as const, id: "thread-A-attachment-pdf", name: "report.pdf", mimeType: "application/pdf", sizeBytes: 42, }; useRightPanelStore.getState().open(refA, "files"); useRightPanelStore.getState().openAttachment(refA, attachment); expect(selectThreadRightPanelState(useRightPanelStore.getState().byThreadKey, refA)).toEqual({ isOpen: true, activeSurfaceId: "attachment:thread-A-attachment-pdf", surfaces: [ { id: "attachment:thread-A-attachment-pdf", kind: "file", relativePath: "report.pdf", revealLine: null, revealRequestId: 0, attachment, }, ], }); }); it("keeps attachment and workspace file ids disjoint", () => { useRightPanelStore.getState().openFile(refA, "attachment:shared-id"); useRightPanelStore.getState().openAttachment(refA, { type: "file",