Skip to content

Commit ea2e260

Browse files
committed
fix(desktop): resume startup sync after window reveal
Summary: Emit authoritative window visibility from every explicit reveal path and let the renderer consume the visibility payload before resuming deferred startup synchronization. Refs: - Change: spec/changes/active/republish-0-5-9-20260714 Verification: - pnpm --filter @prompthub/desktop test -- tests/unit/main/shortcuts.test.ts tests/unit/main/tray-command-dispatcher.test.ts --run: passed (10 tests) - pnpm --filter @prompthub/desktop typecheck: passed - Playwright hidden-startup WebDAV regression repeated 3 times: passed
1 parent 70e293e commit ea2e260

7 files changed

Lines changed: 36 additions & 8 deletions

File tree

apps/desktop/src/main/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,7 @@ const trayController = createTrayController({
231231
command,
232232
createWindow,
233233
getWindow: () => mainWindow,
234+
onWindowShown: () => emitWindowVisibility(true),
234235
sendCommand: (pendingCommand) =>
235236
sendToMainWindow(IPC_CHANNELS.APP_COMMAND, pendingCommand),
236237
}),
@@ -240,7 +241,7 @@ const trayController = createTrayController({
240241
},
241242
onToggleWindow: () => {
242243
if (mainWindow && !mainWindow.isDestroyed()) {
243-
toggleWindowForShowApp(mainWindow);
244+
toggleWindowForShowApp(mainWindow, emitWindowVisibility);
244245
} else {
245246
void createWindow();
246247
}
@@ -266,6 +267,7 @@ if (!gotTheLock) {
266267
}
267268
mainWindow.show();
268269
mainWindow.focus();
270+
emitWindowVisibility(true);
269271
} else {
270272
await createWindow();
271273
}
@@ -279,6 +281,7 @@ async function createWindow() {
279281
if (mainWindow.isMinimized()) mainWindow.restore();
280282
mainWindow.show();
281283
mainWindow.focus();
284+
emitWindowVisibility(true);
282285
return;
283286
}
284287

@@ -508,7 +511,7 @@ ipcMain.handle("window:isVisible", () => {
508511

509512
ipcMain.on("window:toggleVisibility", () => {
510513
if (mainWindow) {
511-
toggleWindowForShowApp(mainWindow);
514+
toggleWindowForShowApp(mainWindow, emitWindowVisibility);
512515
}
513516
});
514517

apps/desktop/src/main/shortcuts.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -61,21 +61,27 @@ type ShortcutWindow = Pick<
6161
'isMinimized' | 'restore' | 'isVisible' | 'show' | 'hide' | 'focus'
6262
>;
6363

64-
export function toggleWindowForShowApp(win: ShortcutWindow): void {
64+
export function toggleWindowForShowApp(
65+
win: ShortcutWindow,
66+
onVisibilityChange?: (isVisible: boolean) => void,
67+
): void {
6568
if (win.isMinimized()) {
6669
win.restore();
6770
win.show();
6871
win.focus();
72+
onVisibilityChange?.(true);
6973
return;
7074
}
7175

7276
if (win.isVisible()) {
7377
win.hide();
78+
onVisibilityChange?.(false);
7479
return;
7580
}
7681

7782
win.show();
7883
win.focus();
84+
onVisibilityChange?.(true);
7985
}
8086

8187
/**
@@ -181,7 +187,9 @@ function registerSingleShortcut(action: string, accelerator: string): boolean {
181187
// For show-app shortcut: toggle window visibility
182188
// 如果是显示应用快捷键,切换窗口显示状态
183189
if (action === 'showApp') {
184-
toggleWindowForShowApp(win);
190+
toggleWindowForShowApp(win, (isVisible) => {
191+
win.webContents.send('window:visibility-changed', isVisible);
192+
});
185193
}
186194

187195
// Send shortcut event to renderer

apps/desktop/src/main/tray-command-dispatcher.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,13 +16,15 @@ interface DispatchTrayAppCommandOptions {
1616
command: AppCommand;
1717
createWindow: () => Promise<void>;
1818
getWindow: () => TrayCommandWindow | null;
19+
onWindowShown?: () => void;
1920
sendCommand: (command: AppCommand) => void;
2021
}
2122

2223
export async function dispatchTrayAppCommand({
2324
command,
2425
createWindow,
2526
getWindow,
27+
onWindowShown,
2628
sendCommand,
2729
}: DispatchTrayAppCommandOptions): Promise<boolean> {
2830
let windowRef = getWindow();
@@ -39,6 +41,7 @@ export async function dispatchTrayAppCommand({
3941
}
4042
windowRef.show();
4143
windowRef.focus();
44+
onWindowShown?.();
4245

4346
if (windowRef.webContents.isLoading()) {
4447
windowRef.webContents.once("did-finish-load", () => {

apps/desktop/src/renderer/App.tsx

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -779,7 +779,10 @@ function App() {
779779
isWindowVisibleRef.current && document.visibilityState !== "hidden",
780780
});
781781

782-
const handleBackgroundTaskResume = () => {
782+
const handleBackgroundTaskResume = (nextVisibility?: unknown) => {
783+
if (typeof nextVisibility === "boolean") {
784+
isWindowVisibleRef.current = nextVisibility;
785+
}
783786
void localDataRefresh.refresh().catch((error) => {
784787
console.error("Failed to refresh local data after resume:", error);
785788
});

apps/desktop/tests/e2e/app.spec.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -108,7 +108,7 @@ test.describe("E2E: Skill smoke", () => {
108108
.poll(async () => (await getE2EStats(page))?.webdav.stat ?? -1)
109109
.toBe(0);
110110

111-
await showAppWindow(app);
111+
await page.evaluate(() => window.electron?.toggleVisibility());
112112
await expect.poll(() => isAppWindowVisible(app)).toBe(true);
113113

114114
await expect

apps/desktop/tests/unit/main/shortcuts.test.ts

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -47,12 +47,14 @@ describe("main shortcuts", () => {
4747
hide: vi.fn(),
4848
focus: vi.fn(),
4949
};
50+
const onVisibilityChange = vi.fn();
5051

51-
toggleWindowForShowApp(win as any);
52+
toggleWindowForShowApp(win as any, onVisibilityChange);
5253

5354
expect(win.hide).toHaveBeenCalledTimes(1);
5455
expect(win.show).not.toHaveBeenCalled();
5556
expect(win.focus).not.toHaveBeenCalled();
57+
expect(onVisibilityChange).toHaveBeenCalledWith(false);
5658
});
5759

5860
it("restores and focuses a minimized window for showApp", async () => {
@@ -65,13 +67,15 @@ describe("main shortcuts", () => {
6567
hide: vi.fn(),
6668
focus: vi.fn(),
6769
};
70+
const onVisibilityChange = vi.fn();
6871

69-
toggleWindowForShowApp(win as any);
72+
toggleWindowForShowApp(win as any, onVisibilityChange);
7073

7174
expect(win.restore).toHaveBeenCalledTimes(1);
7275
expect(win.show).toHaveBeenCalledTimes(1);
7376
expect(win.focus).toHaveBeenCalledTimes(1);
7477
expect(win.hide).not.toHaveBeenCalled();
78+
expect(onVisibilityChange).toHaveBeenCalledWith(true);
7579
});
7680

7781
it("registers showApp as a true toggle in the global shortcut callback", async () => {
@@ -100,6 +104,10 @@ describe("main shortcuts", () => {
100104
expect(mockUnregisterAll).toHaveBeenCalledTimes(1);
101105
expect(mockRegister).toHaveBeenCalled();
102106
expect(win.hide).toHaveBeenCalledTimes(1);
107+
expect(win.webContents.send).toHaveBeenCalledWith(
108+
"window:visibility-changed",
109+
false,
110+
);
103111
expect(win.webContents.send).toHaveBeenCalledWith(
104112
"shortcut:triggered",
105113
"showApp",

apps/desktop/tests/unit/main/tray-command-dispatcher.test.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,17 +33,20 @@ describe("dispatchTrayAppCommand", () => {
3333
it("shows, focuses, and immediately sends to an existing window", async () => {
3434
const windowRef = createWindow();
3535
const sendCommand = vi.fn();
36+
const onWindowShown = vi.fn();
3637

3738
await expect(
3839
dispatchTrayAppCommand({
3940
command: { type: "settings:open" },
4041
createWindow: vi.fn(),
4142
getWindow: () => windowRef,
43+
onWindowShown,
4244
sendCommand,
4345
}),
4446
).resolves.toBe(true);
4547
expect(windowRef.show).toHaveBeenCalledOnce();
4648
expect(windowRef.focus).toHaveBeenCalledOnce();
49+
expect(onWindowShown).toHaveBeenCalledOnce();
4750
expect(sendCommand).toHaveBeenCalledWith({ type: "settings:open" });
4851
});
4952

0 commit comments

Comments
 (0)