Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 16 additions & 6 deletions apps/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -119,6 +119,7 @@ let backendLogSink: RotatingFileSink | null = null;
let restoreStdIoCapture: (() => void) | null = null;
const previewControllers = new WeakMap<BrowserWindow, DesktopPreviewController>();
const popOutWindows = new Map<BrowserWindow, BrowserWindow>();
const popOutParents = new WeakMap<BrowserWindow, BrowserWindow>();

let destructiveMenuIconCache: Electron.NativeImage | null | undefined;
const desktopRuntimeInfo = resolveDesktopRuntimeInfo({
Expand Down Expand Up @@ -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<DesktopUpdateState>): void {
updateState = { ...updateState, ...patch };
emitUpdateState();
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -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,
Expand All @@ -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()) {
Expand Down
27 changes: 27 additions & 0 deletions apps/desktop/src/rendererUrl.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand All @@ -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({
Expand Down
5 changes: 5 additions & 0 deletions apps/desktop/src/rendererUrl.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
export interface DesktopRendererUrlInput {
readonly baseUrl?: string | undefined;
readonly isDevelopment: boolean;
readonly devServerUrl?: string | undefined;
readonly scheme: string;
Expand All @@ -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) {
Expand Down
Loading