mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
refactor: centralize host route builders and add dedicated new-agent route
This commit is contained in:
@@ -45,6 +45,7 @@ import {
|
||||
} from "@/utils/os-notifications";
|
||||
import { buildNotificationRoute } from "@/utils/notification-routing";
|
||||
import {
|
||||
buildHostRootRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceTabRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
@@ -350,7 +351,7 @@ function OfferLinkListener({
|
||||
if (cancelled) return;
|
||||
const serverId = (profile as any)?.serverId;
|
||||
if (typeof serverId !== "string" || !serverId) return;
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
})
|
||||
.catch((error) => {
|
||||
if (cancelled) return;
|
||||
@@ -471,14 +472,14 @@ export default function RootLayout() {
|
||||
>
|
||||
<Stack.Screen name="index" />
|
||||
<Stack.Screen name="settings" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/index" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]/tab/[tabId]" />
|
||||
<Stack.Screen name="h/[serverId]/workspace/[workspaceId]" />
|
||||
<Stack.Screen
|
||||
name="h/[serverId]/agent/[agentId]"
|
||||
options={{ gestureEnabled: false }}
|
||||
/>
|
||||
<Stack.Screen name="h/[serverId]/index" />
|
||||
<Stack.Screen name="h/[serverId]/agents" />
|
||||
<Stack.Screen name="h/[serverId]/new-agent" />
|
||||
<Stack.Screen name="h/[serverId]/settings" />
|
||||
<Stack.Screen name="pair-scan" />
|
||||
</Stack>
|
||||
|
||||
@@ -3,6 +3,7 @@ import { useLocalSearchParams, useRouter } from "expo-router";
|
||||
import { useSessionStore } from "@/stores/session-store";
|
||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||
import {
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
@@ -54,7 +55,7 @@ export default function HostAgentReadyRoute() {
|
||||
}
|
||||
if (!client || !isConnected) {
|
||||
redirectedRef.current = true;
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
}
|
||||
}, [agentCwd, agentId, client, isConnected, router, serverId]);
|
||||
|
||||
@@ -79,14 +80,14 @@ export default function HostAgentReadyRoute() {
|
||||
router.replace(buildHostWorkspaceAgentTabRoute(serverId, cwd, agentId) as any);
|
||||
return;
|
||||
}
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
})
|
||||
.catch(() => {
|
||||
if (cancelled || redirectedRef.current) {
|
||||
return;
|
||||
}
|
||||
redirectedRef.current = true;
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
});
|
||||
|
||||
return () => {
|
||||
|
||||
9
packages/app/src/app/h/[serverId]/new-agent.tsx
Normal file
9
packages/app/src/app/h/[serverId]/new-agent.tsx
Normal file
@@ -0,0 +1,9 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
|
||||
|
||||
export default function HostNewAgentRoute() {
|
||||
const params = useLocalSearchParams<{ serverId?: string }>();
|
||||
const serverId = typeof params.serverId === "string" ? params.serverId : "";
|
||||
|
||||
return <DraftAgentScreen forcedServerId={serverId} />;
|
||||
}
|
||||
@@ -1,20 +1,32 @@
|
||||
import { useLocalSearchParams } from "expo-router";
|
||||
import { useLocalSearchParams, usePathname } from "expo-router";
|
||||
import { WorkspaceScreen } from "@/screens/workspace/workspace-screen";
|
||||
import {
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
parseHostWorkspaceTabRouteFromPathname,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
function readNonEmptyParam(value: unknown): string | null {
|
||||
if (typeof value !== "string") {
|
||||
return null;
|
||||
}
|
||||
const trimmed = value.trim();
|
||||
return trimmed.length > 0 ? trimmed : null;
|
||||
}
|
||||
|
||||
export default function HostWorkspaceLayout() {
|
||||
const params = useLocalSearchParams<{
|
||||
serverId?: string;
|
||||
workspaceId?: string;
|
||||
tabId?: string;
|
||||
}>();
|
||||
|
||||
const tabId = typeof params.tabId === "string" ? params.tabId : "";
|
||||
const pathname = usePathname();
|
||||
const params = useLocalSearchParams<{ tabId?: string }>();
|
||||
const tabRoute = parseHostWorkspaceTabRouteFromPathname(pathname);
|
||||
const activeRoute = tabRoute ?? parseHostWorkspaceRouteFromPathname(pathname);
|
||||
const serverId = activeRoute?.serverId ?? "";
|
||||
const workspaceId = activeRoute?.workspaceId ?? "";
|
||||
const routeTabId = tabRoute?.tabId ?? readNonEmptyParam(params.tabId);
|
||||
|
||||
return (
|
||||
<WorkspaceScreen
|
||||
serverId={typeof params.serverId === "string" ? params.serverId : ""}
|
||||
workspaceId={typeof params.workspaceId === "string" ? params.workspaceId : ""}
|
||||
routeTabId={tabId || null}
|
||||
serverId={serverId}
|
||||
workspaceId={workspaceId}
|
||||
routeTabId={routeTabId}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useUnistyles } from "react-native-unistyles";
|
||||
import { DraftAgentScreen } from "@/screens/agent/draft-agent-screen";
|
||||
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||
import { useFormPreferences } from "@/hooks/use-form-preferences";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
|
||||
export default function Index() {
|
||||
const router = useRouter();
|
||||
@@ -44,7 +45,7 @@ export default function Index() {
|
||||
if (!targetServerId) {
|
||||
return;
|
||||
}
|
||||
router.replace(`/h/${encodeURIComponent(targetServerId)}` as any);
|
||||
router.replace(buildHostRootRoute(targetServerId) as any);
|
||||
}, [preferencesLoading, registryLoading, router, targetServerId]);
|
||||
|
||||
if (registryLoading || preferencesLoading) {
|
||||
|
||||
@@ -12,6 +12,7 @@ import { decodeOfferFragmentPayload, normalizeHostPort } from "@/utils/daemon-en
|
||||
import { probeConnection } from "@/utils/test-daemon-connection";
|
||||
import { ConnectionOfferSchema } from "@server/shared/connection-offer";
|
||||
import {
|
||||
buildHostRootRoute,
|
||||
buildHostSettingsRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
@@ -169,7 +170,7 @@ export default function PairScanScreen() {
|
||||
const returnToSource = useCallback(
|
||||
(serverId: string) => {
|
||||
if (source === "onboarding") {
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
return;
|
||||
}
|
||||
if (source === "editHost" && targetServerId) {
|
||||
|
||||
@@ -21,21 +21,11 @@ import { useTauriDragHandlers, useTrafficLightPadding } from '@/utils/tauri-wind
|
||||
import { Combobox } from '@/components/ui/combobox'
|
||||
import { useDaemonRegistry } from '@/contexts/daemon-registry-context'
|
||||
import { getHostRuntimeStore } from '@/runtime/host-runtime'
|
||||
import { useSessionStore } from '@/stores/session-store'
|
||||
import { formatConnectionStatus } from '@/utils/daemons'
|
||||
import { HEADER_INNER_HEIGHT, HEADER_INNER_HEIGHT_MOBILE } from '@/constants/layout'
|
||||
import {
|
||||
checkoutStatusQueryKey,
|
||||
type CheckoutStatusPayload,
|
||||
} from '@/hooks/use-checkout-status-query'
|
||||
import { queryClient } from '@/query/query-client'
|
||||
import {
|
||||
buildNewAgentRoute,
|
||||
resolveNewAgentWorkingDir,
|
||||
resolveSelectedAgentForNewAgent,
|
||||
} from '@/utils/new-agent-routing'
|
||||
import {
|
||||
buildHostAgentsRoute,
|
||||
buildHostNewAgentRoute,
|
||||
buildHostSettingsRoute,
|
||||
mapPathnameToServer,
|
||||
parseServerIdFromPathname,
|
||||
@@ -47,7 +37,7 @@ interface LeftSidebarProps {
|
||||
selectedAgentId?: string
|
||||
}
|
||||
|
||||
export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
||||
export function LeftSidebar({ selectedAgentId: _selectedAgentId }: LeftSidebarProps) {
|
||||
const { theme } = useUnistyles()
|
||||
const insets = useSafeAreaInsets()
|
||||
const isMobile = UnistylesRuntime.breakpoint === 'xs' || UnistylesRuntime.breakpoint === 'sm'
|
||||
@@ -79,13 +69,24 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
||||
.join('|')
|
||||
)
|
||||
const activeServerIdFromPath = useMemo(() => parseServerIdFromPathname(pathname), [pathname])
|
||||
const activeServerId = activeServerIdFromPath ?? daemons[0]?.serverId ?? null
|
||||
const activeDaemon = useMemo(() => {
|
||||
if (daemons.length === 0) {
|
||||
return null
|
||||
}
|
||||
if (activeServerIdFromPath) {
|
||||
const routeMatch = daemons.find((entry) => entry.serverId === activeServerIdFromPath)
|
||||
if (routeMatch) {
|
||||
return routeMatch
|
||||
}
|
||||
}
|
||||
return daemons[0] ?? null
|
||||
}, [activeServerIdFromPath, daemons])
|
||||
const activeServerId = activeDaemon?.serverId ?? null
|
||||
const activeHostLabel = useMemo(() => {
|
||||
if (!activeServerId) return 'No host'
|
||||
const daemon = daemons.find((entry) => entry.serverId === activeServerId)
|
||||
const trimmed = daemon?.label?.trim()
|
||||
return trimmed && trimmed.length > 0 ? trimmed : activeServerId
|
||||
}, [activeServerId, daemons])
|
||||
if (!activeDaemon) return 'No host'
|
||||
const trimmed = activeDaemon.label?.trim()
|
||||
return trimmed && trimmed.length > 0 ? trimmed : activeDaemon.serverId
|
||||
}, [activeDaemon])
|
||||
const activeHostStatus = activeServerId
|
||||
? (runtime.getSnapshot(activeServerId)?.connectionStatus ?? 'connecting')
|
||||
: 'idle'
|
||||
@@ -150,33 +151,11 @@ export function LeftSidebar({ selectedAgentId }: LeftSidebarProps) {
|
||||
}, [closeToAgent])
|
||||
|
||||
const handleCreateAgentClean = useCallback(() => {
|
||||
let targetServerId = activeServerId
|
||||
let targetWorkingDir: string | null = null
|
||||
|
||||
const selectedAgent = resolveSelectedAgentForNewAgent({
|
||||
pathname,
|
||||
selectedAgentId,
|
||||
})
|
||||
if (selectedAgent) {
|
||||
targetServerId = selectedAgent.serverId
|
||||
const agent = useSessionStore
|
||||
.getState()
|
||||
.sessions[selectedAgent.serverId]?.agents?.get(selectedAgent.agentId)
|
||||
const cwd = agent?.cwd?.trim()
|
||||
if (cwd) {
|
||||
const checkout =
|
||||
queryClient.getQueryData<CheckoutStatusPayload>(
|
||||
checkoutStatusQueryKey(selectedAgent.serverId, cwd)
|
||||
) ?? null
|
||||
targetWorkingDir = resolveNewAgentWorkingDir(cwd, checkout)
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetServerId) {
|
||||
if (!activeServerId) {
|
||||
return
|
||||
}
|
||||
router.push(buildNewAgentRoute(targetServerId, targetWorkingDir) as any)
|
||||
}, [activeServerId, pathname, selectedAgentId])
|
||||
router.push(buildHostNewAgentRoute(activeServerId) as any)
|
||||
}, [activeServerId])
|
||||
|
||||
// Mobile: close sidebar and navigate
|
||||
const handleCreateAgentCleanMobile = useCallback(() => {
|
||||
|
||||
@@ -11,6 +11,7 @@ import { PairLinkModal } from "./pair-link-modal";
|
||||
import { NameHostModal } from "./name-host-modal";
|
||||
import { resolveAppVersion } from "@/utils/app-version";
|
||||
import { formatVersionWithPrefix } from "@/desktop/updates/desktop-updates";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
|
||||
const styles = StyleSheet.create((theme) => ({
|
||||
container: {
|
||||
@@ -105,7 +106,7 @@ export function WelcomeScreen({ onHostAdded }: WelcomeScreenProps) {
|
||||
|
||||
const finishOnboarding = useCallback(
|
||||
(serverId: string) => {
|
||||
router.replace(`/h/${encodeURIComponent(serverId)}` as any);
|
||||
router.replace(buildHostRootRoute(serverId) as any);
|
||||
},
|
||||
[router]
|
||||
);
|
||||
|
||||
@@ -3,21 +3,12 @@ import type { TextInput } from "react-native";
|
||||
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";
|
||||
import {
|
||||
checkoutStatusQueryKey,
|
||||
type CheckoutStatusPayload,
|
||||
} from "@/hooks/use-checkout-status-query";
|
||||
import { queryClient } from "@/query/query-client";
|
||||
import {
|
||||
clearCommandCenterFocusRestoreElement,
|
||||
takeCommandCenterFocusRestoreElement,
|
||||
} from "@/utils/command-center-focus-restore";
|
||||
import {
|
||||
buildNewAgentRoute,
|
||||
resolveNewAgentWorkingDir,
|
||||
} from "@/utils/new-agent-routing";
|
||||
import {
|
||||
buildHostNewAgentRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
buildHostSettingsRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
@@ -63,12 +54,6 @@ function parseAgentKeyFromPathname(pathname: string): string | null {
|
||||
return `${match.serverId}:${match.agentId}`;
|
||||
}
|
||||
|
||||
function parseAgentRouteFromPathname(
|
||||
pathname: string
|
||||
): { serverId: string; agentId: string } | null {
|
||||
return parseHostAgentRouteFromPathname(pathname);
|
||||
}
|
||||
|
||||
type CommandCenterActionDefinition = {
|
||||
id: string;
|
||||
title: string;
|
||||
@@ -157,24 +142,7 @@ export function useCommandCenter() {
|
||||
const newAgentRoute = useMemo<Href>(() => {
|
||||
const serverIdFromPath =
|
||||
parseServerIdFromPathname(pathname) ?? fallbackServerId;
|
||||
const routeAgent = parseAgentRouteFromPathname(pathname);
|
||||
if (!routeAgent) {
|
||||
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) as Href;
|
||||
}
|
||||
|
||||
const checkout =
|
||||
queryClient.getQueryData<CheckoutStatusPayload>(
|
||||
checkoutStatusQueryKey(serverId, cwd)
|
||||
) ?? null;
|
||||
const workingDir = resolveNewAgentWorkingDir(cwd, checkout);
|
||||
return buildNewAgentRoute(serverId, workingDir) as Href;
|
||||
return serverIdFromPath ? (buildHostNewAgentRoute(serverIdFromPath) as Href) : "/";
|
||||
}, [fallbackServerId, pathname]);
|
||||
|
||||
const settingsRoute = useMemo<Href>(() => {
|
||||
|
||||
@@ -6,16 +6,7 @@ import { useSessionStore } from "@/stores/session-store";
|
||||
import { useKeyboardShortcutsStore } from "@/stores/keyboard-shortcuts-store";
|
||||
import { setCommandCenterFocusRestoreElement } from "@/utils/command-center-focus-restore";
|
||||
import {
|
||||
checkoutStatusQueryKey,
|
||||
type CheckoutStatusPayload,
|
||||
} from "@/hooks/use-checkout-status-query";
|
||||
import { queryClient } from "@/query/query-client";
|
||||
import {
|
||||
buildNewAgentRoute,
|
||||
resolveSelectedAgentForNewAgent,
|
||||
resolveNewAgentWorkingDir,
|
||||
} from "@/utils/new-agent-routing";
|
||||
import {
|
||||
buildHostNewAgentRoute,
|
||||
buildHostWorkspaceRoute,
|
||||
parseHostAgentRouteFromPathname,
|
||||
parseHostWorkspaceRouteFromPathname,
|
||||
@@ -85,26 +76,6 @@ export function useKeyboardShortcuts({
|
||||
|
||||
const navigateToNewAgent = (): boolean => {
|
||||
let targetServerId = parseServerIdFromPathname(pathname);
|
||||
let targetWorkingDir: string | null = null;
|
||||
const selectedAgent = resolveSelectedAgentForNewAgent({
|
||||
pathname,
|
||||
selectedAgentId,
|
||||
});
|
||||
if (selectedAgent) {
|
||||
targetServerId = selectedAgent.serverId;
|
||||
const agent = useSessionStore
|
||||
.getState()
|
||||
.sessions[selectedAgent.serverId]
|
||||
?.agents?.get(selectedAgent.agentId);
|
||||
const cwd = agent?.cwd?.trim();
|
||||
if (cwd) {
|
||||
const checkout =
|
||||
queryClient.getQueryData<CheckoutStatusPayload>(
|
||||
checkoutStatusQueryKey(selectedAgent.serverId, cwd)
|
||||
) ?? null;
|
||||
targetWorkingDir = resolveNewAgentWorkingDir(cwd, checkout);
|
||||
}
|
||||
}
|
||||
|
||||
if (!targetServerId) {
|
||||
const sessionServerIds = Object.keys(useSessionStore.getState().sessions);
|
||||
@@ -115,7 +86,7 @@ export function useKeyboardShortcuts({
|
||||
return false;
|
||||
}
|
||||
|
||||
router.push(buildNewAgentRoute(targetServerId, targetWorkingDir) as any);
|
||||
router.push(buildHostNewAgentRoute(targetServerId) as any);
|
||||
return true;
|
||||
};
|
||||
|
||||
|
||||
@@ -60,6 +60,7 @@ import { shouldClearAgentAttentionOnView } from "@/utils/agent-attention";
|
||||
import type { DaemonClient } from "@server/client/daemon-client";
|
||||
import { useExplorerOpenGesture } from "@/hooks/use-explorer-open-gesture";
|
||||
import type { ExplorerCheckoutContext } from "@/stores/panel-store";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
|
||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||
const IS_DEV = Boolean((globalThis as { __DEV__?: boolean }).__DEV__);
|
||||
@@ -528,7 +529,7 @@ function AgentScreenContent({
|
||||
return;
|
||||
}
|
||||
hasRedirectedArchivedAgentRef.current = true;
|
||||
const route: Href = `/h/${encodeURIComponent(serverId)}` as Href;
|
||||
const route: Href = buildHostRootRoute(serverId) as Href;
|
||||
router.replace(route);
|
||||
}, [agent?.archivedAt, resolvedAgentId, router, serverId]);
|
||||
|
||||
|
||||
@@ -5,6 +5,7 @@ import { BackHeader } from "@/components/headers/back-header";
|
||||
import { AgentList } from "@/components/agent-list";
|
||||
import { useAllAgentsList } from "@/hooks/use-all-agents-list";
|
||||
import { router } from "expo-router";
|
||||
import { buildHostRootRoute } from "@/utils/host-routes";
|
||||
|
||||
export function AgentsScreen({ serverId }: { serverId: string }) {
|
||||
const { agents, isRevalidating, refreshAll } = useAllAgentsList({
|
||||
@@ -43,7 +44,7 @@ export function AgentsScreen({ serverId }: { serverId: string }) {
|
||||
<View style={styles.container}>
|
||||
<BackHeader
|
||||
title="All agents"
|
||||
onBack={() => router.replace(`/h/${encodeURIComponent(serverId)}` as any)}
|
||||
onBack={() => router.replace(buildHostRootRoute(serverId) as any)}
|
||||
/>
|
||||
<AgentList
|
||||
agents={sortedAgents}
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
import { useCallback, useEffect, useMemo, useReducer } from "react";
|
||||
import { useCallback, useEffect, useMemo, useReducer, useRef } from "react";
|
||||
import { Keyboard, Platform, ScrollView, Text, View } from "react-native";
|
||||
import { StyleSheet } from "react-native-unistyles";
|
||||
import { AgentInputArea } from "@/components/agent-input-area";
|
||||
import { AgentConfigRow } from "@/components/agent-form/agent-form-dropdowns";
|
||||
import { FileDropZone } from "@/components/file-drop-zone";
|
||||
import { AgentStreamView } from "@/components/agent-stream-view";
|
||||
import type { ImageAttachment } from "@/components/message-input";
|
||||
import { MAX_CONTENT_WIDTH } from "@/constants/layout";
|
||||
import { useAgentFormState } from "@/hooks/use-agent-form-state";
|
||||
import { useHostRuntimeSession } from "@/runtime/host-runtime";
|
||||
@@ -63,6 +65,7 @@ export function WorkspaceDraftAgentTab({
|
||||
onCreated,
|
||||
}: WorkspaceDraftAgentTabProps) {
|
||||
const { client, isConnected } = useHostRuntimeSession(serverId);
|
||||
const addImagesRef = useRef<((images: ImageAttachment[]) => void) | null>(null);
|
||||
|
||||
const setPendingCreateAttempt = useCreateFlowStore((state) => state.setPending);
|
||||
const updatePendingAgentId = useCreateFlowStore((state) => state.updateAgentId);
|
||||
@@ -322,64 +325,75 @@ export function WorkspaceDraftAgentTab({
|
||||
workspaceId,
|
||||
]);
|
||||
|
||||
const handleFilesDropped = useCallback((files: ImageAttachment[]) => {
|
||||
addImagesRef.current?.(files);
|
||||
}, []);
|
||||
|
||||
const handleAddImagesCallback = useCallback((addImages: (images: ImageAttachment[]) => void) => {
|
||||
addImagesRef.current = addImages;
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.contentContainer}>
|
||||
{machine.tag === "creating" && draftAgent ? (
|
||||
<View style={styles.streamContainer}>
|
||||
<AgentStreamView
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
agent={draftAgent}
|
||||
streamItems={optimisticStreamItems}
|
||||
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
||||
/>
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
|
||||
<View style={styles.configSection}>
|
||||
<AgentConfigRow
|
||||
providerDefinitions={providerDefinitions}
|
||||
selectedProvider={selectedProvider}
|
||||
onSelectProvider={setProviderFromUser}
|
||||
modeOptions={modeOptions}
|
||||
selectedMode={selectedMode}
|
||||
onSelectMode={setModeFromUser}
|
||||
models={availableModels}
|
||||
selectedModel={selectedModel}
|
||||
onSelectModel={setModelFromUser}
|
||||
isModelLoading={isModelLoading}
|
||||
thinkingOptions={availableThinkingOptions}
|
||||
selectedThinkingOptionId={selectedThinkingOptionId}
|
||||
onSelectThinkingOption={setThinkingOptionFromUser}
|
||||
disabled={isSubmitting}
|
||||
<FileDropZone onFilesDropped={handleFilesDropped}>
|
||||
<View style={styles.container}>
|
||||
<View style={styles.contentContainer}>
|
||||
{machine.tag === "creating" && draftAgent ? (
|
||||
<View style={styles.streamContainer}>
|
||||
<AgentStreamView
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
agent={draftAgent}
|
||||
streamItems={optimisticStreamItems}
|
||||
pendingPermissions={EMPTY_PENDING_PERMISSIONS}
|
||||
/>
|
||||
|
||||
{formErrorMessage ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
) : (
|
||||
<ScrollView style={styles.scrollView} contentContainerStyle={styles.configScrollContent}>
|
||||
<View style={styles.configSection}>
|
||||
<AgentConfigRow
|
||||
providerDefinitions={providerDefinitions}
|
||||
selectedProvider={selectedProvider}
|
||||
onSelectProvider={setProviderFromUser}
|
||||
modeOptions={modeOptions}
|
||||
selectedMode={selectedMode}
|
||||
onSelectMode={setModeFromUser}
|
||||
models={availableModels}
|
||||
selectedModel={selectedModel}
|
||||
onSelectModel={setModelFromUser}
|
||||
isModelLoading={isModelLoading}
|
||||
thinkingOptions={availableThinkingOptions}
|
||||
selectedThinkingOptionId={selectedThinkingOptionId}
|
||||
onSelectThinkingOption={setThinkingOptionFromUser}
|
||||
disabled={isSubmitting}
|
||||
/>
|
||||
|
||||
<View style={styles.inputAreaWrapper}>
|
||||
<AgentInputArea
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isSubmitting}
|
||||
blurOnSubmit={true}
|
||||
value={promptValue}
|
||||
onChangeText={(next) => dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
|
||||
autoFocus={machine.tag === "draft"}
|
||||
commandDraftConfig={draftCommandConfig}
|
||||
draftId={draftId}
|
||||
/>
|
||||
{formErrorMessage ? (
|
||||
<View style={styles.errorContainer}>
|
||||
<Text style={styles.errorText}>{formErrorMessage}</Text>
|
||||
</View>
|
||||
) : null}
|
||||
</View>
|
||||
</ScrollView>
|
||||
)}
|
||||
</View>
|
||||
|
||||
<View style={styles.inputAreaWrapper}>
|
||||
<AgentInputArea
|
||||
agentId={tabId}
|
||||
serverId={serverId}
|
||||
onSubmitMessage={handleCreateFromInput}
|
||||
isSubmitLoading={isSubmitting}
|
||||
blurOnSubmit={true}
|
||||
value={promptValue}
|
||||
onChangeText={(next) => dispatch({ type: "DRAFT_SET_PROMPT", text: next })}
|
||||
autoFocus={machine.tag === "draft"}
|
||||
onAddImages={handleAddImagesCallback}
|
||||
commandDraftConfig={draftCommandConfig}
|
||||
draftId={draftId}
|
||||
/>
|
||||
</View>
|
||||
</View>
|
||||
</View>
|
||||
</FileDropZone>
|
||||
);
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,7 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import {
|
||||
buildHostNewAgentRoute,
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
buildHostWorkspaceFileTabRoute,
|
||||
buildHostWorkspaceRoute,
|
||||
@@ -70,6 +72,14 @@ describe("workspace route parsing", () => {
|
||||
expect(buildHostWorkspaceRoute("local", "/tmp/repo")).toBe("/h/local/workspace/L3RtcC9yZXBv");
|
||||
});
|
||||
|
||||
it("builds host root routes", () => {
|
||||
expect(buildHostRootRoute("local")).toBe("/h/local");
|
||||
});
|
||||
|
||||
it("builds host new-agent routes", () => {
|
||||
expect(buildHostNewAgentRoute("local")).toBe("/h/local/new-agent");
|
||||
});
|
||||
|
||||
it("builds workspace agent tab routes", () => {
|
||||
expect(buildHostWorkspaceAgentTabRoute("local", "/tmp/repo", "agent-1")).toBe(
|
||||
"/h/local/workspace/L3RtcC9yZXBv/tab/agent_agent-1"
|
||||
|
||||
@@ -359,25 +359,41 @@ export function buildHostAgentDetailRoute(
|
||||
if (!normalizedServerId || !normalizedAgentId) {
|
||||
return "/";
|
||||
}
|
||||
return `/h/${encodeSegment(normalizedServerId)}/agent/${encodeSegment(
|
||||
return `${buildHostRootRoute(normalizedServerId)}/agent/${encodeSegment(
|
||||
normalizedAgentId
|
||||
)}`;
|
||||
}
|
||||
|
||||
export function buildHostAgentsRoute(serverId: string): string {
|
||||
export function buildHostRootRoute(serverId: string): string {
|
||||
const normalized = trimNonEmpty(serverId);
|
||||
if (!normalized) {
|
||||
return "/";
|
||||
}
|
||||
return `/h/${encodeSegment(normalized)}/agents`;
|
||||
return `/h/${encodeSegment(normalized)}`;
|
||||
}
|
||||
|
||||
export function buildHostAgentsRoute(serverId: string): string {
|
||||
const base = buildHostRootRoute(serverId);
|
||||
if (base === "/") {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/agents`;
|
||||
}
|
||||
|
||||
export function buildHostNewAgentRoute(serverId: string): string {
|
||||
const base = buildHostRootRoute(serverId);
|
||||
if (base === "/") {
|
||||
return "/";
|
||||
}
|
||||
return `${base}/new-agent`;
|
||||
}
|
||||
|
||||
export function buildHostSettingsRoute(serverId: string): string {
|
||||
const normalized = trimNonEmpty(serverId);
|
||||
if (!normalized) {
|
||||
const base = buildHostRootRoute(serverId);
|
||||
if (base === "/") {
|
||||
return "/";
|
||||
}
|
||||
return `/h/${encodeSegment(normalized)}/settings`;
|
||||
return `${base}/settings`;
|
||||
}
|
||||
|
||||
export function mapPathnameToServer(
|
||||
@@ -390,13 +406,16 @@ export function mapPathnameToServer(
|
||||
}
|
||||
|
||||
const suffix = pathname.replace(/^\/h\/[^/]+\/?/, "");
|
||||
const base = `/h/${encodeSegment(normalized)}`;
|
||||
const base = buildHostRootRoute(normalized);
|
||||
if (suffix.startsWith("settings")) {
|
||||
return `${base}/settings`;
|
||||
}
|
||||
if (suffix.startsWith("agents")) {
|
||||
return `${base}/agents`;
|
||||
}
|
||||
if (suffix.startsWith("new-agent")) {
|
||||
return `${base}/new-agent`;
|
||||
}
|
||||
if (suffix.startsWith("workspace/")) {
|
||||
return `${base}/${suffix}`;
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import {
|
||||
buildHostAgentDetailRoute,
|
||||
buildHostRootRoute,
|
||||
buildHostWorkspaceAgentTabRoute,
|
||||
} from "@/utils/host-routes";
|
||||
|
||||
@@ -40,7 +41,7 @@ export function buildNotificationRoute(data: NotificationData): string {
|
||||
return buildHostAgentDetailRoute(serverId, agentId);
|
||||
}
|
||||
if (serverId) {
|
||||
return `/h/${encodeURIComponent(serverId)}`;
|
||||
return buildHostRootRoute(serverId);
|
||||
}
|
||||
return "/";
|
||||
}
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
import { v4 as uuidv4 } from "uuid";
|
||||
import type { Logger } from "pino";
|
||||
import { exec } from "node:child_process";
|
||||
import { promisify } from "node:util";
|
||||
import { sep } from "node:path";
|
||||
import type { TerminalManager } from "../terminal/terminal-manager.js";
|
||||
import type { TerminalSession } from "../terminal/terminal.js";
|
||||
import {
|
||||
createWorktree,
|
||||
getWorktreeTerminalSpecs,
|
||||
listPaseoWorktrees,
|
||||
resolveWorktreeRuntimeEnv,
|
||||
runWorktreeSetupCommands,
|
||||
WorktreeSetupError,
|
||||
@@ -42,6 +46,12 @@ export interface CreateAgentWorktreeOptions {
|
||||
const MAX_WORKTREE_SETUP_COMMAND_OUTPUT_BYTES = 64 * 1024;
|
||||
const WORKTREE_SETUP_TRUNCATION_MARKER = "\n...<output truncated in the middle>...\n";
|
||||
const WORKTREE_BOOTSTRAP_TERMINAL_READY_TIMEOUT_MS = 1_500;
|
||||
const READ_ONLY_GIT_ENV: NodeJS.ProcessEnv = {
|
||||
...process.env,
|
||||
GIT_OPTIONAL_LOCKS: "0",
|
||||
};
|
||||
const execAsync = promisify(exec);
|
||||
const worktreeSetupEligibility = new WeakMap<WorktreeConfig, boolean>();
|
||||
|
||||
type MiddleTruncationAccumulator = {
|
||||
totalBytes: number;
|
||||
@@ -153,7 +163,18 @@ function renderMiddleTruncationAccumulator(
|
||||
export async function createAgentWorktree(
|
||||
options: CreateAgentWorktreeOptions
|
||||
): Promise<WorktreeConfig> {
|
||||
return createWorktree({
|
||||
const existingWorktree = await findExistingPaseoWorktreeBySlug(options);
|
||||
if (existingWorktree) {
|
||||
const branchName = await resolveBranchNameForWorktreePath(existingWorktree.path);
|
||||
const reusedWorktree = {
|
||||
branchName,
|
||||
worktreePath: existingWorktree.path,
|
||||
};
|
||||
worktreeSetupEligibility.set(reusedWorktree, false);
|
||||
return reusedWorktree;
|
||||
}
|
||||
|
||||
const createdWorktree = await createWorktree({
|
||||
branchName: options.branchName,
|
||||
cwd: options.cwd,
|
||||
baseBranch: options.baseBranch,
|
||||
@@ -161,6 +182,29 @@ export async function createAgentWorktree(
|
||||
runSetup: false,
|
||||
paseoHome: options.paseoHome,
|
||||
});
|
||||
worktreeSetupEligibility.set(createdWorktree, true);
|
||||
return createdWorktree;
|
||||
}
|
||||
|
||||
async function findExistingPaseoWorktreeBySlug(options: CreateAgentWorktreeOptions) {
|
||||
const worktrees = await listPaseoWorktrees({
|
||||
cwd: options.cwd,
|
||||
paseoHome: options.paseoHome,
|
||||
});
|
||||
const slugSuffix = `${sep}${options.worktreeSlug}`;
|
||||
return worktrees.find((worktree) => worktree.path.endsWith(slugSuffix));
|
||||
}
|
||||
|
||||
async function resolveBranchNameForWorktreePath(worktreePath: string): Promise<string> {
|
||||
const { stdout } = await execAsync("git branch --show-current", {
|
||||
cwd: worktreePath,
|
||||
env: READ_ONLY_GIT_ENV,
|
||||
});
|
||||
const branchName = stdout.trim();
|
||||
if (!branchName) {
|
||||
throw new Error(`Unable to resolve branch for existing worktree: ${worktreePath}`);
|
||||
}
|
||||
return branchName;
|
||||
}
|
||||
|
||||
function formatDurationMs(durationMs: number): string {
|
||||
@@ -467,6 +511,10 @@ async function runWorktreeTerminalBootstrap(
|
||||
export async function runAsyncWorktreeBootstrap(
|
||||
options: RunAsyncWorktreeBootstrapOptions
|
||||
): Promise<void> {
|
||||
if (worktreeSetupEligibility.get(options.worktree) === false) {
|
||||
return;
|
||||
}
|
||||
|
||||
const setupCallId = uuidv4();
|
||||
let setupResults: WorktreeSetupCommandResult[] = [];
|
||||
let runtimeEnv: WorktreeRuntimeEnv | null = null;
|
||||
|
||||
Reference in New Issue
Block a user