diff --git a/apps/desktop/src/main.ts b/apps/desktop/src/main.ts index 43492133a..20bfec19c 100644 --- a/apps/desktop/src/main.ts +++ b/apps/desktop/src/main.ts @@ -119,6 +119,7 @@ let backendLogSink: RotatingFileSink | null = null; let restoreStdIoCapture: (() => void) | null = null; const previewControllers = new WeakMap(); const popOutWindows = new Map(); +const popOutParents = new WeakMap(); let destructiveMenuIconCache: Electron.NativeImage | null | undefined; const desktopRuntimeInfo = resolveDesktopRuntimeInfo({ @@ -787,6 +788,10 @@ function resolvePreviewWindow(sender: Electron.WebContents): BrowserWindow | nul ); } +function resolvePreviewParentWindow(window: BrowserWindow): BrowserWindow { + return popOutParents.get(window) ?? window; +} + function setUpdateState(patch: Partial): void { updateState = { ...updateState, ...patch }; emitUpdateState(); @@ -1409,8 +1414,9 @@ function registerIpcHandlers(): void { ipcMain.removeHandler(PREVIEW_POP_OUT_CHANNEL); ipcMain.handle(PREVIEW_POP_OUT_CHANNEL, async (event) => { - const parentWindow = resolvePreviewWindow(event.sender); - if (!parentWindow) return; + const sourceWindow = resolvePreviewWindow(event.sender); + if (!sourceWindow) return; + const parentWindow = resolvePreviewParentWindow(sourceWindow); // If there is already a pop-out window for this parent, focus it. const existing = popOutWindows.get(parentWindow); @@ -1445,6 +1451,7 @@ function registerIpcHandlers(): void { }); previewControllers.set(popOut, popOutController); popOutWindows.set(parentWindow, popOut); + popOutParents.set(popOut, parentWindow); // Transfer tabs from parent to pop-out. parentController.transferTo(popOutController); @@ -1497,13 +1504,15 @@ function registerIpcHandlers(): void { popOutController.transferTo(parentController); emitPreviewState(parentWindow, parentController.getState()); } + popOutParents.delete(popOut); previewControllers.delete(popOut); }); - // Load the same app URL but with a query parameter so the renderer - // can detect pop-out mode and render a simplified UI if needed. + // Preserve the active chat route so the pop-out window keeps the same + // thread-scoped preview chrome and actions as the attached renderer. void popOut.loadURL( resolveDesktopRendererUrl({ + baseUrl: event.sender.getURL(), isDevelopment, devServerUrl: process.env.VITE_DEV_SERVER_URL, scheme: DESKTOP_SCHEME, @@ -1514,8 +1523,9 @@ function registerIpcHandlers(): void { ipcMain.removeHandler(PREVIEW_POP_IN_CHANNEL); ipcMain.handle(PREVIEW_POP_IN_CHANNEL, async (event) => { - const parentWindow = resolvePreviewWindow(event.sender); - if (!parentWindow) return; + const sourceWindow = resolvePreviewWindow(event.sender); + if (!sourceWindow) return; + const parentWindow = resolvePreviewParentWindow(sourceWindow); const popOut = popOutWindows.get(parentWindow); if (popOut && !popOut.isDestroyed()) { diff --git a/apps/desktop/src/rendererUrl.test.ts b/apps/desktop/src/rendererUrl.test.ts index f2171e66f..3318e5359 100644 --- a/apps/desktop/src/rendererUrl.test.ts +++ b/apps/desktop/src/rendererUrl.test.ts @@ -24,6 +24,19 @@ describe("resolveDesktopRendererUrl", () => { ).toBe("okcode://app/index.html?popout=true"); }); + it("preserves the current packaged renderer hash route when a base URL is provided", () => { + expect( + resolveDesktopRendererUrl({ + baseUrl: "okcode://app/index.html#/thread-123", + isDevelopment: false, + scheme: "okcode", + query: { + popout: true, + }, + }), + ).toBe("okcode://app/index.html?popout=true#/thread-123"); + }); + it("adds query parameters to the dev server URL", () => { expect( resolveDesktopRendererUrl({ @@ -50,6 +63,20 @@ describe("resolveDesktopRendererUrl", () => { ).toBe("http://127.0.0.1:5173/?client=desktop&popout=true"); }); + it("preserves the current dev renderer hash route when a base URL is provided", () => { + expect( + resolveDesktopRendererUrl({ + baseUrl: "http://127.0.0.1:5173/?client=desktop#/thread-123", + isDevelopment: true, + devServerUrl: "http://127.0.0.1:5173/", + scheme: "okcode", + query: { + popout: true, + }, + }), + ).toBe("http://127.0.0.1:5173/?client=desktop&popout=true#/thread-123"); + }); + it("requires a dev server URL in development mode", () => { expect(() => resolveDesktopRendererUrl({ diff --git a/apps/desktop/src/rendererUrl.ts b/apps/desktop/src/rendererUrl.ts index be2195ed4..583a2e6ec 100644 --- a/apps/desktop/src/rendererUrl.ts +++ b/apps/desktop/src/rendererUrl.ts @@ -1,4 +1,5 @@ export interface DesktopRendererUrlInput { + readonly baseUrl?: string | undefined; readonly isDevelopment: boolean; readonly devServerUrl?: string | undefined; readonly scheme: string; @@ -22,6 +23,10 @@ function applyQuery(url: URL, query: DesktopRendererUrlInput["query"]): URL { } export function resolveDesktopRendererUrl(input: DesktopRendererUrlInput): string { + if (input.baseUrl) { + return applyQuery(new URL(input.baseUrl), input.query).toString(); + } + if (input.isDevelopment) { const devServerUrl = input.devServerUrl; if (!devServerUrl) {