Merge branch 'main' into git-diff-panel-reorg

# Conflicts:
#	scripts/dev.sh
This commit is contained in:
Mohamed Boudra
2026-01-24 22:13:32 +07:00
6 changed files with 109 additions and 46 deletions
+2 -33
View File
@@ -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,
@@ -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) {
<MenuIcon size={16} color={menuIconColor} />
</Pressable>
{title && (
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
<View style={styles.titleContainer}>
<Text style={styles.title} numberOfLines={1}>
{title}
</Text>
{subtitle && (
<Text style={styles.subtitle} numberOfLines={1}>
{subtitle}
</Text>
)}
</View>
)}
</>
}
@@ -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,
},
}));
+16 -3
View File
@@ -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<string>
): 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<UserModifiedFields>(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).
@@ -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 */}
<MenuHeader
title={effectiveAgent.title || "Agent"}
subtitle={headerSubtitle}
rightContent={
<View style={styles.headerRightContent}>
<Pressable onPress={toggleFileExplorer} style={styles.menuButton}>
@@ -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;
}
+1 -1
View File
@@ -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