Update files

This commit is contained in:
Mohamed Boudra
2026-01-28 13:25:48 +07:00
parent c615d422ca
commit 96b211a2cf
16 changed files with 229 additions and 323 deletions
+1 -2
View File
@@ -171,7 +171,7 @@ export function AgentList({
void queryClient.prefetchQuery({ void queryClient.prefetchQuery({
queryKey, queryKey,
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }), queryFn: async () => await client.getCheckoutStatus(agent.cwd),
staleTime: CHECKOUT_STATUS_STALE_TIME, staleTime: CHECKOUT_STATUS_STALE_TIME,
}); });
} }
@@ -192,7 +192,6 @@ export function AgentList({
const checkoutQuery = useCheckoutStatusCacheOnly({ const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId, serverId: agent.serverId,
agentId: agent.id,
cwd: agent.cwd, cwd: agent.cwd,
}); });
const checkout = checkoutQuery.data ?? null; const checkout = checkoutQuery.data ?? null;
@@ -251,7 +251,7 @@ function SidebarContent({
isMobile, isMobile,
}: SidebarContentProps) { }: SidebarContentProps) {
const { theme } = useUnistyles(); const { theme } = useUnistyles();
const { status } = useCheckoutStatusQuery({ serverId, agentId, cwd }); const { status } = useCheckoutStatusQuery({ serverId, cwd });
const isGit = status?.isGit ?? false; const isGit = status?.isGit ?? false;
// If not a git repo, only show files tab // If not a git repo, only show files tab
+39 -30
View File
@@ -432,7 +432,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
const [actionError, setActionError] = useState<string | null>(null); const [actionError, setActionError] = useState<string | null>(null);
const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge"); const [shipDefault, setShipDefault] = useState<"merge" | "pr">("merge");
const { status, isLoading: isStatusLoading, isFetching: isStatusFetching, isError: isStatusError, error: statusError, refresh: refreshStatus } = const { status, isLoading: isStatusLoading, isFetching: isStatusFetching, isError: isStatusError, error: statusError, refresh: refreshStatus } =
useCheckoutStatusQuery({ serverId, agentId, cwd }); useCheckoutStatusQuery({ serverId, cwd });
const gitStatus = status && status.isGit ? status : null; const gitStatus = status && status.isGit ? status : null;
const isGit = Boolean(gitStatus); const isGit = Boolean(gitStatus);
const notGit = status !== null && !status.isGit && !status.error; const notGit = status !== null && !status.isGit && !status.error;
@@ -456,7 +456,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
refresh: refreshDiff, refresh: refreshDiff,
} = useCheckoutDiffQuery({ } = useCheckoutDiffQuery({
serverId, serverId,
agentId, cwd,
mode: diffMode, mode: diffMode,
baseRef, baseRef,
enabled: isGit, enabled: isGit,
@@ -467,7 +467,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
refresh: refreshPrStatus, refresh: refreshPrStatus,
} = useCheckoutPrStatusQuery({ } = useCheckoutPrStatusQuery({
serverId, serverId,
agentId, cwd,
enabled: isGit, enabled: isGit,
}); });
// Track user-initiated refresh to avoid iOS RefreshControl animation on background fetches // Track user-initiated refresh to avoid iOS RefreshControl animation on background fetches
@@ -583,16 +583,12 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
}); });
}, [agentId, diffMetrics, isDiffFetching, isDiffLoading, serverId]); }, [agentId, diffMetrics, isDiffFetching, isDiffLoading, serverId]);
const agentExists = useSessionStore((state) =>
state.sessions[serverId]?.agents?.has(agentId) ?? false
);
const commitMutation = useMutation({ const commitMutation = useMutation({
mutationFn: async () => { mutationFn: async () => {
if (!client) { if (!client) {
throw new Error("Daemon client unavailable"); throw new Error("Daemon client unavailable");
} }
const payload = await client.checkoutCommit(agentId, { addAll: true }); const payload = await client.checkoutCommit(cwd, { addAll: true });
if (payload.error) { if (payload.error) {
throw new Error(payload.error.message); throw new Error(payload.error.message);
} }
@@ -614,7 +610,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (!client) { if (!client) {
throw new Error("Daemon client unavailable"); throw new Error("Daemon client unavailable");
} }
const payload = await client.checkoutPrCreate(agentId, {}); const payload = await client.checkoutPrCreate(cwd, {});
if (payload.error) { if (payload.error) {
throw new Error(payload.error.message); throw new Error(payload.error.message);
} }
@@ -635,7 +631,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (!client) { if (!client) {
throw new Error("Daemon client unavailable"); throw new Error("Daemon client unavailable");
} }
const payload = await client.checkoutMerge(agentId, { const payload = await client.checkoutMerge(cwd, {
baseRef, baseRef,
strategy: "merge", strategy: "merge",
requireCleanTarget: true, requireCleanTarget: true,
@@ -661,7 +657,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (!client) { if (!client) {
throw new Error("Daemon client unavailable"); throw new Error("Daemon client unavailable");
} }
const payload = await client.checkoutMergeFromBase(agentId, { const payload = await client.checkoutMergeFromBase(cwd, {
baseRef, baseRef,
requireCleanTarget: true, requireCleanTarget: true,
}); });
@@ -686,7 +682,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (!client) { if (!client) {
throw new Error("Daemon client unavailable"); throw new Error("Daemon client unavailable");
} }
const payload = await client.checkoutPush(agentId); const payload = await client.checkoutPush(cwd);
if (payload.error) { if (payload.error) {
throw new Error(payload.error.message); throw new Error(payload.error.message);
} }
@@ -753,14 +749,6 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
const keyExtractor = useCallback((item: ParsedDiffFile) => item.path, []); const keyExtractor = useCallback((item: ParsedDiffFile) => item.path, []);
if (!agentExists) {
return (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
);
}
const hasChanges = files.length > 0; const hasChanges = files.length > 0;
const diffErrorMessage = const diffErrorMessage =
diffPayloadError?.message ?? diffPayloadError?.message ??
@@ -770,6 +758,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
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 aheadCount = gitStatus?.aheadBehind?.ahead ?? 0; const aheadCount = gitStatus?.aheadBehind?.ahead ?? 0;
const aheadOfOrigin = gitStatus?.aheadOfOrigin ?? 0;
const baseRefLabel = useMemo(() => { const baseRefLabel = useMemo(() => {
if (!baseRef) return "base"; if (!baseRef) return "base";
const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim(); const trimmed = baseRef.replace(/^refs\/(heads|remotes)\//, "").trim();
@@ -855,13 +844,15 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
// ========================================================================== // ==========================================================================
// Rules (in priority order): // Rules (in priority order):
// 1. Uncommitted changes → "Commit" is primary // 1. Uncommitted changes → "Commit" is primary
// 2. Has PR → "View PR" is primary // 2. Ahead of origin (unpushed commits) → "Push" is primary
// 3. Ahead of base → "Merge branch" or "Create PR" based on shipDefault preference // 3. Has PR → "View PR" is primary
// 4. Nothing to do → no primary CTA // 4. Ahead of base → "Merge branch" or "Create PR" based on shipDefault preference
// 5. Nothing to do → no primary CTA
// ========================================================================== // ==========================================================================
type PrimaryCTA = type PrimaryCTA =
| { type: "commit" } | { type: "commit" }
| { type: "push" }
| { type: "view-pr"; url: string } | { type: "view-pr"; url: string }
| { type: "ship"; action: "merge" | "create-pr" } | { type: "ship"; action: "merge" | "create-pr" }
| null; | null;
@@ -874,12 +865,17 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
return { type: "commit" }; return { type: "commit" };
} }
// Rule 2: Has PR → View PR // Rule 2: Ahead of origin → Push
if (aheadOfOrigin > 0 && !pushDisabled) {
return { type: "push" };
}
// Rule 3: Has PR → View PR
if (hasPullRequest && prStatus?.url) { if (hasPullRequest && prStatus?.url) {
return { type: "view-pr", url: prStatus.url }; return { type: "view-pr", url: prStatus.url };
} }
// Rule 3: Ahead of base → Ship (merge or create PR based on preference) // Rule 4: Ahead of base → Ship (merge or create PR based on preference)
if (aheadCount > 0) { if (aheadCount > 0) {
const preferredAction = shipDefault === "merge" ? "merge" : "create-pr"; const preferredAction = shipDefault === "merge" ? "merge" : "create-pr";
// If preferred action is disabled, fall back to the other // If preferred action is disabled, fall back to the other
@@ -892,15 +888,17 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
return { type: "ship", action: preferredAction }; return { type: "ship", action: preferredAction };
} }
// Rule 4: Nothing to do // Rule 5: Nothing to do
return null; return null;
}, [isGit, hasUncommittedChanges, hasPullRequest, prStatus?.url, aheadCount, shipDefault, mergeDisabled, prDisabled]); }, [isGit, hasUncommittedChanges, aheadOfOrigin, pushDisabled, hasPullRequest, prStatus?.url, aheadCount, shipDefault, mergeDisabled, prDisabled]);
const primaryCTALabel = useMemo(() => { const primaryCTALabel = useMemo(() => {
if (!primaryCTA) return ""; if (!primaryCTA) return "";
switch (primaryCTA.type) { switch (primaryCTA.type) {
case "commit": case "commit":
return "Commit"; return "Commit";
case "push":
return "Push";
case "view-pr": case "view-pr":
return "View PR"; return "View PR";
case "ship": case "ship":
@@ -913,24 +911,28 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
switch (primaryCTA.type) { switch (primaryCTA.type) {
case "commit": case "commit":
return commitDisabled; return commitDisabled;
case "push":
return pushDisabled;
case "view-pr": case "view-pr":
return false; // View PR is never disabled return false; // View PR is never disabled
case "ship": case "ship":
return primaryCTA.action === "merge" ? mergeDisabled : prDisabled; return primaryCTA.action === "merge" ? mergeDisabled : prDisabled;
} }
}, [primaryCTA, actionsDisabled, commitDisabled, mergeDisabled, prDisabled]); }, [primaryCTA, actionsDisabled, commitDisabled, pushDisabled, mergeDisabled, prDisabled]);
const primaryCTAStatus: ActionStatus = useMemo(() => { const primaryCTAStatus: ActionStatus = useMemo(() => {
if (!primaryCTA) return "idle"; if (!primaryCTA) return "idle";
switch (primaryCTA.type) { switch (primaryCTA.type) {
case "commit": case "commit":
return commitAction.status; return commitAction.status;
case "push":
return pushAction.status;
case "view-pr": case "view-pr":
return "idle"; // View PR is instant, no status return "idle"; // View PR is instant, no status
case "ship": case "ship":
return primaryCTA.action === "merge" ? mergeAction.status : prCreateAction.status; return primaryCTA.action === "merge" ? mergeAction.status : prCreateAction.status;
} }
}, [primaryCTA, commitAction.status, mergeAction.status, prCreateAction.status]); }, [primaryCTA, commitAction.status, pushAction.status, mergeAction.status, prCreateAction.status]);
const primaryCTADisplayLabel = useMemo(() => { const primaryCTADisplayLabel = useMemo(() => {
if (!primaryCTA) return ""; if (!primaryCTA) return "";
@@ -941,6 +943,10 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
if (status === "pending") return "Committing..."; if (status === "pending") return "Committing...";
if (status === "success") return "Committed"; if (status === "success") return "Committed";
return "Commit"; return "Commit";
case "push":
if (status === "pending") return "Pushing...";
if (status === "success") return "Pushed";
return "Push";
case "view-pr": case "view-pr":
return "View PR"; return "View PR";
case "ship": case "ship":
@@ -962,6 +968,9 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
case "commit": case "commit":
commitAction.trigger(); commitAction.trigger();
break; break;
case "push":
pushAction.trigger();
break;
case "view-pr": case "view-pr":
openURLInNewTab(primaryCTA.url); openURLInNewTab(primaryCTA.url);
break; break;
@@ -973,7 +982,7 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
} }
break; break;
} }
}, [primaryCTA, primaryCTADisabled, primaryCTAStatus, commitAction, mergeAction, prCreateAction]); }, [primaryCTA, primaryCTADisabled, primaryCTAStatus, commitAction, pushAction, mergeAction, prCreateAction]);
return ( return (
<View style={styles.container}> <View style={styles.container}>
@@ -93,7 +93,6 @@ function SectionHeader({
// For project sections, try to get repo name from checkout status // For project sections, try to get repo name from checkout status
const checkoutQuery = useCheckoutStatusCacheOnly({ const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: section.firstAgentServerId ?? "", serverId: section.firstAgentServerId ?? "",
agentId: section.firstAgentId ?? "",
cwd: section.workingDir ?? "", cwd: section.workingDir ?? "",
}); });
const checkout = checkoutQuery.data ?? null; const checkout = checkoutQuery.data ?? null;
@@ -299,7 +298,7 @@ export function GroupedAgentList({
void queryClient.prefetchQuery({ void queryClient.prefetchQuery({
queryKey, queryKey,
queryFn: async () => await client.getCheckoutStatus(agent.id, { cwd: agent.cwd }), queryFn: async () => await client.getCheckoutStatus(agent.cwd),
staleTime: CHECKOUT_STATUS_STALE_TIME, staleTime: CHECKOUT_STATUS_STALE_TIME,
}); });
} }
@@ -409,7 +408,6 @@ export function GroupedAgentList({
const checkoutQuery = useCheckoutStatusCacheOnly({ const checkoutQuery = useCheckoutStatusCacheOnly({
serverId: agent.serverId, serverId: agent.serverId,
agentId: agent.id,
cwd: agent.cwd, cwd: agent.cwd,
}); });
const checkout = checkoutQuery.data ?? null; const checkout = checkoutQuery.data ?? null;
@@ -1,15 +1,15 @@
export type CheckoutStatusRevalidationParams = { export type CheckoutStatusRevalidationParams = {
serverId: string; serverId: string;
agentId: string; cwd: string;
isOpen: boolean; isOpen: boolean;
explorerTab: string; explorerTab: string;
}; };
export function checkoutStatusRevalidationKey(params: CheckoutStatusRevalidationParams): string | null { export function checkoutStatusRevalidationKey(params: CheckoutStatusRevalidationParams): string | null {
if (!params.agentId) return null; if (!params.cwd) return null;
if (!params.isOpen) return null; if (!params.isOpen) return null;
if (params.explorerTab !== "changes") return null; if (params.explorerTab !== "changes") return null;
return `${params.serverId}:${params.agentId}`; return `${params.serverId}:${params.cwd}`;
} }
export function nextCheckoutStatusRefetchDecision( export function nextCheckoutStatusRefetchDecision(
@@ -9,16 +9,16 @@ const CHECKOUT_DIFF_STALE_TIME = 30_000;
function checkoutDiffQueryKey( function checkoutDiffQueryKey(
serverId: string, serverId: string,
agentId: string, cwd: string,
mode: "uncommitted" | "base", mode: "uncommitted" | "base",
baseRef?: string baseRef?: string
) { ) {
return ["checkoutDiff", serverId, agentId, mode, baseRef ?? ""] as const; return ["checkoutDiff", serverId, cwd, mode, baseRef ?? ""] as const;
} }
interface UseCheckoutDiffQueryOptions { interface UseCheckoutDiffQueryOptions {
serverId: string; serverId: string;
agentId: string; cwd: string;
mode: "uncommitted" | "base"; mode: "uncommitted" | "base";
baseRef?: string; baseRef?: string;
enabled?: boolean; enabled?: boolean;
@@ -31,7 +31,7 @@ export type HighlightToken = NonNullable<DiffLine["tokens"]>[number];
export function useCheckoutDiffQuery({ export function useCheckoutDiffQuery({
serverId, serverId,
agentId, cwd,
mode, mode,
baseRef, baseRef,
enabled = true, enabled = true,
@@ -51,27 +51,27 @@ export function useCheckoutDiffQuery({
const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen; const isOpen = isMobile ? mobileView === "file-explorer" : desktopFileExplorerOpen;
const query = useQuery({ const query = useQuery({
queryKey: checkoutDiffQueryKey(serverId, agentId, mode, baseRef), queryKey: checkoutDiffQueryKey(serverId, cwd, mode, baseRef),
queryFn: async () => { queryFn: async () => {
if (!client) { if (!client) {
throw new Error("Daemon client not available"); throw new Error("Daemon client not available");
} }
return await client.getCheckoutDiff(agentId, { mode, baseRef }); return await client.getCheckoutDiff(cwd, { mode, baseRef });
}, },
enabled: !!client && isConnected && !!agentId && enabled, enabled: !!client && isConnected && !!cwd && enabled,
staleTime: CHECKOUT_DIFF_STALE_TIME, staleTime: CHECKOUT_DIFF_STALE_TIME,
refetchInterval: 10_000, refetchInterval: 10_000,
}); });
// Revalidate when sidebar opens with "changes" tab active // Revalidate when sidebar opens with "changes" tab active
useEffect(() => { useEffect(() => {
if (!isOpen || explorerTab !== "changes" || !agentId) { if (!isOpen || explorerTab !== "changes" || !cwd) {
return; return;
} }
queryClient.invalidateQueries({ queryClient.invalidateQueries({
queryKey: checkoutDiffQueryKey(serverId, agentId, mode, baseRef), queryKey: checkoutDiffQueryKey(serverId, cwd, mode, baseRef),
}); });
}, [isOpen, explorerTab, serverId, agentId, mode, baseRef, queryClient]); }, [isOpen, explorerTab, serverId, cwd, mode, baseRef, queryClient]);
const refresh = useCallback(() => { const refresh = useCallback(() => {
return query.refetch(); return query.refetch();
@@ -4,13 +4,13 @@ import type { CheckoutPrStatusResponse } from "@server/shared/messages";
const CHECKOUT_PR_STATUS_STALE_TIME = 20_000; const CHECKOUT_PR_STATUS_STALE_TIME = 20_000;
function checkoutPrStatusQueryKey(serverId: string, agentId: string) { function checkoutPrStatusQueryKey(serverId: string, cwd: string) {
return ["checkoutPrStatus", serverId, agentId] as const; return ["checkoutPrStatus", serverId, cwd] as const;
} }
interface UseCheckoutPrStatusQueryOptions { interface UseCheckoutPrStatusQueryOptions {
serverId: string; serverId: string;
agentId: string; cwd: string;
enabled?: boolean; enabled?: boolean;
} }
@@ -18,7 +18,7 @@ export type CheckoutPrStatusPayload = CheckoutPrStatusResponse["payload"];
export function useCheckoutPrStatusQuery({ export function useCheckoutPrStatusQuery({
serverId, serverId,
agentId, cwd,
enabled = true, enabled = true,
}: UseCheckoutPrStatusQueryOptions) { }: UseCheckoutPrStatusQueryOptions) {
const client = useSessionStore( const client = useSessionStore(
@@ -29,14 +29,14 @@ export function useCheckoutPrStatusQuery({
); );
const query = useQuery({ const query = useQuery({
queryKey: checkoutPrStatusQueryKey(serverId, agentId), queryKey: checkoutPrStatusQueryKey(serverId, cwd),
queryFn: async () => { queryFn: async () => {
if (!client) { if (!client) {
throw new Error("Daemon client not available"); throw new Error("Daemon client not available");
} }
return await client.checkoutPrStatus(agentId); return await client.checkoutPrStatus(cwd);
}, },
enabled: !!client && isConnected && !!agentId && enabled, enabled: !!client && isConnected && !!cwd && enabled,
staleTime: CHECKOUT_PR_STATUS_STALE_TIME, staleTime: CHECKOUT_PR_STATUS_STALE_TIME,
refetchInterval: 15_000, refetchInterval: 15_000,
}); });
@@ -7,7 +7,7 @@ describe("useCheckoutStatusQuery", () => {
expect( expect(
checkoutStatusRevalidationKey({ checkoutStatusRevalidationKey({
serverId: "daemon-1", serverId: "daemon-1",
agentId: "agent-1", cwd: "/path/to/project",
isOpen: false, isOpen: false,
explorerTab: "changes", explorerTab: "changes",
}) })
@@ -18,7 +18,7 @@ describe("useCheckoutStatusQuery", () => {
expect( expect(
checkoutStatusRevalidationKey({ checkoutStatusRevalidationKey({
serverId: "daemon-1", serverId: "daemon-1",
agentId: "agent-1", cwd: "/path/to/project",
isOpen: true, isOpen: true,
explorerTab: "files", explorerTab: "files",
}) })
@@ -29,11 +29,11 @@ describe("useCheckoutStatusQuery", () => {
expect( expect(
checkoutStatusRevalidationKey({ checkoutStatusRevalidationKey({
serverId: "daemon-1", serverId: "daemon-1",
agentId: "agent-1", cwd: "/path/to/project",
isOpen: true, isOpen: true,
explorerTab: "changes", explorerTab: "changes",
}) })
).toBe("daemon-1:agent-1"); ).toBe("daemon-1:/path/to/project");
}); });
}); });
@@ -17,21 +17,19 @@ export function checkoutStatusQueryKey(serverId: string, cwd: string) {
interface UseCheckoutStatusQueryOptions { interface UseCheckoutStatusQueryOptions {
serverId: string; serverId: string;
agentId: string;
cwd: string; cwd: string;
} }
export type CheckoutStatusPayload = CheckoutStatusResponse["payload"]; export type CheckoutStatusPayload = CheckoutStatusResponse["payload"];
function fetchCheckoutStatus( function fetchCheckoutStatus(
client: { getCheckoutStatus: (agentId: string, options?: { cwd?: string }) => Promise<CheckoutStatusPayload> }, client: { getCheckoutStatus: (cwd: string) => Promise<CheckoutStatusPayload> },
agentId: string,
cwd: string cwd: string
): Promise<CheckoutStatusPayload> { ): Promise<CheckoutStatusPayload> {
return client.getCheckoutStatus(agentId, { cwd }); return client.getCheckoutStatus(cwd);
} }
export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutStatusQueryOptions) { export function useCheckoutStatusQuery({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const client = useSessionStore( const client = useSessionStore(
(state) => state.sessions[serverId]?.client ?? null (state) => state.sessions[serverId]?.client ?? null
); );
@@ -51,9 +49,9 @@ 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, cwd); return await fetchCheckoutStatus(client, cwd);
}, },
enabled: !!client && isConnected && !!agentId && !!cwd, enabled: !!client && isConnected && !!cwd,
staleTime: CHECKOUT_STATUS_STALE_TIME, staleTime: CHECKOUT_STATUS_STALE_TIME,
refetchInterval: 10_000, refetchInterval: 10_000,
refetchIntervalInBackground: true, refetchIntervalInBackground: true,
@@ -62,8 +60,8 @@ export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutSt
// Revalidate when sidebar is open with "changes" tab active. // Revalidate when sidebar is open with "changes" tab active.
const revalidationKey = useMemo( const revalidationKey = useMemo(
() => checkoutStatusRevalidationKey({ serverId, agentId, isOpen, explorerTab }), () => checkoutStatusRevalidationKey({ serverId, cwd, isOpen, explorerTab }),
[serverId, agentId, isOpen, explorerTab] [serverId, cwd, isOpen, explorerTab]
); );
const lastRevalidationKey = useRef<string | null>(null); const lastRevalidationKey = useRef<string | null>(null);
useEffect(() => { useEffect(() => {
@@ -88,7 +86,7 @@ export function useCheckoutStatusQuery({ serverId, agentId, cwd }: UseCheckoutSt
* initiating a fetch. Useful for list rows where a parent component prefetches * initiating a fetch. Useful for list rows where a parent component prefetches
* only the visible agents. * only the visible agents.
*/ */
export function useCheckoutStatusCacheOnly({ serverId, agentId, cwd }: UseCheckoutStatusQueryOptions) { export function useCheckoutStatusCacheOnly({ serverId, cwd }: UseCheckoutStatusQueryOptions) {
const client = useSessionStore( const client = useSessionStore(
(state) => state.sessions[serverId]?.client ?? null (state) => state.sessions[serverId]?.client ?? null
); );
@@ -99,7 +97,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, cwd); return await fetchCheckoutStatus(client, cwd);
}, },
enabled: false, enabled: false,
staleTime: CHECKOUT_STATUS_STALE_TIME, staleTime: CHECKOUT_STATUS_STALE_TIME,
@@ -414,7 +414,6 @@ function AgentScreenContent({
// Checkout status for header subtitle // Checkout status for header subtitle
const checkoutStatusQuery = useCheckoutStatusQuery({ const checkoutStatusQuery = useCheckoutStatusQuery({
serverId, serverId,
agentId: resolvedAgentId ?? "",
cwd: agent?.cwd ?? "", cwd: agent?.cwd ?? "",
}); });
const checkout = checkoutStatusQuery.status; const checkout = checkoutStatusQuery.status;
@@ -75,14 +75,14 @@ describe("DaemonClientV2", () => {
mock.triggerOpen(); mock.triggerOpen();
await connectPromise; await connectPromise;
const p1 = client.getCheckoutStatus("agent-1"); const p1 = client.getCheckoutStatus("/tmp/project");
const p2 = client.getCheckoutStatus("agent-1"); const p2 = client.getCheckoutStatus("/tmp/project");
expect(mock.sent).toHaveLength(1); expect(mock.sent).toHaveLength(1);
const request = JSON.parse(mock.sent[0]) as { const request = JSON.parse(mock.sent[0]) as {
type: "session"; type: "session";
message: { type: "checkout_status_request"; agentId: string; requestId: string }; message: { type: "checkout_status_request"; cwd: string; requestId: string };
}; };
const response = { const response = {
@@ -90,8 +90,7 @@ describe("DaemonClientV2", () => {
message: { message: {
type: "checkout_status_response", type: "checkout_status_response",
payload: { payload: {
agentId: "agent-1", cwd: "/tmp/project",
cwd: "/tmp",
error: null, error: null,
requestId: request.message.requestId, requestId: request.message.requestId,
isGit: false, isGit: false,
@@ -109,16 +108,16 @@ describe("DaemonClientV2", () => {
mock.triggerMessage(JSON.stringify(response)); mock.triggerMessage(JSON.stringify(response));
const [r1, r2] = await Promise.all([p1, p2]); const [r1, r2] = await Promise.all([p1, p2]);
expect(r1).toMatchObject({ agentId: "agent-1", requestId: request.message.requestId, isGit: false }); expect(r1).toMatchObject({ cwd: "/tmp/project", requestId: request.message.requestId, isGit: false });
expect(r2).toMatchObject({ agentId: "agent-1", requestId: request.message.requestId, isGit: false }); expect(r2).toMatchObject({ cwd: "/tmp/project", requestId: request.message.requestId, isGit: false });
// After completion, a new call should issue a new request. // After completion, a new call should issue a new request.
const p3 = client.getCheckoutStatus("agent-1"); const p3 = client.getCheckoutStatus("/tmp/project");
expect(mock.sent).toHaveLength(2); expect(mock.sent).toHaveLength(2);
const request2 = JSON.parse(mock.sent[1]) as { const request2 = JSON.parse(mock.sent[1]) as {
type: "session"; type: "session";
message: { type: "checkout_status_request"; agentId: string; requestId: string }; message: { type: "checkout_status_request"; cwd: string; requestId: string };
}; };
mock.triggerMessage( mock.triggerMessage(
+20 -22
View File
@@ -1130,14 +1130,13 @@ export class DaemonClientV2 {
// ============================================================================ // ============================================================================
async getCheckoutStatus( async getCheckoutStatus(
agentId: string, cwd: string,
options?: { cwd?: string; requestId?: string } options?: { requestId?: string }
): Promise<CheckoutStatusPayload> { ): Promise<CheckoutStatusPayload> {
const requestId = options?.requestId; const requestId = options?.requestId;
const cwd = options?.cwd;
if (!requestId) { if (!requestId) {
const existing = this.checkoutStatusInFlight.get(agentId); const existing = this.checkoutStatusInFlight.get(cwd);
if (existing) { if (existing) {
return existing; return existing;
} }
@@ -1146,7 +1145,6 @@ export class DaemonClientV2 {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_status_request", type: "checkout_status_request",
agentId,
cwd, cwd,
requestId: resolvedRequestId, requestId: resolvedRequestId,
}); });
@@ -1170,10 +1168,10 @@ export class DaemonClientV2 {
})(); })();
if (!requestId) { if (!requestId) {
this.checkoutStatusInFlight.set(agentId, responsePromise); this.checkoutStatusInFlight.set(cwd, responsePromise);
responsePromise.finally(() => { responsePromise.finally(() => {
if (this.checkoutStatusInFlight.get(agentId) === responsePromise) { if (this.checkoutStatusInFlight.get(cwd) === responsePromise) {
this.checkoutStatusInFlight.delete(agentId); this.checkoutStatusInFlight.delete(cwd);
} }
}); });
} }
@@ -1182,14 +1180,14 @@ export class DaemonClientV2 {
} }
async getCheckoutDiff( async getCheckoutDiff(
agentId: string, cwd: string,
compare: { mode: "uncommitted" | "base"; baseRef?: string }, compare: { mode: "uncommitted" | "base"; baseRef?: string },
requestId?: string requestId?: string
): Promise<CheckoutDiffPayload> { ): Promise<CheckoutDiffPayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_diff_request", type: "checkout_diff_request",
agentId, cwd,
compare, compare,
requestId: resolvedRequestId, requestId: resolvedRequestId,
}); });
@@ -1211,14 +1209,14 @@ export class DaemonClientV2 {
} }
async checkoutCommit( async checkoutCommit(
agentId: string, cwd: string,
input: { message?: string; addAll?: boolean }, input: { message?: string; addAll?: boolean },
requestId?: string requestId?: string
): Promise<CheckoutCommitPayload> { ): Promise<CheckoutCommitPayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_commit_request", type: "checkout_commit_request",
agentId, cwd,
message: input.message, message: input.message,
addAll: input.addAll, addAll: input.addAll,
requestId: resolvedRequestId, requestId: resolvedRequestId,
@@ -1241,14 +1239,14 @@ export class DaemonClientV2 {
} }
async checkoutMerge( async checkoutMerge(
agentId: string, cwd: string,
input: { baseRef?: string; strategy?: "merge" | "squash"; requireCleanTarget?: boolean }, input: { baseRef?: string; strategy?: "merge" | "squash"; requireCleanTarget?: boolean },
requestId?: string requestId?: string
): Promise<CheckoutMergePayload> { ): Promise<CheckoutMergePayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_merge_request", type: "checkout_merge_request",
agentId, cwd,
baseRef: input.baseRef, baseRef: input.baseRef,
strategy: input.strategy, strategy: input.strategy,
requireCleanTarget: input.requireCleanTarget, requireCleanTarget: input.requireCleanTarget,
@@ -1272,14 +1270,14 @@ export class DaemonClientV2 {
} }
async checkoutMergeFromBase( async checkoutMergeFromBase(
agentId: string, cwd: string,
input: { baseRef?: string; requireCleanTarget?: boolean }, input: { baseRef?: string; requireCleanTarget?: boolean },
requestId?: string requestId?: string
): Promise<CheckoutMergeFromBasePayload> { ): Promise<CheckoutMergeFromBasePayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_merge_from_base_request", type: "checkout_merge_from_base_request",
agentId, cwd,
baseRef: input.baseRef, baseRef: input.baseRef,
requireCleanTarget: input.requireCleanTarget, requireCleanTarget: input.requireCleanTarget,
requestId: resolvedRequestId, requestId: resolvedRequestId,
@@ -1301,11 +1299,11 @@ export class DaemonClientV2 {
return response; return response;
} }
async checkoutPush(agentId: string, requestId?: string): Promise<CheckoutPushPayload> { async checkoutPush(cwd: string, requestId?: string): Promise<CheckoutPushPayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_push_request", type: "checkout_push_request",
agentId, cwd,
requestId: resolvedRequestId, requestId: resolvedRequestId,
}); });
const response = this.waitFor( const response = this.waitFor(
@@ -1326,14 +1324,14 @@ export class DaemonClientV2 {
} }
async checkoutPrCreate( async checkoutPrCreate(
agentId: string, cwd: string,
input: { title?: string; body?: string; baseRef?: string }, input: { title?: string; body?: string; baseRef?: string },
requestId?: string requestId?: string
): Promise<CheckoutPrCreatePayload> { ): Promise<CheckoutPrCreatePayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_pr_create_request", type: "checkout_pr_create_request",
agentId, cwd,
title: input.title, title: input.title,
body: input.body, body: input.body,
baseRef: input.baseRef, baseRef: input.baseRef,
@@ -1357,13 +1355,13 @@ export class DaemonClientV2 {
} }
async checkoutPrStatus( async checkoutPrStatus(
agentId: string, cwd: string,
requestId?: string requestId?: string
): Promise<CheckoutPrStatusPayload> { ): Promise<CheckoutPrStatusPayload> {
const resolvedRequestId = this.createRequestId(requestId); const resolvedRequestId = this.createRequestId(requestId);
const message = SessionInboundMessageSchema.parse({ const message = SessionInboundMessageSchema.parse({
type: "checkout_pr_status_request", type: "checkout_pr_status_request",
agentId, cwd,
requestId: resolvedRequestId, requestId: resolvedRequestId,
}); });
const response = this.waitFor( const response = this.waitFor(
@@ -191,7 +191,7 @@ describe("daemon checkout ship loop", () => {
}); });
agentId = agent.id; agentId = agent.id;
const status = await ctx.client.getCheckoutStatus(agent.id); const status = await ctx.client.getCheckoutStatus(worktree.worktreePath);
expect(status.isGit).toBe(true); expect(status.isGit).toBe(true);
expect(status.isPaseoOwnedWorktree).toBe(true); expect(status.isPaseoOwnedWorktree).toBe(true);
expect(status.repoRoot).toContain(repoDir); expect(status.repoRoot).toContain(repoDir);
@@ -207,13 +207,13 @@ describe("daemon checkout ship loop", () => {
const renamePayload = getStructuredContent(renameResult); const renamePayload = getStructuredContent(renameResult);
expect(renamePayload?.success).toBe(true); expect(renamePayload?.success).toBe(true);
const updatedStatus = await ctx.client.getCheckoutStatus(agent.id); const updatedStatus = await ctx.client.getCheckoutStatus(worktree.worktreePath);
expect(updatedStatus.currentBranch).toBe("ship-loop-ready"); expect(updatedStatus.currentBranch).toBe("ship-loop-ready");
const readmePath = path.join(worktree.worktreePath, "README.md"); const readmePath = path.join(worktree.worktreePath, "README.md");
writeFileSync(readmePath, "init\nship loop update\n"); writeFileSync(readmePath, "init\nship loop update\n");
const diffUncommitted = await ctx.client.getCheckoutDiff(agent.id, { const diffUncommitted = await ctx.client.getCheckoutDiff(worktree.worktreePath, {
mode: "uncommitted", mode: "uncommitted",
}); });
expect(diffUncommitted.error).toBeNull(); expect(diffUncommitted.error).toBeNull();
@@ -221,7 +221,7 @@ describe("daemon checkout ship loop", () => {
const timelineBeforeCommit = const timelineBeforeCommit =
ctx.daemon.daemon.agentManager.getTimeline(agent.id).length; ctx.daemon.daemon.agentManager.getTimeline(agent.id).length;
const commitResult = await ctx.client.checkoutCommit(agent.id, { const commitResult = await ctx.client.checkoutCommit(worktree.worktreePath, {
addAll: true, addAll: true,
}); });
expect(commitResult.error).toBeNull(); expect(commitResult.error).toBeNull();
@@ -230,12 +230,12 @@ describe("daemon checkout ship loop", () => {
ctx.daemon.daemon.agentManager.getTimeline(agent.id).length; ctx.daemon.daemon.agentManager.getTimeline(agent.id).length;
expect(timelineAfterCommit).toBe(timelineBeforeCommit); expect(timelineAfterCommit).toBe(timelineBeforeCommit);
const diffAfterCommit = await ctx.client.getCheckoutDiff(agent.id, { const diffAfterCommit = await ctx.client.getCheckoutDiff(worktree.worktreePath, {
mode: "uncommitted", mode: "uncommitted",
}); });
expect(diffAfterCommit.files.length).toBe(0); expect(diffAfterCommit.files.length).toBe(0);
const baseDiff = await ctx.client.getCheckoutDiff(agent.id, { const baseDiff = await ctx.client.getCheckoutDiff(worktree.worktreePath, {
mode: "base", mode: "base",
baseRef: "main", baseRef: "main",
}); });
@@ -243,7 +243,7 @@ describe("daemon checkout ship loop", () => {
const timelineBeforePr = const timelineBeforePr =
ctx.daemon.daemon.agentManager.getTimeline(agent.id).length; ctx.daemon.daemon.agentManager.getTimeline(agent.id).length;
const prCreate = await ctx.client.checkoutPrCreate(agent.id, { const prCreate = await ctx.client.checkoutPrCreate(worktree.worktreePath, {
baseRef: "main", baseRef: "main",
}); });
expect(prCreate.error).toBeNull(); expect(prCreate.error).toBeNull();
@@ -252,12 +252,12 @@ describe("daemon checkout ship loop", () => {
ctx.daemon.daemon.agentManager.getTimeline(agent.id).length; ctx.daemon.daemon.agentManager.getTimeline(agent.id).length;
expect(timelineAfterPr).toBe(timelineBeforePr); expect(timelineAfterPr).toBe(timelineBeforePr);
const prStatus = await ctx.client.checkoutPrStatus(agent.id); const prStatus = await ctx.client.checkoutPrStatus(worktree.worktreePath);
expect(prStatus.error).toBeNull(); expect(prStatus.error).toBeNull();
expect(prStatus.status?.url).toContain(repoName); expect(prStatus.status?.url).toContain(repoName);
expect(prStatus.status?.state).toBeTruthy(); expect(prStatus.status?.state).toBeTruthy();
const mergeResult = await ctx.client.checkoutMerge(agent.id, { const mergeResult = await ctx.client.checkoutMerge(worktree.worktreePath, {
baseRef: "main", baseRef: "main",
strategy: "merge", strategy: "merge",
requireCleanTarget: true, requireCleanTarget: true,
@@ -265,14 +265,14 @@ describe("daemon checkout ship loop", () => {
expect(mergeResult.error).toBeNull(); expect(mergeResult.error).toBeNull();
expect(mergeResult.success).toBe(true); expect(mergeResult.success).toBe(true);
const statusAfterMerge = await ctx.client.getCheckoutStatus(agent.id); const statusAfterMerge = await ctx.client.getCheckoutStatus(worktree.worktreePath);
expect(statusAfterMerge.isGit).toBe(true); expect(statusAfterMerge.isGit).toBe(true);
if (statusAfterMerge.isGit) { if (statusAfterMerge.isGit) {
expect(statusAfterMerge.baseRef).toBe("main"); expect(statusAfterMerge.baseRef).toBe("main");
expect(statusAfterMerge.aheadBehind?.ahead ?? 0).toBe(0); expect(statusAfterMerge.aheadBehind?.ahead ?? 0).toBe(0);
} }
const baseDiffAfterMerge = await ctx.client.getCheckoutDiff(agent.id, { const baseDiffAfterMerge = await ctx.client.getCheckoutDiff(worktree.worktreePath, {
mode: "base", mode: "base",
baseRef: "main", baseRef: "main",
}); });
@@ -353,7 +353,7 @@ describe("daemon checkout ship loop", () => {
}); });
agentId = agent.id; agentId = agent.id;
const status = await ctx.client.getCheckoutStatus(agent.id); const status = await ctx.client.getCheckoutStatus(worktree.worktreePath);
expect(status.isGit).toBe(true); expect(status.isGit).toBe(true);
if (status.isGit) { if (status.isGit) {
expect(status.hasRemote).toBe(true); expect(status.hasRemote).toBe(true);
@@ -374,14 +374,14 @@ describe("daemon checkout ship loop", () => {
// Add a commit on the agent branch. // Add a commit on the agent branch.
writeFileSync(path.join(worktree.worktreePath, "feature.txt"), "feature\n"); writeFileSync(path.join(worktree.worktreePath, "feature.txt"), "feature\n");
const commitResult = await ctx.client.checkoutCommit(agent.id, { const commitResult = await ctx.client.checkoutCommit(worktree.worktreePath, {
message: "feature commit", message: "feature commit",
addAll: true, addAll: true,
}); });
expect(commitResult.error).toBeNull(); expect(commitResult.error).toBeNull();
expect(commitResult.success).toBe(true); expect(commitResult.success).toBe(true);
const mergeFromBase = await ctx.client.checkoutMergeFromBase(agent.id, { const mergeFromBase = await ctx.client.checkoutMergeFromBase(worktree.worktreePath, {
baseRef: "main", baseRef: "main",
requireCleanTarget: true, requireCleanTarget: true,
}); });
@@ -394,7 +394,7 @@ describe("daemon checkout ship loop", () => {
stdio: "pipe", stdio: "pipe",
}); });
const pushResult = await ctx.client.checkoutPush(agent.id); const pushResult = await ctx.client.checkoutPush(worktree.worktreePath);
expect(pushResult.error).toBeNull(); expect(pushResult.error).toBeNull();
expect(pushResult.success).toBe(true); expect(pushResult.success).toBe(true);
} finally { } finally {
@@ -423,15 +423,15 @@ describe("daemon checkout ship loop", () => {
}); });
agentId = agent.id; agentId = agent.id;
const status = await ctx.client.getCheckoutStatus(agent.id); const status = await ctx.client.getCheckoutStatus(worktree.worktreePath);
expect(status.isGit).toBe(false); expect(status.isGit).toBe(false);
const diff = await ctx.client.getCheckoutDiff(agent.id, { const diff = await ctx.client.getCheckoutDiff(worktree.worktreePath, {
mode: "uncommitted", mode: "uncommitted",
}); });
expect(diff.error?.code).toBe("NOT_GIT_REPO"); expect(diff.error?.code).toBe("NOT_GIT_REPO");
const commit = await ctx.client.checkoutCommit(agent.id, { const commit = await ctx.client.checkoutCommit(worktree.worktreePath, {
message: "Should fail", message: "Should fail",
addAll: true, addAll: true,
}); });
+68 -184
View File
@@ -4,10 +4,10 @@ import { exec } from "child_process";
import { promisify, inspect } from "util"; import { promisify, inspect } from "util";
import { join, resolve, sep } from "path"; import { join, resolve, sep } from "path";
import invariant from "tiny-invariant"; import invariant from "tiny-invariant";
import { z } from "zod";
import { streamText, stepCountIs } from "ai"; import { streamText, stepCountIs } from "ai";
import type { ToolSet } from "ai"; import type { ToolSet } from "ai";
import type { ModelMessage } from "@ai-sdk/provider-utils"; import type { ModelMessage } from "@ai-sdk/provider-utils";
import { z } from "zod";
import { import {
createOpenRouter, createOpenRouter,
OpenRouterProviderOptions, OpenRouterProviderOptions,
@@ -121,6 +121,12 @@ let restartRequested = false;
const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0]; const DEFAULT_AGENT_PROVIDER = AGENT_PROVIDER_IDS[0];
const RESTART_EXIT_DELAY_MS = 250; const RESTART_EXIT_DELAY_MS = 250;
/**
* Default model used for auto-generating commit messages and PR descriptions.
* Uses Claude Haiku for speed and cost efficiency.
*/
const AUTO_GEN_MODEL = "haiku";
type ProcessingPhase = "idle" | "transcribing" | "llm"; type ProcessingPhase = "idle" | "transcribing" | "llm";
type NormalizedGitOptions = { type NormalizedGitOptions = {
@@ -2357,18 +2363,8 @@ export class Session {
return resolvedCandidate.startsWith(resolvedRoot + sep); return resolvedCandidate.startsWith(resolvedRoot + sep);
} }
private buildEphemeralAgentConfig(agent: ManagedAgent, title: string): AgentSessionConfig { private async generateCommitMessage(cwd: string): Promise<string> {
return { const diff = await getCheckoutDiff(cwd, { mode: "uncommitted" }, { paseoHome: this.paseoHome });
...agent.config,
cwd: agent.cwd,
title,
parentAgentId: agent.id,
internal: true,
};
}
private async generateCommitMessage(agent: ManagedAgent): Promise<string> {
const diff = await getCheckoutDiff(agent.cwd, { mode: "uncommitted" }, { paseoHome: this.paseoHome });
const schema = z.object({ const schema = z.object({
message: z message: z
.string() .string()
@@ -2385,7 +2381,13 @@ export class Session {
try { try {
const result = await generateStructuredAgentResponse({ const result = await generateStructuredAgentResponse({
manager: this.agentManager, manager: this.agentManager,
agentConfig: this.buildEphemeralAgentConfig(agent, "Commit generator"), agentConfig: {
provider: "claude",
model: AUTO_GEN_MODEL,
cwd,
title: "Commit generator",
internal: true,
},
prompt, prompt,
schema, schema,
schemaName: "CommitMessage", schemaName: "CommitMessage",
@@ -2400,12 +2402,12 @@ export class Session {
} }
} }
private async generatePullRequestText(agent: ManagedAgent, baseRef?: string): Promise<{ private async generatePullRequestText(cwd: string, baseRef?: string): Promise<{
title: string; title: string;
body: string; body: string;
}> { }> {
const diff = await getCheckoutDiff( const diff = await getCheckoutDiff(
agent.cwd, cwd,
{ {
mode: "base", mode: "base",
baseRef, baseRef,
@@ -2425,7 +2427,13 @@ export class Session {
try { try {
return await generateStructuredAgentResponse({ return await generateStructuredAgentResponse({
manager: this.agentManager, manager: this.agentManager,
agentConfig: this.buildEphemeralAgentConfig(agent, "PR generator"), agentConfig: {
provider: "claude",
model: AUTO_GEN_MODEL,
cwd,
title: "PR generator",
internal: true,
},
prompt, prompt,
schema, schema,
schemaName: "PullRequest", schemaName: "PullRequest",
@@ -2853,31 +2861,7 @@ export class Session {
private async handleCheckoutStatusRequest( private async handleCheckoutStatusRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_status_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_status_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
// 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: {
agentId,
cwd: "",
isGit: false,
repoRoot: null,
currentBranch: null,
isDirty: null,
baseRef: null,
aheadBehind: null,
hasRemote: false,
remoteUrl: null,
isPaseoOwnedWorktree: false,
error: { code: "UNKNOWN", message: `Agent not found and no cwd provided: ${agentId}` },
requestId,
},
});
return;
}
try { try {
const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome }); const status = await getCheckoutStatus(cwd, { paseoHome: this.paseoHome });
@@ -2885,7 +2869,6 @@ export class Session {
this.emit({ this.emit({
type: "checkout_status_response", type: "checkout_status_response",
payload: { payload: {
agentId,
cwd, cwd,
isGit: false, isGit: false,
repoRoot: null, repoRoot: null,
@@ -2893,6 +2876,7 @@ export class Session {
isDirty: null, isDirty: null,
baseRef: null, baseRef: null,
aheadBehind: null, aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false, hasRemote: false,
remoteUrl: null, remoteUrl: null,
isPaseoOwnedWorktree: false, isPaseoOwnedWorktree: false,
@@ -2907,7 +2891,6 @@ export class Session {
this.emit({ this.emit({
type: "checkout_status_response", type: "checkout_status_response",
payload: { payload: {
agentId,
cwd, cwd,
isGit: true, isGit: true,
repoRoot: status.repoRoot ?? null, repoRoot: status.repoRoot ?? null,
@@ -2915,6 +2898,7 @@ export class Session {
isDirty: status.isDirty ?? null, isDirty: status.isDirty ?? null,
baseRef: status.baseRef, baseRef: status.baseRef,
aheadBehind: status.aheadBehind ?? null, aheadBehind: status.aheadBehind ?? null,
aheadOfOrigin: status.aheadOfOrigin ?? null,
hasRemote: status.hasRemote, hasRemote: status.hasRemote,
remoteUrl: status.remoteUrl, remoteUrl: status.remoteUrl,
isPaseoOwnedWorktree: true, isPaseoOwnedWorktree: true,
@@ -2928,7 +2912,6 @@ export class Session {
this.emit({ this.emit({
type: "checkout_status_response", type: "checkout_status_response",
payload: { payload: {
agentId,
cwd, cwd,
isGit: true, isGit: true,
repoRoot: status.repoRoot ?? null, repoRoot: status.repoRoot ?? null,
@@ -2936,6 +2919,7 @@ export class Session {
isDirty: status.isDirty ?? null, isDirty: status.isDirty ?? null,
baseRef: status.baseRef ?? null, baseRef: status.baseRef ?? null,
aheadBehind: status.aheadBehind ?? null, aheadBehind: status.aheadBehind ?? null,
aheadOfOrigin: status.aheadOfOrigin ?? null,
hasRemote: status.hasRemote, hasRemote: status.hasRemote,
remoteUrl: status.remoteUrl, remoteUrl: status.remoteUrl,
isPaseoOwnedWorktree: false, isPaseoOwnedWorktree: false,
@@ -2947,7 +2931,6 @@ export class Session {
this.emit({ this.emit({
type: "checkout_status_response", type: "checkout_status_response",
payload: { payload: {
agentId,
cwd, cwd,
isGit: false, isGit: false,
repoRoot: null, repoRoot: null,
@@ -2955,6 +2938,7 @@ export class Session {
isDirty: null, isDirty: null,
baseRef: null, baseRef: null,
aheadBehind: null, aheadBehind: null,
aheadOfOrigin: null,
hasRemote: false, hasRemote: false,
remoteUrl: null, remoteUrl: null,
isPaseoOwnedWorktree: false, isPaseoOwnedWorktree: false,
@@ -2968,24 +2952,11 @@ export class Session {
private async handleCheckoutDiffRequest( private async handleCheckoutDiffRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_diff_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_diff_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId, compare } = msg; const { cwd, requestId, compare } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_diff_response",
payload: {
agentId,
files: [],
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
const diffResult = await getCheckoutDiff( const diffResult = await getCheckoutDiff(
agent.cwd, cwd,
{ {
mode: compare.mode, mode: compare.mode,
baseRef: compare.baseRef, baseRef: compare.baseRef,
@@ -2996,7 +2967,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_diff_response", type: "checkout_diff_response",
payload: { payload: {
agentId, cwd,
files: diffResult.structured ?? [], files: diffResult.structured ?? [],
error: null, error: null,
requestId, requestId,
@@ -3006,7 +2977,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_diff_response", type: "checkout_diff_response",
payload: { payload: {
agentId, cwd,
files: [], files: [],
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
@@ -3018,31 +2989,18 @@ export class Session {
private async handleCheckoutCommitRequest( private async handleCheckoutCommitRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_commit_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_commit_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_commit_response",
payload: {
agentId,
success: false,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
let message = msg.message?.trim() ?? ""; let message = msg.message?.trim() ?? "";
if (!message) { if (!message) {
message = await this.generateCommitMessage(agent); message = await this.generateCommitMessage(cwd);
} }
if (!message) { if (!message) {
throw new Error("Commit message is required"); throw new Error("Commit message is required");
} }
await commitChanges(agent.cwd, { await commitChanges(cwd, {
message, message,
addAll: msg.addAll ?? true, addAll: msg.addAll ?? true,
}); });
@@ -3050,7 +3008,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_commit_response", type: "checkout_commit_response",
payload: { payload: {
agentId, cwd,
success: true, success: true,
error: null, error: null,
requestId, requestId,
@@ -3060,7 +3018,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_commit_response", type: "checkout_commit_response",
payload: { payload: {
agentId, cwd,
success: false, success: false,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
@@ -3072,29 +3030,14 @@ export class Session {
private async handleCheckoutMergeRequest( private async handleCheckoutMergeRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_merge_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_merge_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_merge_response",
payload: {
agentId,
success: false,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
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) {
// `getCheckoutStatus` can return `isGit=false` in transient situations (e.g. cwd lookup
// running during other git operations). Double-check with git directly before failing.
try { try {
await execAsync("git rev-parse --is-inside-work-tree", { await execAsync("git rev-parse --is-inside-work-tree", {
cwd: agent.cwd, cwd,
env: READ_ONLY_GIT_ENV, env: READ_ONLY_GIT_ENV,
}); });
} catch (error) { } catch (error) {
@@ -3104,13 +3047,13 @@ export class Session {
: error instanceof Error : error instanceof Error
? error.message ? error.message
: String(error); : String(error);
throw new Error(`Not a git repository: ${agent.cwd}\n${details}`.trim()); throw new Error(`Not a git repository: ${cwd}\n${details}`.trim());
} }
} }
if (msg.requireCleanTarget) { if (msg.requireCleanTarget) {
const { stdout } = await execAsync("git status --porcelain", { const { stdout } = await execAsync("git status --porcelain", {
cwd: agent.cwd, cwd,
env: READ_ONLY_GIT_ENV, env: READ_ONLY_GIT_ENV,
}); });
if (stdout.trim().length > 0) { if (stdout.trim().length > 0) {
@@ -3127,7 +3070,7 @@ export class Session {
} }
await mergeToBase( await mergeToBase(
agent.cwd, cwd,
{ {
baseRef, baseRef,
mode: msg.strategy === "squash" ? "squash" : "merge", mode: msg.strategy === "squash" ? "squash" : "merge",
@@ -3138,7 +3081,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_merge_response", type: "checkout_merge_response",
payload: { payload: {
agentId, cwd,
success: true, success: true,
error: null, error: null,
requestId, requestId,
@@ -3148,7 +3091,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_merge_response", type: "checkout_merge_response",
payload: { payload: {
agentId, cwd,
success: false, success: false,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
@@ -3160,25 +3103,12 @@ export class Session {
private async handleCheckoutMergeFromBaseRequest( private async handleCheckoutMergeFromBaseRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_merge_from_base_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_merge_from_base_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_merge_from_base_response",
payload: {
agentId,
success: false,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
if (msg.requireCleanTarget ?? true) { if (msg.requireCleanTarget ?? true) {
const { stdout } = await execAsync("git status --porcelain", { const { stdout } = await execAsync("git status --porcelain", {
cwd: agent.cwd, cwd,
env: READ_ONLY_GIT_ENV, env: READ_ONLY_GIT_ENV,
}); });
if (stdout.trim().length > 0) { if (stdout.trim().length > 0) {
@@ -3187,7 +3117,7 @@ export class Session {
} }
await mergeFromBase( await mergeFromBase(
agent.cwd, cwd,
{ {
baseRef: msg.baseRef, baseRef: msg.baseRef,
requireCleanTarget: msg.requireCleanTarget ?? true, requireCleanTarget: msg.requireCleanTarget ?? true,
@@ -3197,7 +3127,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_merge_from_base_response", type: "checkout_merge_from_base_response",
payload: { payload: {
agentId, cwd,
success: true, success: true,
error: null, error: null,
requestId, requestId,
@@ -3207,7 +3137,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_merge_from_base_response", type: "checkout_merge_from_base_response",
payload: { payload: {
agentId, cwd,
success: false, success: false,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
@@ -3219,27 +3149,14 @@ export class Session {
private async handleCheckoutPushRequest( private async handleCheckoutPushRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_push_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_push_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_push_response",
payload: {
agentId,
success: false,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
await pushCurrentBranch(agent.cwd); await pushCurrentBranch(cwd);
this.emit({ this.emit({
type: "checkout_push_response", type: "checkout_push_response",
payload: { payload: {
agentId, cwd,
success: true, success: true,
error: null, error: null,
requestId, requestId,
@@ -3249,7 +3166,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_push_response", type: "checkout_push_response",
payload: { payload: {
agentId, cwd,
success: false, success: false,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
@@ -3261,39 +3178,19 @@ export class Session {
private async handleCheckoutPrCreateRequest( private async handleCheckoutPrCreateRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_pr_create_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_pr_create_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_pr_create_response",
payload: {
agentId,
url: null,
number: null,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
let title = msg.title?.trim() ?? ""; let title = msg.title?.trim() ?? "";
let body = msg.body?.trim() ?? ""; let body = msg.body?.trim() ?? "";
if (!title || !body) { if (!title || !body) {
const generated = await this.generatePullRequestText(agent, msg.baseRef); const generated = await this.generatePullRequestText(cwd, msg.baseRef);
if (!title) { if (!title) title = generated.title;
title = generated.title; if (!body) body = generated.body;
}
if (!body) {
body = generated.body;
}
}
if (!title) {
throw new Error("Pull request title is required");
} }
const result = await createPullRequest(agent.cwd, { const result = await createPullRequest(cwd, {
title, title,
body, body,
base: msg.baseRef, base: msg.baseRef,
@@ -3302,7 +3199,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_pr_create_response", type: "checkout_pr_create_response",
payload: { payload: {
agentId, cwd,
url: result.url ?? null, url: result.url ?? null,
number: result.number ?? null, number: result.number ?? null,
error: null, error: null,
@@ -3313,7 +3210,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_pr_create_response", type: "checkout_pr_create_response",
payload: { payload: {
agentId, cwd,
url: null, url: null,
number: null, number: null,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
@@ -3326,27 +3223,14 @@ export class Session {
private async handleCheckoutPrStatusRequest( private async handleCheckoutPrStatusRequest(
msg: Extract<SessionInboundMessage, { type: "checkout_pr_status_request" }> msg: Extract<SessionInboundMessage, { type: "checkout_pr_status_request" }>
): Promise<void> { ): Promise<void> {
const { agentId, requestId } = msg; const { cwd, requestId } = msg;
const agent = this.agentManager.getAgent(agentId);
if (!agent) {
this.emit({
type: "checkout_pr_status_response",
payload: {
agentId,
status: null,
error: { code: "UNKNOWN", message: `Agent not found: ${agentId}` },
requestId,
},
});
return;
}
try { try {
const status = await getPullRequestStatus(agent.cwd); const status = await getPullRequestStatus(cwd);
this.emit({ this.emit({
type: "checkout_pr_status_response", type: "checkout_pr_status_response",
payload: { payload: {
agentId, cwd,
status, status,
error: null, error: null,
requestId, requestId,
@@ -3356,7 +3240,7 @@ export class Session {
this.emit({ this.emit({
type: "checkout_pr_status_response", type: "checkout_pr_status_response",
payload: { payload: {
agentId, cwd,
status: null, status: null,
error: this.toCheckoutError(error), error: this.toCheckoutError(error),
requestId, requestId,
+18 -18
View File
@@ -476,22 +476,20 @@ 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(), cwd: 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(),
}); });
export const CheckoutDiffRequestSchema = z.object({ export const CheckoutDiffRequestSchema = z.object({
type: z.literal("checkout_diff_request"), type: z.literal("checkout_diff_request"),
agentId: z.string(), cwd: z.string(),
compare: CheckoutDiffCompareSchema, compare: CheckoutDiffCompareSchema,
requestId: z.string(), requestId: z.string(),
}); });
export const CheckoutCommitRequestSchema = z.object({ export const CheckoutCommitRequestSchema = z.object({
type: z.literal("checkout_commit_request"), type: z.literal("checkout_commit_request"),
agentId: z.string(), cwd: z.string(),
message: z.string().optional(), message: z.string().optional(),
addAll: z.boolean().optional(), addAll: z.boolean().optional(),
requestId: z.string(), requestId: z.string(),
@@ -499,7 +497,7 @@ export const CheckoutCommitRequestSchema = z.object({
export const CheckoutMergeRequestSchema = z.object({ export const CheckoutMergeRequestSchema = z.object({
type: z.literal("checkout_merge_request"), type: z.literal("checkout_merge_request"),
agentId: z.string(), cwd: z.string(),
baseRef: z.string().optional(), baseRef: z.string().optional(),
strategy: z.enum(["merge", "squash"]).optional(), strategy: z.enum(["merge", "squash"]).optional(),
requireCleanTarget: z.boolean().optional(), requireCleanTarget: z.boolean().optional(),
@@ -508,7 +506,7 @@ export const CheckoutMergeRequestSchema = z.object({
export const CheckoutMergeFromBaseRequestSchema = z.object({ export const CheckoutMergeFromBaseRequestSchema = z.object({
type: z.literal("checkout_merge_from_base_request"), type: z.literal("checkout_merge_from_base_request"),
agentId: z.string(), cwd: z.string(),
baseRef: z.string().optional(), baseRef: z.string().optional(),
requireCleanTarget: z.boolean().optional(), requireCleanTarget: z.boolean().optional(),
requestId: z.string(), requestId: z.string(),
@@ -516,13 +514,13 @@ export const CheckoutMergeFromBaseRequestSchema = z.object({
export const CheckoutPushRequestSchema = z.object({ export const CheckoutPushRequestSchema = z.object({
type: z.literal("checkout_push_request"), type: z.literal("checkout_push_request"),
agentId: z.string(), cwd: z.string(),
requestId: z.string(), requestId: z.string(),
}); });
export const CheckoutPrCreateRequestSchema = z.object({ export const CheckoutPrCreateRequestSchema = z.object({
type: z.literal("checkout_pr_create_request"), type: z.literal("checkout_pr_create_request"),
agentId: z.string(), cwd: z.string(),
title: z.string().optional(), title: z.string().optional(),
body: z.string().optional(), body: z.string().optional(),
baseRef: z.string().optional(), baseRef: z.string().optional(),
@@ -531,7 +529,7 @@ export const CheckoutPrCreateRequestSchema = z.object({
export const CheckoutPrStatusRequestSchema = z.object({ export const CheckoutPrStatusRequestSchema = z.object({
type: z.literal("checkout_pr_status_request"), type: z.literal("checkout_pr_status_request"),
agentId: z.string(), cwd: z.string(),
requestId: z.string(), requestId: z.string(),
}); });
@@ -1052,7 +1050,6 @@ const AheadBehindSchema = z.object({
}); });
const CheckoutStatusCommonSchema = z.object({ const CheckoutStatusCommonSchema = z.object({
agentId: z.string(),
cwd: z.string(), cwd: z.string(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1066,6 +1063,7 @@ const CheckoutStatusNotGitSchema = CheckoutStatusCommonSchema.extend({
isDirty: z.null(), isDirty: z.null(),
baseRef: z.null(), baseRef: z.null(),
aheadBehind: z.null(), aheadBehind: z.null(),
aheadOfOrigin: z.null(),
hasRemote: z.boolean(), hasRemote: z.boolean(),
remoteUrl: z.null(), remoteUrl: z.null(),
}); });
@@ -1078,6 +1076,7 @@ const CheckoutStatusGitNonPaseoSchema = CheckoutStatusCommonSchema.extend({
isDirty: z.boolean(), isDirty: z.boolean(),
baseRef: z.string().nullable(), baseRef: z.string().nullable(),
aheadBehind: AheadBehindSchema.nullable(), aheadBehind: AheadBehindSchema.nullable(),
aheadOfOrigin: z.number().nullable(),
hasRemote: z.boolean(), hasRemote: z.boolean(),
remoteUrl: z.string().nullable(), remoteUrl: z.string().nullable(),
}); });
@@ -1090,6 +1089,7 @@ const CheckoutStatusGitPaseoSchema = CheckoutStatusCommonSchema.extend({
isDirty: z.boolean(), isDirty: z.boolean(),
baseRef: z.string(), baseRef: z.string(),
aheadBehind: AheadBehindSchema.nullable(), aheadBehind: AheadBehindSchema.nullable(),
aheadOfOrigin: z.number().nullable(),
hasRemote: z.boolean(), hasRemote: z.boolean(),
remoteUrl: z.string().nullable(), remoteUrl: z.string().nullable(),
}); });
@@ -1106,7 +1106,7 @@ export const CheckoutStatusResponseSchema = z.object({
export const CheckoutDiffResponseSchema = z.object({ export const CheckoutDiffResponseSchema = z.object({
type: z.literal("checkout_diff_response"), type: z.literal("checkout_diff_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
files: z.array(ParsedDiffFileSchema), files: z.array(ParsedDiffFileSchema),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1116,7 +1116,7 @@ export const CheckoutDiffResponseSchema = z.object({
export const CheckoutCommitResponseSchema = z.object({ export const CheckoutCommitResponseSchema = z.object({
type: z.literal("checkout_commit_response"), type: z.literal("checkout_commit_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
success: z.boolean(), success: z.boolean(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1126,7 +1126,7 @@ export const CheckoutCommitResponseSchema = z.object({
export const CheckoutMergeResponseSchema = z.object({ export const CheckoutMergeResponseSchema = z.object({
type: z.literal("checkout_merge_response"), type: z.literal("checkout_merge_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
success: z.boolean(), success: z.boolean(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1136,7 +1136,7 @@ export const CheckoutMergeResponseSchema = z.object({
export const CheckoutMergeFromBaseResponseSchema = z.object({ export const CheckoutMergeFromBaseResponseSchema = z.object({
type: z.literal("checkout_merge_from_base_response"), type: z.literal("checkout_merge_from_base_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
success: z.boolean(), success: z.boolean(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1146,7 +1146,7 @@ export const CheckoutMergeFromBaseResponseSchema = z.object({
export const CheckoutPushResponseSchema = z.object({ export const CheckoutPushResponseSchema = z.object({
type: z.literal("checkout_push_response"), type: z.literal("checkout_push_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
success: z.boolean(), success: z.boolean(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
@@ -1156,7 +1156,7 @@ export const CheckoutPushResponseSchema = z.object({
export const CheckoutPrCreateResponseSchema = z.object({ export const CheckoutPrCreateResponseSchema = z.object({
type: z.literal("checkout_pr_create_response"), type: z.literal("checkout_pr_create_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
url: z.string().nullable(), url: z.string().nullable(),
number: z.number().nullable(), number: z.number().nullable(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
@@ -1175,7 +1175,7 @@ const CheckoutPrStatusSchema = z.object({
export const CheckoutPrStatusResponseSchema = z.object({ export const CheckoutPrStatusResponseSchema = z.object({
type: z.literal("checkout_pr_status_response"), type: z.literal("checkout_pr_status_response"),
payload: z.object({ payload: z.object({
agentId: z.string(), cwd: z.string(),
status: CheckoutPrStatusSchema.nullable(), status: CheckoutPrStatusSchema.nullable(),
error: CheckoutErrorSchema.nullable(), error: CheckoutErrorSchema.nullable(),
requestId: z.string(), requestId: z.string(),
+22
View File
@@ -70,6 +70,7 @@ export type CheckoutStatusGitNonPaseo = {
isDirty: boolean; isDirty: boolean;
baseRef: string | null; baseRef: string | null;
aheadBehind: AheadBehind | null; aheadBehind: AheadBehind | null;
aheadOfOrigin: number | null;
hasRemote: boolean; hasRemote: boolean;
remoteUrl: string | null; remoteUrl: string | null;
isPaseoOwnedWorktree: false; isPaseoOwnedWorktree: false;
@@ -82,6 +83,7 @@ export type CheckoutStatusGitPaseo = {
isDirty: boolean; isDirty: boolean;
baseRef: string; baseRef: string;
aheadBehind: AheadBehind | null; aheadBehind: AheadBehind | null;
aheadOfOrigin: number | null;
hasRemote: boolean; hasRemote: boolean;
remoteUrl: string | null; remoteUrl: string | null;
isPaseoOwnedWorktree: true; isPaseoOwnedWorktree: true;
@@ -393,6 +395,22 @@ async function getAheadBehind(cwd: string, baseRef: string, currentBranch: strin
return { ahead, behind }; return { ahead, behind };
} }
async function getAheadOfOrigin(cwd: string, currentBranch: string): Promise<number | null> {
if (!currentBranch) {
return null;
}
try {
const { stdout } = await execAsync(
`git rev-list --count origin/${currentBranch}..${currentBranch}`,
{ cwd, env: READ_ONLY_GIT_ENV }
);
const count = Number.parseInt(stdout.trim(), 10);
return Number.isNaN(count) ? null : count;
} catch {
return null;
}
}
async function getUntrackedDiff(cwd: string): Promise<string> { async function getUntrackedDiff(cwd: string): Promise<string> {
let untrackedDiff = ""; let untrackedDiff = "";
try { try {
@@ -443,6 +461,8 @@ export async function getCheckoutStatus(
const baseRef = configured.baseRef ?? (await resolveBaseRef(repoInfo.path)); const baseRef = configured.baseRef ?? (await resolveBaseRef(repoInfo.path));
const aheadBehind = const aheadBehind =
baseRef && currentBranch ? await getAheadBehind(cwd, baseRef, currentBranch) : null; baseRef && currentBranch ? await getAheadBehind(cwd, baseRef, currentBranch) : null;
const aheadOfOrigin =
hasRemote && currentBranch ? await getAheadOfOrigin(cwd, currentBranch) : null;
if (configured.isPaseoOwnedWorktree) { if (configured.isPaseoOwnedWorktree) {
return { return {
@@ -452,6 +472,7 @@ export async function getCheckoutStatus(
isDirty, isDirty,
baseRef: configured.baseRef, baseRef: configured.baseRef,
aheadBehind, aheadBehind,
aheadOfOrigin,
hasRemote, hasRemote,
remoteUrl, remoteUrl,
isPaseoOwnedWorktree: true, isPaseoOwnedWorktree: true,
@@ -465,6 +486,7 @@ export async function getCheckoutStatus(
isDirty, isDirty,
baseRef, baseRef,
aheadBehind, aheadBehind,
aheadOfOrigin,
hasRemote, hasRemote,
remoteUrl, remoteUrl,
isPaseoOwnedWorktree: false, isPaseoOwnedWorktree: false,