diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx
index dc1b919b1..0ae68e115 100644
--- a/packages/app/src/app/_layout.tsx
+++ b/packages/app/src/app/_layout.tsx
@@ -43,6 +43,10 @@ import {
type WebNotificationClickDetail,
} from "@/utils/os-notifications";
import { buildNotificationRoute } from "@/utils/notification-routing";
+import {
+ buildHostAgentDraftRoute,
+ parseHostAgentRouteFromPathname,
+} from "@/utils/host-routes";
polyfillCrypto();
@@ -268,53 +272,8 @@ function AppContainer({ children, selectedAgentId }: AppContainerProps) {
function ProvidersWrapper({ children }: { children: ReactNode }) {
const { settings, isLoading: settingsLoading } = useAppSettings();
const { daemons, isLoading: registryLoading, upsertDaemonFromOfferUrl } = useDaemonRegistry();
- const { connectionStates } = useDaemonConnections();
const isLoading = settingsLoading || registryLoading;
- const isInitialConnectionPending = useMemo(() => {
- if (daemons.length === 0) return false;
- return daemons.some((daemon) => {
- const record = connectionStates.get(daemon.serverId);
- if (!record) return true;
- if (record.hasEverReceivedAgentList) return false;
- return (
- record.status === "idle" ||
- record.status === "connecting" ||
- (record.status === "online" && !record.agentListReady)
- );
- });
- }, [daemons, connectionStates]);
-
- const [connectionTimedOut, setConnectionTimedOut] = useState(false);
-
- useEffect(() => {
- if (!isInitialConnectionPending) {
- setConnectionTimedOut(false);
- return;
- }
- const timer = setTimeout(() => setConnectionTimedOut(true), 5000);
- return () => clearTimeout(timer);
- }, [isInitialConnectionPending]);
-
- const connectingMessage = useMemo(() => {
- if (!isInitialConnectionPending) return null;
- const pending = daemons.filter((daemon) => {
- const record = connectionStates.get(daemon.serverId);
- if (!record) return true;
- if (record.hasEverReceivedAgentList) return false;
- return (
- record.status === "idle" ||
- record.status === "connecting" ||
- (record.status === "online" && !record.agentListReady)
- );
- });
- if (pending.length === 1) {
- const label = pending[0].label?.trim();
- return label ? `Connecting to ${label}...` : "Connecting...";
- }
- return "Connecting...";
- }, [isInitialConnectionPending, daemons, connectionStates]);
-
// Apply theme setting on mount and when it changes
useEffect(() => {
if (isLoading) return;
@@ -330,10 +289,6 @@ function ProvidersWrapper({ children }: { children: ReactNode }) {
return ;
}
- if (connectingMessage && !connectionTimedOut) {
- return ;
- }
-
return (
@@ -359,7 +314,7 @@ function OfferLinkListener({
if (cancelled) return;
const serverId = (profile as any)?.serverId;
if (typeof serverId !== "string" || !serverId) return;
- router.replace({ pathname: "/", params: { serverId } });
+ router.replace(buildHostAgentDraftRoute(serverId) as any);
})
.catch((error) => {
if (cancelled) return;
@@ -389,13 +344,8 @@ function AppWithSidebar({ children }: { children: ReactNode }) {
// Parse selectedAgentKey directly from pathname
// useLocalSearchParams doesn't update when navigating between same-pattern routes
const selectedAgentKey = useMemo(() => {
- // Match /agent/[serverId]/[agentId] pattern
- const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)\/?$/);
- if (match) {
- const [, serverId, agentId] = match;
- return `${serverId}:${agentId}`;
- }
- return undefined;
+ const match = parseHostAgentRouteFromPathname(pathname);
+ return match ? `${match.serverId}:${match.agentId}` : undefined;
}, [pathname]);
return (
@@ -478,14 +428,14 @@ export default function RootLayout() {
}}
>
-
-
-
-
+
+
+
+
diff --git a/packages/app/src/app/agent/[id].tsx b/packages/app/src/app/agent/[id].tsx
deleted file mode 100644
index bbd32ceb4..000000000
--- a/packages/app/src/app/agent/[id].tsx
+++ /dev/null
@@ -1,8 +0,0 @@
-import { useLocalSearchParams } from "expo-router";
-import { LegacyAgentIdScreen } from "@/screens/agent/legacy-agent-id-screen";
-
-export default function LegacyAgentRoute() {
- const { id } = useLocalSearchParams<{ id?: string }>();
- return ;
-}
-
diff --git a/packages/app/src/app/agent/[serverId]/[agentId].tsx b/packages/app/src/app/agent/[serverId]/[agentId].tsx
deleted file mode 100644
index 993cba09b..000000000
--- a/packages/app/src/app/agent/[serverId]/[agentId].tsx
+++ /dev/null
@@ -1,16 +0,0 @@
-import { useLocalSearchParams } from "expo-router";
-import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen";
-
-export default function AgentReadyRoute() {
- const { serverId, agentId } = useLocalSearchParams<{
- serverId?: string;
- agentId?: string;
- }>();
-
- return (
-
- );
-}
diff --git a/packages/app/src/app/agent/index.tsx b/packages/app/src/app/agent/index.tsx
deleted file mode 100644
index 4a1a5c7d7..000000000
--- a/packages/app/src/app/agent/index.tsx
+++ /dev/null
@@ -1,6 +0,0 @@
-import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
-
-export default function AgentDraftRoute() {
- return ;
-}
-
diff --git a/packages/app/src/app/h/[serverId]/agent/[agentId].tsx b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx
new file mode 100644
index 000000000..8e908de4a
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/agent/[agentId].tsx
@@ -0,0 +1,17 @@
+import { useLocalSearchParams } from "expo-router";
+import { AgentReadyScreen } from "@/screens/agent/agent-ready-screen";
+
+export default function HostAgentReadyRoute() {
+ const params = useLocalSearchParams<{
+ serverId?: string;
+ agentId?: string;
+ }>();
+
+ return (
+
+ );
+}
+
diff --git a/packages/app/src/app/h/[serverId]/agent/index.tsx b/packages/app/src/app/h/[serverId]/agent/index.tsx
new file mode 100644
index 000000000..b1aaa55ac
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/agent/index.tsx
@@ -0,0 +1,10 @@
+import { useLocalSearchParams } from "expo-router";
+import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
+
+export default function HostDraftAgentRoute() {
+ const params = useLocalSearchParams<{ serverId?: string }>();
+ const serverId = typeof params.serverId === "string" ? params.serverId : "";
+
+ return ;
+}
+
diff --git a/packages/app/src/app/h/[serverId]/agents.tsx b/packages/app/src/app/h/[serverId]/agents.tsx
new file mode 100644
index 000000000..7772cfa71
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/agents.tsx
@@ -0,0 +1,9 @@
+import { useLocalSearchParams } from "expo-router";
+import { AgentsScreen } from "@/screens/agents-screen";
+
+export default function HostAgentsRoute() {
+ const params = useLocalSearchParams<{ serverId?: string }>();
+ const serverId = typeof params.serverId === "string" ? params.serverId : "";
+
+ return ;
+}
diff --git a/packages/app/src/app/h/[serverId]/index.tsx b/packages/app/src/app/h/[serverId]/index.tsx
new file mode 100644
index 000000000..1de679d5e
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/index.tsx
@@ -0,0 +1,19 @@
+import { useEffect } from "react";
+import { useLocalSearchParams, useRouter } from "expo-router";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
+
+export default function HostIndexRoute() {
+ const router = useRouter();
+ const params = useLocalSearchParams<{ serverId?: string }>();
+ const serverId = typeof params.serverId === "string" ? params.serverId : "";
+
+ useEffect(() => {
+ if (!serverId) {
+ return;
+ }
+ router.replace(buildHostAgentDraftRoute(serverId) as any);
+ }, [router, serverId]);
+
+ return null;
+}
+
diff --git a/packages/app/src/app/h/[serverId]/settings.tsx b/packages/app/src/app/h/[serverId]/settings.tsx
new file mode 100644
index 000000000..38d368f98
--- /dev/null
+++ b/packages/app/src/app/h/[serverId]/settings.tsx
@@ -0,0 +1,3 @@
+import SettingsScreen from "@/screens/settings-screen";
+
+export default SettingsScreen;
diff --git a/packages/app/src/app/index.tsx b/packages/app/src/app/index.tsx
index 2455b9f99..c6f37eccd 100644
--- a/packages/app/src/app/index.tsx
+++ b/packages/app/src/app/index.tsx
@@ -1,5 +1,59 @@
+import { useEffect, useMemo } from "react";
+import { ActivityIndicator, View } from "react-native";
+import { useRouter } from "expo-router";
+import { useUnistyles } from "react-native-unistyles";
import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
+import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
+import { useFormPreferences } from "@/hooks/use-form-preferences";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
export default function Index() {
- return ;
+ const router = useRouter();
+ const { theme } = useUnistyles();
+ const { daemons, isLoading: registryLoading } = useDaemonRegistry();
+ const { preferences, isLoading: preferencesLoading } = useFormPreferences();
+
+ const targetServerId = useMemo(() => {
+ if (daemons.length === 0) {
+ return null;
+ }
+ if (preferences.serverId) {
+ const match = daemons.find((daemon) => daemon.serverId === preferences.serverId);
+ if (match) {
+ return match.serverId;
+ }
+ }
+ return daemons[0]?.serverId ?? null;
+ }, [daemons, preferences.serverId]);
+
+ useEffect(() => {
+ if (registryLoading || preferencesLoading) {
+ return;
+ }
+ if (!targetServerId) {
+ return;
+ }
+ router.replace(buildHostAgentDraftRoute(targetServerId) as any);
+ }, [preferencesLoading, registryLoading, router, targetServerId]);
+
+ if (registryLoading || preferencesLoading) {
+ return (
+
+
+
+ );
+ }
+
+ if (!targetServerId) {
+ return ;
+ }
+
+ return null;
}
diff --git a/packages/app/src/app/pair-scan.tsx b/packages/app/src/app/pair-scan.tsx
index 78845bea3..ad4397fa5 100644
--- a/packages/app/src/app/pair-scan.tsx
+++ b/packages/app/src/app/pair-scan.tsx
@@ -11,6 +11,10 @@ import { NameHostModal } from "@/components/name-host-modal";
import { decodeOfferFragmentPayload, normalizeHostPort } from "@/utils/daemon-endpoints";
import { probeConnection } from "@/utils/test-daemon-connection";
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
+import {
+ buildHostAgentDraftRoute,
+ buildHostSettingsRoute,
+} from "@/utils/host-routes";
const styles = StyleSheet.create((theme) => ({
container: {
@@ -138,8 +142,14 @@ export default function PairScanScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
const router = useRouter();
- const params = useLocalSearchParams<{ source?: string; targetServerId?: string }>();
+ const params = useLocalSearchParams<{
+ source?: string;
+ sourceServerId?: string;
+ targetServerId?: string;
+ }>();
const source = typeof params.source === "string" ? params.source : "settings";
+ const sourceServerId =
+ typeof params.sourceServerId === "string" ? params.sourceServerId : null;
const targetServerId = typeof params.targetServerId === "string" ? params.targetServerId : null;
const { daemons, upsertDaemonFromOfferUrl, updateHost } = useDaemonRegistry();
@@ -160,34 +170,47 @@ export default function PairScanScreen() {
const returnToSource = useCallback(
(serverId: string) => {
if (source === "onboarding") {
- router.replace({ pathname: "/", params: { serverId } });
+ router.replace(buildHostAgentDraftRoute(serverId) as any);
return;
}
if (source === "editHost" && targetServerId) {
- router.replace({ pathname: "/settings", params: { editHost: targetServerId } });
+ const settingsServerId = sourceServerId ?? targetServerId;
+ router.replace({
+ pathname: buildHostSettingsRoute(settingsServerId),
+ params: { editHost: targetServerId },
+ } as any);
return;
}
// settings (default): return to previous screen
try {
router.back();
} catch {
- router.replace("/settings");
+ const settingsServerId = sourceServerId ?? serverId;
+ router.replace(buildHostSettingsRoute(settingsServerId) as any);
}
},
- [router, source, targetServerId]
+ [router, source, sourceServerId, targetServerId]
);
const closeToSource = useCallback(() => {
if (source === "editHost" && targetServerId) {
- router.replace({ pathname: "/settings", params: { editHost: targetServerId } });
+ const settingsServerId = sourceServerId ?? targetServerId;
+ router.replace({
+ pathname: buildHostSettingsRoute(settingsServerId),
+ params: { editHost: targetServerId },
+ } as any);
return;
}
try {
router.back();
} catch {
- router.replace("/settings");
+ if (sourceServerId) {
+ router.replace(buildHostSettingsRoute(sourceServerId) as any);
+ return;
+ }
+ router.replace("/" as any);
}
- }, [router, source, targetServerId]);
+ }, [router, source, sourceServerId, targetServerId]);
useEffect(() => {
if (Platform.OS === "web") return;
diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx
index 963c28fa4..7a8757910 100644
--- a/packages/app/src/components/agent-list.tsx
+++ b/packages/app/src/components/agent-list.tsx
@@ -28,6 +28,10 @@ import {
buildAgentNavigationKey,
startNavigationTiming,
} from "@/utils/navigation-timing";
+import {
+ buildHostAgentDetailRoute,
+ parseHostAgentRouteFromPathname,
+} from "@/utils/host-routes";
interface AgentListProps {
agents: AggregatedAgent[];
@@ -114,12 +118,12 @@ export function AgentList({
params: { serverId, agentId },
});
- const shouldReplace = pathname.startsWith("/agent/");
+ const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
onAgentSelect?.();
- navigate(`/agent/${serverId}/${agentId}` as any);
+ navigate(buildHostAgentDetailRoute(serverId, agentId) as any);
},
[isActionSheetVisible, pathname, onAgentSelect]
);
diff --git a/packages/app/src/components/dictation-controls.tsx b/packages/app/src/components/dictation-controls.tsx
index 1c50b8159..eeb60f512 100644
--- a/packages/app/src/components/dictation-controls.tsx
+++ b/packages/app/src/components/dictation-controls.tsx
@@ -1,6 +1,6 @@
import { View, Text, Pressable, ActivityIndicator } from "react-native";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
-import { X, ArrowUp, RefreshCcw, Check, Mic } from "lucide-react-native";
+import { X, ArrowUp, RefreshCcw, Check, Mic, Pencil } from "lucide-react-native";
import { VolumeMeter } from "./volume-meter";
import { FOOTER_HEIGHT } from "@/constants/layout";
import type { DictationStatus } from "@/hooks/use-dictation";
@@ -233,7 +233,7 @@ export function DictationOverlay({
{ backgroundColor: "rgba(255, 255, 255, 0.25)" },
]}
>
- {
- router.replace("/agent" as any);
+ router.replace(buildHostAgentDraftRoute(serverId) as any);
})
.catch((err) => {
const message = err instanceof Error ? err.message : "Failed to archive worktree";
diff --git a/packages/app/src/components/multi-daemon-session-host.tsx b/packages/app/src/components/multi-daemon-session-host.tsx
index eb55b08a1..2201c8fc2 100644
--- a/packages/app/src/components/multi-daemon-session-host.tsx
+++ b/packages/app/src/components/multi-daemon-session-host.tsx
@@ -3,13 +3,11 @@ import {
useDaemonRegistry,
type HostProfile,
} from "@/contexts/daemon-registry-context";
-import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import {
buildDaemonWebSocketUrl,
buildRelayWebSocketUrl,
} from "@/utils/daemon-endpoints";
-import { probeConnection } from "@/utils/test-daemon-connection";
-import { useEffect, useMemo, useRef, useState } from "react";
+import { useMemo } from "react";
import type { ActiveConnection } from "@/contexts/daemon-connections-context";
type Candidate = {
@@ -67,120 +65,17 @@ function buildCandidates(host: HostProfile): Candidate[] {
}
function ManagedDaemonSession({ daemon }: { daemon: HostProfile }) {
- const { connectionStates } = useDaemonConnections();
- const { updateHost } = useDaemonRegistry();
-
const candidates = useMemo(() => buildCandidates(daemon), [daemon]);
- const [activeIndex, setActiveIndex] = useState(0);
- const active = candidates[activeIndex] ?? candidates[0] ?? null;
+ const active =
+ candidates.find((candidate) => candidate.connectionId === daemon.preferredConnectionId) ??
+ candidates[0] ??
+ null;
const activeUrl = active?.url ?? null;
- const lastAttemptedUrlRef = useRef(null);
- const pendingPreferenceWriteRef = useRef(false);
- const upgradeProbeInFlightRef = useRef(false);
-
- useEffect(() => {
- if (!activeUrl) {
- return;
- }
- // If the active URL fell out of the candidate set (e.g. endpoints updated), snap back.
- const idx = candidates.findIndex((c) => c.url === activeUrl);
- if (idx === -1) {
- setActiveIndex(0);
- }
- // eslint-disable-next-line react-hooks/exhaustive-deps
- }, [candidates.map((c) => c.url).join("|")]);
-
if (!activeUrl) {
return null;
}
- const connection = connectionStates.get(daemon.serverId);
- const status = connection?.status ?? "idle";
- const lastError = connection?.lastError ?? null;
-
- useEffect(() => {
- if (!connection) return;
-
- if (status === "online") {
- if (!active) return;
- if (pendingPreferenceWriteRef.current) return;
- if (daemon.preferredConnectionId === active.connectionId) return;
-
- pendingPreferenceWriteRef.current = true;
- void updateHost(daemon.serverId, {
- preferredConnectionId: active.connectionId,
- }).finally(() => {
- pendingPreferenceWriteRef.current = false;
- });
- return;
- }
-
- if ((status === "error" || (status === "offline" && lastError)) && candidates.length > 1) {
- if (lastAttemptedUrlRef.current === activeUrl) {
- return;
- }
- lastAttemptedUrlRef.current = activeUrl;
- if (activeIndex < candidates.length - 1) {
- setActiveIndex((idx) => Math.min(idx + 1, candidates.length - 1));
- }
- }
- }, [
- active,
- activeIndex,
- activeUrl,
- candidates.length,
- daemon.preferredConnectionId,
- daemon.serverId,
- lastError,
- status,
- updateHost,
- connection,
- ]);
-
- useEffect(() => {
- if (!connection) return;
- if (status !== "online") return;
- if (activeIndex === 0) return;
-
- const best = candidates[0];
- if (!best) return;
- if (best.activeConnection.type !== "direct") return;
-
- const intervalMs = 15_000;
- let cancelled = false;
-
- const attemptUpgrade = async () => {
- if (cancelled) return;
- if (upgradeProbeInFlightRef.current) return;
- if (activeIndex === 0) return;
-
- upgradeProbeInFlightRef.current = true;
- try {
- const { serverId } = await probeConnection(
- { id: "probe", type: "direct", endpoint: best.activeConnection.endpoint },
- { timeoutMs: 2000 },
- );
- if (cancelled) return;
- if (serverId !== daemon.serverId) return;
-
- lastAttemptedUrlRef.current = null;
- setActiveIndex(0);
- } catch {
- // ignore - we'll retry periodically
- } finally {
- upgradeProbeInFlightRef.current = false;
- }
- };
-
- void attemptUpgrade();
- const interval = setInterval(() => void attemptUpgrade(), intervalMs);
- return () => {
- cancelled = true;
- clearInterval(interval);
- };
- }, [activeIndex, candidates, connection, daemon.serverId, status]);
-
return (
void;
- onCreateAgent: (workingDir: string) => void;
+ onCreateAgent: (serverId: string, workingDir: string) => void;
onDrag: () => void;
isDragging: boolean;
}
@@ -109,11 +114,12 @@ function SectionHeader({
const handleCreatePress = useCallback(
(e: { stopPropagation: () => void }) => {
e.stopPropagation();
- if (createAgentWorkingDir) {
- onCreateAgent(createAgentWorkingDir);
+ const serverId = section.firstAgentServerId;
+ if (createAgentWorkingDir && serverId) {
+ onCreateAgent(serverId, createAgentWorkingDir);
}
},
- [onCreateAgent, createAgentWorkingDir]
+ [onCreateAgent, createAgentWorkingDir, section.firstAgentServerId]
);
return (
@@ -405,12 +411,12 @@ export function SidebarAgentList({
params: { serverId, agentId },
});
- const shouldReplace = pathname.startsWith("/agent/");
+ const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
onAgentSelect?.();
- navigate(`/agent/${serverId}/${agentId}` as any);
+ navigate(buildHostAgentDetailRoute(serverId, agentId) as any);
},
[isActionSheetVisible, pathname, onAgentSelect]
);
@@ -432,9 +438,9 @@ export function SidebarAgentList({
}, [actionAgent, actionClient]);
const handleCreateAgentInProject = useCallback(
- (workingDir: string) => {
+ (serverId: string, workingDir: string) => {
onAgentSelect?.();
- router.push(`/agent?workingDir=${encodeURIComponent(workingDir)}` as any);
+ router.push(buildNewAgentRoute(serverId, workingDir) as any);
},
[onAgentSelect]
);
diff --git a/packages/app/src/components/sliding-sidebar.tsx b/packages/app/src/components/sliding-sidebar.tsx
index f060a6179..0bce11f1e 100644
--- a/packages/app/src/components/sliding-sidebar.tsx
+++ b/packages/app/src/components/sliding-sidebar.tsx
@@ -1,4 +1,4 @@
-import { useCallback, useMemo, useState, useEffect } from "react";
+import { useCallback, useMemo, useState, useEffect, useRef } from "react";
import { View, Pressable, Text, Platform } from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import Animated, {
@@ -10,7 +10,7 @@ import Animated, {
import { Gesture, GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import { Plus, Settings, Users } from "lucide-react-native";
-import { router } from "expo-router";
+import { router, usePathname } from "expo-router";
import { usePanelStore } from "@/stores/panel-store";
import { SidebarAgentList } from "./sidebar-agent-list";
import { SidebarAgentListSkeleton } from "./sidebar-agent-list-skeleton";
@@ -20,6 +20,17 @@ import { useTauriDragHandlers, useTrafficLightPadding } from "@/utils/tauri-wind
import { useSidebarCollapsedSectionsStore } from "@/stores/sidebar-collapsed-sections-store";
import { useKeyboardNavStore } from "@/stores/keyboard-nav-store";
import { deriveSidebarShortcutAgentKeys } from "@/utils/sidebar-shortcuts";
+import { Combobox } from "@/components/ui/combobox";
+import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
+import { useDaemonConnections } from "@/contexts/daemon-connections-context";
+import { formatConnectionStatus } from "@/utils/daemons";
+import {
+ buildHostAgentDraftRoute,
+ buildHostAgentsRoute,
+ buildHostSettingsRoute,
+ mapPathnameToServer,
+ parseServerIdFromPathname,
+} from "@/utils/host-routes";
const DESKTOP_SIDEBAR_WIDTH = 320;
@@ -35,6 +46,42 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
const mobileView = usePanelStore((state) => state.mobileView);
const desktopAgentListOpen = usePanelStore((state) => state.desktop.agentListOpen);
const closeToAgent = usePanelStore((state) => state.closeToAgent);
+ const pathname = usePathname();
+ const { daemons } = useDaemonRegistry();
+ const { connectionStates } = useDaemonConnections();
+ const activeServerIdFromPath = useMemo(
+ () => parseServerIdFromPathname(pathname),
+ [pathname]
+ );
+ const activeServerId = activeServerIdFromPath ?? daemons[0]?.serverId ?? null;
+ const activeHostLabel = useMemo(() => {
+ if (!activeServerId) return "No host";
+ const daemon = daemons.find((entry) => entry.serverId === activeServerId);
+ const trimmed = daemon?.label?.trim();
+ return trimmed && trimmed.length > 0 ? trimmed : activeServerId;
+ }, [activeServerId, daemons]);
+ const activeHostStatus = activeServerId
+ ? (connectionStates.get(activeServerId)?.status ?? "idle")
+ : "idle";
+ const activeHostStatusColor =
+ activeHostStatus === "online"
+ ? theme.colors.palette.green[400]
+ : activeHostStatus === "connecting"
+ ? theme.colors.palette.amber[500]
+ : theme.colors.palette.red[500];
+ const hostOptions = useMemo(
+ () =>
+ daemons.map((daemon) => ({
+ id: daemon.serverId,
+ label: daemon.label?.trim() || daemon.serverId,
+ description: formatConnectionStatus(
+ connectionStates.get(daemon.serverId)?.status ?? "idle"
+ ),
+ })),
+ [connectionStates, daemons]
+ );
+ const hostTriggerRef = useRef(null);
+ const [isHostPickerOpen, setIsHostPickerOpen] = useState(false);
// Derive isOpen from the unified panel state
const isOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
@@ -45,7 +92,7 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
isInitialLoad,
isRevalidating,
refreshAll,
- } = useSidebarAgentsGrouped({ isOpen });
+ } = useSidebarAgentsGrouped({ isOpen, serverId: activeServerId });
const {
translateX,
backdropOpacity,
@@ -88,8 +135,11 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
}, [closeToAgent]);
const handleCreateAgentClean = useCallback(() => {
- router.push("/agent" as any);
- }, []);
+ if (!activeServerId) {
+ return;
+ }
+ router.push(buildHostAgentDraftRoute(activeServerId) as any);
+ }, [activeServerId]);
// Mobile: close sidebar and navigate
const handleCreateAgentCleanMobile = useCallback(() => {
@@ -104,14 +154,20 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
// Mobile: close sidebar and navigate
const handleSettingsMobile = useCallback(() => {
+ if (!activeServerId) {
+ return;
+ }
closeToAgent();
- router.push("/settings");
- }, [closeToAgent]);
+ router.push(buildHostSettingsRoute(activeServerId) as any);
+ }, [activeServerId, closeToAgent]);
// Desktop: just navigate, don't close
const handleSettingsDesktop = useCallback(() => {
- router.push("/settings");
- }, []);
+ if (!activeServerId) {
+ return;
+ }
+ router.push(buildHostSettingsRoute(activeServerId) as any);
+ }, [activeServerId]);
// Mobile: close sidebar when agent is selected
// Snap immediately since navigation interrupts animations
@@ -122,13 +178,35 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
}, [closeToAgent, translateX, backdropOpacity, windowWidth]);
const handleViewMore = useCallback(() => {
+ if (!activeServerId) {
+ return;
+ }
if (isMobile) {
translateX.value = -windowWidth;
backdropOpacity.value = 0;
closeToAgent();
}
- router.push("/agents");
- }, [backdropOpacity, closeToAgent, isMobile, translateX, windowWidth]);
+ router.push(buildHostAgentsRoute(activeServerId) as any);
+ }, [
+ activeServerId,
+ backdropOpacity,
+ closeToAgent,
+ isMobile,
+ translateX,
+ windowWidth,
+ ]);
+
+ const handleHostSelect = useCallback(
+ (nextServerId: string) => {
+ if (!nextServerId) {
+ return;
+ }
+ const nextPath = mapPathnameToServer(pathname, nextServerId);
+ setIsHostPickerOpen(false);
+ router.push(nextPath as any);
+ },
+ [pathname]
+ );
// Close gesture (swipe left to close when sidebar is open)
// Only activates on leftward swipe, fails on rightward or vertical movement
@@ -206,18 +284,46 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
{/* Header */}
-
- {({ hovered }) => (
- <>
-
- New agent
- >
- )}
-
+
+
+ {({ hovered }) => (
+ <>
+
+ New agent
+ >
+ )}
+
+ setIsHostPickerOpen(true)}
+ disabled={hostOptions.length === 0}
+ >
+
+
+ {activeHostLabel}
+
+
+
+
{/* Middle: scrollable agent list */}
@@ -286,18 +392,46 @@ export function SlidingSidebar({ selectedAgentId }: SlidingSidebarProps) {
style={[styles.sidebarHeader, { paddingTop: trafficLightPadding.top || styles.sidebarHeader.paddingTop }]}
{...dragHandlers}
>
-
- {({ hovered }) => (
- <>
-
- New agent
- >
- )}
-
+
+
+ {({ hovered }) => (
+ <>
+
+ New agent
+ >
+ )}
+
+ setIsHostPickerOpen(true)}
+ disabled={hostOptions.length === 0}
+ >
+
+
+ {activeHostLabel}
+
+
+
+
{/* Middle: scrollable agent list */}
@@ -383,12 +517,19 @@ const styles = StyleSheet.create((theme) => ({
borderBottomColor: theme.colors.border,
userSelect: "none",
},
+ sidebarHeaderRow: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "space-between",
+ gap: theme.spacing[2],
+ },
newAgentButton: {
flexDirection: "row",
alignItems: "center",
gap: theme.spacing[2],
paddingVertical: theme.spacing[1],
paddingHorizontal: theme.spacing[1],
+ flexShrink: 0,
},
newAgentButtonHovered: {},
newAgentButtonText: {
@@ -399,6 +540,25 @@ const styles = StyleSheet.create((theme) => ({
newAgentButtonTextHovered: {
color: theme.colors.foreground,
},
+ hostTrigger: {
+ flexDirection: "row",
+ alignItems: "center",
+ justifyContent: "flex-end",
+ gap: theme.spacing[2],
+ minWidth: 0,
+ maxWidth: "55%",
+ paddingVertical: theme.spacing[1],
+ paddingHorizontal: theme.spacing[1],
+ },
+ hostStatusDot: {
+ width: 8,
+ height: 8,
+ borderRadius: theme.borderRadius.full,
+ },
+ hostTriggerText: {
+ fontSize: theme.fontSize.sm,
+ color: theme.colors.foregroundMuted,
+ },
sidebarFooter: {
flexDirection: "row",
alignItems: "center",
diff --git a/packages/app/src/components/welcome-screen.tsx b/packages/app/src/components/welcome-screen.tsx
index d40ef5194..db82cf4ca 100644
--- a/packages/app/src/components/welcome-screen.tsx
+++ b/packages/app/src/components/welcome-screen.tsx
@@ -9,6 +9,7 @@ import { useSessionStore } from "@/stores/session-store";
import { AddHostModal } from "./add-host-modal";
import { PairLinkModal } from "./pair-link-modal";
import { NameHostModal } from "./name-host-modal";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
const styles = StyleSheet.create((theme) => ({
container: {
@@ -90,7 +91,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
const finishOnboarding = useCallback(
(serverId: string) => {
- router.replace({ pathname: "/", params: { serverId } });
+ router.replace(buildHostAgentDraftRoute(serverId) as any);
},
[router]
);
diff --git a/packages/app/src/hooks/use-all-agents-list.ts b/packages/app/src/hooks/use-all-agents-list.ts
index 35efdd68a..14c695f6e 100644
--- a/packages/app/src/hooks/use-all-agents-list.ts
+++ b/packages/app/src/hooks/use-all-agents-list.ts
@@ -1,6 +1,5 @@
import { useCallback, useMemo } from "react";
-import { useQueries, useQueryClient } from "@tanstack/react-query";
-import { useShallow } from "zustand/shallow";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore, type Agent } from "@/stores/session-store";
import type { AggregatedAgent, AggregatedAgentsResult } from "@/hooks/use-aggregated-agents";
@@ -31,106 +30,69 @@ function toAggregatedAgent(params: {
};
}
-export function useAllAgentsList(): AggregatedAgentsResult {
+export function useAllAgentsList(options?: {
+ serverId?: string | null;
+}): AggregatedAgentsResult {
const { connectionStates } = useDaemonConnections();
const queryClient = useQueryClient();
+ const serverId = useMemo(() => {
+ const value = options?.serverId;
+ return typeof value === "string" && value.trim().length > 0
+ ? value.trim()
+ : null;
+ }, [options?.serverId]);
- const sessionClients = useSessionStore(
- useShallow((state) => {
- const result: Record<
- string,
- NonNullable | null
- > = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.client ?? null;
+ const session = useSessionStore((state) =>
+ serverId ? state.sessions[serverId] : undefined
+ );
+ const client = session?.client ?? null;
+ const isConnected = session?.connection.isConnected ?? false;
+ const liveAgents = session?.agents ?? null;
+ const canFetch = Boolean(serverId && client && isConnected);
+
+ const agentsQuery = useQuery({
+ queryKey: ["allAgents", serverId] as const,
+ queryFn: async () => {
+ if (!client) {
+ throw new Error("Daemon client not available");
}
- return result;
- })
- );
-
- const sessionConnections = useSessionStore(
- useShallow((state) => {
- const result: Record = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.connection.isConnected;
- }
- return result;
- })
- );
-
- const liveAgents = useSessionStore(
- useShallow((state) => {
- const result: Record | undefined> = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.agents;
- }
- return result;
- })
- );
-
- const serverEntries = useMemo(
- () =>
- Object.keys(sessionClients).map((serverId) => ({
- serverId,
- client: sessionClients[serverId] ?? null,
- isConnected: sessionConnections[serverId] ?? false,
- })),
- [sessionClients, sessionConnections]
- );
-
- const queries = useQueries({
- queries: serverEntries.map(({ serverId, client, isConnected }) => ({
- queryKey: ["allAgents", serverId] as const,
- queryFn: async () => {
- if (!client) {
- throw new Error("Daemon client not available");
- }
- return await client.fetchAgents();
- },
- enabled: Boolean(client) && isConnected,
- staleTime: ALL_AGENTS_STALE_TIME,
- refetchOnMount: "always" as const,
- })),
+ return await client.fetchAgents();
+ },
+ enabled: canFetch,
+ staleTime: ALL_AGENTS_STALE_TIME,
+ refetchOnMount: "always" as const,
});
const refreshAll = useCallback(() => {
- for (const { serverId } of serverEntries) {
- void queryClient.invalidateQueries({
- queryKey: ["allAgents", serverId],
- });
+ if (!serverId) {
+ return;
}
- }, [queryClient, serverEntries]);
+ void queryClient.invalidateQueries({
+ queryKey: ["allAgents", serverId],
+ });
+ }, [queryClient, serverId]);
const agents = useMemo(() => {
- const all: AggregatedAgent[] = [];
+ if (!serverId) {
+ return [];
+ }
+ const data = agentsQuery.data ?? [];
+ const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
+ const list: AggregatedAgent[] = [];
- for (let idx = 0; idx < serverEntries.length; idx++) {
- const entry = serverEntries[idx];
- if (!entry) {
- continue;
- }
- const data = queries[idx]?.data;
- if (!data) {
- continue;
- }
- const serverLabel =
- connectionStates.get(entry.serverId)?.daemon.label ?? entry.serverId;
- const liveById = liveAgents[entry.serverId];
-
- for (const snapshot of data) {
- const normalized = normalizeAgentSnapshot(snapshot, entry.serverId);
- const live = liveById?.get(snapshot.id);
- all.push(
- toAggregatedAgent({
- source: live ?? normalized,
- serverId: entry.serverId,
- serverLabel,
- })
- );
- }
+ for (const snapshot of data) {
+ const normalized = normalizeAgentSnapshot(snapshot, serverId);
+ const live = liveAgents?.get(snapshot.id);
+ list.push(
+ toAggregatedAgent({
+ source: live ?? normalized,
+ serverId,
+ serverLabel,
+ })
+ );
}
- all.sort((left, right) => {
+ list.sort((left, right) => {
const leftRunning = left.status === "running";
const rightRunning = right.status === "running";
if (leftRunning && !rightRunning) {
@@ -142,10 +104,11 @@ export function useAllAgentsList(): AggregatedAgentsResult {
return right.lastActivityAt.getTime() - left.lastActivityAt.getTime();
});
- return all;
- }, [serverEntries, queries, connectionStates, liveAgents]);
+ return list;
+ }, [agentsQuery.data, connectionStates, liveAgents, serverId]);
- const isFetching = queries.some((query) => query.isPending || query.isFetching);
+ const isFetching =
+ canFetch && (agentsQuery.isPending || agentsQuery.isFetching);
const isInitialLoad = isFetching && agents.length === 0;
const isRevalidating = isFetching && agents.length > 0;
diff --git a/packages/app/src/hooks/use-command-center.ts b/packages/app/src/hooks/use-command-center.ts
index fefeaa974..14470b883 100644
--- a/packages/app/src/hooks/use-command-center.ts
+++ b/packages/app/src/hooks/use-command-center.ts
@@ -17,6 +17,12 @@ import {
buildNewAgentRoute,
resolveNewAgentWorkingDir,
} from "@/utils/new-agent-routing";
+import {
+ buildHostAgentDetailRoute,
+ buildHostSettingsRoute,
+ parseHostAgentRouteFromPathname,
+ parseServerIdFromPathname,
+} from "@/utils/host-routes";
import type { ShortcutKey } from "@/utils/format-shortcut";
import { focusWithRetries } from "@/utils/web-focus";
@@ -46,19 +52,17 @@ function sortAgents(left: AggregatedAgent, right: AggregatedAgent): number {
}
function parseAgentKeyFromPathname(pathname: string): string | null {
- const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)/);
- if (!match) return null;
- return `${match[1]}:${match[2]}`;
+ const match = parseHostAgentRouteFromPathname(pathname);
+ if (!match) {
+ return null;
+ }
+ return `${match.serverId}:${match.agentId}`;
}
function parseAgentRouteFromPathname(
pathname: string
): { serverId: string; agentId: string } | null {
- const match = pathname.match(/^\/agent\/([^/]+)\/([^/]+)/);
- if (!match) return null;
- const [, serverId, agentId] = match;
- if (!serverId || !agentId) return null;
- return { serverId, agentId };
+ return parseHostAgentRouteFromPathname(pathname);
}
type CommandCenterActionDefinition = {
@@ -67,7 +71,7 @@ type CommandCenterActionDefinition = {
icon?: "plus" | "settings";
shortcutKeys?: ShortcutKey[];
keywords: string[];
- buildRoute: (params: { newAgentRoute: string }) => string;
+ buildRoute: (params: { newAgentRoute: string; settingsRoute: string }) => string;
};
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
@@ -84,7 +88,7 @@ const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
title: "Settings",
icon: "settings",
keywords: ["settings", "preferences", "config", "configuration"],
- buildRoute: () => "/settings",
+ buildRoute: ({ settingsRoute }) => settingsRoute,
},
];
@@ -137,22 +141,26 @@ export function useCommandCenter() {
return filtered;
}, [agents, query]);
+ const fallbackServerId = agents[0]?.serverId ?? null;
+
const agentKeyFromPathname = useMemo(
() => parseAgentKeyFromPathname(pathname),
[pathname]
);
const newAgentRoute = useMemo(() => {
+ const serverIdFromPath =
+ parseServerIdFromPathname(pathname) ?? fallbackServerId;
const routeAgent = parseAgentRouteFromPathname(pathname);
if (!routeAgent) {
- return "/agent";
+ return serverIdFromPath ? buildNewAgentRoute(serverIdFromPath) : "/";
}
const { serverId, agentId } = routeAgent;
const currentAgent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
const cwd = currentAgent?.cwd?.trim();
if (!cwd) {
- return "/agent";
+ return buildNewAgentRoute(serverId);
}
const checkout =
@@ -160,8 +168,14 @@ export function useCommandCenter() {
checkoutStatusQueryKey(serverId, cwd)
) ?? null;
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
- return buildNewAgentRoute(workingDir);
- }, [pathname]);
+ return buildNewAgentRoute(serverId, workingDir);
+ }, [fallbackServerId, pathname]);
+
+ const settingsRoute = useMemo(() => {
+ const serverIdFromPath =
+ parseServerIdFromPathname(pathname) ?? fallbackServerId;
+ return serverIdFromPath ? buildHostSettingsRoute(serverIdFromPath) : "/";
+ }, [fallbackServerId, pathname]);
const actionItems = useMemo(() => {
return COMMAND_CENTER_ACTIONS.filter((action) =>
@@ -171,10 +185,10 @@ export function useCommandCenter() {
id: action.id,
title: action.title,
icon: action.icon,
- route: action.buildRoute({ newAgentRoute }),
+ route: action.buildRoute({ newAgentRoute, settingsRoute }),
shortcutKeys: action.shortcutKeys,
}));
- }, [newAgentRoute, query]);
+ }, [newAgentRoute, query, settingsRoute]);
const items = useMemo(() => {
const next: CommandCenterItem[] = [];
@@ -203,14 +217,14 @@ export function useCommandCenter() {
const session = useSessionStore.getState().sessions[agent.serverId];
session?.client?.clearAgentAttention(agent.id);
- const shouldReplace = pathname.startsWith("/agent/");
+ const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
requestFocusChatInput(agentKey(agent));
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
- navigate(`/agent/${agent.serverId}/${agent.id}` as any);
+ navigate(buildHostAgentDetailRoute(agent.serverId, agent.id) as any);
},
[pathname, requestFocusChatInput, setOpen]
);
diff --git a/packages/app/src/hooks/use-global-keyboard-nav.ts b/packages/app/src/hooks/use-global-keyboard-nav.ts
index a0235cedb..fc0e7d478 100644
--- a/packages/app/src/hooks/use-global-keyboard-nav.ts
+++ b/packages/app/src/hooks/use-global-keyboard-nav.ts
@@ -15,6 +15,11 @@ import {
buildNewAgentRoute,
resolveNewAgentWorkingDir,
} from "@/utils/new-agent-routing";
+import {
+ buildHostAgentDetailRoute,
+ parseHostAgentRouteFromPathname,
+ parseServerIdFromPathname,
+} from "@/utils/host-routes";
export function useGlobalKeyboardNav({
enabled,
@@ -86,18 +91,20 @@ export function useGlobalKeyboardNav({
}
const { serverId, agentId } = parsed;
- const shouldReplace = pathname.startsWith("/agent/");
+ const shouldReplace = Boolean(parseHostAgentRouteFromPathname(pathname));
const navigate = shouldReplace ? router.replace : router.push;
- navigate(`/agent/${serverId}/${agentId}` as any);
+ navigate(buildHostAgentDetailRoute(serverId, agentId) as any);
};
const navigateToNewAgent = () => {
- let target = "/agent";
+ let targetServerId = parseServerIdFromPathname(pathname);
+ let targetWorkingDir: string | null = null;
if (selectedAgentId) {
const separatorIndex = selectedAgentId.indexOf(":");
if (separatorIndex > 0) {
const serverId = selectedAgentId.slice(0, separatorIndex);
const agentId = selectedAgentId.slice(separatorIndex + 1);
+ targetServerId = serverId;
const agent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
const cwd = agent?.cwd?.trim();
if (cwd) {
@@ -105,13 +112,21 @@ export function useGlobalKeyboardNav({
queryClient.getQueryData(
checkoutStatusQueryKey(serverId, cwd)
) ?? null;
- const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
- target = buildNewAgentRoute(workingDir);
+ targetWorkingDir = resolveNewAgentWorkingDir(cwd, checkout);
}
}
}
- router.push(target as any);
+ if (!targetServerId) {
+ const sessionServerIds = Object.keys(useSessionStore.getState().sessions);
+ targetServerId = sessionServerIds[0] ?? null;
+ }
+
+ if (!targetServerId) {
+ return;
+ }
+
+ router.push(buildNewAgentRoute(targetServerId, targetWorkingDir) as any);
};
const handleKeyDown = (event: KeyboardEvent) => {
diff --git a/packages/app/src/hooks/use-sidebar-agents-grouped.ts b/packages/app/src/hooks/use-sidebar-agents-grouped.ts
index 2522b173c..928e54af8 100644
--- a/packages/app/src/hooks/use-sidebar-agents-grouped.ts
+++ b/packages/app/src/hooks/use-sidebar-agents-grouped.ts
@@ -1,6 +1,5 @@
import { useCallback, useEffect, useMemo } from "react";
-import { useQueries, useQueryClient } from "@tanstack/react-query";
-import { useShallow } from "zustand/shallow";
+import { useQuery, useQueryClient } from "@tanstack/react-query";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useSessionStore, type Agent } from "@/stores/session-store";
import type { AggregatedAgent } from "@/hooks/use-aggregated-agents";
@@ -15,7 +14,8 @@ const SIDEBAR_GROUPS_STALE_TIME = 15_000;
const SIDEBAR_GROUPS_REFETCH_INTERVAL = 10_000;
const MAX_AGENTS_PER_PROJECT = 5;
-type SidebarGroupsPayload = FetchAgentsGroupedByProjectResponseMessage["payload"];
+type SidebarGroupsPayload =
+ FetchAgentsGroupedByProjectResponseMessage["payload"];
export type SidebarCheckoutLite =
SidebarGroupsPayload["groups"][number]["agents"][number]["checkout"];
type MutableSidebarGroup = {
@@ -68,95 +68,63 @@ function toAggregatedAgent(params: {
export function useSidebarAgentsGrouped(options?: {
isOpen?: boolean;
+ serverId?: string | null;
}): SidebarAgentsGroupedResult {
const { connectionStates } = useDaemonConnections();
const queryClient = useQueryClient();
const isOpen = options?.isOpen ?? true;
+ const serverId = useMemo(() => {
+ const value = options?.serverId;
+ return typeof value === "string" && value.trim().length > 0
+ ? value.trim()
+ : null;
+ }, [options?.serverId]);
- const sessionClients = useSessionStore(
- useShallow((state) => {
- const result: Record<
- string,
- NonNullable | null
- > = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.client ?? null;
+ const session = useSessionStore((state) =>
+ serverId ? state.sessions[serverId] : undefined
+ );
+ const client = session?.client ?? null;
+ const liveAgents = session?.agents ?? null;
+ const isConnected = session?.connection.isConnected ?? false;
+ const canFetch = Boolean(serverId && client && isConnected);
+
+ const groupedQuery = useQuery({
+ queryKey: ["sidebarAgentsGrouped", serverId] as const,
+ queryFn: async () => {
+ if (!client) {
+ throw new Error("Daemon client not available");
}
- return result;
- })
- );
-
- const sessionAgents = useSessionStore(
- useShallow((state) => {
- const result: Record | undefined> = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.agents;
- }
- return result;
- })
- );
-
- const sessionConnections = useSessionStore(
- useShallow((state) => {
- const result: Record = {};
- for (const [serverId, session] of Object.entries(state.sessions)) {
- result[serverId] = session.connection.isConnected;
- }
- return result;
- })
- );
-
- const serverEntries = useMemo(
- () =>
- Object.keys(sessionClients).map((serverId) => ({
- serverId,
- client: sessionClients[serverId] ?? null,
- isConnected: sessionConnections[serverId] ?? false,
- })),
- [sessionClients, sessionConnections]
- );
-
- const groupedQueries = useQueries({
- queries: serverEntries.map(({ serverId, client, isConnected }) => ({
- queryKey: ["sidebarAgentsGrouped", serverId] as const,
- queryFn: async () => {
- if (!client) {
- throw new Error("Daemon client not available");
- }
- return await client.fetchAgentsGroupedByProject({
- filter: { labels: { ui: "true" } },
- });
- },
- enabled: Boolean(client) && isConnected,
- staleTime: SIDEBAR_GROUPS_STALE_TIME,
- refetchInterval: isOpen ? SIDEBAR_GROUPS_REFETCH_INTERVAL : false,
- refetchIntervalInBackground: isOpen,
- refetchOnMount: "always" as const,
- })),
+ return await client.fetchAgentsGroupedByProject({
+ filter: { labels: { ui: "true" } },
+ });
+ },
+ enabled: canFetch,
+ staleTime: SIDEBAR_GROUPS_STALE_TIME,
+ refetchInterval: isOpen ? SIDEBAR_GROUPS_REFETCH_INTERVAL : false,
+ refetchIntervalInBackground: isOpen,
+ refetchOnMount: "always" as const,
});
const projectOrder = useSectionOrderStore((state) => state.projectOrder);
const setProjectOrder = useSectionOrderStore((state) => state.setProjectOrder);
const { sections, checkoutByAgentKey, hasAnyData } = useMemo(() => {
+ if (!serverId) {
+ return {
+ sections: [] as SidebarSectionData[],
+ checkoutByAgentKey: new Map(),
+ hasAnyData: false,
+ };
+ }
+
const groupsByKey = new Map();
const checkoutLookup = new Map();
const seenAgentKeys = new Set();
- const groupedFetchReadyByServer = new Map();
-
- for (let idx = 0; idx < serverEntries.length; idx++) {
- const { serverId } = serverEntries[idx] ?? {};
- if (!serverId) {
- continue;
- }
- groupedFetchReadyByServer.set(serverId, groupedQueries[idx]?.isFetched ?? false);
- const payload = groupedQueries[idx]?.data as SidebarGroupsPayload | undefined;
- if (!payload) {
- continue;
- }
- const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
- const liveAgents = sessionAgents[serverId];
+ const payload = groupedQuery.data as SidebarGroupsPayload | undefined;
+ const groupedFetchReady = groupedQuery.isFetched;
+ const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
+ if (payload) {
for (const group of payload.groups) {
const existing: MutableSidebarGroup =
groupsByKey.get(group.projectKey) ??
@@ -180,7 +148,10 @@ export function useSidebarAgentsGrouped(options?: {
const agentKey = `${serverId}:${entry.agent.id}`;
seenAgentKeys.add(agentKey);
- checkoutLookup.set(agentKey, live?.projectPlacement?.checkout ?? entry.checkout);
+ checkoutLookup.set(
+ agentKey,
+ live?.projectPlacement?.checkout ?? entry.checkout
+ );
existing.agents.push(nextAgent);
}
@@ -188,16 +159,7 @@ export function useSidebarAgentsGrouped(options?: {
}
}
- for (const { serverId } of serverEntries) {
- if (!groupedFetchReadyByServer.get(serverId)) {
- continue;
- }
- const serverLabel = connectionStates.get(serverId)?.daemon.label ?? serverId;
- const liveAgents = sessionAgents[serverId];
- if (!liveAgents) {
- continue;
- }
-
+ if (groupedFetchReady && liveAgents) {
for (const live of liveAgents.values()) {
if (live.archivedAt || live.labels.ui !== "true") {
continue;
@@ -270,7 +232,14 @@ export function useSidebarAgentsGrouped(options?: {
checkoutByAgentKey: checkoutLookup,
hasAnyData: nextSections.length > 0,
};
- }, [serverEntries, groupedQueries, connectionStates, sessionAgents, projectOrder]);
+ }, [
+ connectionStates,
+ groupedQuery.data,
+ groupedQuery.isFetched,
+ liveAgents,
+ projectOrder,
+ serverId,
+ ]);
useEffect(() => {
const currentKeys = sections.map((section) => section.projectKey);
@@ -282,16 +251,16 @@ export function useSidebarAgentsGrouped(options?: {
}, [sections, projectOrder, setProjectOrder]);
const refreshAll = useCallback(() => {
- for (const { serverId } of serverEntries) {
- void queryClient.invalidateQueries({
- queryKey: ["sidebarAgentsGrouped", serverId],
- });
+ if (!serverId) {
+ return;
}
- }, [queryClient, serverEntries]);
+ void queryClient.invalidateQueries({
+ queryKey: ["sidebarAgentsGrouped", serverId],
+ });
+ }, [queryClient, serverId]);
- const isFetching = groupedQueries.some(
- (query) => query.isPending || query.isFetching
- );
+ const isFetching =
+ canFetch && (groupedQuery.isPending || groupedQuery.isFetching);
const isInitialLoad = isFetching && !hasAnyData;
const isRevalidating = isFetching && hasAnyData;
@@ -304,3 +273,4 @@ export function useSidebarAgentsGrouped(options?: {
refreshAll,
};
}
+
diff --git a/packages/app/src/screens/agent/agent-ready-screen.tsx b/packages/app/src/screens/agent/agent-ready-screen.tsx
index 291945e82..d722d4e55 100644
--- a/packages/app/src/screens/agent/agent-ready-screen.tsx
+++ b/packages/app/src/screens/agent/agent-ready-screen.tsx
@@ -70,6 +70,7 @@ import {
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
const DROPDOWN_WIDTH = 220;
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
@@ -124,7 +125,11 @@ export function AgentReadyScreen({
targetMs: 300,
});
}
- router.replace("/agent" as any);
+ if (targetServerId) {
+ router.replace(buildHostAgentDraftRoute(targetServerId) as any);
+ return;
+ }
+ router.replace("/" as any);
}, [resolvedAgentId, resolvedServerId, router]);
const focusServerId = resolvedServerId;
diff --git a/packages/app/src/screens/agent/draft-agent-screen.tsx b/packages/app/src/screens/agent/draft-agent-screen.tsx
index 201234156..6892d37ee 100644
--- a/packages/app/src/screens/agent/draft-agent-screen.tsx
+++ b/packages/app/src/screens/agent/draft-agent-screen.tsx
@@ -14,7 +14,7 @@ import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyl
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
import Animated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated";
-import { Folder, GitBranch, Menu, Monitor, PanelLeft } from "lucide-react-native";
+import { Folder, GitBranch, Menu, PanelLeft } from "lucide-react-native";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentInputArea } from "@/components/agent-input-area";
import { AgentStreamView } from "@/components/agent-stream-view";
@@ -29,7 +29,6 @@ import {
} from "@/hooks/use-checkout-status-query";
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
-import { formatConnectionStatus } from "@/utils/daemons";
import { buildBranchComboOptions, normalizeBranchOptionName } from "@/utils/branch-suggestions";
import { shortenPath } from "@/utils/shorten-path";
import { usePanelStore } from "@/stores/panel-store";
@@ -47,6 +46,7 @@ import type {
AgentSessionConfig,
} from "@server/server/agent/agent-sdk-types";
import { AGENT_PROVIDER_DEFINITIONS } from "@server/server/agent/provider-manifest";
+import { buildHostAgentDetailRoute } from "@/utils/host-routes";
const DRAFT_AGENT_ID = "__new_agent__";
const EMPTY_PENDING_PERMISSIONS = new Map();
@@ -112,11 +112,13 @@ type DraftAgentParams = {
type DraftAgentScreenProps = {
isVisible?: boolean;
onCreateFlowActiveChange?: (active: boolean) => void;
+ forcedServerId?: string;
};
export function DraftAgentScreen({
isVisible = true,
onCreateFlowActiveChange,
+ forcedServerId,
}: DraftAgentScreenProps = {}) {
const { theme } = useUnistyles();
const router = useRouter();
@@ -144,7 +146,11 @@ export function DraftAgentScreen({
};
});
- const resolvedServerId = getParamValue(params.serverId);
+ const forcedServerIdParam = forcedServerId?.trim();
+ const resolvedServerId =
+ forcedServerIdParam && forcedServerIdParam.length > 0
+ ? forcedServerIdParam
+ : getParamValue(params.serverId);
const resolvedProvider = getValidProvider(getParamValue(params.provider));
const resolvedMode = getValidMode(resolvedProvider, getParamValue(params.modeId));
const resolvedModel = getParamValue(params.model);
@@ -215,25 +221,9 @@ export function DraftAgentScreen({
isCreateFlow: true,
onlineServerIds,
});
- const daemonLabelByServerId = useMemo(() => {
- const map = new Map();
- for (const daemon of daemons) {
- const label = daemon.label?.trim();
- map.set(daemon.serverId, label && label.length > 0 ? label : daemon.serverId);
- }
- return map;
- }, [daemons]);
const hostEntry = selectedServerId
? connectionStates.get(selectedServerId)
: undefined;
- const selectedDaemonLabel = selectedServerId
- ? daemonLabelByServerId.get(selectedServerId)
- : null;
- const hostLabel =
- selectedDaemonLabel ??
- hostEntry?.daemon.label ??
- selectedServerId ??
- "Select host";
const isMobile =
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
const isSidebarOpen = isMobile ? mobileView === "agent-list" : desktopAgentListOpen;
@@ -242,7 +232,6 @@ export function DraftAgentScreen({
? theme.colors.foreground
: theme.colors.foregroundMuted;
- const [isHostOpen, setIsHostOpen] = useState(false);
const [worktreeMode, setWorktreeMode] = useState<"none" | "create" | "attach">("none");
const [baseBranch, setBaseBranch] = useState("");
const [worktreeSlug, setWorktreeSlug] = useState("");
@@ -252,7 +241,6 @@ export function DraftAgentScreen({
const [isBranchOpen, setIsBranchOpen] = useState(false);
const [branchSearchQuery, setBranchSearchQuery] = useState("");
const [debouncedBranchSearchQuery, setDebouncedBranchSearchQuery] = useState("");
- const hostAnchorRef = useRef(null);
const workingDirAnchorRef = useRef(null);
const worktreeAnchorRef = useRef(null);
const branchAnchorRef = useRef(null);
@@ -670,17 +658,6 @@ export function DraftAgentScreen({
worktreeMode === "create"
? "Create new worktree"
: selectedWorktreeLabel || "Select worktree";
- const hostOptions = useMemo(
- () =>
- daemons.map((daemon) => ({
- id: daemon.serverId,
- label: daemonLabelByServerId.get(daemon.serverId) ?? daemon.serverId,
- description: formatConnectionStatus(
- connectionStates.get(daemon.serverId)?.status ?? "idle"
- ),
- })),
- [connectionStates, daemonLabelByServerId, daemons]
- );
const worktreeComboOptions = useMemo(
() => [
{
@@ -930,7 +907,9 @@ export function DraftAgentScreen({
const agentId = (result as { id?: string })?.id;
if (agentId && selectedServerId) {
updatePendingAgentId(agentId);
- router.replace(`/agent/${selectedServerId}/${agentId}` as any);
+ router.replace(
+ buildHostAgentDetailRoute(selectedServerId, agentId) as any
+ );
return;
}
@@ -1031,7 +1010,7 @@ export function DraftAgentScreen({
- setIsHostOpen(true)}
- icon={}
- showLabel={false}
- />
{isDirectoryNotExists && (
@@ -1117,17 +1086,6 @@ export function DraftAgentScreen({
{attachWorktreeError ? {attachWorktreeError} : null}
{worktreeOptionsError ? {worktreeOptionsError} : null}
- setSelectedServerIdFromUser(serverId)}
- title="Host"
- searchPlaceholder="Search hosts..."
- open={isHostOpen}
- onOpenChange={setIsHostOpen}
- anchorRef={hostAnchorRef}
- />
-
{
- router.replace("/agent" as any);
- }, [router]);
+ const firstMatchServerId = matches[0]?.serverId ?? null;
+ if (firstMatchServerId) {
+ router.replace(buildHostAgentDraftRoute(firstMatchServerId) as any);
+ return;
+ }
+ router.replace("/" as any);
+ }, [matches, router]);
const handleSelectMatch = useCallback(
(match: AgentMatch) => {
- router.replace(`/agent/${match.serverId}/${match.agent.id}` as any);
+ router.replace(buildHostAgentDetailRoute(match.serverId, match.agent.id) as any);
},
[router]
);
diff --git a/packages/app/src/app/agents.tsx b/packages/app/src/screens/agents-screen.tsx
similarity index 82%
rename from packages/app/src/app/agents.tsx
rename to packages/app/src/screens/agents-screen.tsx
index 4b803c9fe..90d779be4 100644
--- a/packages/app/src/app/agents.tsx
+++ b/packages/app/src/screens/agents-screen.tsx
@@ -4,9 +4,13 @@ import { StyleSheet } from "react-native-unistyles";
import { BackHeader } from "@/components/headers/back-header";
import { AgentList } from "@/components/agent-list";
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
+import { router } from "expo-router";
-export default function AgentsScreen() {
- const { agents, isRevalidating, refreshAll } = useAllAgentsList();
+export function AgentsScreen({ serverId }: { serverId: string }) {
+ const { agents, isRevalidating, refreshAll } = useAllAgentsList({
+ serverId,
+ });
// Track user-initiated refresh to avoid showing spinner on background revalidation
const [isManualRefresh, setIsManualRefresh] = useState(false);
@@ -33,7 +37,10 @@ export default function AgentsScreen() {
return (
-
+ router.replace(buildHostAgentDraftRoute(serverId) as any)}
+ />
({
export default function SettingsScreen() {
const { theme } = useUnistyles();
const insets = useSafeAreaInsets();
- const params = useLocalSearchParams<{ editHost?: string }>();
+ const params = useLocalSearchParams<{ editHost?: string; serverId?: string }>();
+ const routeServerId = typeof params.serverId === "string" ? params.serverId.trim() : "";
const { settings, isLoading: settingsLoading, updateSettings, resetSettings } = useAppSettings();
const {
daemons,
@@ -674,16 +675,19 @@ export default function SettingsScreen() {
setIsAddHostMethodVisible(false);
setIsPasteLinkVisible(true);
}}
- onScanQr={() => {
- const targetServerId = addConnectionTargetServerId;
- const source = targetServerId ? "editHost" : "settings";
- closeAddConnectionFlow();
- router.push({
- pathname: "/pair-scan",
- params: targetServerId ? { source, targetServerId } : { source },
- });
- }}
- />
+ onScanQr={() => {
+ const targetServerId = addConnectionTargetServerId;
+ const source = targetServerId ? "editHost" : "settings";
+ const sourceServerId = routeServerId || targetServerId || undefined;
+ closeAddConnectionFlow();
+ router.push({
+ pathname: "/pair-scan",
+ params: targetServerId
+ ? { source, targetServerId, sourceServerId }
+ : { source, sourceServerId },
+ });
+ }}
+ />
0 ? trimmed : null;
+}
+
+function encodeSegment(value: string): string {
+ return encodeURIComponent(value);
+}
+
+export function parseServerIdFromPathname(pathname: string): string | null {
+ const match = pathname.match(/^\/h\/([^/]+)(?:\/|$)/);
+ if (!match) {
+ return null;
+ }
+ const raw = match[1];
+ if (!raw) {
+ return null;
+ }
+ try {
+ return trimNonEmpty(decodeURIComponent(raw));
+ } catch {
+ return trimNonEmpty(raw);
+ }
+}
+
+export function parseHostAgentRouteFromPathname(
+ pathname: string
+): { serverId: string; agentId: string } | null {
+ const match = pathname.match(/^\/h\/([^/]+)\/agent\/([^/]+)(?:\/|$)/);
+ if (!match) {
+ return null;
+ }
+
+ const [, encodedServerId, encodedAgentId] = match;
+ if (!encodedServerId || !encodedAgentId) {
+ return null;
+ }
+
+ const decode = (value: string) => {
+ try {
+ return decodeURIComponent(value);
+ } catch {
+ return value;
+ }
+ };
+
+ const serverId = trimNonEmpty(decode(encodedServerId));
+ const agentId = trimNonEmpty(decode(encodedAgentId));
+ if (!serverId || !agentId) {
+ return null;
+ }
+
+ return { serverId, agentId };
+}
+
+export function buildHostAgentDraftRoute(serverId: string): string {
+ const normalized = trimNonEmpty(serverId);
+ if (!normalized) {
+ return "/";
+ }
+ return `/h/${encodeSegment(normalized)}/agent`;
+}
+
+export function buildHostAgentDetailRoute(
+ serverId: string,
+ agentId: string
+): string {
+ const normalizedServerId = trimNonEmpty(serverId);
+ const normalizedAgentId = trimNonEmpty(agentId);
+ if (!normalizedServerId || !normalizedAgentId) {
+ return "/";
+ }
+ return `/h/${encodeSegment(normalizedServerId)}/agent/${encodeSegment(
+ normalizedAgentId
+ )}`;
+}
+
+export function buildHostAgentsRoute(serverId: string): string {
+ const normalized = trimNonEmpty(serverId);
+ if (!normalized) {
+ return "/";
+ }
+ return `/h/${encodeSegment(normalized)}/agents`;
+}
+
+export function buildHostSettingsRoute(serverId: string): string {
+ const normalized = trimNonEmpty(serverId);
+ if (!normalized) {
+ return "/";
+ }
+ return `/h/${encodeSegment(normalized)}/settings`;
+}
+
+export function mapPathnameToServer(
+ pathname: string,
+ nextServerId: string
+): string {
+ const normalized = trimNonEmpty(nextServerId);
+ if (!normalized) {
+ return "/";
+ }
+
+ const suffix = pathname.replace(/^\/h\/[^/]+\/?/, "");
+ const base = `/h/${encodeSegment(normalized)}`;
+ if (suffix.startsWith("settings")) {
+ return `${base}/settings`;
+ }
+ if (suffix.startsWith("agents")) {
+ return `${base}/agents`;
+ }
+ return `${base}/agent`;
+}
+
diff --git a/packages/app/src/utils/new-agent-routing.test.ts b/packages/app/src/utils/new-agent-routing.test.ts
index 19ba51606..a9486afea 100644
--- a/packages/app/src/utils/new-agent-routing.test.ts
+++ b/packages/app/src/utils/new-agent-routing.test.ts
@@ -7,14 +7,14 @@ import {
} from "./new-agent-routing";
describe("buildNewAgentRoute", () => {
- it("falls back to /agent when no working directory is provided", () => {
- expect(buildNewAgentRoute(undefined)).toBe("/agent");
- expect(buildNewAgentRoute(" ")).toBe("/agent");
+ it("falls back to host-scoped draft route when no working directory is provided", () => {
+ expect(buildNewAgentRoute("srv-1", undefined)).toBe("/h/srv-1/agent");
+ expect(buildNewAgentRoute("srv-1", " ")).toBe("/h/srv-1/agent");
});
it("encodes the working directory query parameter", () => {
- expect(buildNewAgentRoute("/Users/me/dev/paseo")).toBe(
- "/agent?workingDir=%2FUsers%2Fme%2Fdev%2Fpaseo"
+ expect(buildNewAgentRoute("srv-1", "/Users/me/dev/paseo")).toBe(
+ "/h/srv-1/agent?workingDir=%2FUsers%2Fme%2Fdev%2Fpaseo"
);
});
});
diff --git a/packages/app/src/utils/new-agent-routing.ts b/packages/app/src/utils/new-agent-routing.ts
index ac3caf3ee..7d097ed8a 100644
--- a/packages/app/src/utils/new-agent-routing.ts
+++ b/packages/app/src/utils/new-agent-routing.ts
@@ -1,4 +1,5 @@
import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query";
+import { buildHostAgentDraftRoute } from "@/utils/host-routes";
export function resolveNewAgentWorkingDir(
cwd: string,
@@ -7,10 +8,17 @@ export function resolveNewAgentWorkingDir(
return (checkout?.isPaseoOwnedWorktree ? checkout.mainRepoRoot : null) ?? cwd;
}
-export function buildNewAgentRoute(workingDir?: string | null): string {
+export function buildNewAgentRoute(
+ serverId: string,
+ workingDir?: string | null
+): string {
+ const baseRoute = buildHostAgentDraftRoute(serverId);
+ if (baseRoute === "/") {
+ return baseRoute;
+ }
const trimmedWorkingDir = workingDir?.trim();
if (!trimmedWorkingDir) {
- return "/agent";
+ return baseRoute;
}
- return `/agent?workingDir=${encodeURIComponent(trimmedWorkingDir)}`;
+ return `${baseRoute}?workingDir=${encodeURIComponent(trimmedWorkingDir)}`;
}
diff --git a/packages/app/src/utils/notification-routing.test.ts b/packages/app/src/utils/notification-routing.test.ts
index df3256bb4..b03ce00df 100644
--- a/packages/app/src/utils/notification-routing.test.ts
+++ b/packages/app/src/utils/notification-routing.test.ts
@@ -33,16 +33,17 @@ describe("resolveNotificationTarget", () => {
describe("buildNotificationRoute", () => {
it("routes directly to server-scoped agent path when both ids are present", () => {
expect(buildNotificationRoute({ serverId: "srv-1", agentId: "agent-1" })).toBe(
- "/agent/srv-1/agent-1"
+ "/h/srv-1/agent/agent-1"
);
});
- it("falls back to legacy agent route when serverId is absent", () => {
- expect(buildNotificationRoute({ agentId: "agent-legacy" })).toBe("/agent/agent-legacy");
+ it("falls back to host-scoped draft route when only serverId is present", () => {
+ expect(buildNotificationRoute({ serverId: "srv-only" })).toBe("/h/srv-only/agent");
});
- it("falls back to agents list when no agent id is present", () => {
- expect(buildNotificationRoute({ serverId: "srv-only" })).toBe("/agents");
+ it("falls back to root when no server id is present", () => {
+ expect(buildNotificationRoute({ agentId: "agent-legacy" })).toBe("/");
+ expect(buildNotificationRoute(undefined)).toBe("/");
});
it("encodes path segments", () => {
@@ -51,6 +52,6 @@ describe("buildNotificationRoute", () => {
serverId: "srv/with/slash",
agentId: "agent with space",
})
- ).toBe("/agent/srv%2Fwith%2Fslash/agent%20with%20space");
+ ).toBe("/h/srv%2Fwith%2Fslash/agent/agent%20with%20space");
});
});
diff --git a/packages/app/src/utils/notification-routing.ts b/packages/app/src/utils/notification-routing.ts
index 7b1e5eff7..e70699b8a 100644
--- a/packages/app/src/utils/notification-routing.ts
+++ b/packages/app/src/utils/notification-routing.ts
@@ -1,3 +1,8 @@
+import {
+ buildHostAgentDetailRoute,
+ buildHostAgentDraftRoute,
+} from "@/utils/host-routes";
+
type NotificationData = Record | null | undefined;
function readNonEmptyString(
@@ -25,10 +30,10 @@ export function resolveNotificationTarget(data: NotificationData): {
export function buildNotificationRoute(data: NotificationData): string {
const { serverId, agentId } = resolveNotificationTarget(data);
if (serverId && agentId) {
- return `/agent/${encodeURIComponent(serverId)}/${encodeURIComponent(agentId)}`;
+ return buildHostAgentDetailRoute(serverId, agentId);
}
- if (agentId) {
- return `/agent/${encodeURIComponent(agentId)}`;
+ if (serverId) {
+ return buildHostAgentDraftRoute(serverId);
}
- return "/agents";
+ return "/";
}
diff --git a/packages/app/src/utils/os-notifications.test.ts b/packages/app/src/utils/os-notifications.test.ts
index 1fa5a7f5c..2d3ed3f03 100644
--- a/packages/app/src/utils/os-notifications.test.ts
+++ b/packages/app/src/utils/os-notifications.test.ts
@@ -161,6 +161,6 @@ describe("sendOsNotification", () => {
expect(clicked.onclick).toBeTypeOf("function");
clicked.onclick?.({} as Event);
- expect(assign).toHaveBeenCalledWith("/agent/srv%20with%20space/agent%2F1");
+ expect(assign).toHaveBeenCalledWith("/h/srv%20with%20space/agent/agent%2F1");
});
});