Update files
This commit is contained in:
@@ -171,7 +171,7 @@ export function AgentList({
|
|||||||
|
|
||||||
void queryClient.prefetchQuery({
|
void queryClient.prefetchQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: async () => await client.getCheckoutStatus(agent.id),
|
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }),
|
||||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -362,7 +362,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
const client = useSessionStore(
|
const client = useSessionStore(
|
||||||
(state) => state.sessions[serverId]?.client ?? null
|
(state) => state.sessions[serverId]?.client ?? null
|
||||||
);
|
);
|
||||||
const [diffMode, setDiffMode] = useState<"uncommitted" | "base">("uncommitted");
|
const [diffModeOverride, setDiffModeOverride] = useState<"uncommitted" | "base" | null>(null);
|
||||||
const [actionError, setActionError] = useState<string | null>(null);
|
const [actionError, setActionError] = useState<string | null>(null);
|
||||||
const [actionStatus, setActionStatus] = useState<string | null>(null);
|
const [actionStatus, setActionStatus] = useState<string | null>(null);
|
||||||
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
|
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
|
||||||
@@ -375,6 +375,12 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
status?.error?.message ??
|
status?.error?.message ??
|
||||||
(isStatusError && statusError instanceof Error ? statusError.message : null);
|
(isStatusError && statusError instanceof Error ? statusError.message : null);
|
||||||
const baseRef = gitStatus?.baseRef ?? undefined;
|
const baseRef = gitStatus?.baseRef ?? undefined;
|
||||||
|
|
||||||
|
// Auto-select diff mode based on state: uncommitted when dirty, base when clean
|
||||||
|
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
|
||||||
|
const autoDiffMode = hasUncommittedChanges ? "uncommitted" : "base";
|
||||||
|
const diffMode = diffModeOverride ?? autoDiffMode;
|
||||||
|
|
||||||
const {
|
const {
|
||||||
files,
|
files,
|
||||||
payloadError: diffPayloadError,
|
payloadError: diffPayloadError,
|
||||||
@@ -485,6 +491,11 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
}
|
}
|
||||||
}, [isDiffFetching, isStatusFetching, isManualRefresh]);
|
}, [isDiffFetching, isStatusFetching, isManualRefresh]);
|
||||||
|
|
||||||
|
// Clear diff mode override when auto mode changes (e.g., after commit)
|
||||||
|
useEffect(() => {
|
||||||
|
setDiffModeOverride(null);
|
||||||
|
}, [autoDiffMode]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!isPerfLoggingEnabled()) {
|
if (!isPerfLoggingEnabled()) {
|
||||||
return;
|
return;
|
||||||
@@ -697,7 +708,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
const branchLabel =
|
const branchLabel =
|
||||||
gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
|
gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
|
||||||
const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading;
|
const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading;
|
||||||
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
|
|
||||||
const aheadCount = gitStatus?.aheadBehind?.ahead ?? 0;
|
const aheadCount = gitStatus?.aheadBehind?.ahead ?? 0;
|
||||||
const canShowCommit = isGit && hasUncommittedChanges;
|
const canShowCommit = isGit && hasUncommittedChanges;
|
||||||
const canShowShip = isGit && aheadCount > 0;
|
const canShowShip = isGit && aheadCount > 0;
|
||||||
@@ -780,7 +790,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
const hasPullRequest = Boolean(prStatus?.url);
|
const hasPullRequest = Boolean(prStatus?.url);
|
||||||
const prActionLabel = hasPullRequest ? "Open PR" : "Create PR";
|
const prActionLabel = hasPullRequest ? "View PR" : "Create PR";
|
||||||
|
|
||||||
type ShipActionKey = "merge" | "pr";
|
type ShipActionKey = "merge" | "pr";
|
||||||
const shipActions: { key: ShipActionKey; label: string; disabled: boolean; isPending: boolean }[] =
|
const shipActions: { key: ShipActionKey; label: string; disabled: boolean; isPending: boolean }[] =
|
||||||
@@ -817,6 +827,10 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
return hasPullRequest ? false : prMutation.isPending;
|
return hasPullRequest ? false : prMutation.isPending;
|
||||||
}, [hasPullRequest, mergeMutation.isPending, prMutation.isPending, resolvedShipPrimary]);
|
}, [hasPullRequest, mergeMutation.isPending, prMutation.isPending, resolvedShipPrimary]);
|
||||||
|
|
||||||
|
// When there are uncommitted changes, Commit becomes the primary CTA
|
||||||
|
const showCommitAsPrimary = canShowCommit;
|
||||||
|
const showShipSplitButton = canShowShip && !showCommitAsPrimary;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<View style={styles.container}>
|
<View style={styles.container}>
|
||||||
<View style={styles.header} testID="changes-header">
|
<View style={styles.header} testID="changes-header">
|
||||||
@@ -831,7 +845,86 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
</View>
|
</View>
|
||||||
{isGit ? (
|
{isGit ? (
|
||||||
<View style={styles.headerRight}>
|
<View style={styles.headerRight}>
|
||||||
{canShowShip ? (
|
{showCommitAsPrimary ? (
|
||||||
|
<View style={styles.shipSplitButton}>
|
||||||
|
<Pressable
|
||||||
|
testID="changes-commit-primary"
|
||||||
|
style={[
|
||||||
|
styles.shipPrimaryButton,
|
||||||
|
commitDisabled && styles.shipPrimaryButtonDisabled,
|
||||||
|
]}
|
||||||
|
onPress={() => commitMutation.mutate()}
|
||||||
|
disabled={commitDisabled}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="Commit changes"
|
||||||
|
>
|
||||||
|
{commitMutation.isPending ? (
|
||||||
|
<ActivityIndicator size="small" color={theme.colors.foreground} />
|
||||||
|
) : (
|
||||||
|
<Text style={styles.shipPrimaryText}>Commit</Text>
|
||||||
|
)}
|
||||||
|
</Pressable>
|
||||||
|
<DropdownMenu>
|
||||||
|
<DropdownMenuTrigger
|
||||||
|
testID="changes-commit-caret"
|
||||||
|
style={styles.shipCaretButton}
|
||||||
|
accessibilityRole="button"
|
||||||
|
accessibilityLabel="More options"
|
||||||
|
>
|
||||||
|
<ChevronDown size={16} color={theme.colors.foregroundMuted} />
|
||||||
|
</DropdownMenuTrigger>
|
||||||
|
<DropdownMenuContent align="end" width={260} testID="changes-commit-menu">
|
||||||
|
{canShowShip ? (
|
||||||
|
<>
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="changes-commit-menu-merge"
|
||||||
|
disabled={mergeDisabled}
|
||||||
|
description={mergeDisabled && hasUncommittedChanges ? "Requires clean working tree" : undefined}
|
||||||
|
onSelect={() => {
|
||||||
|
void persistShipDefault("merge");
|
||||||
|
mergeMutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Merge branch
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID={hasPullRequest ? "changes-commit-menu-view-pr" : "changes-commit-menu-create-pr"}
|
||||||
|
disabled={prDisabled}
|
||||||
|
onSelect={() => {
|
||||||
|
void persistShipDefault("pr");
|
||||||
|
if (hasPullRequest && prStatus?.url) {
|
||||||
|
void Linking.openURL(prStatus.url);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
prMutation.mutate();
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{prActionLabel}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
</>
|
||||||
|
) : null}
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="changes-commit-menu-merge-from-base"
|
||||||
|
disabled={mergeFromBaseDisabled}
|
||||||
|
description={mergeFromBaseDisabled && hasUncommittedChanges ? "Requires clean working tree" : undefined}
|
||||||
|
onSelect={() => mergeFromBaseMutation.mutate()}
|
||||||
|
>
|
||||||
|
Merge from {baseRefLabel}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="changes-commit-menu-push"
|
||||||
|
disabled={pushDisabled}
|
||||||
|
description={pushDisabled && !(gitStatus?.hasRemote ?? false) ? "No remote configured" : undefined}
|
||||||
|
onSelect={() => pushMutation.mutate()}
|
||||||
|
>
|
||||||
|
Push to remote
|
||||||
|
</DropdownMenuItem>
|
||||||
|
</DropdownMenuContent>
|
||||||
|
</DropdownMenu>
|
||||||
|
</View>
|
||||||
|
) : showShipSplitButton ? (
|
||||||
<View style={styles.shipSplitButton}>
|
<View style={styles.shipSplitButton}>
|
||||||
<Pressable
|
<Pressable
|
||||||
testID="changes-ship-primary"
|
testID="changes-ship-primary"
|
||||||
@@ -881,7 +974,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID="changes-ship-merge"
|
testID="changes-ship-merge"
|
||||||
disabled={mergeDisabled}
|
disabled={mergeDisabled}
|
||||||
description={mergeDisabled && hasUncommittedChanges ? "Requires clean working tree" : undefined}
|
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
void persistShipDefault("merge");
|
void persistShipDefault("merge");
|
||||||
mergeMutation.mutate();
|
mergeMutation.mutate();
|
||||||
@@ -891,7 +983,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID={hasPullRequest ? "changes-ship-open-pr" : "changes-ship-create-pr"}
|
testID={hasPullRequest ? "changes-ship-view-pr" : "changes-ship-create-pr"}
|
||||||
disabled={prDisabled}
|
disabled={prDisabled}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
void persistShipDefault("pr");
|
void persistShipDefault("pr");
|
||||||
@@ -902,7 +994,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
prMutation.mutate();
|
prMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasPullRequest ? "Open PR" : "Create PR"}
|
{prActionLabel}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
</DropdownMenuContent>
|
</DropdownMenuContent>
|
||||||
</DropdownMenu>
|
</DropdownMenu>
|
||||||
@@ -923,7 +1015,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
<>
|
<>
|
||||||
{resolvedShipPrimary === "merge" ? (
|
{resolvedShipPrimary === "merge" ? (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID={hasPullRequest ? "changes-menu-open-pr" : "changes-menu-create-pr"}
|
testID={hasPullRequest ? "changes-menu-view-pr" : "changes-menu-create-pr"}
|
||||||
disabled={prDisabled}
|
disabled={prDisabled}
|
||||||
onSelect={() => {
|
onSelect={() => {
|
||||||
void persistShipDefault("pr");
|
void persistShipDefault("pr");
|
||||||
@@ -934,7 +1026,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
prMutation.mutate();
|
prMutation.mutate();
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{hasPullRequest ? "Open PR" : "Create PR"}
|
{prActionLabel}
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
) : (
|
) : (
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
@@ -970,6 +1062,13 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
Push to remote
|
Push to remote
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
<DropdownMenuItem
|
||||||
|
testID="changes-menu-toggle-view"
|
||||||
|
onSelect={() => setDiffModeOverride(diffMode === "uncommitted" ? "base" : "uncommitted")}
|
||||||
|
>
|
||||||
|
{diffMode === "uncommitted" ? `Show changes vs ${baseRefLabel}` : "Show uncommitted changes"}
|
||||||
|
</DropdownMenuItem>
|
||||||
|
<DropdownMenuSeparator />
|
||||||
<DropdownMenuItem
|
<DropdownMenuItem
|
||||||
testID="changes-menu-archive"
|
testID="changes-menu-archive"
|
||||||
destructive
|
destructive
|
||||||
@@ -985,81 +1084,15 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
</View>
|
</View>
|
||||||
|
|
||||||
{isGit ? (
|
{isGit ? (
|
||||||
<View style={styles.toolbarRow} testID="changes-toolbar">
|
<View style={styles.diffStatusRow} testID="changes-diff-status">
|
||||||
<View style={styles.toolbarLeft}>
|
<Text style={styles.diffStatusText}>
|
||||||
{canShowCommit ? (
|
{diffMode === "uncommitted" ? "Uncommitted changes" : `Changes vs ${baseRefLabel}`}
|
||||||
<Pressable
|
</Text>
|
||||||
testID="changes-action-commit"
|
|
||||||
style={[
|
|
||||||
styles.secondaryActionButton,
|
|
||||||
commitDisabled && styles.secondaryActionButtonDisabled,
|
|
||||||
]}
|
|
||||||
onPress={() => commitMutation.mutate()}
|
|
||||||
disabled={commitDisabled}
|
|
||||||
>
|
|
||||||
{commitMutation.isPending ? (
|
|
||||||
<ActivityIndicator size={12} color={theme.colors.foreground} style={styles.buttonSpinner} />
|
|
||||||
) : (
|
|
||||||
<Text style={styles.secondaryActionText}>Commit</Text>
|
|
||||||
)}
|
|
||||||
</Pressable>
|
|
||||||
) : (
|
|
||||||
<View style={styles.toolbarLeftSpacer} />
|
|
||||||
)}
|
|
||||||
</View>
|
|
||||||
<View style={styles.toolbarRight}>
|
|
||||||
<DropdownMenu>
|
|
||||||
<DropdownMenuTrigger
|
|
||||||
testID="changes-view-selector"
|
|
||||||
style={styles.viewSelector}
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Change diff view"
|
|
||||||
>
|
|
||||||
<Text style={styles.viewSelectorText}>
|
|
||||||
{diffMode === "uncommitted" ? "Working" : "Base"}
|
|
||||||
</Text>
|
|
||||||
<ChevronDown size={14} color={theme.colors.foregroundMuted} />
|
|
||||||
</DropdownMenuTrigger>
|
|
||||||
<DropdownMenuContent align="end" width={180} testID="changes-view-menu">
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID="changes-mode-uncommitted"
|
|
||||||
selected={diffMode === "uncommitted"}
|
|
||||||
onSelect={() => setDiffMode("uncommitted")}
|
|
||||||
>
|
|
||||||
Working
|
|
||||||
</DropdownMenuItem>
|
|
||||||
<DropdownMenuItem
|
|
||||||
testID="changes-mode-base"
|
|
||||||
selected={diffMode === "base"}
|
|
||||||
onSelect={() => setDiffMode("base")}
|
|
||||||
>
|
|
||||||
Base
|
|
||||||
</DropdownMenuItem>
|
|
||||||
</DropdownMenuContent>
|
|
||||||
</DropdownMenu>
|
|
||||||
</View>
|
|
||||||
</View>
|
</View>
|
||||||
) : null}
|
) : null}
|
||||||
|
|
||||||
{actionStatus ? <Text style={styles.actionStatusText}>{actionStatus}</Text> : null}
|
{actionStatus ? <Text style={styles.actionStatusText}>{actionStatus}</Text> : null}
|
||||||
{actionError ? <Text style={styles.actionErrorText}>{actionError}</Text> : null}
|
{actionError ? <Text style={styles.actionErrorText}>{actionError}</Text> : null}
|
||||||
{prStatus ? (
|
|
||||||
<Pressable
|
|
||||||
style={styles.prStatusRow}
|
|
||||||
testID="changes-pr-status"
|
|
||||||
accessibilityRole="button"
|
|
||||||
accessibilityLabel="Open pull request"
|
|
||||||
onPress={() => {
|
|
||||||
if (!prStatus.url) return;
|
|
||||||
void Linking.openURL(prStatus.url);
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Text style={styles.prStatusLabel}>PR</Text>
|
|
||||||
<Text style={styles.prStatusValue}>
|
|
||||||
{prStatus.state} {prStatus.url ? `· ${prStatus.url}` : ""}
|
|
||||||
</Text>
|
|
||||||
</Pressable>
|
|
||||||
) : null}
|
|
||||||
{prErrorMessage ? (
|
{prErrorMessage ? (
|
||||||
<Text style={styles.actionErrorText}>{prErrorMessage}</Text>
|
<Text style={styles.actionErrorText}>{prErrorMessage}</Text>
|
||||||
) : null}
|
) : null}
|
||||||
@@ -1103,64 +1136,15 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontWeight: theme.fontWeight.medium,
|
fontWeight: theme.fontWeight.medium,
|
||||||
flexShrink: 1,
|
flexShrink: 1,
|
||||||
},
|
},
|
||||||
viewSelector: {
|
diffStatusRow: {
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
paddingHorizontal: theme.spacing[2],
|
|
||||||
paddingVertical: theme.spacing[1],
|
|
||||||
borderRadius: theme.borderRadius.lg,
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.border,
|
|
||||||
},
|
|
||||||
viewSelectorText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
fontWeight: theme.fontWeight.medium,
|
|
||||||
},
|
|
||||||
toolbarRow: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
paddingHorizontal: theme.spacing[3],
|
||||||
paddingTop: theme.spacing[2],
|
paddingVertical: theme.spacing[2],
|
||||||
paddingBottom: theme.spacing[3],
|
|
||||||
borderBottomWidth: 1,
|
borderBottomWidth: 1,
|
||||||
borderBottomColor: theme.colors.border,
|
borderBottomColor: theme.colors.border,
|
||||||
},
|
},
|
||||||
toolbarLeft: {
|
diffStatusText: {
|
||||||
flex: 1,
|
|
||||||
minWidth: 0,
|
|
||||||
},
|
|
||||||
toolbarLeftSpacer: {
|
|
||||||
height: 36,
|
|
||||||
},
|
|
||||||
toolbarRight: {
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[1],
|
|
||||||
flexShrink: 0,
|
|
||||||
},
|
|
||||||
secondaryActionButton: {
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingVertical: theme.spacing[2],
|
|
||||||
borderRadius: theme.borderRadius.md,
|
|
||||||
backgroundColor: theme.colors.surface2,
|
|
||||||
borderWidth: theme.borderWidth[1],
|
|
||||||
borderColor: theme.colors.borderAccent,
|
|
||||||
alignSelf: "flex-start",
|
|
||||||
},
|
|
||||||
secondaryActionButtonDisabled: {
|
|
||||||
opacity: 0.5,
|
|
||||||
},
|
|
||||||
secondaryActionText: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
color: theme.colors.foreground,
|
color: theme.colors.foregroundMuted,
|
||||||
fontWeight: theme.fontWeight.medium,
|
|
||||||
},
|
|
||||||
buttonSpinner: {
|
|
||||||
height: theme.fontSize.xs * 1.4,
|
|
||||||
},
|
},
|
||||||
shipSplitButton: {
|
shipSplitButton: {
|
||||||
flexDirection: "row",
|
flexDirection: "row",
|
||||||
@@ -1266,24 +1250,6 @@ const styles = StyleSheet.create((theme) => ({
|
|||||||
fontSize: theme.fontSize.xs,
|
fontSize: theme.fontSize.xs,
|
||||||
color: theme.colors.destructive,
|
color: theme.colors.destructive,
|
||||||
},
|
},
|
||||||
prStatusRow: {
|
|
||||||
paddingHorizontal: theme.spacing[3],
|
|
||||||
paddingBottom: theme.spacing[2],
|
|
||||||
flexDirection: "row",
|
|
||||||
alignItems: "center",
|
|
||||||
gap: theme.spacing[2],
|
|
||||||
},
|
|
||||||
prStatusLabel: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foregroundMuted,
|
|
||||||
textTransform: "uppercase",
|
|
||||||
letterSpacing: 0.6,
|
|
||||||
},
|
|
||||||
prStatusValue: {
|
|
||||||
fontSize: theme.fontSize.xs,
|
|
||||||
color: theme.colors.foreground,
|
|
||||||
flexShrink: 1,
|
|
||||||
},
|
|
||||||
diffContainer: {
|
diffContainer: {
|
||||||
flex: 1,
|
flex: 1,
|
||||||
minHeight: 0,
|
minHeight: 0,
|
||||||
|
|||||||
@@ -299,7 +299,7 @@ export function GroupedAgentList({
|
|||||||
|
|
||||||
void queryClient.prefetchQuery({
|
void queryClient.prefetchQuery({
|
||||||
queryKey,
|
queryKey,
|
||||||
queryFn: async () => await client.getCheckoutStatus(agent.id),
|
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }),
|
||||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -194,8 +194,6 @@ type FileDownloadTokenPayload = Extract<
|
|||||||
{ type: "file_download_token_response" }
|
{ type: "file_download_token_response" }
|
||||||
>["payload"];
|
>["payload"];
|
||||||
|
|
||||||
const SESSION_SNAPSHOT_STORAGE_PREFIX = "@paseo:session-snapshot:";
|
|
||||||
|
|
||||||
// Module-level map for agent initialization promises
|
// Module-level map for agent initialization promises
|
||||||
// Key: `${serverId}:${agentId}`, Value: { promise, resolve, reject }
|
// Key: `${serverId}:${agentId}`, Value: { promise, resolve, reject }
|
||||||
// This survives Fast Refresh because it's outside React component tree
|
// This survives Fast Refresh because it's outside React component tree
|
||||||
@@ -210,60 +208,6 @@ function getInitKey(serverId: string, agentId: string): string {
|
|||||||
return `${serverId}:${agentId}`;
|
return `${serverId}:${agentId}`;
|
||||||
}
|
}
|
||||||
|
|
||||||
type PersistedSessionSnapshot = {
|
|
||||||
agents: AgentSnapshotPayload[];
|
|
||||||
savedAt: string;
|
|
||||||
};
|
|
||||||
|
|
||||||
const getSessionSnapshotStorageKey = (serverId: string): string => {
|
|
||||||
return `${SESSION_SNAPSHOT_STORAGE_PREFIX}${serverId}`;
|
|
||||||
};
|
|
||||||
|
|
||||||
async function loadPersistedSessionSnapshot(
|
|
||||||
serverId: string
|
|
||||||
): Promise<PersistedSessionSnapshot | null> {
|
|
||||||
try {
|
|
||||||
const raw = await AsyncStorage.getItem(
|
|
||||||
getSessionSnapshotStorageKey(serverId)
|
|
||||||
);
|
|
||||||
if (!raw) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
const parsed = JSON.parse(raw) as PersistedSessionSnapshot;
|
|
||||||
if (!Array.isArray(parsed?.agents)) {
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return parsed;
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[Session] Failed to load persisted snapshot for ${serverId}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function persistSessionSnapshot(
|
|
||||||
serverId: string,
|
|
||||||
snapshot: { agents: AgentSnapshotPayload[] }
|
|
||||||
) {
|
|
||||||
try {
|
|
||||||
const payload: PersistedSessionSnapshot = {
|
|
||||||
agents: snapshot.agents,
|
|
||||||
savedAt: new Date().toISOString(),
|
|
||||||
};
|
|
||||||
await AsyncStorage.setItem(
|
|
||||||
getSessionSnapshotStorageKey(serverId),
|
|
||||||
JSON.stringify(payload)
|
|
||||||
);
|
|
||||||
} catch (error) {
|
|
||||||
console.error(
|
|
||||||
`[Session] Failed to persist snapshot for ${serverId}`,
|
|
||||||
error
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function normalizeAgentSnapshot(
|
function normalizeAgentSnapshot(
|
||||||
snapshot: AgentSnapshotPayload,
|
snapshot: AgentSnapshotPayload,
|
||||||
serverId: string
|
serverId: string
|
||||||
@@ -462,7 +406,6 @@ export function SessionProvider({
|
|||||||
) => Promise<void>)
|
) => Promise<void>)
|
||||||
| null
|
| null
|
||||||
>(null);
|
>(null);
|
||||||
const hasHydratedSnapshotRef = useRef(false);
|
|
||||||
const hasRequestedInitialSnapshotRef = useRef(false);
|
const hasRequestedInitialSnapshotRef = useRef(false);
|
||||||
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||||
null
|
null
|
||||||
@@ -621,61 +564,6 @@ export function SessionProvider({
|
|||||||
};
|
};
|
||||||
}, [serverId, updateConnectionStatus]);
|
}, [serverId, updateConnectionStatus]);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
hasHydratedSnapshotRef.current = false;
|
|
||||||
setHasHydratedAgents(serverId, false);
|
|
||||||
}, [serverId, setHasHydratedAgents]);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
let isMounted = true;
|
|
||||||
|
|
||||||
const hydrateFromSnapshot = async () => {
|
|
||||||
if (hasHydratedSnapshotRef.current) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
hasHydratedSnapshotRef.current = true;
|
|
||||||
const snapshot = await loadPersistedSessionSnapshot(serverId);
|
|
||||||
if (!snapshot || !isMounted) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const agents = new Map();
|
|
||||||
const pendingPermissions = new Map();
|
|
||||||
const agentLastActivity = new Map();
|
|
||||||
|
|
||||||
for (const agentSnapshot of snapshot.agents) {
|
|
||||||
const agent = normalizeAgentSnapshot(agentSnapshot, serverId);
|
|
||||||
agents.set(agent.id, agent);
|
|
||||||
agentLastActivity.set(agent.id, agent.lastActivityAt);
|
|
||||||
for (const request of agent.pendingPermissions) {
|
|
||||||
const key = derivePendingPermissionKey(agent.id, request);
|
|
||||||
pendingPermissions.set(key, { key, agentId: agent.id, request });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
setAgents(serverId, (prev) => {
|
|
||||||
if (prev.size > 0) {
|
|
||||||
return prev;
|
|
||||||
}
|
|
||||||
return agents;
|
|
||||||
});
|
|
||||||
|
|
||||||
// Initialize agentLastActivity slice (top-level)
|
|
||||||
for (const [agentId, timestamp] of agentLastActivity.entries()) {
|
|
||||||
setAgentLastActivity(agentId, timestamp);
|
|
||||||
}
|
|
||||||
|
|
||||||
setPendingPermissions(serverId, pendingPermissions);
|
|
||||||
setHasHydratedAgents(serverId, true);
|
|
||||||
};
|
|
||||||
|
|
||||||
void hydrateFromSnapshot();
|
|
||||||
|
|
||||||
return () => {
|
|
||||||
isMounted = false;
|
|
||||||
};
|
|
||||||
}, [serverId, setAgents, setPendingPermissions, setHasHydratedAgents]);
|
|
||||||
|
|
||||||
const updateExplorerState = useCallback(
|
const updateExplorerState = useCallback(
|
||||||
(agentId: string, updater: (state: any) => any) => {
|
(agentId: string, updater: (state: any) => any) => {
|
||||||
setFileExplorer(serverId, (prev) => {
|
setFileExplorer(serverId, (prev) => {
|
||||||
@@ -953,7 +841,6 @@ export function SessionProvider({
|
|||||||
|
|
||||||
return changed ? next : prev;
|
return changed ? next : prev;
|
||||||
});
|
});
|
||||||
void persistSessionSnapshot(serverId, { agents: agentsList });
|
|
||||||
setHasHydratedAgents(serverId, true);
|
setHasHydratedAgents(serverId, true);
|
||||||
updateConnectionStatus(serverId, {
|
updateConnectionStatus(serverId, {
|
||||||
status: "online",
|
status: "online",
|
||||||
|
|||||||
@@ -24,10 +24,11 @@ interface UseCheckoutStatusQueryOptions {
|
|||||||
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
||||||
|
|
||||||
function fetchCheckoutStatus(
|
function fetchCheckoutStatus(
|
||||||
client: { getCheckoutStatus: (agentId: string) => Promise<CheckoutStatusPayload> },
|
client: { getCheckoutStatus: (agentId: string, options?: { cwd?: string }) => Promise<CheckoutStatusPayload> },
|
||||||
agentId: string
|
agentId: string,
|
||||||
|
cwd: string
|
||||||
): Promise<CheckoutStatusPayload> {
|
): Promise<CheckoutStatusPayload> {
|
||||||
return client.getCheckoutStatus(agentId);
|
return client.getCheckoutStatus(agentId, { cwd });
|
||||||
}
|
}
|
||||||
|
|
||||||
export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutStatusQueryOptions) {
|
export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutStatusQueryOptions) {
|
||||||
@@ -50,7 +51,7 @@ export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutSt
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon client not available");
|
throw new Error("Daemon client not available");
|
||||||
}
|
}
|
||||||
return await fetchCheckoutStatus(client, agentId);
|
return await fetchCheckoutStatus(client, agentId, cwd);
|
||||||
},
|
},
|
||||||
enabled: !!client && isConnected && !!agentId && !!cwd,
|
enabled: !!client && isConnected && !!agentId && !!cwd,
|
||||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
@@ -98,7 +99,7 @@ export function useCheckoutStatusCacheOnly({ serverId, agentId, cwd }: UseChecko
|
|||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon client not available");
|
throw new Error("Daemon client not available");
|
||||||
}
|
}
|
||||||
return await fetchCheckoutStatus(client, agentId);
|
return await fetchCheckoutStatus(client, agentId, cwd);
|
||||||
},
|
},
|
||||||
enabled: false,
|
enabled: false,
|
||||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
|||||||
@@ -1131,8 +1131,11 @@ export class DaemonClientV2 {
|
|||||||
|
|
||||||
async getCheckoutStatus(
|
async getCheckoutStatus(
|
||||||
agentId: string,
|
agentId: string,
|
||||||
requestId?: string
|
options?: { cwd?: string; requestId?: string }
|
||||||
): Promise<CheckoutStatusPayload> {
|
): Promise<CheckoutStatusPayload> {
|
||||||
|
const requestId = options?.requestId;
|
||||||
|
const cwd = options?.cwd;
|
||||||
|
|
||||||
if (!requestId) {
|
if (!requestId) {
|
||||||
const existing = this.checkoutStatusInFlight.get(agentId);
|
const existing = this.checkoutStatusInFlight.get(agentId);
|
||||||
if (existing) {
|
if (existing) {
|
||||||
@@ -1144,6 +1147,7 @@ export class DaemonClientV2 {
|
|||||||
const message = SessionInboundMessageSchema.parse({
|
const message = SessionInboundMessageSchema.parse({
|
||||||
type: "checkout_status_request",
|
type: "checkout_status_request",
|
||||||
agentId,
|
agentId,
|
||||||
|
cwd,
|
||||||
requestId: resolvedRequestId,
|
requestId: resolvedRequestId,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2855,7 +2855,9 @@ export class Session {
|
|||||||
): Promise<void> {
|
): Promise<void> {
|
||||||
const { agentId, requestId } = msg;
|
const { agentId, requestId } = msg;
|
||||||
const agent = this.agentManager.getAgent(agentId);
|
const agent = this.agentManager.getAgent(agentId);
|
||||||
if (!agent) {
|
// Use cwd from agent if found, otherwise fall back to cwd from message
|
||||||
|
const cwd = agent?.cwd ?? msg.cwd;
|
||||||
|
if (!cwd) {
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
@@ -2870,7 +2872,7 @@ export class Session {
|
|||||||
hasRemote: false,
|
hasRemote: false,
|
||||||
remoteUrl: null,
|
remoteUrl: null,
|
||||||
isPaseoOwnedWorktree: false,
|
isPaseoOwnedWorktree: false,
|
||||||
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
|
error: { code: "UNKNOWN", message: `Agent not found and no cwd provided: ${agentId}` },
|
||||||
requestId,
|
requestId,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
@@ -2878,13 +2880,13 @@ export class Session {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const status = await getCheckoutStatus(agent.cwd, { paseoHome: this.paseoHome });
|
const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
|
||||||
if (!status.isGit) {
|
if (!status.isGit) {
|
||||||
this.emit({
|
this.emit({
|
||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
cwd: agent.cwd,
|
cwd,
|
||||||
isGit: false,
|
isGit: false,
|
||||||
repoRoot: null,
|
repoRoot: null,
|
||||||
currentBranch: null,
|
currentBranch: null,
|
||||||
@@ -2906,7 +2908,7 @@ export class Session {
|
|||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
cwd: agent.cwd,
|
cwd,
|
||||||
isGit: true,
|
isGit: true,
|
||||||
repoRoot: status.repoRoot ?? null,
|
repoRoot: status.repoRoot ?? null,
|
||||||
currentBranch: status.currentBranch ?? null,
|
currentBranch: status.currentBranch ?? null,
|
||||||
@@ -2927,7 +2929,7 @@ export class Session {
|
|||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
cwd: agent.cwd,
|
cwd,
|
||||||
isGit: true,
|
isGit: true,
|
||||||
repoRoot: status.repoRoot ?? null,
|
repoRoot: status.repoRoot ?? null,
|
||||||
currentBranch: status.currentBranch ?? null,
|
currentBranch: status.currentBranch ?? null,
|
||||||
@@ -2946,7 +2948,7 @@ export class Session {
|
|||||||
type: "checkout_status_response",
|
type: "checkout_status_response",
|
||||||
payload: {
|
payload: {
|
||||||
agentId,
|
agentId,
|
||||||
cwd: agent.cwd,
|
cwd,
|
||||||
isGit: false,
|
isGit: false,
|
||||||
repoRoot: null,
|
repoRoot: null,
|
||||||
currentBranch: null,
|
currentBranch: null,
|
||||||
|
|||||||
@@ -477,6 +477,8 @@ const CheckoutDiffCompareSchema = z.object({
|
|||||||
export const CheckoutStatusRequestSchema = z.object({
|
export const CheckoutStatusRequestSchema = z.object({
|
||||||
type: z.literal("checkout_status_request"),
|
type: z.literal("checkout_status_request"),
|
||||||
agentId: z.string(),
|
agentId: z.string(),
|
||||||
|
/** Optional cwd to use if the agent is not live in memory (e.g. from persisted agents.json) */
|
||||||
|
cwd: z.string().optional(),
|
||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
|||||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from "fs";
|
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from "fs";
|
||||||
import { join } from "path";
|
import { join } from "path";
|
||||||
import { tmpdir } from "os";
|
import { tmpdir } from "os";
|
||||||
import { findProjectIcon, getProjectIcon, ICON_PATTERNS, PRIORITY_DIRS, IGNORED_DIRS } from "./project-icon.js";
|
import { findProjectIcon, getProjectIcon, ICON_PATTERNS, PRIORITY_DIRS, IGNORED_DIRS, MONOREPO_PACKAGE_DIRS } from "./project-icon.js";
|
||||||
|
|
||||||
function createTempDir(): string {
|
function createTempDir(): string {
|
||||||
return realpathSync(mkdtempSync(join(tmpdir(), "project-icon-test-")));
|
return realpathSync(mkdtempSync(join(tmpdir(), "project-icon-test-")));
|
||||||
@@ -55,6 +55,13 @@ describe("findProjectIcon", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("MONOREPO_PACKAGE_DIRS", () => {
|
||||||
|
it("includes common monorepo package directories", () => {
|
||||||
|
expect(MONOREPO_PACKAGE_DIRS).toContain("packages");
|
||||||
|
expect(MONOREPO_PACKAGE_DIRS).toContain("apps");
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it("returns null when no icon is found", async () => {
|
it("returns null when no icon is found", async () => {
|
||||||
const result = await findProjectIcon(tempDir);
|
const result = await findProjectIcon(tempDir);
|
||||||
expect(result).toBeNull();
|
expect(result).toBeNull();
|
||||||
@@ -175,6 +182,51 @@ describe("findProjectIcon", () => {
|
|||||||
// Should return the first one based on pattern order (favicon.ico comes first)
|
// Should return the first one based on pattern order (favicon.ico comes first)
|
||||||
expect(result).toBe(join(tempDir, "favicon.ico"));
|
expect(result).toBe(join(tempDir, "favicon.ico"));
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("monorepo package directories", () => {
|
||||||
|
it("finds icon in packages/*/public directory", async () => {
|
||||||
|
mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true });
|
||||||
|
writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "icon");
|
||||||
|
|
||||||
|
const result = await findProjectIcon(tempDir);
|
||||||
|
expect(result).toBe(join(tempDir, "packages", "app", "public", "favicon.ico"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds icon in apps/*/public directory", async () => {
|
||||||
|
mkdirSync(join(tempDir, "apps", "web", "public"), { recursive: true });
|
||||||
|
writeFileSync(join(tempDir, "apps", "web", "public", "favicon.png"), "icon");
|
||||||
|
|
||||||
|
const result = await findProjectIcon(tempDir);
|
||||||
|
expect(result).toBe(join(tempDir, "apps", "web", "public", "favicon.png"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("finds icon in packages/* root", async () => {
|
||||||
|
mkdirSync(join(tempDir, "packages", "ui"), { recursive: true });
|
||||||
|
writeFileSync(join(tempDir, "packages", "ui", "logo.svg"), "icon");
|
||||||
|
|
||||||
|
const result = await findProjectIcon(tempDir);
|
||||||
|
expect(result).toBe(join(tempDir, "packages", "ui", "logo.svg"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prioritizes root priority dirs over monorepo dirs", async () => {
|
||||||
|
mkdirSync(join(tempDir, "public"), { recursive: true });
|
||||||
|
mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true });
|
||||||
|
writeFileSync(join(tempDir, "public", "favicon.ico"), "root icon");
|
||||||
|
writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "package icon");
|
||||||
|
|
||||||
|
const result = await findProjectIcon(tempDir);
|
||||||
|
expect(result).toBe(join(tempDir, "public", "favicon.ico"));
|
||||||
|
});
|
||||||
|
|
||||||
|
it("prioritizes monorepo dirs over root dir (non-priority)", async () => {
|
||||||
|
mkdirSync(join(tempDir, "packages", "app", "public"), { recursive: true });
|
||||||
|
writeFileSync(join(tempDir, "logo.png"), "root icon");
|
||||||
|
writeFileSync(join(tempDir, "packages", "app", "public", "favicon.ico"), "package icon");
|
||||||
|
|
||||||
|
const result = await findProjectIcon(tempDir);
|
||||||
|
expect(result).toBe(join(tempDir, "packages", "app", "public", "favicon.ico"));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("getProjectIcon", () => {
|
describe("getProjectIcon", () => {
|
||||||
|
|||||||
@@ -9,6 +9,9 @@ export const ICON_PATTERNS = [
|
|||||||
"favicon.ico",
|
"favicon.ico",
|
||||||
"favicon.png",
|
"favicon.png",
|
||||||
"favicon.svg",
|
"favicon.svg",
|
||||||
|
"favico.ico",
|
||||||
|
"favico.png",
|
||||||
|
"favico.svg",
|
||||||
"icon.png",
|
"icon.png",
|
||||||
"icon.svg",
|
"icon.svg",
|
||||||
"app-icon.png",
|
"app-icon.png",
|
||||||
@@ -24,6 +27,11 @@ export const ICON_PATTERNS = [
|
|||||||
*/
|
*/
|
||||||
export const PRIORITY_DIRS = ["public", "static", "assets", "images", "img"];
|
export const PRIORITY_DIRS = ["public", "static", "assets", "images", "img"];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Monorepo package directory patterns to scan (e.g., packages/app, apps/web).
|
||||||
|
*/
|
||||||
|
export const MONOREPO_PACKAGE_DIRS = ["packages", "apps"];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Directories to ignore during search.
|
* Directories to ignore during search.
|
||||||
*/
|
*/
|
||||||
@@ -306,6 +314,54 @@ export async function findProjectIcon(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Then search monorepo package directories (packages/*, apps/*)
|
||||||
|
for (const monoDir of MONOREPO_PACKAGE_DIRS) {
|
||||||
|
const monoPath = join(projectDir, monoDir);
|
||||||
|
let packageEntries: string[];
|
||||||
|
try {
|
||||||
|
packageEntries = await readdir(monoPath);
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const packageName of packageEntries) {
|
||||||
|
const packagePath = join(monoPath, packageName);
|
||||||
|
try {
|
||||||
|
const packageStats = await stat(packagePath);
|
||||||
|
if (!packageStats.isDirectory()) continue;
|
||||||
|
} catch {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search priority dirs within the package
|
||||||
|
for (const priorityDir of PRIORITY_DIRS) {
|
||||||
|
const priorityPath = join(packagePath, priorityDir);
|
||||||
|
try {
|
||||||
|
const priorityStats = await stat(priorityPath);
|
||||||
|
if (priorityStats.isDirectory()) {
|
||||||
|
const result = await searchDirRecursively(
|
||||||
|
priorityPath,
|
||||||
|
ICON_PATTERNS,
|
||||||
|
ignoredDirsSet,
|
||||||
|
maxDepth - 1
|
||||||
|
);
|
||||||
|
if (result) {
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Directory doesn't exist, continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Search package root
|
||||||
|
const found = await findIconInDir(packagePath, ICON_PATTERNS);
|
||||||
|
if (found) {
|
||||||
|
return found;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Then search root and any other non-priority directories
|
// Then search root and any other non-priority directories
|
||||||
const found = await findDirRecursively(projectDir);
|
const found = await findDirRecursively(projectDir);
|
||||||
if (found) {
|
if (found) {
|
||||||
|
|||||||
Reference in New Issue
Block a user