diff --git a/packages/app/src/components/agent-list.tsx b/packages/app/src/components/agent-list.tsx index a444976d2..1ecbeaab3 100644 --- a/packages/app/src/components/agent-list.tsx +++ b/packages/app/src/components/agent-list.tsx @@ -14,12 +14,12 @@ import { StyleSheet, useUnistyles } from "react-native-unistyles"; import { useQueryClient } from "@tanstack/react-query"; import { formatTimeAgo } from "@/utils/time"; import { shortenPath } from "@/utils/shorten-path"; +import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info"; import { type AggregatedAgent } from "@/hooks/use-aggregated-agents"; import { useSessionStore } from "@/stores/session-store"; import { CHECKOUT_STATUS_STALE_TIME, checkoutStatusQueryKey, - type CheckoutStatusPayload, useCheckoutStatusCacheOnly, } from "@/hooks/use-checkout-status-query"; import { @@ -105,35 +105,6 @@ export function AgentList({ setActionAgent(null); }, [actionAgent, deleteAgent]); - const deriveBranchLabel = useCallback((checkout: CheckoutStatusPayload | null): string | null => { - if (!checkout || !checkout.isGit) { - return null; - } - const currentBranch: string | null = checkout.currentBranch ?? null; - const baseRef: string | null = checkout.baseRef ?? null; - if (!currentBranch) { - return null; - } - if (baseRef && currentBranch === baseRef) { - return null; - } - return currentBranch; - }, []); - - const deriveProjectPath = useCallback( - (agent: AggregatedAgent, checkout: CheckoutStatusPayload | null): string => { - const basePath = checkout?.isGit ? (checkout.repoRoot ?? agent.cwd) : agent.cwd; - const worktreeMarker = ".paseo/worktrees/"; - const idx = basePath.indexOf(worktreeMarker); - if (idx !== -1) { - const afterMarker = basePath.slice(idx + worktreeMarker.length); - return afterMarker; - } - return basePath; - }, - [] - ); - const viewabilityConfig = useMemo( () => ({ itemVisiblePercentThreshold: 30 }), [] @@ -190,7 +161,7 @@ export function AgentList({ agentId: agent.id, }); const checkout = checkoutQuery.data ?? null; - const projectPath = deriveProjectPath(agent, checkout); + const projectPath = deriveProjectPath(agent.cwd, checkout); const branchLabel = deriveBranchLabel(checkout); return ( @@ -233,8 +204,6 @@ export function AgentList({ ); }, [ - deriveBranchLabel, - deriveProjectPath, handleAgentLongPress, handleAgentPress, selectedAgentId, diff --git a/packages/app/src/components/headers/menu-header.tsx b/packages/app/src/components/headers/menu-header.tsx index c211e7072..b861cf4c5 100644 --- a/packages/app/src/components/headers/menu-header.tsx +++ b/packages/app/src/components/headers/menu-header.tsx @@ -1,5 +1,5 @@ import type { ReactNode } from "react"; -import { Pressable, Text } from "react-native"; +import { Pressable, Text, View } from "react-native"; import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { Menu, PanelLeft } from "lucide-react-native"; import { ScreenHeader } from "./screen-header"; @@ -7,10 +7,11 @@ import { usePanelStore } from "@/stores/panel-store"; interface MenuHeaderProps { title?: string; + subtitle?: string; rightContent?: ReactNode; } -export function MenuHeader({ title, rightContent }: MenuHeaderProps) { +export function MenuHeader({ title, subtitle, rightContent }: MenuHeaderProps) { const { theme } = useUnistyles(); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; @@ -32,9 +33,16 @@ export function MenuHeader({ title, rightContent }: MenuHeaderProps) { {title && ( - - {title} - + + + {title} + + {subtitle && ( + + {subtitle} + + )} + )} } @@ -55,13 +63,21 @@ const styles = StyleSheet.create((theme) => ({ }, borderRadius: theme.borderRadius.lg, }, - title: { + titleContainer: { flex: 1, - fontSize: theme.fontSize.lg, + gap: theme.spacing[0], + }, + title: { + fontSize: theme.fontSize.base, fontWeight: { - xs: theme.fontWeight.semibold, - md: "400", + xs: "400", + md: "300", }, color: theme.colors.foreground, }, + subtitle: { + fontSize: theme.fontSize.sm, + fontWeight: "300", + color: theme.colors.foregroundMuted, + }, })); diff --git a/packages/app/src/hooks/use-agent-form-state.ts b/packages/app/src/hooks/use-agent-form-state.ts index a18f37b15..337baefd1 100644 --- a/packages/app/src/hooks/use-agent-form-state.ts +++ b/packages/app/src/hooks/use-agent-form-state.ts @@ -9,6 +9,7 @@ import type { AgentProvider, } from "@server/server/agent/agent-sdk-types"; import { useSessionStore } from "@/stores/session-store"; +import { useDaemonRegistry } from "@/contexts/daemon-registry-context"; import { useFormPreferences, type FormPreferences } from "./use-form-preferences"; // Explicit overrides from URL params or "New Agent" button @@ -104,7 +105,8 @@ function resolveFormState( preferences: FormPreferences | null, availableModels: AgentModelDefinition[] | null, userModified: UserModifiedFields, - currentState: FormState + currentState: FormState, + validServerIds: Set ): FormState { // Start with current state - we only update non-user-modified fields const result = { ...currentState }; @@ -177,10 +179,11 @@ function resolveFormState( } // 4. Resolve serverId (independent) + // Only use stored serverId if the host still exists in the registry if (!userModified.serverId) { if (initialValues?.serverId !== undefined) { result.serverId = initialValues.serverId; - } else if (preferences?.serverId) { + } else if (preferences?.serverId && validServerIds.has(preferences.serverId)) { result.serverId = preferences.serverId; } // else keep current @@ -240,6 +243,14 @@ export function useAgentFormState( updateProviderPreferences, } = useFormPreferences(); + const { daemons } = useDaemonRegistry(); + + // Build a set of valid server IDs for preference validation + const validServerIds = useMemo( + () => new Set(daemons.map((d) => d.id)), + [daemons] + ); + // Track which fields the user has explicitly modified const [userModified, setUserModified] = useState(INITIAL_USER_MODIFIED); @@ -303,7 +314,8 @@ export function useAgentFormState( preferences, availableModels, userModified, - formStateRef.current + formStateRef.current, + validServerIds ); // Only update if something changed @@ -326,6 +338,7 @@ export function useAgentFormState( preferences, availableModels, userModified, + validServerIds, ]); // Persist inferred serverId so reloads keep the selection (e.g. URL serverId or first-time load). diff --git a/packages/app/src/screens/agent/agent-ready-screen.tsx b/packages/app/src/screens/agent/agent-ready-screen.tsx index 3c7b2148e..f3e007ab6 100644 --- a/packages/app/src/screens/agent/agent-ready-screen.tsx +++ b/packages/app/src/screens/agent/agent-ready-screen.tsx @@ -57,6 +57,8 @@ import { 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 { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query"; import { DropdownMenu, DropdownMenuContent, @@ -406,6 +408,13 @@ function AgentScreenContent({ ? branchError ?? "Unavailable" : branchLabel ?? "Unknown"; + // Checkout status for header subtitle + const checkoutStatusQuery = useCheckoutStatusQuery({ + serverId, + agentId: resolvedAgentId ?? "", + }); + const checkout = checkoutStatusQuery.status; + useEffect(() => { if (!resolvedAgentId) { setFocusedAgentId(null); @@ -490,6 +499,20 @@ function AgentScreenContent({ const effectiveAgent = agent ?? placeholderAgent; + // Header subtitle: project path + branch (matching agent list row format) + const headerProjectPath = effectiveAgent + ? deriveProjectPath(effectiveAgent.cwd, checkout) + : null; + const headerBranchLabel = deriveBranchLabel(checkout); + const headerSubtitle = useMemo(() => { + if (!headerProjectPath) return undefined; + const path = shortenPath(headerProjectPath); + if (headerBranchLabel) { + return `${path} · ${headerBranchLabel}`; + } + return path; + }, [headerProjectPath, headerBranchLabel]); + useEffect(() => { if (!isPendingCreateForRoute || !pendingCreate) { return; @@ -596,6 +619,7 @@ function AgentScreenContent({ {/* Header */} diff --git a/packages/app/src/utils/agent-display-info.ts b/packages/app/src/utils/agent-display-info.ts new file mode 100644 index 000000000..8fb4351b0 --- /dev/null +++ b/packages/app/src/utils/agent-display-info.ts @@ -0,0 +1,41 @@ +import type { CheckoutStatusPayload } from "@/hooks/use-checkout-status-query"; + +/** + * Derives the branch label to display for an agent. + * Returns null if there's no branch to show (not a git repo, or on the base branch). + */ +export function deriveBranchLabel( + checkout: CheckoutStatusPayload | null +): string | null { + if (!checkout || !checkout.isGit) { + return null; + } + const currentBranch: string | null = checkout.currentBranch ?? null; + const baseRef: string | null = checkout.baseRef ?? null; + if (!currentBranch) { + return null; + } + if (baseRef && currentBranch === baseRef) { + return null; + } + return currentBranch; +} + +/** + * Derives the project path to display for an agent. + * If inside a Paseo worktree, shows just the worktree-relative path. + * Otherwise uses the repo root or cwd. + */ +export function deriveProjectPath( + cwd: string, + checkout: CheckoutStatusPayload | null +): string { + const basePath = checkout?.isGit ? (checkout.repoRoot ?? cwd) : cwd; + const worktreeMarker = ".paseo/worktrees/"; + const idx = basePath.indexOf(worktreeMarker); + if (idx !== -1) { + const afterMarker = basePath.slice(idx + worktreeMarker.length); + return afterMarker; + } + return basePath; +} diff --git a/scripts/dev.sh b/scripts/dev.sh index b72ab5c2b..02a115300 100755 --- a/scripts/dev.sh +++ b/scripts/dev.sh @@ -28,7 +28,7 @@ echo " Metro: http://localhost:${METRO_PORT}" echo "══════════════════════════════════════════════════════" # Export for child processes (overrides .env values) -export PASEO_LISTEN="127.0.0.1:${DAEMON_PORT}" +export PASEO_LISTEN="0.0.0.0:${DAEMON_PORT}" export PASEO_CORS_ORIGINS="${CORS_ORIGINS}" # Run both with concurrently