Merge branch 'main' into git-diff-panel-reorg
# Conflicts: # scripts/dev.sh
This commit is contained in:
@@ -14,12 +14,12 @@ 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";
|
||||||
import { shortenPath } from "@/utils/shorten-path";
|
import { shortenPath } from "@/utils/shorten-path";
|
||||||
|
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
|
||||||
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
import { type AggregatedAgent } from "@/hooks/use-aggregated-agents";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
import {
|
import {
|
||||||
CHECKOUT_STATUS_STALE_TIME,
|
CHECKOUT_STATUS_STALE_TIME,
|
||||||
checkoutStatusQueryKey,
|
checkoutStatusQueryKey,
|
||||||
type CheckoutStatusPayload,
|
|
||||||
useCheckoutStatusCacheOnly,
|
useCheckoutStatusCacheOnly,
|
||||||
} from "@/hooks/use-checkout-status-query";
|
} from "@/hooks/use-checkout-status-query";
|
||||||
import {
|
import {
|
||||||
@@ -105,35 +105,6 @@ export function AgentList({
|
|||||||
setActionAgent(null);
|
setActionAgent(null);
|
||||||
}, [actionAgent, deleteAgent]);
|
}, [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(
|
const viewabilityConfig = useMemo(
|
||||||
() => ({ itemVisiblePercentThreshold: 30 }),
|
() => ({ itemVisiblePercentThreshold: 30 }),
|
||||||
[]
|
[]
|
||||||
@@ -190,7 +161,7 @@ export function AgentList({
|
|||||||
agentId: agent.id,
|
agentId: agent.id,
|
||||||
});
|
});
|
||||||
const checkout = checkoutQuery.data ?? null;
|
const checkout = checkoutQuery.data ?? null;
|
||||||
const projectPath = deriveProjectPath(agent, checkout);
|
const projectPath = deriveProjectPath(agent.cwd, checkout);
|
||||||
const branchLabel = deriveBranchLabel(checkout);
|
const branchLabel = deriveBranchLabel(checkout);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -233,8 +204,6 @@ export function AgentList({
|
|||||||
);
|
);
|
||||||
},
|
},
|
||||||
[
|
[
|
||||||
deriveBranchLabel,
|
|
||||||
deriveProjectPath,
|
|
||||||
handleAgentLongPress,
|
handleAgentLongPress,
|
||||||
handleAgentPress,
|
handleAgentPress,
|
||||||
selectedAgentId,
|
selectedAgentId,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
import type { ReactNode } from "react";
|
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 { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles";
|
||||||
import { Menu, PanelLeft } from "lucide-react-native";
|
import { Menu, PanelLeft } from "lucide-react-native";
|
||||||
import { ScreenHeader } from "./screen-header";
|
import { ScreenHeader } from "./screen-header";
|
||||||
@@ -7,10 +7,11 @@ import { usePanelStore } from "@/stores/panel-store";
|
|||||||
|
|
||||||
interface MenuHeaderProps {
|
interface MenuHeaderProps {
|
||||||
title?: string;
|
title?: string;
|
||||||
|
subtitle?: string;
|
||||||
rightContent?: ReactNode;
|
rightContent?: ReactNode;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
|
export function MenuHeader({ title, subtitle, rightContent }: MenuHeaderProps) {
|
||||||
const { theme } = useUnistyles();
|
const { theme } = useUnistyles();
|
||||||
const isMobile =
|
const isMobile =
|
||||||
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm";
|
||||||
@@ -32,9 +33,16 @@ export function MenuHeader({ title, rightContent }: MenuHeaderProps) {
|
|||||||
<MenuIcon size={16} color={menuIconColor} />
|
<MenuIcon size={16} color={menuIconColor} />
|
||||||
</Pressable>
|
</Pressable>
|
||||||
{title && (
|
{title && (
|
||||||
<Text style={styles.title} numberOfLines={1}>
|
<View style={styles.titleContainer}>
|
||||||
{title}
|
<Text style={styles.title} numberOfLines={1}>
|
||||||
</Text>
|
{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,
|
borderRadius: theme.borderRadius.lg,
|
||||||
},
|
},
|
||||||
title: {
|
titleContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
fontSize: theme.fontSize.lg,
|
gap: theme.spacing[0],
|
||||||
|
},
|
||||||
|
title: {
|
||||||
|
fontSize: theme.fontSize.base,
|
||||||
fontWeight: {
|
fontWeight: {
|
||||||
xs: theme.fontWeight.semibold,
|
xs: "400",
|
||||||
md: "400",
|
md: "300",
|
||||||
},
|
},
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foreground,
|
||||||
},
|
},
|
||||||
|
subtitle: {
|
||||||
|
fontSize: theme.fontSize.sm,
|
||||||
|
fontWeight: "300",
|
||||||
|
color: theme.colors.foregroundMuted,
|
||||||
|
},
|
||||||
}));
|
}));
|
||||||
|
|||||||
@@ -9,6 +9,7 @@ import type {
|
|||||||
AgentProvider,
|
AgentProvider,
|
||||||
} from "@server/server/agent/agent-sdk-types";
|
} from "@server/server/agent/agent-sdk-types";
|
||||||
import { useSessionStore } from "@/stores/session-store";
|
import { useSessionStore } from "@/stores/session-store";
|
||||||
|
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||||
import { useFormPreferences, type FormPreferences } from "./use-form-preferences";
|
import { useFormPreferences, type FormPreferences } from "./use-form-preferences";
|
||||||
|
|
||||||
// Explicit overrides from URL params or "New Agent" button
|
// Explicit overrides from URL params or "New Agent" button
|
||||||
@@ -104,7 +105,8 @@ function resolveFormState(
|
|||||||
preferences: FormPreferences | null,
|
preferences: FormPreferences | null,
|
||||||
availableModels: AgentModelDefinition[] | null,
|
availableModels: AgentModelDefinition[] | null,
|
||||||
userModified: UserModifiedFields,
|
userModified: UserModifiedFields,
|
||||||
currentState: FormState
|
currentState: FormState,
|
||||||
|
validServerIds: Set<string>
|
||||||
): FormState {
|
): FormState {
|
||||||
// Start with current state - we only update non-user-modified fields
|
// Start with current state - we only update non-user-modified fields
|
||||||
const result = { ...currentState };
|
const result = { ...currentState };
|
||||||
@@ -177,10 +179,11 @@ function resolveFormState(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 4. Resolve serverId (independent)
|
// 4. Resolve serverId (independent)
|
||||||
|
// Only use stored serverId if the host still exists in the registry
|
||||||
if (!userModified.serverId) {
|
if (!userModified.serverId) {
|
||||||
if (initialValues?.serverId !== undefined) {
|
if (initialValues?.serverId !== undefined) {
|
||||||
result.serverId = initialValues.serverId;
|
result.serverId = initialValues.serverId;
|
||||||
} else if (preferences?.serverId) {
|
} else if (preferences?.serverId && validServerIds.has(preferences.serverId)) {
|
||||||
result.serverId = preferences.serverId;
|
result.serverId = preferences.serverId;
|
||||||
}
|
}
|
||||||
// else keep current
|
// else keep current
|
||||||
@@ -240,6 +243,14 @@ export function useAgentFormState(
|
|||||||
updateProviderPreferences,
|
updateProviderPreferences,
|
||||||
} = useFormPreferences();
|
} = 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
|
// Track which fields the user has explicitly modified
|
||||||
const [userModified, setUserModified] = useState<UserModifiedFields>(INITIAL_USER_MODIFIED);
|
const [userModified, setUserModified] = useState<UserModifiedFields>(INITIAL_USER_MODIFIED);
|
||||||
|
|
||||||
@@ -303,7 +314,8 @@ export function useAgentFormState(
|
|||||||
preferences,
|
preferences,
|
||||||
availableModels,
|
availableModels,
|
||||||
userModified,
|
userModified,
|
||||||
formStateRef.current
|
formStateRef.current,
|
||||||
|
validServerIds
|
||||||
);
|
);
|
||||||
|
|
||||||
// Only update if something changed
|
// Only update if something changed
|
||||||
@@ -326,6 +338,7 @@ export function useAgentFormState(
|
|||||||
preferences,
|
preferences,
|
||||||
availableModels,
|
availableModels,
|
||||||
userModified,
|
userModified,
|
||||||
|
validServerIds,
|
||||||
]);
|
]);
|
||||||
|
|
||||||
// Persist inferred serverId so reloads keep the selection (e.g. URL serverId or first-time load).
|
// 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 { 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 { shortenPath } from "@/utils/shorten-path";
|
||||||
|
import { deriveBranchLabel, deriveProjectPath } from "@/utils/agent-display-info";
|
||||||
|
import { useCheckoutStatusQuery } from "@/hooks/use-checkout-status-query";
|
||||||
import {
|
import {
|
||||||
DropdownMenu,
|
DropdownMenu,
|
||||||
DropdownMenuContent,
|
DropdownMenuContent,
|
||||||
@@ -406,6 +408,13 @@ function AgentScreenContent({
|
|||||||
? branchError ?? "Unavailable"
|
? branchError ?? "Unavailable"
|
||||||
: branchLabel ?? "Unknown";
|
: branchLabel ?? "Unknown";
|
||||||
|
|
||||||
|
// Checkout status for header subtitle
|
||||||
|
const checkoutStatusQuery = useCheckoutStatusQuery({
|
||||||
|
serverId,
|
||||||
|
agentId: resolvedAgentId ?? "",
|
||||||
|
});
|
||||||
|
const checkout = checkoutStatusQuery.status;
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!resolvedAgentId) {
|
if (!resolvedAgentId) {
|
||||||
setFocusedAgentId(null);
|
setFocusedAgentId(null);
|
||||||
@@ -490,6 +499,20 @@ function AgentScreenContent({
|
|||||||
|
|
||||||
const effectiveAgent = agent ?? placeholderAgent;
|
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(() => {
|
useEffect(() => {
|
||||||
if (!isPendingCreateForRoute || !pendingCreate) {
|
if (!isPendingCreateForRoute || !pendingCreate) {
|
||||||
return;
|
return;
|
||||||
@@ -596,6 +619,7 @@ function AgentScreenContent({
|
|||||||
{/* Header */}
|
{/* Header */}
|
||||||
<MenuHeader
|
<MenuHeader
|
||||||
title={effectiveAgent.title || "Agent"}
|
title={effectiveAgent.title || "Agent"}
|
||||||
|
subtitle={headerSubtitle}
|
||||||
rightContent={
|
rightContent={
|
||||||
<View style={styles.headerRightContent}>
|
<View style={styles.headerRightContent}>
|
||||||
<Pressable onPress={toggleFileExplorer} style={styles.menuButton}>
|
<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
@@ -28,7 +28,7 @@ echo " Metro: http://localhost:${METRO_PORT}"
|
|||||||
echo "══════════════════════════════════════════════════════"
|
echo "══════════════════════════════════════════════════════"
|
||||||
|
|
||||||
# Export for child processes (overrides .env values)
|
# 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}"
|
export PASEO_CORS_ORIGINS="${CORS_ORIGINS}"
|
||||||
|
|
||||||
# Run both with concurrently
|
# Run both with concurrently
|
||||||
|
|||||||
Reference in New Issue
Block a user