feat: add workspace-aware notification routing and clean up agent screen header

This commit is contained in:
Mohamed Boudra
2026-03-04 10:37:43 +07:00
parent 3c066f5dd9
commit 6de7756952
8 changed files with 73 additions and 417 deletions
+8 -3
View File
@@ -10,7 +10,7 @@ import {
} from "react-native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useCallback, useMemo, useState, type ReactElement } from "react";
import { router, usePathname } from "expo-router";
import { router, usePathname, type Href } from "expo-router";
import { StyleSheet, useUnistyles } from "react-native-unistyles";
import { useQueryClient } from "@tanstack/react-query";
import { formatTimeAgo } from "@/utils/time";
@@ -33,7 +33,7 @@ import {
startNavigationTiming,
} from "@/utils/navigation-timing";
import {
buildHostAgentDetailRoute,
buildHostWorkspaceAgentTabRoute,
} from "@/utils/host-routes";
interface AgentListProps {
@@ -189,7 +189,12 @@ export function AgentList({
onAgentSelect?.();
navigate(buildHostAgentDetailRoute(serverId, agentId, agent.cwd) as any);
const route: Href = buildHostWorkspaceAgentTabRoute(
serverId,
agent.cwd,
agentId
) as Href;
navigate(route);
},
[isActionSheetVisible, pathname, onAgentSelect]
);
@@ -8,7 +8,6 @@ import {
Alert,
StatusBar,
ScrollView,
RefreshControl,
type GestureResponderEvent,
} from 'react-native'
import { useQueries } from '@tanstack/react-query'
@@ -55,7 +54,6 @@ import { useCheckoutGitActionsStore } from '@/stores/checkout-git-actions-store'
import { buildSidebarShortcutModel } from '@/utils/sidebar-shortcuts'
import { hasVisibleOrderChanged, mergeWithRemainder } from '@/utils/sidebar-reorder'
import {
decideLongPressMove,
shouldOpenContextMenuOnPressOut,
} from '@/utils/sidebar-gesture-arbitration'
@@ -276,10 +274,9 @@ function useLongPressDragInteraction(input: {
}
touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
})
.onTouchesMove((event, stateManager) => {
.onTouchesMove((event) => {
const touch = event.changedTouches[0]
if (!touch || event.numberOfTouches !== 1) {
stateManager.fail()
return
}
@@ -289,28 +286,27 @@ function useLongPressDragInteraction(input: {
return
}
const decision = decideLongPressMove({
longPressArmed: longPressArmedRef.current,
didStartDrag: didStartDragRef.current,
startPoint: start,
currentPoint: { x: touch.absoluteX, y: touch.absoluteY },
cancelSlopPx: CANCEL_SLOP_PX,
dragSlopPx: DRAG_SLOP_PX,
})
if (decision === 'cancel_long_press') {
longPressCancelledRef.current = true
stateManager.fail()
if (didStartDragRef.current) {
return
}
if (decision === 'start_drag') {
const dx = touch.absoluteX - start.x
const dy = touch.absoluteY - start.y
const distance = Math.sqrt(dx * dx + dy * dy)
if (!longPressArmedRef.current) {
if (distance > CANCEL_SLOP_PX) {
longPressCancelledRef.current = true
}
return
}
if (distance > DRAG_SLOP_PX) {
didStartDragRef.current = true
input.drag()
stateManager.fail()
}
})
}, [input])
}, [input.drag])
return {
didLongPressRef,
@@ -1038,10 +1034,6 @@ export function SidebarWorkspaceList({
]
)
const refreshControl = onRefresh ? (
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />
) : undefined
const content = (
<>
{projects.length === 0 ? (
@@ -1070,7 +1062,6 @@ export function SidebarWorkspaceList({
style={styles.list}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={refreshControl}
testID="sidebar-project-workspace-list-scroll"
>
{content}
@@ -1080,7 +1071,6 @@ export function SidebarWorkspaceList({
style={styles.list}
contentContainerStyle={styles.listContent}
showsVerticalScrollIndicator={false}
refreshControl={refreshControl}
testID="sidebar-project-workspace-list-scroll"
>
{content}
+16 -13
View File
@@ -1,6 +1,6 @@
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
import type { TextInput } from "react-native";
import { router, usePathname } from "expo-router";
import { router, usePathname, type Href } from "expo-router";
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
import { useSessionStore } from "@/stores/session-store";
@@ -75,7 +75,7 @@ type CommandCenterActionDefinition = {
icon?: "plus" | "settings";
shortcutKeys?: ShortcutKey[];
keywords: string[];
buildRoute: (params: { newAgentRoute: string; settingsRoute: string }) => string;
buildRoute: (params: { newAgentRoute: Href; settingsRoute: Href }) => Href;
};
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
@@ -113,7 +113,7 @@ export type CommandCenterActionItem = {
id: string;
title: string;
icon?: "plus" | "settings";
route: string;
route: Href;
shortcutKeys?: ShortcutKey[];
};
@@ -154,19 +154,19 @@ export function useCommandCenter() {
[pathname]
);
const newAgentRoute = useMemo(() => {
const newAgentRoute = useMemo<Href>(() => {
const serverIdFromPath =
parseServerIdFromPathname(pathname) ?? fallbackServerId;
const routeAgent = parseAgentRouteFromPathname(pathname);
if (!routeAgent) {
return serverIdFromPath ? buildNewAgentRoute(serverIdFromPath) : "/";
return serverIdFromPath ? (buildNewAgentRoute(serverIdFromPath) as Href) : "/";
}
const { serverId, agentId } = routeAgent;
const currentAgent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
const cwd = currentAgent?.cwd?.trim();
if (!cwd) {
return buildNewAgentRoute(serverId);
return buildNewAgentRoute(serverId) as Href;
}
const checkout =
@@ -174,13 +174,13 @@ export function useCommandCenter() {
checkoutStatusQueryKey(serverId, cwd)
) ?? null;
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
return buildNewAgentRoute(serverId, workingDir);
return buildNewAgentRoute(serverId, workingDir) as Href;
}, [fallbackServerId, pathname]);
const settingsRoute = useMemo(() => {
const settingsRoute = useMemo<Href>(() => {
const serverIdFromPath =
parseServerIdFromPathname(pathname) ?? fallbackServerId;
return serverIdFromPath ? buildHostSettingsRoute(serverIdFromPath) : "/";
return serverIdFromPath ? (buildHostSettingsRoute(serverIdFromPath) as Href) : "/";
}, [fallbackServerId, pathname]);
const actionItems = useMemo(() => {
@@ -230,9 +230,12 @@ export function useCommandCenter() {
// Don't restore focus back to the prior element after we navigate.
clearCommandCenterFocusRestoreElement();
setOpen(false);
navigate(
buildHostWorkspaceAgentTabRoute(agent.serverId, agent.cwd, agent.id) as any
);
const route: Href = buildHostWorkspaceAgentTabRoute(
agent.serverId,
agent.cwd,
agent.id
) as Href;
navigate(route);
},
[pathname, requestMessageInputAction, setOpen]
);
@@ -241,7 +244,7 @@ export function useCommandCenter() {
didNavigateRef.current = true;
clearCommandCenterFocusRestoreElement();
setOpen(false);
router.push(action.route as any);
router.push(action.route);
}, [setOpen]);
const handleSelectItem = useCallback(
@@ -3,30 +3,16 @@ import {
View,
Text,
ActivityIndicator,
Pressable,
ScrollView,
Platform,
BackHandler,
} from "react-native";
import { useRouter } from "expo-router";
import * as Clipboard from "expo-clipboard";
import { useRouter, type Href } from "expo-router";
import { useFocusEffect } from "@react-navigation/native";
import { useSafeAreaInsets } from "react-native-safe-area-context";
import { useQueryClient } from "@tanstack/react-query";
import ReanimatedAnimated from "react-native-reanimated";
import { GestureDetector } from "react-native-gesture-handler";
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
import {
MoreVertical,
GitBranch,
Folder,
RotateCcw,
PanelRight,
CheckCircle2,
} from "lucide-react-native";
import { MenuHeader } from "@/components/headers/menu-header";
import { BackHeader } from "@/components/headers/back-header";
import { HeaderToggleButton } from "@/components/headers/header-toggle-button";
import { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view";
import { AgentInputArea } from "@/components/agent-input-area";
import { ExplorerSidebar } from "@/components/explorer-sidebar";
@@ -48,13 +34,8 @@ import type { StreamItem } from "@/types/stream";
import {
buildAgentNavigationKey,
endNavigationTiming,
HOME_NAVIGATION_KEY,
startNavigationTiming,
} from "@/utils/navigation-timing";
import { extractAgentModel } from "@/utils/extract-agent-model";
import { startPerfMonitor } from "@/utils/perf-monitor";
import { shortenPath } from "@/utils/shorten-path";
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
import {
checkoutStatusQueryKey,
type CheckoutStatusPayload,
@@ -78,17 +59,9 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
import type { DaemonClient } from "@server/client/daemon-client";
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
import {
DropdownMenu,
DropdownMenuContent,
DropdownMenuItem,
DropdownMenuSeparator,
DropdownMenuTrigger,
} from "@/components/ui/dropdown-menu";
import { buildHostAgentDraftRoute } from "@/utils/host-routes";
import type { ExplorerCheckoutContext } from "@/stores/panel-store";
const DROPDOWN_WIDTH = 220;
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
@@ -102,17 +75,14 @@ function logAgentExplorer(event: string, details: Record<string, unknown>): void
export function AgentReadyScreen({
serverId,
agentId,
showHeader = true,
showExplorerSidebar = true,
wrapWithExplorerSidebarProvider = true,
}: {
serverId: string;
agentId: string;
showHeader?: boolean;
showExplorerSidebar?: boolean;
wrapWithExplorerSidebarProvider?: boolean;
}) {
const router = useRouter();
const resolvedAgentId = agentId?.trim() || undefined;
const resolvedServerId = serverId?.trim() || undefined;
const { daemons } = useDaemonRegistry();
@@ -136,33 +106,6 @@ export function AgentReadyScreen({
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
const handleBackToHome = useCallback(() => {
const targetServerId = resolvedServerId;
const targetAgentId = resolvedAgentId ?? null;
if (targetServerId && targetAgentId) {
startNavigationTiming(HOME_NAVIGATION_KEY, {
from: "agent",
to: "home",
targetMs: 300,
params: {
serverId: targetServerId,
agentId: targetAgentId,
},
});
} else {
startNavigationTiming(HOME_NAVIGATION_KEY, {
from: "agent",
to: "home",
targetMs: 300,
});
}
if (targetServerId) {
router.replace(buildHostAgentDraftRoute(targetServerId) as any);
return;
}
router.replace("/" as any);
}, [resolvedAgentId, resolvedServerId, router]);
const focusServerId = resolvedServerId;
const navigationStatus = isRuntimeSessionAvailable
? "ready"
@@ -187,7 +130,6 @@ export function AgentReadyScreen({
if (!resolvedServerId || !runtimeClient) {
return (
<AgentSessionUnavailableState
onBack={handleBackToHome}
serverLabel={serverLabel}
connectionStatus={connectionStatus}
lastError={lastConnectionError}
@@ -203,7 +145,6 @@ export function AgentReadyScreen({
client={runtimeClient}
isConnected={runtimeIsConnected}
connectionStatus={connectionStatus}
showHeader={showHeader}
showExplorerSidebar={showExplorerSidebar}
/>
);
@@ -221,7 +162,6 @@ type AgentScreenContentProps = {
client: DaemonClient;
isConnected: boolean;
connectionStatus: HostRuntimeConnectionStatus;
showHeader: boolean;
showExplorerSidebar: boolean;
};
@@ -242,7 +182,6 @@ function AgentScreenContent({
client,
isConnected,
connectionStatus,
showHeader,
showExplorerSidebar,
}: AgentScreenContentProps) {
const { theme } = useUnistyles();
@@ -269,7 +208,6 @@ function AgentScreenContent({
const mobileView = usePanelStore((state) => state.mobileView);
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
const closeToAgent = usePanelStore((state) => state.closeToAgent);
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
@@ -348,19 +286,6 @@ function AgentScreenContent({
}
openFileExplorer();
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
const handleToggleExplorer = useCallback(() => {
logAgentExplorer("handleToggleExplorer", {
isExplorerOpen,
mobileView,
isMobile,
});
if (isExplorerOpen) {
toggleFileExplorer();
return;
}
openExplorerForActiveCheckout();
}, [isExplorerOpen, isMobile, mobileView, openExplorerForActiveCheckout, toggleFileExplorer]);
useEffect(() => {
if (Platform.OS !== "web") {
return;
@@ -501,7 +426,7 @@ function AgentScreenContent({
const focusedAgentId = useSessionStore(
(state) => state.sessions[serverId]?.focusedAgentId ?? null
);
const { ensureAgentIsInitialized, refreshAgent } = useAgentInitialization({
const { ensureAgentIsInitialized } = useAgentInitialization({
serverId,
client: hasSession ? client : null,
});
@@ -604,7 +529,8 @@ function AgentScreenContent({
return;
}
hasRedirectedArchivedAgentRef.current = true;
router.replace(buildHostAgentDraftRoute(serverId) as any);
const route: Href = buildHostAgentDraftRoute(serverId) as Href;
router.replace(route);
}, [agent?.archivedAt, resolvedAgentId, router, serverId]);
useEffect(() => {
@@ -730,19 +656,6 @@ function AgentScreenContent({
});
const effectiveAgent = viewState.tag === "ready" ? viewState.agent : null;
const agentModel = extractAgentModel(effectiveAgent ?? agent);
const modelDisplayValue = agentModel ?? "Unknown";
const providerLabel = (effectiveAgent?.provider ?? "Provider").replace(
/^\w/,
(m) => m.toUpperCase()
);
const providerSessionId =
effectiveAgent?.runtimeInfo?.sessionId ?? effectiveAgent?.persistence?.sessionId ?? null;
// Header subtitle: project path + branch (matching agent list row format)
const headerProjectPath = effectiveAgent
? deriveProjectPath(effectiveAgent.cwd, checkout)
: null;
useEffect(() => {
if (!isPendingCreateForRoute || !pendingCreate) {
return;
@@ -952,33 +865,6 @@ function AgentScreenContent({
resolvedAgentId,
]);
const handleRefreshAgent = useCallback(() => {
if (!resolvedAgentId || !hasSession) {
return;
}
void refreshAgent(resolvedAgentId).catch((error) => {
console.warn("[AgentScreen] refreshAgent failed", { agentId: resolvedAgentId, error });
});
}, [hasSession, refreshAgent, resolvedAgentId]);
const handleCopyMeta = useCallback(
async (label: string, value: string | null | undefined) => {
if (!value) {
return;
}
try {
await Clipboard.setStringAsync(value);
toast.show(`Copied ${label}`, {
variant: "success",
icon: <CheckCircle2 size={theme.iconSize.md} color={theme.colors.primary} />,
});
} catch {
toast.error("Copy failed");
}
},
[theme.colors.primary, toast]
);
const isHistoryRefreshCatchingUp =
viewState.tag === "ready" &&
viewState.sync.status === "catching_up" &&
@@ -1003,7 +889,6 @@ function AgentScreenContent({
if (viewState.tag === "not_found") {
return (
<View style={styles.container} testID="agent-not-found">
{showHeader ? <MenuHeader title="Agent" /> : null}
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
@@ -1014,7 +899,6 @@ function AgentScreenContent({
if (viewState.tag === "error") {
return (
<View style={styles.container} testID="agent-load-error">
{showHeader ? <MenuHeader title="Agent" /> : null}
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Failed to load agent</Text>
<Text style={styles.statusText}>{viewState.message}</Text>
@@ -1026,7 +910,6 @@ function AgentScreenContent({
if (viewState.tag === "boot" || !effectiveAgent) {
return (
<View style={styles.container} testID="agent-loading">
{showHeader ? <MenuHeader title="Agent" /> : null}
<View style={styles.errorContainer}>
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
</View>
@@ -1041,210 +924,6 @@ function AgentScreenContent({
disabled={isArchivingCurrentAgent}
>
<View style={styles.container}>
{/* Header */}
{showHeader ? (
<MenuHeader
title={effectiveAgent.title || "Agent"}
rightContent={
<View style={styles.headerRightContent}>
<HeaderToggleButton
onPress={handleToggleExplorer}
tooltipLabel="Toggle explorer"
tooltipKeys={["mod", "E"]}
tooltipSide="left"
style={styles.menuButton}
accessible
accessibilityRole="button"
accessibilityLabel={isExplorerOpen ? "Close explorer" : "Open explorer"}
accessibilityState={{ expanded: isExplorerOpen }}
>
{isMobile ? (
checkout?.isGit ? (
<GitBranch
size={theme.iconSize.lg}
color={
isExplorerOpen
? theme.colors.foreground
: theme.colors.foregroundMuted
}
/>
) : (
<Folder
size={theme.iconSize.lg}
color={
isExplorerOpen
? theme.colors.foreground
: theme.colors.foregroundMuted
}
/>
)
) : (
<PanelRight
size={theme.iconSize.md}
color={
isExplorerOpen
? theme.colors.foreground
: theme.colors.foregroundMuted
}
/>
)}
</HeaderToggleButton>
<DropdownMenu
onOpenChange={(open) => {
if (open && agent?.cwd) {
checkoutStatusQuery.refresh().catch(() => {});
}
}}
>
<DropdownMenuTrigger testID="agent-overflow-menu" style={styles.menuButton}>
<MoreVertical
size={isMobile ? theme.iconSize.lg : theme.iconSize.md}
color={theme.colors.foregroundMuted}
/>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" width={DROPDOWN_WIDTH} testID="agent-overflow-content">
<View style={styles.menuMetaContainer} testID="agent-details-sheet">
<Pressable
testID="agent-details-agent-id"
style={({ hovered, pressed }) => [
styles.menuMetaRow,
(hovered || pressed) && styles.menuMetaRowActive,
]}
onPress={() => {
void handleCopyMeta("Directory", effectiveAgent.cwd);
}}
>
<Text style={styles.menuMetaLabel} numberOfLines={1}>
Directory
</Text>
<Text
testID="agent-details-agent-id-value"
style={styles.menuMetaValue}
numberOfLines={1}
ellipsizeMode="middle"
>
{shortenPath(effectiveAgent.cwd)}
</Text>
</Pressable>
<Pressable
style={({ hovered, pressed }) => [
styles.menuMetaRow,
(hovered || pressed) && styles.menuMetaRowActive,
]}
onPress={() => {
void handleCopyMeta("Model", modelDisplayValue);
}}
>
<Text style={styles.menuMetaLabel} numberOfLines={1}>
Model
</Text>
<Text
style={styles.menuMetaValue}
numberOfLines={1}
ellipsizeMode="middle"
>
{modelDisplayValue}
</Text>
</Pressable>
{checkout?.isGit && checkout.currentBranch && checkout.currentBranch !== "HEAD" ? (
<Pressable
style={({ hovered, pressed }) => [
styles.menuMetaRow,
(hovered || pressed) && styles.menuMetaRowActive,
]}
onPress={() => {
if (checkoutStatusQuery.isFetching) {
return;
}
void handleCopyMeta("Branch", checkout.currentBranch);
}}
>
<Text style={styles.menuMetaLabel} numberOfLines={1}>
Branch
</Text>
<Text
style={styles.menuMetaValue}
numberOfLines={1}
ellipsizeMode="middle"
>
{checkoutStatusQuery.isFetching ? "Fetching…" : checkout.currentBranch}
</Text>
</Pressable>
) : null}
<Pressable
style={({ hovered, pressed }) => [
styles.menuMetaRow,
(hovered || pressed) && styles.menuMetaRowActive,
]}
onPress={() => {
void handleCopyMeta("Paseo ID", effectiveAgent.id);
}}
>
<Text style={styles.menuMetaLabel} numberOfLines={1}>
Paseo ID
</Text>
<Text
style={styles.menuMetaValue}
numberOfLines={1}
ellipsizeMode="middle"
>
{effectiveAgent.id}
</Text>
</Pressable>
<Pressable
testID="agent-details-persistence-session-id"
style={({ hovered, pressed }) => [
styles.menuMetaRow,
providerSessionId && (hovered || pressed) && styles.menuMetaRowActive,
]}
disabled={!providerSessionId}
onPress={() => {
void handleCopyMeta(`${providerLabel} ID`, providerSessionId);
}}
>
<Text style={styles.menuMetaLabel} numberOfLines={1}>
{providerLabel} ID
</Text>
<Text
testID="agent-details-persistence-session-id-value"
style={[styles.menuMetaValue, !providerSessionId && styles.menuMetaValueError]}
numberOfLines={1}
ellipsizeMode="middle"
>
{providerSessionId ?? "Not available"}
</Text>
</Pressable>
</View>
<DropdownMenuSeparator />
<DropdownMenuItem
leading={<RotateCcw size={theme.iconSize.md} color={theme.colors.foreground} />}
disabled={isInitializing}
trailing={
isInitializing ? (
<ActivityIndicator
size="small"
color={theme.colors.primary}
style={styles.menuItemSpinner}
/>
) : null
}
onSelect={handleRefreshAgent}
>
{isInitializing ? "Refreshing..." : "Refresh"}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</View>
}
/>
) : null}
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
<ReanimatedAnimated.View
@@ -1332,13 +1011,11 @@ function AgentScreenContent({
}
function AgentSessionUnavailableState({
onBack,
serverLabel,
connectionStatus,
lastError,
isUnknownDaemon = false,
}: {
onBack: () => void;
serverLabel: string;
connectionStatus: HostRuntimeConnectionStatus;
lastError: string | null;
@@ -1347,7 +1024,6 @@ function AgentSessionUnavailableState({
if (isUnknownDaemon) {
return (
<View style={styles.container}>
<BackHeader title="Agent" onBack={onBack} />
<View style={styles.centerState}>
<Text style={styles.errorText}>
Cannot open this agent because {serverLabel} is not configured on
@@ -1367,7 +1043,6 @@ function AgentSessionUnavailableState({
return (
<View style={styles.container}>
<BackHeader title="Agent" onBack={onBack} />
<View style={styles.centerState}>
{isConnecting || isPreparingSession ? (
<>
@@ -1411,10 +1086,6 @@ const styles = StyleSheet.create((theme) => ({
flex: 1,
backgroundColor: theme.colors.surface0,
},
headerRightContent: {
flexDirection: "row",
alignItems: "center",
},
contentContainer: {
flex: 1,
overflow: "hidden",
@@ -1506,40 +1177,4 @@ const styles = StyleSheet.create((theme) => ({
color: theme.colors.foregroundMuted,
textAlign: "center",
},
menuButton: {
padding: theme.spacing[3],
borderRadius: theme.borderRadius.lg,
},
menuMetaContainer: {
paddingVertical: theme.spacing[1],
},
menuMetaRow: {
minHeight: 32,
paddingHorizontal: theme.spacing[3],
flexDirection: "row",
alignItems: "center",
justifyContent: "space-between",
gap: theme.spacing[3],
},
menuMetaRowActive: {
backgroundColor: theme.colors.surface2,
},
menuMetaLabel: {
fontSize: theme.fontSize.sm,
color: theme.colors.foregroundMuted,
flexShrink: 0,
},
menuMetaValue: {
fontSize: theme.fontSize.sm,
color: theme.colors.foreground,
flex: 1,
minWidth: 0,
textAlign: "right",
},
menuMetaValueError: {
color: theme.colors.destructive,
},
menuItemSpinner: {
marginLeft: "auto",
},
}));
@@ -2,7 +2,7 @@ import { useCallback, useEffect, useMemo, useReducer, useRef, useState, useSyncE
import { createNameId } from 'mnemonic-id'
import type { ImageAttachment } from '@/components/message-input'
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
import { useLocalSearchParams, useRouter } from 'expo-router'
import { useLocalSearchParams, useRouter, type Href } from 'expo-router'
import { useIsFocused } from '@react-navigation/native'
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
import { useSafeAreaInsets } from 'react-native-safe-area-context'
@@ -1037,9 +1037,12 @@ function DraftAgentScreenContent({
const configuredWorkingDir = config.cwd.trim()
const workspaceId =
createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir
router.replace(
buildHostWorkspaceAgentTabRoute(selectedServerId, workspaceId, agentId) as any
)
const route: Href = buildHostWorkspaceAgentTabRoute(
selectedServerId,
workspaceId,
agentId
) as Href
router.replace(route)
return
}
@@ -1265,11 +1265,10 @@ function WorkspaceScreenContent({
}
if (target.kind === "agent") {
return (
return (
<AgentReadyScreen
serverId={normalizedServerId}
agentId={target.agentId}
showHeader={false}
showExplorerSidebar={false}
wrapWithExplorerSidebarProvider={false}
/>
@@ -15,6 +15,7 @@ describe("resolveNotificationTarget", () => {
).toEqual({
serverId: "server-123",
agentId: "agent-456",
workspaceId: null,
});
});
@@ -22,15 +23,27 @@ describe("resolveNotificationTarget", () => {
expect(resolveNotificationTarget({ serverId: "", agentId: " " })).toEqual({
serverId: null,
agentId: null,
workspaceId: null,
});
expect(resolveNotificationTarget(undefined)).toEqual({
serverId: null,
agentId: null,
workspaceId: null,
});
});
});
describe("buildNotificationRoute", () => {
it("routes directly to workspace tab when workspace id is present", () => {
expect(
buildNotificationRoute({
serverId: "srv-1",
agentId: "agent-1",
workspaceId: "/tmp/repo",
})
).toBe("/h/srv-1/workspace/L3RtcC9yZXBv/tab/agent_agent-1");
});
it("routes directly to server-scoped agent path when both ids are present", () => {
expect(buildNotificationRoute({ serverId: "srv-1", agentId: "agent-1" })).toBe(
"/h/srv-1/agent/agent-1"
@@ -1,6 +1,7 @@
import {
buildHostDraftRoute,
buildHostAgentDetailRoute,
buildHostWorkspaceAgentTabRoute,
} from "@/utils/host-routes";
type NotificationData = Record<string, unknown> | null | undefined;
@@ -20,16 +21,23 @@ function readNonEmptyString(
export function resolveNotificationTarget(data: NotificationData): {
serverId: string | null;
agentId: string | null;
workspaceId: string | null;
} {
return {
serverId: readNonEmptyString(data, "serverId"),
agentId: readNonEmptyString(data, "agentId"),
workspaceId:
readNonEmptyString(data, "workspaceId") ??
readNonEmptyString(data, "cwd"),
};
}
export function buildNotificationRoute(data: NotificationData): string {
const { serverId, agentId } = resolveNotificationTarget(data);
const { serverId, agentId, workspaceId } = resolveNotificationTarget(data);
if (serverId && agentId) {
if (workspaceId) {
return buildHostWorkspaceAgentTabRoute(serverId, workspaceId, agentId);
}
return buildHostAgentDetailRoute(serverId, agentId);
}
if (serverId) {