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";
|
} from "react-native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useCallback, useMemo, useState, type ReactElement } from "react";
|
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 { StyleSheet, useUnistyles } from "react-native-unistyles";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import { formatTimeAgo } from "@/utils/time";
|
import { formatTimeAgo } from "@/utils/time";
|
||||||
@@ -33,7 +33,7 @@ import {
|
|||||||
startNavigationTiming,
|
startNavigationTiming,
|
||||||
} from "@/utils/navigation-timing";
|
} from "@/utils/navigation-timing";
|
||||||
import {
|
import {
|
||||||
buildHostAgentDetailRoute,
|
buildHostWorkspaceAgentTabRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
interface AgentListProps {
|
interface AgentListProps {
|
||||||
@@ -189,7 +189,12 @@ export function AgentList({
|
|||||||
|
|
||||||
onAgentSelect?.();
|
onAgentSelect?.();
|
||||||
|
|
||||||
navigate(buildHostAgentDetailRoute(serverId, agentId, agent.cwd) as any);
|
const route: Href = buildHostWorkspaceAgentTabRoute(
|
||||||
|
serverId,
|
||||||
|
agent.cwd,
|
||||||
|
agentId
|
||||||
|
) as Href;
|
||||||
|
navigate(route);
|
||||||
},
|
},
|
||||||
[isActionSheetVisible, pathname, onAgentSelect]
|
[isActionSheetVisible, pathname, onAgentSelect]
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -8,7 +8,6 @@ import {
|
|||||||
Alert,
|
Alert,
|
||||||
StatusBar,
|
StatusBar,
|
||||||
ScrollView,
|
ScrollView,
|
||||||
RefreshControl,
|
|
||||||
type GestureResponderEvent,
|
type GestureResponderEvent,
|
||||||
} from 'react-native'
|
} from 'react-native'
|
||||||
import { useQueries } from '@tanstack/react-query'
|
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 { buildSidebarShortcutModel } from '@/utils/sidebar-shortcuts'
|
||||||
import { hasVisibleOrderChanged, mergeWithRemainder } from '@/utils/sidebar-reorder'
|
import { hasVisibleOrderChanged, mergeWithRemainder } from '@/utils/sidebar-reorder'
|
||||||
import {
|
import {
|
||||||
decideLongPressMove,
|
|
||||||
shouldOpenContextMenuOnPressOut,
|
shouldOpenContextMenuOnPressOut,
|
||||||
} from '@/utils/sidebar-gesture-arbitration'
|
} from '@/utils/sidebar-gesture-arbitration'
|
||||||
|
|
||||||
@@ -276,10 +274,9 @@ function useLongPressDragInteraction(input: {
|
|||||||
}
|
}
|
||||||
touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
|
touchStartRef.current = { x: touch.absoluteX, y: touch.absoluteY }
|
||||||
})
|
})
|
||||||
.onTouchesMove((event, stateManager) => {
|
.onTouchesMove((event) => {
|
||||||
const touch = event.changedTouches[0]
|
const touch = event.changedTouches[0]
|
||||||
if (!touch || event.numberOfTouches !== 1) {
|
if (!touch || event.numberOfTouches !== 1) {
|
||||||
stateManager.fail()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -289,28 +286,27 @@ function useLongPressDragInteraction(input: {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const decision = decideLongPressMove({
|
if (didStartDragRef.current) {
|
||||||
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()
|
|
||||||
return
|
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
|
didStartDragRef.current = true
|
||||||
input.drag()
|
input.drag()
|
||||||
stateManager.fail()
|
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}, [input])
|
}, [input.drag])
|
||||||
|
|
||||||
return {
|
return {
|
||||||
didLongPressRef,
|
didLongPressRef,
|
||||||
@@ -1038,10 +1034,6 @@ export function SidebarWorkspaceList({
|
|||||||
]
|
]
|
||||||
)
|
)
|
||||||
|
|
||||||
const refreshControl = onRefresh ? (
|
|
||||||
<RefreshControl refreshing={isRefreshing} onRefresh={onRefresh} />
|
|
||||||
) : undefined
|
|
||||||
|
|
||||||
const content = (
|
const content = (
|
||||||
<>
|
<>
|
||||||
{projects.length === 0 ? (
|
{projects.length === 0 ? (
|
||||||
@@ -1070,7 +1062,6 @@ export function SidebarWorkspaceList({
|
|||||||
style={styles.list}
|
style={styles.list}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={refreshControl}
|
|
||||||
testID="sidebar-project-workspace-list-scroll"
|
testID="sidebar-project-workspace-list-scroll"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
@@ -1080,7 +1071,6 @@ export function SidebarWorkspaceList({
|
|||||||
style={styles.list}
|
style={styles.list}
|
||||||
contentContainerStyle={styles.listContent}
|
contentContainerStyle={styles.listContent}
|
||||||
showsVerticalScrollIndicator={false}
|
showsVerticalScrollIndicator={false}
|
||||||
refreshControl={refreshControl}
|
|
||||||
testID="sidebar-project-workspace-list-scroll"
|
testID="sidebar-project-workspace-list-scroll"
|
||||||
>
|
>
|
||||||
{content}
|
{content}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
import { useCallback, useEffect, useMemo, useRef, useState } from "react";
|
||||||
import type { TextInput } from "react-native";
|
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 { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||||
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { useAggregatedAgents, type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
@@ -75,7 +75,7 @@ type CommandCenterActionDefinition = {
|
|||||||
icon?: "plus" | "settings";
|
icon?: "plus" | "settings";
|
||||||
shortcutKeys?: ShortcutKey[];
|
shortcutKeys?: ShortcutKey[];
|
||||||
keywords: string[];
|
keywords: string[];
|
||||||
buildRoute: (params: { newAgentRoute: string; settingsRoute: string }) => string;
|
buildRoute: (params: { newAgentRoute: Href; settingsRoute: Href }) => Href;
|
||||||
};
|
};
|
||||||
|
|
||||||
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
|
const COMMAND_CENTER_ACTIONS: readonly CommandCenterActionDefinition[] = [
|
||||||
@@ -113,7 +113,7 @@ export type CommandCenterActionItem = {
|
|||||||
id: string;
|
id: string;
|
||||||
title: string;
|
title: string;
|
||||||
icon?: "plus" | "settings";
|
icon?: "plus" | "settings";
|
||||||
route: string;
|
route: Href;
|
||||||
shortcutKeys?: ShortcutKey[];
|
shortcutKeys?: ShortcutKey[];
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -154,19 +154,19 @@ export function useCommandCenter() {
|
|||||||
[pathname]
|
[pathname]
|
||||||
);
|
);
|
||||||
|
|
||||||
const newAgentRoute = useMemo(() => {
|
const newAgentRoute = useMemo<Href>(() => {
|
||||||
const serverIdFromPath =
|
const serverIdFromPath =
|
||||||
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
||||||
const routeAgent = parseAgentRouteFromPathname(pathname);
|
const routeAgent = parseAgentRouteFromPathname(pathname);
|
||||||
if (!routeAgent) {
|
if (!routeAgent) {
|
||||||
return serverIdFromPath ? buildNewAgentRoute(serverIdFromPath) : "/";
|
return serverIdFromPath ? (buildNewAgentRoute(serverIdFromPath) as Href) : "/";
|
||||||
}
|
}
|
||||||
|
|
||||||
const { serverId, agentId } = routeAgent;
|
const { serverId, agentId } = routeAgent;
|
||||||
const currentAgent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
|
const currentAgent = useSessionStore.getState().sessions[serverId]?.agents?.get(agentId);
|
||||||
const cwd = currentAgent?.cwd?.trim();
|
const cwd = currentAgent?.cwd?.trim();
|
||||||
if (!cwd) {
|
if (!cwd) {
|
||||||
return buildNewAgentRoute(serverId);
|
return buildNewAgentRoute(serverId) as Href;
|
||||||
}
|
}
|
||||||
|
|
||||||
const checkout =
|
const checkout =
|
||||||
@@ -174,13 +174,13 @@ export function useCommandCenter() {
|
|||||||
checkoutStatusQueryKey(serverId, cwd)
|
checkoutStatusQueryKey(serverId, cwd)
|
||||||
) ?? null;
|
) ?? null;
|
||||||
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
|
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
|
||||||
return buildNewAgentRoute(serverId, workingDir);
|
return buildNewAgentRoute(serverId, workingDir) as Href;
|
||||||
}, [fallbackServerId, pathname]);
|
}, [fallbackServerId, pathname]);
|
||||||
|
|
||||||
const settingsRoute = useMemo(() => {
|
const settingsRoute = useMemo<Href>(() => {
|
||||||
const serverIdFromPath =
|
const serverIdFromPath =
|
||||||
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
||||||
return serverIdFromPath ? buildHostSettingsRoute(serverIdFromPath) : "/";
|
return serverIdFromPath ? (buildHostSettingsRoute(serverIdFromPath) as Href) : "/";
|
||||||
}, [fallbackServerId, pathname]);
|
}, [fallbackServerId, pathname]);
|
||||||
|
|
||||||
const actionItems = useMemo(() => {
|
const actionItems = useMemo(() => {
|
||||||
@@ -230,9 +230,12 @@ export function useCommandCenter() {
|
|||||||
// Don't restore focus back to the prior element after we navigate.
|
// Don't restore focus back to the prior element after we navigate.
|
||||||
clearCommandCenterFocusRestoreElement();
|
clearCommandCenterFocusRestoreElement();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
navigate(
|
const route: Href = buildHostWorkspaceAgentTabRoute(
|
||||||
buildHostWorkspaceAgentTabRoute(agent.serverId, agent.cwd, agent.id) as any
|
agent.serverId,
|
||||||
);
|
agent.cwd,
|
||||||
|
agent.id
|
||||||
|
) as Href;
|
||||||
|
navigate(route);
|
||||||
},
|
},
|
||||||
[pathname, requestMessageInputAction, setOpen]
|
[pathname, requestMessageInputAction, setOpen]
|
||||||
);
|
);
|
||||||
@@ -241,7 +244,7 @@ export function useCommandCenter() {
|
|||||||
didNavigateRef.current = true;
|
didNavigateRef.current = true;
|
||||||
clearCommandCenterFocusRestoreElement();
|
clearCommandCenterFocusRestoreElement();
|
||||||
setOpen(false);
|
setOpen(false);
|
||||||
router.push(action.route as any);
|
router.push(action.route);
|
||||||
}, [setOpen]);
|
}, [setOpen]);
|
||||||
|
|
||||||
const handleSelectItem = useCallback(
|
const handleSelectItem = useCallback(
|
||||||
|
|||||||
@@ -3,30 +3,16 @@ import {
|
|||||||
View,
|
View,
|
||||||
Text,
|
Text,
|
||||||
ActivityIndicator,
|
ActivityIndicator,
|
||||||
Pressable,
|
|
||||||
ScrollView,
|
|
||||||
Platform,
|
Platform,
|
||||||
BackHandler,
|
BackHandler,
|
||||||
} from "react-native";
|
} from "react-native";
|
||||||
import { useRouter } from "expo-router";
|
import { useRouter, type Href } from "expo-router";
|
||||||
import * as Clipboard from "expo-clipboard";
|
|
||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useQueryClient } from "@tanstack/react-query";
|
import { useQueryClient } from "@tanstack/react-query";
|
||||||
import ReanimatedAnimated from "react-native-reanimated";
|
import ReanimatedAnimated from "react-native-reanimated";
|
||||||
import { GestureDetector } from "react-native-gesture-handler";
|
import { GestureDetector } from "react-native-gesture-handler";
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
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 { AgentStreamView, type AgentStreamViewHandle } from "@/components/agent-stream-view";
|
||||||
import { AgentInputArea } from "@/components/agent-input-area";
|
import { AgentInputArea } from "@/components/agent-input-area";
|
||||||
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
import { ExplorerSidebar } from "@/components/explorer-sidebar";
|
||||||
@@ -48,13 +34,8 @@ import type { StreamItem } from "@/types/stream";
|
|||||||
import {
|
import {
|
||||||
buildAgentNavigationKey,
|
buildAgentNavigationKey,
|
||||||
endNavigationTiming,
|
endNavigationTiming,
|
||||||
HOME_NAVIGATION_KEY,
|
|
||||||
startNavigationTiming,
|
|
||||||
} from "@/utils/navigation-timing";
|
} from "@/utils/navigation-timing";
|
||||||
import { extractAgentModel } from "@/utils/extract-agent-model";
|
|
||||||
import { startPerfMonitor } from "@/utils/perf-monitor";
|
import { startPerfMonitor } from "@/utils/perf-monitor";
|
||||||
import { shortenPath } from "@/utils/shorten-path";
|
|
||||||
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
|
|
||||||
import {
|
import {
|
||||||
checkoutStatusQueryKey,
|
checkoutStatusQueryKey,
|
||||||
type CheckoutStatusPayload,
|
type CheckoutStatusPayload,
|
||||||
@@ -78,17 +59,9 @@ import { useKeyboardShiftStyle } from "@/hooks/use-keyboard-shift-style";
|
|||||||
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||||
import type { DaemonClient } from "@server/client/daemon-client";
|
import type { DaemonClient } from "@server/client/daemon-client";
|
||||||
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
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 { buildHostAgentDraftRoute } from "@/utils/host-routes";
|
||||||
import type { ExplorerCheckoutContext } from "@/stores/panel-store";
|
import type { ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||||
|
|
||||||
const DROPDOWN_WIDTH = 220;
|
|
||||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
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({
|
export function AgentReadyScreen({
|
||||||
serverId,
|
serverId,
|
||||||
agentId,
|
agentId,
|
||||||
showHeader = true,
|
|
||||||
showExplorerSidebar = true,
|
showExplorerSidebar = true,
|
||||||
wrapWithExplorerSidebarProvider = true,
|
wrapWithExplorerSidebarProvider = true,
|
||||||
}: {
|
}: {
|
||||||
serverId: string;
|
serverId: string;
|
||||||
agentId: string;
|
agentId: string;
|
||||||
showHeader?: boolean;
|
|
||||||
showExplorerSidebar?: boolean;
|
showExplorerSidebar?: boolean;
|
||||||
wrapWithExplorerSidebarProvider?: boolean;
|
wrapWithExplorerSidebarProvider?: boolean;
|
||||||
}) {
|
}) {
|
||||||
const router = useRouter();
|
|
||||||
const resolvedAgentId = agentId?.trim() || undefined;
|
const resolvedAgentId = agentId?.trim() || undefined;
|
||||||
const resolvedServerId = serverId?.trim() || undefined;
|
const resolvedServerId = serverId?.trim() || undefined;
|
||||||
const { daemons } = useDaemonRegistry();
|
const { daemons } = useDaemonRegistry();
|
||||||
@@ -136,33 +106,6 @@ export function AgentReadyScreen({
|
|||||||
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
const lastConnectionError = runtimeSnapshot?.lastError ?? null;
|
||||||
const isRuntimeSessionAvailable = Boolean(resolvedServerId && runtimeClient);
|
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 focusServerId = resolvedServerId;
|
||||||
const navigationStatus = isRuntimeSessionAvailable
|
const navigationStatus = isRuntimeSessionAvailable
|
||||||
? "ready"
|
? "ready"
|
||||||
@@ -187,7 +130,6 @@ export function AgentReadyScreen({
|
|||||||
if (!resolvedServerId || !runtimeClient) {
|
if (!resolvedServerId || !runtimeClient) {
|
||||||
return (
|
return (
|
||||||
<AgentSessionUnavailableState
|
<AgentSessionUnavailableState
|
||||||
onBack={handleBackToHome}
|
|
||||||
serverLabel={serverLabel}
|
serverLabel={serverLabel}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
lastError={lastConnectionError}
|
lastError={lastConnectionError}
|
||||||
@@ -203,7 +145,6 @@ export function AgentReadyScreen({
|
|||||||
client={runtimeClient}
|
client={runtimeClient}
|
||||||
isConnected={runtimeIsConnected}
|
isConnected={runtimeIsConnected}
|
||||||
connectionStatus={connectionStatus}
|
connectionStatus={connectionStatus}
|
||||||
showHeader={showHeader}
|
|
||||||
showExplorerSidebar={showExplorerSidebar}
|
showExplorerSidebar={showExplorerSidebar}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
@@ -221,7 +162,6 @@ type AgentScreenContentProps = {
|
|||||||
client: DaemonClient;
|
client: DaemonClient;
|
||||||
isConnected: boolean;
|
isConnected: boolean;
|
||||||
connectionStatus: HostRuntimeConnectionStatus;
|
connectionStatus: HostRuntimeConnectionStatus;
|
||||||
showHeader: boolean;
|
|
||||||
showExplorerSidebar: boolean;
|
showExplorerSidebar: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -242,7 +182,6 @@ function AgentScreenContent({
|
|||||||
client,
|
client,
|
||||||
isConnected,
|
isConnected,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
showHeader,
|
|
||||||
showExplorerSidebar,
|
showExplorerSidebar,
|
||||||
}: AgentScreenContentProps) {
|
}: AgentScreenContentProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
@@ -269,7 +208,6 @@ function AgentScreenContent({
|
|||||||
|
|
||||||
const mobileView = usePanelStore((state) => state.mobileView);
|
const mobileView = usePanelStore((state) => state.mobileView);
|
||||||
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
const desktopFileExplorerOpen = usePanelStore((state) => state.desktop.fileExplorerOpen);
|
||||||
const toggleFileExplorer = usePanelStore((state) => state.toggleFileExplorer);
|
|
||||||
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
const openFileExplorer = usePanelStore((state) => state.openFileExplorer);
|
||||||
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
const closeToAgent = usePanelStore((state) => state.closeToAgent);
|
||||||
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
|
const setActiveExplorerCheckout = usePanelStore((state) => state.setActiveExplorerCheckout);
|
||||||
@@ -348,19 +286,6 @@ function AgentScreenContent({
|
|||||||
}
|
}
|
||||||
openFileExplorer();
|
openFileExplorer();
|
||||||
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
}, [activateExplorerTabForCheckout, openFileExplorer, resolveCurrentExplorerCheckout]);
|
||||||
const handleToggleExplorer = useCallback(() => {
|
|
||||||
logAgentExplorer("handleToggleExplorer", {
|
|
||||||
isExplorerOpen,
|
|
||||||
mobileView,
|
|
||||||
isMobile,
|
|
||||||
});
|
|
||||||
if (isExplorerOpen) {
|
|
||||||
toggleFileExplorer();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
openExplorerForActiveCheckout();
|
|
||||||
}, [isExplorerOpen, isMobile, mobileView, openExplorerForActiveCheckout, toggleFileExplorer]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (Platform.OS !== "web") {
|
if (Platform.OS !== "web") {
|
||||||
return;
|
return;
|
||||||
@@ -501,7 +426,7 @@ function AgentScreenContent({
|
|||||||
const focusedAgentId = useSessionStore(
|
const focusedAgentId = useSessionStore(
|
||||||
(state) => state.sessions[serverId]?.focusedAgentId ?? null
|
(state) => state.sessions[serverId]?.focusedAgentId ?? null
|
||||||
);
|
);
|
||||||
const { ensureAgentIsInitialized, refreshAgent } = useAgentInitialization({
|
const { ensureAgentIsInitialized } = useAgentInitialization({
|
||||||
serverId,
|
serverId,
|
||||||
client: hasSession ? client : null,
|
client: hasSession ? client : null,
|
||||||
});
|
});
|
||||||
@@ -604,7 +529,8 @@ function AgentScreenContent({
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
hasRedirectedArchivedAgentRef.current = true;
|
hasRedirectedArchivedAgentRef.current = true;
|
||||||
router.replace(buildHostAgentDraftRoute(serverId) as any);
|
const route: Href = buildHostAgentDraftRoute(serverId) as Href;
|
||||||
|
router.replace(route);
|
||||||
}, [agent?.archivedAt, resolvedAgentId, router, serverId]);
|
}, [agent?.archivedAt, resolvedAgentId, router, serverId]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -730,19 +656,6 @@ function AgentScreenContent({
|
|||||||
});
|
});
|
||||||
|
|
||||||
const effectiveAgent = viewState.tag === "ready" ? viewState.agent : null;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isPendingCreateForRoute || !pendingCreate) {
|
if (!isPendingCreateForRoute || !pendingCreate) {
|
||||||
return;
|
return;
|
||||||
@@ -952,33 +865,6 @@ function AgentScreenContent({
|
|||||||
resolvedAgentId,
|
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 =
|
const isHistoryRefreshCatchingUp =
|
||||||
viewState.tag === "ready" &&
|
viewState.tag === "ready" &&
|
||||||
viewState.sync.status === "catching_up" &&
|
viewState.sync.status === "catching_up" &&
|
||||||
@@ -1003,7 +889,6 @@ function AgentScreenContent({
|
|||||||
if (viewState.tag === "not_found") {
|
if (viewState.tag === "not_found") {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-not-found">
|
<View style={styles.container} testID="agent-not-found">
|
||||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorText}>Agent not found</Text>
|
<Text style={styles.errorText}>Agent not found</Text>
|
||||||
</View>
|
</View>
|
||||||
@@ -1014,7 +899,6 @@ function AgentScreenContent({
|
|||||||
if (viewState.tag === "error") {
|
if (viewState.tag === "error") {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-load-error">
|
<View style={styles.container} testID="agent-load-error">
|
||||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<Text style={styles.errorText}>Failed to load agent</Text>
|
<Text style={styles.errorText}>Failed to load agent</Text>
|
||||||
<Text style={styles.statusText}>{viewState.message}</Text>
|
<Text style={styles.statusText}>{viewState.message}</Text>
|
||||||
@@ -1026,7 +910,6 @@ function AgentScreenContent({
|
|||||||
if (viewState.tag === "boot" || !effectiveAgent) {
|
if (viewState.tag === "boot" || !effectiveAgent) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container} testID="agent-loading">
|
<View style={styles.container} testID="agent-loading">
|
||||||
{showHeader ? <MenuHeader title="Agent" /> : null}
|
|
||||||
<View style={styles.errorContainer}>
|
<View style={styles.errorContainer}>
|
||||||
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
|
<ActivityIndicator size="large" color={theme.colors.foregroundMuted} />
|
||||||
</View>
|
</View>
|
||||||
@@ -1041,210 +924,6 @@ function AgentScreenContent({
|
|||||||
disabled={isArchivingCurrentAgent}
|
disabled={isArchivingCurrentAgent}
|
||||||
>
|
>
|
||||||
<View style={styles.container}>
|
<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 */}
|
{/* Content Area with Keyboard Animation */}
|
||||||
<View style={styles.contentContainer}>
|
<View style={styles.contentContainer}>
|
||||||
<ReanimatedAnimated.View
|
<ReanimatedAnimated.View
|
||||||
@@ -1332,13 +1011,11 @@ function AgentScreenContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
function AgentSessionUnavailableState({
|
function AgentSessionUnavailableState({
|
||||||
onBack,
|
|
||||||
serverLabel,
|
serverLabel,
|
||||||
connectionStatus,
|
connectionStatus,
|
||||||
lastError,
|
lastError,
|
||||||
isUnknownDaemon = false,
|
isUnknownDaemon = false,
|
||||||
}: {
|
}: {
|
||||||
onBack: () => void;
|
|
||||||
serverLabel: string;
|
serverLabel: string;
|
||||||
connectionStatus: HostRuntimeConnectionStatus;
|
connectionStatus: HostRuntimeConnectionStatus;
|
||||||
lastError: string | null;
|
lastError: string | null;
|
||||||
@@ -1347,7 +1024,6 @@ function AgentSessionUnavailableState({
|
|||||||
if (isUnknownDaemon) {
|
if (isUnknownDaemon) {
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BackHeader title="Agent" onBack={onBack} />
|
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
<Text style={styles.errorText}>
|
<Text style={styles.errorText}>
|
||||||
Cannot open this agent because {serverLabel} is not configured on
|
Cannot open this agent because {serverLabel} is not configured on
|
||||||
@@ -1367,7 +1043,6 @@ function AgentSessionUnavailableState({
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<BackHeader title="Agent" onBack={onBack} />
|
|
||||||
<View style={styles.centerState}>
|
<View style={styles.centerState}>
|
||||||
{isConnecting || isPreparingSession ? (
|
{isConnecting || isPreparingSession ? (
|
||||||
<>
|
<>
|
||||||
@@ -1411,10 +1086,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
flex: 1,
|
flex: 1,
|
||||||
backgroundColor: theme.colors.surface0,
|
backgroundColor: theme.colors.surface0,
|
||||||
},
|
},
|
||||||
headerRightContent: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
},
|
|
||||||
contentContainer: {
|
contentContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
overflow: "hidden",
|
overflow: "hidden",
|
||||||
@@ -1506,40 +1177,4 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
color: theme.colors.foregroundMuted,
|
color: theme.colors.foregroundMuted,
|
||||||
textAlign: "center",
|
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 { createNameId } from 'mnemonic-id'
|
||||||
import type { ImageAttachment } from '@/components/message-input'
|
import type { ImageAttachment } from '@/components/message-input'
|
||||||
import { View, Text, Pressable, ScrollView, Keyboard, Platform } from 'react-native'
|
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 { useIsFocused } from '@react-navigation/native'
|
||||||
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
import { StyleSheet, UnistylesRuntime, useUnistyles } from 'react-native-unistyles'
|
||||||
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
import { useSafeAreaInsets } from 'react-native-safe-area-context'
|
||||||
@@ -1037,9 +1037,12 @@ function DraftAgentScreenContent({
|
|||||||
const configuredWorkingDir = config.cwd.trim()
|
const configuredWorkingDir = config.cwd.trim()
|
||||||
const workspaceId =
|
const workspaceId =
|
||||||
createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir
|
createdWorkingDir.length > 0 ? createdWorkingDir : configuredWorkingDir
|
||||||
router.replace(
|
const route: Href = buildHostWorkspaceAgentTabRoute(
|
||||||
buildHostWorkspaceAgentTabRoute(selectedServerId, workspaceId, agentId) as any
|
selectedServerId,
|
||||||
)
|
workspaceId,
|
||||||
|
agentId
|
||||||
|
) as Href
|
||||||
|
router.replace(route)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1265,11 +1265,10 @@ function WorkspaceScreenContent({
|
|||||||
}
|
}
|
||||||
|
|
||||||
if (target.kind === "agent") {
|
if (target.kind === "agent") {
|
||||||
return (
|
return (
|
||||||
<AgentReadyScreen
|
<AgentReadyScreen
|
||||||
serverId={normalizedServerId}
|
serverId={normalizedServerId}
|
||||||
agentId={target.agentId}
|
agentId={target.agentId}
|
||||||
showHeader={false}
|
|
||||||
showExplorerSidebar={false}
|
showExplorerSidebar={false}
|
||||||
wrapWithExplorerSidebarProvider={false}
|
wrapWithExplorerSidebarProvider={false}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ describe("resolveNotificationTarget", () => {
|
|||||||
).toEqual({
|
).toEqual({
|
||||||
serverId: "server-123",
|
serverId: "server-123",
|
||||||
agentId: "agent-456",
|
agentId: "agent-456",
|
||||||
|
workspaceId: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -22,15 +23,27 @@ describe("resolveNotificationTarget", () => {
|
|||||||
expect(resolveNotificationTarget({ serverId: "", agentId: " " })).toEqual({
|
expect(resolveNotificationTarget({ serverId: "", agentId: " " })).toEqual({
|
||||||
serverId: null,
|
serverId: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
|
workspaceId: null,
|
||||||
});
|
});
|
||||||
expect(resolveNotificationTarget(undefined)).toEqual({
|
expect(resolveNotificationTarget(undefined)).toEqual({
|
||||||
serverId: null,
|
serverId: null,
|
||||||
agentId: null,
|
agentId: null,
|
||||||
|
workspaceId: null,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("buildNotificationRoute", () => {
|
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", () => {
|
it("routes directly to server-scoped agent path when both ids are present", () => {
|
||||||
expect(buildNotificationRoute({ serverId: "srv-1", agentId: "agent-1" })).toBe(
|
expect(buildNotificationRoute({ serverId: "srv-1", agentId: "agent-1" })).toBe(
|
||||||
"/h/srv-1/agent/agent-1"
|
"/h/srv-1/agent/agent-1"
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
buildHostDraftRoute,
|
buildHostDraftRoute,
|
||||||
buildHostAgentDetailRoute,
|
buildHostAgentDetailRoute,
|
||||||
|
buildHostWorkspaceAgentTabRoute,
|
||||||
} from "@/utils/host-routes";
|
} from "@/utils/host-routes";
|
||||||
|
|
||||||
type NotificationData = Record<string, unknown> | null | undefined;
|
type NotificationData = Record<string, unknown> | null | undefined;
|
||||||
@@ -20,16 +21,23 @@ function readNonEmptyString(
|
|||||||
export function resolveNotificationTarget(data: NotificationData): {
|
export function resolveNotificationTarget(data: NotificationData): {
|
||||||
serverId: string | null;
|
serverId: string | null;
|
||||||
agentId: string | null;
|
agentId: string | null;
|
||||||
|
workspaceId: string | null;
|
||||||
} {
|
} {
|
||||||
return {
|
return {
|
||||||
serverId: readNonEmptyString(data, "serverId"),
|
serverId: readNonEmptyString(data, "serverId"),
|
||||||
agentId: readNonEmptyString(data, "agentId"),
|
agentId: readNonEmptyString(data, "agentId"),
|
||||||
|
workspaceId:
|
||||||
|
readNonEmptyString(data, "workspaceId") ??
|
||||||
|
readNonEmptyString(data, "cwd"),
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export function buildNotificationRoute(data: NotificationData): string {
|
export function buildNotificationRoute(data: NotificationData): string {
|
||||||
const { serverId, agentId } = resolveNotificationTarget(data);
|
const { serverId, agentId, workspaceId } = resolveNotificationTarget(data);
|
||||||
if (serverId && agentId) {
|
if (serverId && agentId) {
|
||||||
|
if (workspaceId) {
|
||||||
|
return buildHostWorkspaceAgentTabRoute(serverId, workspaceId, agentId);
|
||||||
|
}
|
||||||
return buildHostAgentDetailRoute(serverId, agentId);
|
return buildHostAgentDetailRoute(serverId, agentId);
|
||||||
}
|
}
|
||||||
if (serverId) {
|
if (serverId) {
|
||||||
|
|||||||
Reference in New Issue
Block a user