mirror of
https://github.com/getpaseo/paseo.git
synced 2026-07-29 12:01:31 +00:00
Update files
This commit is contained in:
@@ -171,7 +171,7 @@ export function AgentList({
|
||||
|
||||
void queryClient.prefetchQuery({
|
||||
queryKey,
|
||||
queryFn: async () => await client.getCheckoutStatus(agent.id),
|
||||
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }),
|
||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -362,7 +362,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
const client = useSessionStore(
|
||||
(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 [actionStatus, setActionStatus] = useState<string | null>(null);
|
||||
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
|
||||
@@ -375,6 +375,12 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
status?.error?.message ??
|
||||
(isStatusError && statusError instanceof Error ? statusError.message : null);
|
||||
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 {
|
||||
files,
|
||||
payloadError: diffPayloadError,
|
||||
@@ -485,6 +491,11 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
}
|
||||
}, [isDiffFetching, isStatusFetching, isManualRefresh]);
|
||||
|
||||
// Clear diff mode override when auto mode changes (e.g., after commit)
|
||||
useEffect(() => {
|
||||
setDiffModeOverride(null);
|
||||
}, [autoDiffMode]);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isPerfLoggingEnabled()) {
|
||||
return;
|
||||
@@ -697,7 +708,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
const branchLabel =
|
||||
gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
|
||||
const actionsDisabled = !isGit || Boolean(status?.error) || isStatusLoading;
|
||||
const hasUncommittedChanges = Boolean(gitStatus?.isDirty);
|
||||
const aheadCount = gitStatus?.aheadBehind?.ahead ?? 0;
|
||||
const canShowCommit = isGit && hasUncommittedChanges;
|
||||
const canShowShip = isGit && aheadCount > 0;
|
||||
@@ -780,7 +790,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
}
|
||||
|
||||
const hasPullRequest = Boolean(prStatus?.url);
|
||||
const prActionLabel = hasPullRequest ? "Open PR" : "Create PR";
|
||||
const prActionLabel = hasPullRequest ? "View PR" : "Create PR";
|
||||
|
||||
type ShipActionKey = "merge" | "pr";
|
||||
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;
|
||||
}, [hasPullRequest, mergeMutation.isPending, prMutation.isPending, resolvedShipPrimary]);
|
||||
|
||||
// When there are uncommitted changes, Commit becomes the primary CTA
|
||||
const showCommitAsPrimary = canShowCommit;
|
||||
const showShipSplitButton = canShowShip && !showCommitAsPrimary;
|
||||
|
||||
return (
|
||||
<View style={styles.container}>
|
||||
<View style={styles.header} testID="changes-header">
|
||||
@@ -831,7 +845,86 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
</View>
|
||||
{isGit ? (
|
||||
<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}>
|
||||
<Pressable
|
||||
testID="changes-ship-primary"
|
||||
@@ -881,7 +974,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
<DropdownMenuItem
|
||||
testID="changes-ship-merge"
|
||||
disabled={mergeDisabled}
|
||||
description={mergeDisabled && hasUncommittedChanges ? "Requires clean working tree" : undefined}
|
||||
onSelect={() => {
|
||||
void persistShipDefault("merge");
|
||||
mergeMutation.mutate();
|
||||
@@ -891,7 +983,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuSeparator />
|
||||
<DropdownMenuItem
|
||||
testID={hasPullRequest ? "changes-ship-open-pr" : "changes-ship-create-pr"}
|
||||
testID={hasPullRequest ? "changes-ship-view-pr" : "changes-ship-create-pr"}
|
||||
disabled={prDisabled}
|
||||
onSelect={() => {
|
||||
void persistShipDefault("pr");
|
||||
@@ -902,7 +994,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
prMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{hasPullRequest ? "Open PR" : "Create PR"}
|
||||
{prActionLabel}
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
@@ -923,7 +1015,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
<>
|
||||
{resolvedShipPrimary === "merge" ? (
|
||||
<DropdownMenuItem
|
||||
testID={hasPullRequest ? "changes-menu-open-pr" : "changes-menu-create-pr"}
|
||||
testID={hasPullRequest ? "changes-menu-view-pr" : "changes-menu-create-pr"}
|
||||
disabled={prDisabled}
|
||||
onSelect={() => {
|
||||
void persistShipDefault("pr");
|
||||
@@ -934,7 +1026,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
prMutation.mutate();
|
||||
}}
|
||||
>
|
||||
{hasPullRequest ? "Open PR" : "Create PR"}
|
||||
{prActionLabel}
|
||||
</DropdownMenuItem>
|
||||
) : (
|
||||
<DropdownMenuItem
|
||||
@@ -970,6 +1062,13 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
Push to remote
|
||||
</DropdownMenuItem>
|
||||
<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
|
||||
testID="changes-menu-archive"
|
||||
destructive
|
||||
@@ -985,81 +1084,15 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
||||
</View>
|
||||
|
||||
{isGit ? (
|
||||
<View style={styles.toolbarRow} testID="changes-toolbar">
|
||||
<View style={styles.toolbarLeft}>
|
||||
{canShowCommit ? (
|
||||
<Pressable
|
||||
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 style={styles.diffStatusRow} testID="changes-diff-status">
|
||||
<Text style={styles.diffStatusText}>
|
||||
{diffMode === "uncommitted" ? "Uncommitted changes" : `Changes vs ${baseRefLabel}`}
|
||||
</Text>
|
||||
</View>
|
||||
) : null}
|
||||
|
||||
{actionStatus ? <Text style={styles.actionStatusText}>{actionStatus}</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 ? (
|
||||
<Text style={styles.actionErrorText}>{prErrorMessage}</Text>
|
||||
) : null}
|
||||
@@ -1103,64 +1136,15 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
flexShrink: 1,
|
||||
},
|
||||
viewSelector: {
|
||||
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],
|
||||
diffStatusRow: {
|
||||
paddingHorizontal: theme.spacing[3],
|
||||
paddingTop: theme.spacing[2],
|
||||
paddingBottom: theme.spacing[3],
|
||||
paddingVertical: theme.spacing[2],
|
||||
borderBottomWidth: 1,
|
||||
borderBottomColor: theme.colors.border,
|
||||
},
|
||||
toolbarLeft: {
|
||||
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: {
|
||||
diffStatusText: {
|
||||
fontSize: theme.fontSize.xs,
|
||||
color: theme.colors.foreground,
|
||||
fontWeight: theme.fontWeight.medium,
|
||||
},
|
||||
buttonSpinner: {
|
||||
height: theme.fontSize.xs * 1.4,
|
||||
color: theme.colors.foregroundMuted,
|
||||
},
|
||||
shipSplitButton: {
|
||||
flexDirection: "row",
|
||||
@@ -1266,24 +1250,6 @@ const styles = StyleSheet.create((theme) => ({
|
||||
fontSize: theme.fontSize.xs,
|
||||
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: {
|
||||
flex: 1,
|
||||
minHeight: 0,
|
||||
|
||||
@@ -299,7 +299,7 @@ export function GroupedAgentList({
|
||||
|
||||
void queryClient.prefetchQuery({
|
||||
queryKey,
|
||||
queryFn: async () => await client.getCheckoutStatus(agent.id),
|
||||
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }),
|
||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -194,8 +194,6 @@ type FileDownloadTokenPayload = Extract<
|
||||
{ type: "file_download_token_response" }
|
||||
>["payload"];
|
||||
|
||||
const SESSION_SNAPSHOT_STORAGE_PREFIX = "@paseo:session-snapshot:";
|
||||
|
||||
// Module-level map for agent initialization promises
|
||||
// Key: `${serverId}:${agentId}`, Value: { promise, resolve, reject }
|
||||
// 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}`;
|
||||
}
|
||||
|
||||
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(
|
||||
snapshot: AgentSnapshotPayload,
|
||||
serverId: string
|
||||
@@ -462,7 +406,6 @@ export function SessionProvider({
|
||||
) => Promise<void>)
|
||||
| null
|
||||
>(null);
|
||||
const hasHydratedSnapshotRef = useRef(false);
|
||||
const hasRequestedInitialSnapshotRef = useRef(false);
|
||||
const sessionStateTimeoutRef = useRef<ReturnType<typeof setTimeout> | null>(
|
||||
null
|
||||
@@ -621,61 +564,6 @@ export function SessionProvider({
|
||||
};
|
||||
}, [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(
|
||||
(agentId: string, updater: (state: any) => any) => {
|
||||
setFileExplorer(serverId, (prev) => {
|
||||
@@ -953,7 +841,6 @@ export function SessionProvider({
|
||||
|
||||
return changed ? next : prev;
|
||||
});
|
||||
void persistSessionSnapshot(serverId, { agents: agentsList });
|
||||
setHasHydratedAgents(serverId, true);
|
||||
updateConnectionStatus(serverId, {
|
||||
status: "online",
|
||||
|
||||
@@ -24,10 +24,11 @@ interface UseCheckoutStatusQueryOptions {
|
||||
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
|
||||
|
||||
function fetchCheckoutStatus(
|
||||
client: { getCheckoutStatus: (agentId: string) => Promise<CheckoutStatusPayload> },
|
||||
agentId: string
|
||||
client: { getCheckoutStatus: (agentId: string, options?: { cwd?: string }) => Promise<CheckoutStatusPayload> },
|
||||
agentId: string,
|
||||
cwd: string
|
||||
): Promise<CheckoutStatusPayload> {
|
||||
return client.getCheckoutStatus(agentId);
|
||||
return client.getCheckoutStatus(agentId, { cwd });
|
||||
}
|
||||
|
||||
export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutStatusQueryOptions) {
|
||||
@@ -50,7 +51,7 @@ export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutSt
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
}
|
||||
return await fetchCheckoutStatus(client, agentId);
|
||||
return await fetchCheckoutStatus(client, agentId, cwd);
|
||||
},
|
||||
enabled: !!client && isConnected && !!agentId && !!cwd,
|
||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||
@@ -98,7 +99,7 @@ export function useCheckoutStatusCacheOnly({ serverId, agentId, cwd }: UseChecko
|
||||
if (!client) {
|
||||
throw new Error("Daemon client not available");
|
||||
}
|
||||
return await fetchCheckoutStatus(client, agentId);
|
||||
return await fetchCheckoutStatus(client, agentId, cwd);
|
||||
},
|
||||
enabled: false,
|
||||
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||
|
||||
@@ -1131,8 +1131,11 @@ export class DaemonClientV2 {
|
||||
|
||||
async getCheckoutStatus(
|
||||
agentId: string,
|
||||
requestId?: string
|
||||
options?: { cwd?: string; requestId?: string }
|
||||
): Promise<CheckoutStatusPayload> {
|
||||
const requestId = options?.requestId;
|
||||
const cwd = options?.cwd;
|
||||
|
||||
if (!requestId) {
|
||||
const existing = this.checkoutStatusInFlight.get(agentId);
|
||||
if (existing) {
|
||||
@@ -1144,6 +1147,7 @@ export class DaemonClientV2 {
|
||||
const message = SessionInboundMessageSchema.parse({
|
||||
type: "checkout_status_request",
|
||||
agentId,
|
||||
cwd,
|
||||
requestId: resolvedRequestId,
|
||||
});
|
||||
|
||||
|
||||
@@ -2855,7 +2855,9 @@ export class Session {
|
||||
): Promise<void> {
|
||||
const { agentId, requestId } = msg;
|
||||
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({
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
@@ -2870,7 +2872,7 @@ export class Session {
|
||||
hasRemote: false,
|
||||
remoteUrl: null,
|
||||
isPaseoOwnedWorktree: false,
|
||||
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
|
||||
error: { code: "UNKNOWN", message: `Agent not found and no cwd provided: ${agentId}` },
|
||||
requestId,
|
||||
},
|
||||
});
|
||||
@@ -2878,13 +2880,13 @@ export class Session {
|
||||
}
|
||||
|
||||
try {
|
||||
const status = await getCheckoutStatus(agent.cwd, { paseoHome: this.paseoHome });
|
||||
const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
|
||||
if (!status.isGit) {
|
||||
this.emit({
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
agentId,
|
||||
cwd: agent.cwd,
|
||||
cwd,
|
||||
isGit: false,
|
||||
repoRoot: null,
|
||||
currentBranch: null,
|
||||
@@ -2906,7 +2908,7 @@ export class Session {
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
agentId,
|
||||
cwd: agent.cwd,
|
||||
cwd,
|
||||
isGit: true,
|
||||
repoRoot: status.repoRoot ?? null,
|
||||
currentBranch: status.currentBranch ?? null,
|
||||
@@ -2927,7 +2929,7 @@ export class Session {
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
agentId,
|
||||
cwd: agent.cwd,
|
||||
cwd,
|
||||
isGit: true,
|
||||
repoRoot: status.repoRoot ?? null,
|
||||
currentBranch: status.currentBranch ?? null,
|
||||
@@ -2946,7 +2948,7 @@ export class Session {
|
||||
type: "checkout_status_response",
|
||||
payload: {
|
||||
agentId,
|
||||
cwd: agent.cwd,
|
||||
cwd,
|
||||
isGit: false,
|
||||
repoRoot: null,
|
||||
currentBranch: null,
|
||||
|
||||
@@ -477,6 +477,8 @@ const CheckoutDiffCompareSchema = z.object({
|
||||
export const CheckoutStatusRequestSchema = z.object({
|
||||
type: z.literal("checkout_status_request"),
|
||||
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(),
|
||||
});
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ import { describe, it, expect, beforeEach, afterEach } from "vitest";
|
||||
import { mkdtempSync, rmSync, writeFileSync, mkdirSync, realpathSync } from "fs";
|
||||
import { join } from "path";
|
||||
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 {
|
||||
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 () => {
|
||||
const result = await findProjectIcon(tempDir);
|
||||
expect(result).toBeNull();
|
||||
@@ -175,6 +182,51 @@ describe("findProjectIcon", () => {
|
||||
// Should return the first one based on pattern order (favicon.ico comes first)
|
||||
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", () => {
|
||||
|
||||
@@ -9,6 +9,9 @@ export const ICON_PATTERNS = [
|
||||
"favicon.ico",
|
||||
"favicon.png",
|
||||
"favicon.svg",
|
||||
"favico.ico",
|
||||
"favico.png",
|
||||
"favico.svg",
|
||||
"icon.png",
|
||||
"icon.svg",
|
||||
"app-icon.png",
|
||||
@@ -24,6 +27,11 @@ export const ICON_PATTERNS = [
|
||||
*/
|
||||
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.
|
||||
*/
|
||||
@@ -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
|
||||
const found = await findDirRecursively(projectDir);
|
||||
if (found) {
|
||||
|
||||
Reference in New Issue
Block a user