Skip to content

Commit 57c8455

Browse files
authored
Preserve thread route in desktop pop-out windows (#401)
- Keep pop-out windows tied to the original parent preview - Preserve the active hash route when generating renderer URLs - Add coverage for packaged and dev renderer URL handling
1 parent 8baa39d commit 57c8455

3 files changed

Lines changed: 48 additions & 6 deletions

File tree

apps/desktop/src/main.ts

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -119,6 +119,7 @@ let backendLogSink: RotatingFileSink | null = null;
119119
let restoreStdIoCapture: (() => void) | null = null;
120120
const previewControllers = new WeakMap<BrowserWindow, DesktopPreviewController>();
121121
const popOutWindows = new Map<BrowserWindow, BrowserWindow>();
122+
const popOutParents = new WeakMap<BrowserWindow, BrowserWindow>();
122123

123124
let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
124125
const desktopRuntimeInfo = resolveDesktopRuntimeInfo({
@@ -787,6 +788,10 @@ function resolvePreviewWindow(sender: Electron.WebContents): BrowserWindow | nul
787788
);
788789
}
789790

791+
function resolvePreviewParentWindow(window: BrowserWindow): BrowserWindow {
792+
return popOutParents.get(window) ?? window;
793+
}
794+
790795
function setUpdateState(patch: Partial<DesktopUpdateState>): void {
791796
updateState = { ...updateState, ...patch };
792797
emitUpdateState();
@@ -1409,8 +1414,9 @@ function registerIpcHandlers(): void {
14091414

14101415
ipcMain.removeHandler(PREVIEW_POP_OUT_CHANNEL);
14111416
ipcMain.handle(PREVIEW_POP_OUT_CHANNEL, async (event) => {
1412-
const parentWindow = resolvePreviewWindow(event.sender);
1413-
if (!parentWindow) return;
1417+
const sourceWindow = resolvePreviewWindow(event.sender);
1418+
if (!sourceWindow) return;
1419+
const parentWindow = resolvePreviewParentWindow(sourceWindow);
14141420

14151421
// If there is already a pop-out window for this parent, focus it.
14161422
const existing = popOutWindows.get(parentWindow);
@@ -1445,6 +1451,7 @@ function registerIpcHandlers(): void {
14451451
});
14461452
previewControllers.set(popOut, popOutController);
14471453
popOutWindows.set(parentWindow, popOut);
1454+
popOutParents.set(popOut, parentWindow);
14481455

14491456
// Transfer tabs from parent to pop-out.
14501457
parentController.transferTo(popOutController);
@@ -1497,13 +1504,15 @@ function registerIpcHandlers(): void {
14971504
popOutController.transferTo(parentController);
14981505
emitPreviewState(parentWindow, parentController.getState());
14991506
}
1507+
popOutParents.delete(popOut);
15001508
previewControllers.delete(popOut);
15011509
});
15021510

1503-
// Load the same app URL but with a query parameter so the renderer
1504-
// can detect pop-out mode and render a simplified UI if needed.
1511+
// Preserve the active chat route so the pop-out window keeps the same
1512+
// thread-scoped preview chrome and actions as the attached renderer.
15051513
void popOut.loadURL(
15061514
resolveDesktopRendererUrl({
1515+
baseUrl: event.sender.getURL(),
15071516
isDevelopment,
15081517
devServerUrl: process.env.VITE_DEV_SERVER_URL,
15091518
scheme: DESKTOP_SCHEME,
@@ -1514,8 +1523,9 @@ function registerIpcHandlers(): void {
15141523

15151524
ipcMain.removeHandler(PREVIEW_POP_IN_CHANNEL);
15161525
ipcMain.handle(PREVIEW_POP_IN_CHANNEL, async (event) => {
1517-
const parentWindow = resolvePreviewWindow(event.sender);
1518-
if (!parentWindow) return;
1526+
const sourceWindow = resolvePreviewWindow(event.sender);
1527+
if (!sourceWindow) return;
1528+
const parentWindow = resolvePreviewParentWindow(sourceWindow);
15191529

15201530
const popOut = popOutWindows.get(parentWindow);
15211531
if (popOut && !popOut.isDestroyed()) {

apps/desktop/src/rendererUrl.test.ts

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,19 @@ describe("resolveDesktopRendererUrl", () => {
2424
).toBe("okcode://app/index.html?popout=true");
2525
});
2626

27+
it("preserves the current packaged renderer hash route when a base URL is provided", () => {
28+
expect(
29+
resolveDesktopRendererUrl({
30+
baseUrl: "okcode://app/index.html#/thread-123",
31+
isDevelopment: false,
32+
scheme: "okcode",
33+
query: {
34+
popout: true,
35+
},
36+
}),
37+
).toBe("okcode://app/index.html?popout=true#/thread-123");
38+
});
39+
2740
it("adds query parameters to the dev server URL", () => {
2841
expect(
2942
resolveDesktopRendererUrl({
@@ -50,6 +63,20 @@ describe("resolveDesktopRendererUrl", () => {
5063
).toBe("http://127.0.0.1:5173/?client=desktop&popout=true");
5164
});
5265

66+
it("preserves the current dev renderer hash route when a base URL is provided", () => {
67+
expect(
68+
resolveDesktopRendererUrl({
69+
baseUrl: "http://127.0.0.1:5173/?client=desktop#/thread-123",
70+
isDevelopment: true,
71+
devServerUrl: "http://127.0.0.1:5173/",
72+
scheme: "okcode",
73+
query: {
74+
popout: true,
75+
},
76+
}),
77+
).toBe("http://127.0.0.1:5173/?client=desktop&popout=true#/thread-123");
78+
});
79+
5380
it("requires a dev server URL in development mode", () => {
5481
expect(() =>
5582
resolveDesktopRendererUrl({

apps/desktop/src/rendererUrl.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
export interface DesktopRendererUrlInput {
2+
readonly baseUrl?: string | undefined;
23
readonly isDevelopment: boolean;
34
readonly devServerUrl?: string | undefined;
45
readonly scheme: string;
@@ -22,6 +23,10 @@ function applyQuery(url: URL, query: DesktopRendererUrlInput["query"]): URL {
2223
}
2324

2425
export function resolveDesktopRendererUrl(input: DesktopRendererUrlInput): string {
26+
if (input.baseUrl) {
27+
return applyQuery(new URL(input.baseUrl), input.query).toString();
28+
}
29+
2530
if (input.isDevelopment) {
2631
const devServerUrl = input.devServerUrl;
2732
if (!devServerUrl) {

0 commit comments

Comments
 (0)