feat: add workspace-aware notification routing and clean up agent screen header
This commit is contained in:
@@ -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}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
Reference in New Issue
Block a user