Open existing agents from links and the CLI (#2324)

* feat(desktop): open existing agents from links

Register a stable agent deep link and route it through the existing Desktop window. Add a matching CLI command that resolves the local server and activates the requested agent without creating or messaging it.

* fix(desktop): recover agent link delivery
This commit is contained in:
Mohamed Boudra
2026-07-22 18:31:12 +02:00
committed by GitHub
parent 9952615c33
commit 76a5edb020
18 changed files with 504 additions and 42 deletions

View File

@@ -66,6 +66,7 @@ import {
import { registerWorkspaceRouteNavigationRef } from "@/navigation/workspace-route-navigation";
import { ThemedStack } from "@/navigation/themed-stack";
import { shouldUseDesktopDaemon } from "@/desktop/daemon/desktop-daemon";
import { AgentNavigationListener } from "@/desktop/agent-navigation";
import { listenToDesktopEvent } from "@/desktop/electron/events";
import { updateDesktopWindowControls } from "@/desktop/electron/window";
import { getDesktopHost } from "@/desktop/host";
@@ -908,6 +909,7 @@ function AppShell() {
<MobilePanelsProvider>
<HorizontalScrollProvider>
<OpenProjectListener />
<AgentNavigationListener />
<AppWithSidebar>
<WorkspaceRouteNavigationBridge />
<RootStack />

View File

@@ -0,0 +1,69 @@
import { useEffect } from "react";
import { listenToDesktopEvent } from "@/desktop/electron/events";
import { getDesktopHost } from "@/desktop/host";
import { useStableEvent } from "@/hooks/use-stable-event";
import { navigateToAgent } from "@/utils/navigate-to-agent";
interface OpenAgentEventPayload {
serverId?: unknown;
agentId?: unknown;
}
export function AgentNavigationListener() {
const openAgent = useStableEvent((payload: OpenAgentEventPayload | null) => {
const serverId = typeof payload?.serverId === "string" ? payload.serverId.trim() : "";
const agentId = typeof payload?.agentId === "string" ? payload.agentId.trim() : "";
if (!serverId || !agentId) {
return;
}
navigateToAgent({ serverId, agentId });
});
useEffect(() => {
const host = getDesktopHost();
const ready = host?.agentNavigation?.ready;
if (typeof host?.events?.on !== "function" || typeof ready !== "function") {
return;
}
let disposed = false;
let unlisten: (() => void) | null = null;
let retryTimer: ReturnType<typeof setTimeout> | null = null;
const connect = async () => {
let dispose: (() => void) | null = null;
try {
dispose = await listenToDesktopEvent<OpenAgentEventPayload>("open-agent", openAgent);
if (disposed) {
dispose();
return;
}
unlisten = dispose;
const pending = await ready();
if (!disposed && pending) {
openAgent(pending);
}
} catch {
dispose?.();
if (unlisten === dispose) {
unlisten = null;
}
if (!disposed) {
retryTimer = setTimeout(() => void connect(), 1_000);
}
}
};
void connect();
return () => {
disposed = true;
if (retryTimer) {
clearTimeout(retryTimer);
}
unlisten?.();
};
}, [openAgent]);
return null;
}

View File

@@ -123,6 +123,10 @@ export interface DesktopEventsBridge {
on?: (event: string, handler: (payload: unknown) => void) => Promise<() => void> | (() => void);
}
export interface DesktopAgentNavigationBridge {
ready?: () => Promise<{ serverId: string; agentId: string } | null>;
}
export type DesktopBrowserShortcutEvent =
| { browserId?: string; action: "focus-url" }
| { browserId: string; action: "new-tab" };
@@ -169,6 +173,7 @@ export interface DesktopHostBridge {
platform?: string;
invoke?: DesktopInvokeBridge["invoke"];
getPendingOpenProject?: () => Promise<string | null>;
agentNavigation?: DesktopAgentNavigationBridge;
events?: DesktopEventsBridge;
window?: DesktopWindowModuleBridge;
dialog?: DesktopDialogBridge;