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
25 changes: 19 additions & 6 deletions ui/desktop/src/App.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -306,6 +306,8 @@ const ExtensionsRoute = () => {
export function AppInner() {
const [fatalError, setFatalError] = useState<string | null>(null);

const nostrImportInFlight = useRef<string | null>(null);

const navigate = useNavigate();
const setView = useNavigation();

Expand Down Expand Up @@ -397,15 +399,22 @@ export function AppInner() {
const handleOpenSessionShare = async (_event: IpcRendererEvent, ...args: unknown[]) => {
const link = args[0] as string;
window.electron.logInfo('Opening session share link');
try {
if (link.startsWith('goose://sessions/nostr')) {
await importNostrSessionFromDeepLink(link);
navigate('/sessions');
return;
}

if (!link.startsWith('goose://sessions/nostr')) {
toast.error('Unsupported session share link');
navigate('/sessions');
return;
}

if (nostrImportInFlight.current === link) {
window.electron.logInfo('Skipping duplicate Nostr deep link import');
return;
}
nostrImportInFlight.current = link;

try {
await importNostrSessionFromDeepLink(link);
navigate('/sessions');
} catch (error) {
console.error('Unexpected error opening Nostr session share:', error);
trackErrorWithContext(error, {
Expand All @@ -415,6 +424,10 @@ export function AppInner() {
});
toast.error(`Failed to import Nostr session: ${errorMessage(error, 'Unknown error')}`);
navigate('/sessions');
} finally {
if (nostrImportInFlight.current === link) {
nostrImportInFlight.current = null;
}
}
};
window.electron.on('open-shared-session', handleOpenSessionShare);
Expand Down
81 changes: 72 additions & 9 deletions ui/desktop/src/main.ts
Original file line number Diff line number Diff line change
Expand Up @@ -487,7 +487,66 @@ if (process.platform !== 'darwin') {
}
}

const pendingDeepLinks = new Map<number, string>(); // windowId -> deep link URL
const pendingDeepLinks = new Map<number, string>();

function queuePendingDeepLink(windowId: number, url: string): void {
if (pendingDeepLinks.get(windowId) === url) {
return;
}
pendingDeepLinks.set(windowId, url);
}

const reactReadyWindows = new Set<number>();

const DEEPLINK_BURST_DEDUP_MS = 2000;
const recentSessionDeepLinkSends = new Map<string, number>();

function pruneExpiredSessionDeepLinkSends(now: number): void {
for (const [url, sentAt] of recentSessionDeepLinkSends) {
if (now - sentAt >= DEEPLINK_BURST_DEDUP_MS) {
recentSessionDeepLinkSends.delete(url);
}
}
}

function isBurstDuplicateSessionDeepLink(url: string): boolean {
const now = Date.now();
pruneExpiredSessionDeepLinkSends(now);
const sentAt = recentSessionDeepLinkSends.get(url);
return sentAt !== undefined && now - sentAt < DEEPLINK_BURST_DEDUP_MS;
}

function recordSessionDeepLinkSend(url: string): void {
const now = Date.now();
recentSessionDeepLinkSends.set(url, now);
pruneExpiredSessionDeepLinkSends(now);
}

function sendOpenSharedSession(window: BrowserWindow, url: string): void {
if (isBurstDuplicateSessionDeepLink(url)) {
log.info('[Main] Ignoring burst duplicate session deep link');
return;
}
recordSessionDeepLinkSend(url);

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Defer session-link dedup until renderer readiness

When a goose://sessions/... URL arrives just after a window's main frame stops loading but before React has installed the open-shared-session listener, this records the URL even though the IPC sent on the next line can be missed. The macOS duplicate delivered once the renderer is ready is then treated as a burst duplicate and dropped, so the user gets no import; queue until react-ready or an acknowledgement before recording the URL as sent.

Useful? React with 👍 / 👎.

window.webContents.send('open-shared-session', url);
}

function deliverExtensionOrSessionDeepLink(
url: string,
parsedUrl: URL,
targetWindow: BrowserWindow
): void {
if (!reactReadyWindows.has(targetWindow.id) || targetWindow.webContents.isLoadingMainFrame()) {
queuePendingDeepLink(targetWindow.id, url);
return;
}

if (parsedUrl.hostname === 'extension') {
targetWindow.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') {
sendOpenSharedSession(targetWindow, url);
}
}

function getResumeSessionId(parsedUrl: URL): string | null {
try {
Expand Down Expand Up @@ -547,7 +606,7 @@ async function handleProtocolUrl(url: string, parsedUrl: URL) {
}

if (targetWindow.webContents.isLoadingMainFrame()) {
pendingDeepLinks.set(targetWindow.id, url);
queuePendingDeepLink(targetWindow.id, url);
} else {
await processProtocolUrl(url, parsedUrl, targetWindow);
}
Expand All @@ -561,7 +620,7 @@ async function processProtocolUrl(url: string, parsedUrl: URL, window: BrowserWi
if (parsedUrl.hostname === 'extension') {
window.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') {
window.webContents.send('open-shared-session', url);
sendOpenSharedSession(window, url);
} else if (parsedUrl.hostname === 'bot' || parsedUrl.hostname === 'recipe') {
const deeplinkData = parseRecipeDeeplink(url);
const scheduledJobId = parsedUrl.searchParams.get('scheduledJob');
Expand Down Expand Up @@ -636,15 +695,13 @@ app.on('open-url', async (_event, url) => {
const targetWindow = existingWindows[0];
if (targetWindow.isMinimized()) targetWindow.restore();
targetWindow.focus();
if (parsedUrl.hostname === 'extension') {
targetWindow.webContents.send('add-extension', url);
} else if (parsedUrl.hostname === 'sessions') {
targetWindow.webContents.send('open-shared-session', url);
if (parsedUrl.hostname === 'extension' || parsedUrl.hostname === 'sessions') {
deliverExtensionOrSessionDeepLink(url, parsedUrl, targetWindow);
}
} else {
openUrlHandledLaunch = true;
const newWindow = await createChat(app, { dir: openDir || undefined });
pendingDeepLinks.set(newWindow.id, url);
queuePendingDeepLink(newWindow.id, url);
}
}
});
Expand Down Expand Up @@ -1275,6 +1332,7 @@ const createChat = async (app: App, options: CreateChatOptions = {}) => {

pendingInitialMessages.delete(windowId);
pendingDeepLinks.delete(windowId);
reactReadyWindows.delete(windowId);

if (windowPowerSaveBlockers.has(windowId)) {
const blockerId = windowPowerSaveBlockers.get(windowId)!;
Expand Down Expand Up @@ -1350,6 +1408,7 @@ const createLauncher = () => {
activeLauncherWindow = launcherWindow;

launcherWindow.on('closed', () => {
reactReadyWindows.delete(launcherWindow.id);
activeLauncherWindow = null;
});

Expand Down Expand Up @@ -1634,6 +1693,10 @@ ipcMain.on('react-ready', (event) => {
const window = BrowserWindow.fromWebContents(event.sender);
const windowId = window?.id;

if (windowId !== undefined) {
reactReadyWindows.add(windowId);
}

// Send any pending initial message for this window
if (windowId && pendingInitialMessages.has(windowId)) {
const initialMessage = pendingInitialMessages.get(windowId)!;
Expand All @@ -1653,7 +1716,7 @@ ipcMain.on('react-ready', (event) => {
if (parsedUrl.hostname === 'extension') {
window.webContents.send('add-extension', deepLinkUrl);
} else if (parsedUrl.hostname === 'sessions') {
window.webContents.send('open-shared-session', deepLinkUrl);
sendOpenSharedSession(window, deepLinkUrl);
}
} catch (error) {
log.error('Error processing pending deep link:', error);
Expand Down
Loading