consolidate checkout status handling and improve git detection
This commit is contained in:
@@ -791,7 +791,11 @@ export function GitDiffPane({ serverId, agentId, cwd }: GitDiffPaneProps) {
|
|||||||
(isDiffError && diffError instanceof Error ? diffError.message : null);
|
(isDiffError && diffError instanceof Error ? diffError.message : null);
|
||||||
const prErrorMessage = prPayloadError?.message ?? null;
|
const prErrorMessage = prPayloadError?.message ?? null;
|
||||||
const branchLabel =
|
const branchLabel =
|
||||||
gitStatus?.currentBranch ?? (notGit ? "Not a git repository" : "Unknown");
|
gitStatus?.currentBranch && gitStatus.currentBranch !== "HEAD"
|
||||||
|
? 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 aheadOfOrigin = gitStatus?.aheadOfOrigin ?? 0;
|
||||||
|
|||||||
@@ -440,7 +440,11 @@ export function GroupedAgentList({
|
|||||||
});
|
});
|
||||||
const checkout = checkoutQuery.data ?? null;
|
const checkout = checkoutQuery.data ?? null;
|
||||||
const activeBranchLabel = checkout?.isGit
|
const activeBranchLabel = checkout?.isGit
|
||||||
? (checkout.currentBranch ?? checkout.baseRef ?? "git")
|
? ((checkout.currentBranch && checkout.currentBranch !== "HEAD"
|
||||||
|
? checkout.currentBranch
|
||||||
|
: null) ??
|
||||||
|
checkout.baseRef ??
|
||||||
|
"git")
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
const canArchive = !isRunning && !agent.requiresAttention;
|
const canArchive = !isRunning && !agent.requiresAttention;
|
||||||
|
|||||||
@@ -39,7 +39,7 @@ describe("useCheckoutStatusQuery", () => {
|
|||||||
|
|
||||||
describe("nextCheckoutStatusRefetchDecision", () => {
|
describe("nextCheckoutStatusRefetchDecision", () => {
|
||||||
it("refetches only once per key until reset", () => {
|
it("refetches only once per key until reset", () => {
|
||||||
const key = "daemon-1:agent-1";
|
const key = "daemon-1:/path/to/project";
|
||||||
|
|
||||||
expect(nextCheckoutStatusRefetchDecision(null, key)).toEqual({
|
expect(nextCheckoutStatusRefetchDecision(null, key)).toEqual({
|
||||||
nextSeenKey: key,
|
nextSeenKey: key,
|
||||||
@@ -62,11 +62,11 @@ describe("useCheckoutStatusQuery", () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("refetches again when agent changes while active", () => {
|
it("refetches again when cwd changes while active", () => {
|
||||||
expect(
|
expect(
|
||||||
nextCheckoutStatusRefetchDecision("daemon-1:agent-1", "daemon-1:agent-2")
|
nextCheckoutStatusRefetchDecision("daemon-1:/path/a", "daemon-1:/path/b")
|
||||||
).toEqual({
|
).toEqual({
|
||||||
nextSeenKey: "daemon-1:agent-2",
|
nextSeenKey: "daemon-1:/path/b",
|
||||||
shouldRefetch: true,
|
shouldRefetch: true,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -12,7 +12,6 @@ import { useRouter } from "expo-router";
|
|||||||
import { useFocusEffect } from "@react-navigation/native";
|
import { useFocusEffect } from "@react-navigation/native";
|
||||||
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
import { useSafeAreaInsets } from "react-native-safe-area-context";
|
||||||
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
|
||||||
import ReanimatedAnimated, {
|
import ReanimatedAnimated, {
|
||||||
useAnimatedStyle,
|
useAnimatedStyle,
|
||||||
useSharedValue,
|
useSharedValue,
|
||||||
@@ -72,9 +71,6 @@ import {
|
|||||||
const DROPDOWN_WIDTH = 220;
|
const DROPDOWN_WIDTH = 220;
|
||||||
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
const EMPTY_STREAM_ITEMS: StreamItem[] = [];
|
||||||
|
|
||||||
|
|
||||||
type BranchStatus = "idle" | "loading" | "ready" | "error";
|
|
||||||
|
|
||||||
export function AgentReadyScreen({
|
export function AgentReadyScreen({
|
||||||
serverId,
|
serverId,
|
||||||
agentId,
|
agentId,
|
||||||
@@ -372,43 +368,6 @@ function AgentScreenContent({
|
|||||||
const agentModel = extractAgentModel(agent);
|
const agentModel = extractAgentModel(agent);
|
||||||
const modelDisplayValue = agentModel ?? "Unknown";
|
const modelDisplayValue = agentModel ?? "Unknown";
|
||||||
|
|
||||||
const repoInfoQuery = useQuery({
|
|
||||||
queryKey: ["checkoutStatus", serverId, agent?.cwd ?? ""],
|
|
||||||
queryFn: async () => {
|
|
||||||
if (!client) {
|
|
||||||
throw new Error("Daemon client unavailable");
|
|
||||||
}
|
|
||||||
const payload = await client.getCheckoutStatus(agent?.cwd ?? ".");
|
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error.message);
|
|
||||||
}
|
|
||||||
return {
|
|
||||||
cwd: payload.cwd,
|
|
||||||
currentBranch: payload.currentBranch ?? null,
|
|
||||||
};
|
|
||||||
},
|
|
||||||
enabled: Boolean(client && isConnected && agent?.cwd),
|
|
||||||
retry: false,
|
|
||||||
});
|
|
||||||
const { refetch: refetchRepoInfo } = repoInfoQuery;
|
|
||||||
const branchStatus: BranchStatus = !agent?.cwd
|
|
||||||
? "idle"
|
|
||||||
: repoInfoQuery.isPending || repoInfoQuery.isFetching
|
|
||||||
? "loading"
|
|
||||||
: repoInfoQuery.isError
|
|
||||||
? "error"
|
|
||||||
: repoInfoQuery.isSuccess
|
|
||||||
? "ready"
|
|
||||||
: "idle";
|
|
||||||
const branchLabel = repoInfoQuery.data?.currentBranch ?? null;
|
|
||||||
const branchError = repoInfoQuery.error instanceof Error
|
|
||||||
? repoInfoQuery.error.message
|
|
||||||
: null;
|
|
||||||
const branchDisplayValue =
|
|
||||||
branchStatus === "error"
|
|
||||||
? branchError ?? "Unavailable"
|
|
||||||
: branchLabel ?? "Unknown";
|
|
||||||
|
|
||||||
// Checkout status for header subtitle
|
// Checkout status for header subtitle
|
||||||
const checkoutStatusQuery = useCheckoutStatusQuery({
|
const checkoutStatusQuery = useCheckoutStatusQuery({
|
||||||
serverId,
|
serverId,
|
||||||
@@ -648,7 +607,7 @@ function AgentScreenContent({
|
|||||||
<DropdownMenu
|
<DropdownMenu
|
||||||
onOpenChange={(open) => {
|
onOpenChange={(open) => {
|
||||||
if (open && agent?.cwd) {
|
if (open && agent?.cwd) {
|
||||||
refetchRepoInfo().catch(() => {});
|
checkoutStatusQuery.refresh().catch(() => {});
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
@@ -679,31 +638,30 @@ function AgentScreenContent({
|
|||||||
</Text>
|
</Text>
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<View style={styles.menuMetaRow}>
|
{checkout?.isGit && checkout.currentBranch && checkout.currentBranch !== "HEAD" ? (
|
||||||
<Text style={styles.menuMetaLabel}>Branch</Text>
|
<View style={styles.menuMetaRow}>
|
||||||
<View style={styles.menuMetaValueRow}>
|
<Text style={styles.menuMetaLabel}>Branch</Text>
|
||||||
{branchStatus === "loading" ? (
|
<View style={styles.menuMetaValueRow}>
|
||||||
<>
|
{checkoutStatusQuery.isFetching ? (
|
||||||
<ActivityIndicator
|
<>
|
||||||
size="small"
|
<ActivityIndicator
|
||||||
color={theme.colors.foregroundMuted}
|
size="small"
|
||||||
/>
|
color={theme.colors.foregroundMuted}
|
||||||
<Text style={styles.menuMetaPendingText}>Fetching…</Text>
|
/>
|
||||||
</>
|
<Text style={styles.menuMetaPendingText}>Fetching…</Text>
|
||||||
) : (
|
</>
|
||||||
<Text
|
) : (
|
||||||
style={[
|
<Text
|
||||||
styles.menuMetaValue,
|
style={styles.menuMetaValue}
|
||||||
branchStatus === "error" ? styles.menuMetaValueError : null,
|
numberOfLines={1}
|
||||||
]}
|
ellipsizeMode="middle"
|
||||||
numberOfLines={1}
|
>
|
||||||
ellipsizeMode="middle"
|
{checkout.currentBranch}
|
||||||
>
|
</Text>
|
||||||
{branchDisplayValue}
|
)}
|
||||||
</Text>
|
</View>
|
||||||
)}
|
|
||||||
</View>
|
</View>
|
||||||
</View>
|
) : null}
|
||||||
</View>
|
</View>
|
||||||
|
|
||||||
<DropdownMenuSeparator />
|
<DropdownMenuSeparator />
|
||||||
|
|||||||
@@ -27,6 +27,10 @@ import {
|
|||||||
import { FileDropZone } from "@/components/file-drop-zone";
|
import { FileDropZone } from "@/components/file-drop-zone";
|
||||||
import { useQuery } from "@tanstack/react-query";
|
import { useQuery } from "@tanstack/react-query";
|
||||||
import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
import { useAgentFormState, type CreateAgentInitialValues } from "@/hooks/use-agent-form-state";
|
||||||
|
import {
|
||||||
|
CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
checkoutStatusQueryKey,
|
||||||
|
} from "@/hooks/use-checkout-status-query";
|
||||||
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
import { useDaemonConnections } from "@/contexts/daemon-connections-context";
|
||||||
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
import { useDaemonRegistry } from "@/contexts/daemon-registry-context";
|
||||||
import { formatConnectionStatus } from "@/utils/daemons";
|
import { formatConnectionStatus } from "@/utils/daemons";
|
||||||
@@ -309,82 +313,62 @@ export function DraftAgentScreen({
|
|||||||
"Repository details will load automatically once the selected host is back online."
|
"Repository details will load automatically once the selected host is back online."
|
||||||
: null;
|
: null;
|
||||||
|
|
||||||
type RepoInfoState = {
|
const checkoutStatusQuery = useQuery({
|
||||||
cwd: string;
|
queryKey: checkoutStatusQueryKey(selectedServerId ?? "", trimmedWorkingDir),
|
||||||
repoRoot: string;
|
|
||||||
currentBranch: string | null;
|
|
||||||
isDirty: boolean;
|
|
||||||
};
|
|
||||||
const repoInfoQuery = useQuery({
|
|
||||||
queryKey: ["checkoutStatus", selectedServerId, trimmedWorkingDir],
|
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
const client = sessionClient;
|
const client = sessionClient;
|
||||||
if (!client) {
|
if (!client) {
|
||||||
throw new Error("Daemon client unavailable");
|
throw new Error("Daemon client unavailable");
|
||||||
}
|
}
|
||||||
const payload = await client.getCheckoutStatus(trimmedWorkingDir || ".");
|
return await client.getCheckoutStatus(trimmedWorkingDir);
|
||||||
if (payload.error) {
|
|
||||||
throw new Error(payload.error.message);
|
|
||||||
}
|
|
||||||
if (!payload.isGit) {
|
|
||||||
throw new Error("Not a git repository");
|
|
||||||
}
|
|
||||||
// After the isGit check, TypeScript knows we have a git repo
|
|
||||||
return {
|
|
||||||
cwd: payload.cwd,
|
|
||||||
repoRoot: payload.repoRoot,
|
|
||||||
currentBranch: payload.currentBranch,
|
|
||||||
isDirty: payload.isDirty,
|
|
||||||
};
|
|
||||||
},
|
},
|
||||||
enabled:
|
enabled:
|
||||||
|
Boolean(selectedServerId) &&
|
||||||
Boolean(trimmedWorkingDir) &&
|
Boolean(trimmedWorkingDir) &&
|
||||||
!repoAvailabilityError &&
|
!repoAvailabilityError &&
|
||||||
Boolean(sessionClient) &&
|
Boolean(sessionClient) &&
|
||||||
isConnected,
|
isConnected,
|
||||||
retry: false,
|
retry: false,
|
||||||
|
staleTime: CHECKOUT_STATUS_STALE_TIME,
|
||||||
|
refetchOnMount: "always",
|
||||||
});
|
});
|
||||||
const repoInfo = repoInfoQuery.data ?? null;
|
|
||||||
const refetchRepoInfo = repoInfoQuery.refetch;
|
const checkout = checkoutStatusQuery.data ?? null;
|
||||||
const repoRequestError = repoInfoQuery.error as Error | null;
|
const refetchCheckoutStatus = checkoutStatusQuery.refetch;
|
||||||
const repoRequestStatus: "idle" | "loading" | "success" | "error" =
|
const checkoutQueryError =
|
||||||
!shouldInspectRepo || repoAvailabilityError
|
checkoutStatusQuery.error instanceof Error ? checkoutStatusQuery.error.message : null;
|
||||||
? "idle"
|
const checkoutPayloadError = checkout?.error ? checkout.error.message : null;
|
||||||
: repoInfoQuery.isPending || repoInfoQuery.isFetching
|
|
||||||
? "loading"
|
|
||||||
: repoInfoQuery.isError
|
|
||||||
? "error"
|
|
||||||
: repoInfoQuery.isSuccess
|
|
||||||
? "success"
|
|
||||||
: "idle";
|
|
||||||
const isNonGitDirectory =
|
const isNonGitDirectory =
|
||||||
repoRequestStatus === "error" &&
|
Boolean(trimmedWorkingDir) &&
|
||||||
/not in a git repository|not a git repository/i.test(repoRequestError?.message ?? "");
|
checkoutStatusQuery.isSuccess &&
|
||||||
|
checkout?.isGit === false &&
|
||||||
|
checkout?.error == null;
|
||||||
|
|
||||||
const isDirectoryNotExists =
|
const isDirectoryNotExists =
|
||||||
repoRequestStatus === "error" &&
|
checkoutStatusQuery.isError &&
|
||||||
/does not exist|no such file or directory|ENOENT/i.test(repoRequestError?.message ?? "");
|
/does not exist|no such file or directory|ENOENT/i.test(checkoutQueryError ?? "");
|
||||||
|
|
||||||
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
|
const repoInfoStatus: "idle" | "loading" | "ready" | "error" = !shouldInspectRepo
|
||||||
? "idle"
|
? "idle"
|
||||||
: repoAvailabilityError
|
: repoAvailabilityError
|
||||||
? "error"
|
? "error"
|
||||||
: repoRequestStatus === "loading"
|
: checkoutStatusQuery.isPending || checkoutStatusQuery.isFetching
|
||||||
? "loading"
|
? "loading"
|
||||||
: repoRequestStatus === "error"
|
: checkoutStatusQuery.isError || Boolean(checkoutPayloadError)
|
||||||
? isNonGitDirectory
|
? "error"
|
||||||
? "idle"
|
: checkout?.isGit
|
||||||
: "error"
|
|
||||||
: repoRequestStatus === "success"
|
|
||||||
? "ready"
|
? "ready"
|
||||||
: "idle";
|
: "idle";
|
||||||
|
|
||||||
const repoInfoError =
|
const repoInfoError =
|
||||||
repoAvailabilityError ?? (isNonGitDirectory ? null : repoRequestError?.message ?? null);
|
repoAvailabilityError ??
|
||||||
const gitHelperText = isNonGitDirectory
|
(checkoutStatusQuery.isError ? checkoutQueryError : null) ??
|
||||||
? "No git repository detected. Git options are disabled for this directory."
|
checkoutPayloadError;
|
||||||
: null;
|
|
||||||
const isCreateWorktree = worktreeMode === "create";
|
const isCreateWorktree = worktreeMode === "create";
|
||||||
const isAttachWorktree = worktreeMode === "attach";
|
const isAttachWorktree = worktreeMode === "attach";
|
||||||
|
|
||||||
const worktreeListRoot = repoInfo?.repoRoot ?? trimmedWorkingDir;
|
const worktreeListRoot = checkout?.repoRoot ?? trimmedWorkingDir;
|
||||||
const worktreeListQuery = useQuery({
|
const worktreeListQuery = useQuery({
|
||||||
queryKey: ["paseoWorktreeList", selectedServerId, worktreeListRoot],
|
queryKey: ["paseoWorktreeList", selectedServerId, worktreeListRoot],
|
||||||
queryFn: async () => {
|
queryFn: async () => {
|
||||||
@@ -446,10 +430,10 @@ export function DraftAgentScreen({
|
|||||||
setSelectedWorktreePath("");
|
setSelectedWorktreePath("");
|
||||||
}
|
}
|
||||||
if (mode !== "none") {
|
if (mode !== "none") {
|
||||||
refetchRepoInfo();
|
refetchCheckoutStatus();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
[worktreeSlug, refetchRepoInfo]
|
[worktreeSlug, refetchCheckoutStatus]
|
||||||
);
|
);
|
||||||
|
|
||||||
const validateWorktreeName = useCallback(
|
const validateWorktreeName = useCallback(
|
||||||
@@ -567,11 +551,14 @@ export function DraftAgentScreen({
|
|||||||
if (baseBranch) {
|
if (baseBranch) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const current = repoInfo?.currentBranch?.trim();
|
const current = checkout?.isGit ? checkout.currentBranch?.trim() : null;
|
||||||
|
if (!current || current === "HEAD") {
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (current) {
|
if (current) {
|
||||||
setBaseBranch(current);
|
setBaseBranch(current);
|
||||||
}
|
}
|
||||||
}, [isCreateWorktree, isNonGitDirectory, baseBranch, repoInfo?.currentBranch]);
|
}, [isCreateWorktree, isNonGitDirectory, baseBranch, checkout]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (isNonGitDirectory && worktreeMode !== "none") {
|
if (isNonGitDirectory && worktreeMode !== "none") {
|
||||||
@@ -787,7 +774,6 @@ export function DraftAgentScreen({
|
|||||||
baseBranch,
|
baseBranch,
|
||||||
worktreeSlug,
|
worktreeSlug,
|
||||||
selectedWorktreePath,
|
selectedWorktreePath,
|
||||||
repoInfo?.currentBranch,
|
|
||||||
gitBlockingError,
|
gitBlockingError,
|
||||||
baseBranchError,
|
baseBranchError,
|
||||||
isDirectoryNotExists,
|
isDirectoryNotExists,
|
||||||
@@ -891,7 +877,7 @@ export function DraftAgentScreen({
|
|||||||
worktreeMode={worktreeMode}
|
worktreeMode={worktreeMode}
|
||||||
onWorktreeModeChange={handleWorktreeModeChange}
|
onWorktreeModeChange={handleWorktreeModeChange}
|
||||||
worktreeSlug={worktreeSlug}
|
worktreeSlug={worktreeSlug}
|
||||||
currentBranch={repoInfo?.currentBranch ?? null}
|
currentBranch={checkout?.isGit ? checkout.currentBranch ?? null : null}
|
||||||
baseBranch={baseBranch}
|
baseBranch={baseBranch}
|
||||||
onBaseBranchChange={handleBaseBranchChange}
|
onBaseBranchChange={handleBaseBranchChange}
|
||||||
status={repoInfoStatus}
|
status={repoInfoStatus}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@ export function deriveBranchLabel(
|
|||||||
if (!currentBranch) {
|
if (!currentBranch) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
if (currentBranch === "HEAD") {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
if (baseRef && currentBranch === baseRef) {
|
if (baseRef && currentBranch === baseRef) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -219,6 +219,65 @@ describe("parseToolCallDisplay - apply_patch (Codex)", () => {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test("parses apply_patch with kind object (type/move_path) into edit type", () => {
|
||||||
|
const input = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "/Users/moboudra/-paseo/worktrees/paseo/naive-zebra/packages/server/src/server/daemon-keypair.ts",
|
||||||
|
kind: { type: "update", move_path: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "/Users/moboudra/-paseo/worktrees/paseo/naive-zebra/packages/server/src/server/daemon-keypair.ts",
|
||||||
|
patch: "@@ -1,1 +1,1 @@\n-foo\n+bar",
|
||||||
|
kind: { type: "update", move_path: null },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const display = parseToolCallDisplay("apply_patch", input, result);
|
||||||
|
expect(display.type).toBe("edit");
|
||||||
|
expect(display.toolName).toBe("Edit");
|
||||||
|
if (display.type === "edit") {
|
||||||
|
expect(display.filePath).toBe(
|
||||||
|
"/Users/moboudra/-paseo/worktrees/paseo/naive-zebra/packages/server/src/server/daemon-keypair.ts"
|
||||||
|
);
|
||||||
|
expect(display.unifiedDiff).toBe("@@ -1,1 +1,1 @@\n-foo\n+bar");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
test("prefers move_path for display but still finds patch by original path", () => {
|
||||||
|
const input = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "/some/old-path.txt",
|
||||||
|
kind: { type: "update", move_path: "/some/new-path.txt" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
};
|
||||||
|
const result = {
|
||||||
|
files: [
|
||||||
|
{
|
||||||
|
path: "/some/old-path.txt",
|
||||||
|
patch: "@@ -1,1 +1,1 @@\n-old\n+new",
|
||||||
|
kind: { type: "update", move_path: "/some/new-path.txt" },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
success: true,
|
||||||
|
};
|
||||||
|
|
||||||
|
const display = parseToolCallDisplay("apply_patch", input, result);
|
||||||
|
expect(display.type).toBe("edit");
|
||||||
|
if (display.type === "edit") {
|
||||||
|
expect(display.filePath).toBe("/some/new-path.txt");
|
||||||
|
expect(display.unifiedDiff).toBe("@@ -1,1 +1,1 @@\n-old\n+new");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test("parses pending apply_patch (no result yet)", () => {
|
test("parses pending apply_patch (no result yet)", () => {
|
||||||
const input = {
|
const input = {
|
||||||
files: [
|
files: [
|
||||||
|
|||||||
@@ -1041,20 +1041,42 @@ const EditToolCallSchema = z
|
|||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// Codex apply_patch input: { files: [{ path: string, kind: string }] }
|
const ApplyPatchFileKindSchema = z
|
||||||
|
.union([
|
||||||
|
z.string(),
|
||||||
|
z
|
||||||
|
.object({
|
||||||
|
type: z.string().optional(),
|
||||||
|
move_path: z.string().nullable().optional(),
|
||||||
|
movePath: z.string().nullable().optional(),
|
||||||
|
})
|
||||||
|
.passthrough(),
|
||||||
|
])
|
||||||
|
.optional();
|
||||||
|
|
||||||
|
function getApplyPatchMovePath(kind: unknown): string | undefined {
|
||||||
|
if (!kind || typeof kind !== "object") return undefined;
|
||||||
|
const record = kind as Record<string, unknown>;
|
||||||
|
const movePath = typeof record.movePath === "string" ? record.movePath : undefined;
|
||||||
|
const movePathSnake =
|
||||||
|
typeof record.move_path === "string" ? record.move_path : undefined;
|
||||||
|
return movePath ?? movePathSnake ?? undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Codex apply_patch input: { files: [{ path: string, kind: string | object }] }
|
||||||
const ApplyPatchInputSchema = z.object({
|
const ApplyPatchInputSchema = z.object({
|
||||||
files: z.array(z.object({
|
files: z.array(z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
kind: z.string().optional(),
|
kind: ApplyPatchFileKindSchema,
|
||||||
})).min(1),
|
})).min(1),
|
||||||
}).passthrough();
|
}).passthrough();
|
||||||
|
|
||||||
// Codex apply_patch result: { files: [{ path: string, patch: string, kind: string }], message: string, success: boolean }
|
// Codex apply_patch result: { files: [{ path: string, patch: string, kind: string | object }], message: string, success: boolean }
|
||||||
const ApplyPatchResultSchema = z.object({
|
const ApplyPatchResultSchema = z.object({
|
||||||
files: z.array(z.object({
|
files: z.array(z.object({
|
||||||
path: z.string(),
|
path: z.string(),
|
||||||
patch: z.string().optional(),
|
patch: z.string().optional(),
|
||||||
kind: z.string().optional(),
|
kind: ApplyPatchFileKindSchema,
|
||||||
})).optional(),
|
})).optional(),
|
||||||
message: z.string().optional(),
|
message: z.string().optional(),
|
||||||
success: z.boolean().optional(),
|
success: z.boolean().optional(),
|
||||||
@@ -1068,13 +1090,19 @@ const ApplyPatchToolCallSchema = z
|
|||||||
})
|
})
|
||||||
.transform((data): { type: "edit"; filePath: string; oldString: string; newString: string; unifiedDiff?: string } => {
|
.transform((data): { type: "edit"; filePath: string; oldString: string; newString: string; unifiedDiff?: string } => {
|
||||||
const firstFile = data.input.files[0];
|
const firstFile = data.input.files[0];
|
||||||
const filePath = firstFile.path;
|
const movePath = getApplyPatchMovePath(firstFile.kind);
|
||||||
|
const filePath = movePath ?? firstFile.path;
|
||||||
|
|
||||||
// Try to get the patch from the result
|
// Try to get the patch from the result
|
||||||
const resultParsed = ApplyPatchResultSchema.safeParse(data.result);
|
const resultParsed = ApplyPatchResultSchema.safeParse(data.result);
|
||||||
let unifiedDiff: string | undefined;
|
let unifiedDiff: string | undefined;
|
||||||
if (resultParsed.success && resultParsed.data.files) {
|
if (resultParsed.success && resultParsed.data.files) {
|
||||||
const resultFile = resultParsed.data.files.find(f => f.path === filePath) ?? resultParsed.data.files[0];
|
const matchPaths = new Set<string>([firstFile.path]);
|
||||||
|
if (movePath) matchPaths.add(movePath);
|
||||||
|
|
||||||
|
const resultFile =
|
||||||
|
resultParsed.data.files.find((f) => matchPaths.has(f.path)) ??
|
||||||
|
resultParsed.data.files[0];
|
||||||
unifiedDiff = resultFile?.patch;
|
unifiedDiff = resultFile?.patch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,10 @@ import { mkdtempSync, readFileSync, rmSync, writeFileSync, mkdirSync, existsSync
|
|||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
import path from "node:path";
|
import path from "node:path";
|
||||||
|
|
||||||
import { CodexAppServerAgentClient } from "./codex-app-server-agent.js";
|
import {
|
||||||
|
CodexAppServerAgentClient,
|
||||||
|
codexAppServerTurnInputFromPrompt,
|
||||||
|
} from "./codex-app-server-agent.js";
|
||||||
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
import { createTestLogger } from "../../../test-utils/test-logger.js";
|
||||||
import type {
|
import type {
|
||||||
AgentPermissionRequest,
|
AgentPermissionRequest,
|
||||||
@@ -14,6 +17,8 @@ import type {
|
|||||||
|
|
||||||
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
|
const CODEX_TEST_MODEL = "gpt-5.1-codex-mini";
|
||||||
const CODEX_TEST_REASONING_EFFORT = "low";
|
const CODEX_TEST_REASONING_EFFORT = "low";
|
||||||
|
const ONE_BY_ONE_PNG_BASE64 =
|
||||||
|
"iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mP8/x8AAwMCAO+X1r0AAAAASUVORK5CYII=";
|
||||||
|
|
||||||
function isCodexInstalled(): boolean {
|
function isCodexInstalled(): boolean {
|
||||||
try {
|
try {
|
||||||
@@ -64,12 +69,57 @@ function hasApplyPatchFile(item: AgentTimelineItem, fileName: string): boolean {
|
|||||||
describe("Codex app-server provider (integration)", () => {
|
describe("Codex app-server provider (integration)", () => {
|
||||||
const logger = createTestLogger();
|
const logger = createTestLogger();
|
||||||
|
|
||||||
|
test("maps image prompt blocks to Codex localImage input", async () => {
|
||||||
|
const input = await codexAppServerTurnInputFromPrompt(
|
||||||
|
[
|
||||||
|
{ type: "text", text: "hello" },
|
||||||
|
{ type: "image", mimeType: "image/png", data: ONE_BY_ONE_PNG_BASE64 },
|
||||||
|
],
|
||||||
|
logger
|
||||||
|
);
|
||||||
|
const localImage = input.find((item) => (item as any)?.type === "localImage") as
|
||||||
|
| { type: "localImage"; path?: string }
|
||||||
|
| undefined;
|
||||||
|
expect(localImage?.path).toBeTypeOf("string");
|
||||||
|
if (localImage?.path) {
|
||||||
|
expect(existsSync(localImage.path)).toBe(true);
|
||||||
|
rmSync(localImage.path, { force: true });
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())("listModels returns live Codex models", async () => {
|
test.runIf(isCodexInstalled())("listModels returns live Codex models", async () => {
|
||||||
const client = new CodexAppServerAgentClient(logger);
|
const client = new CodexAppServerAgentClient(logger);
|
||||||
const models = await client.listModels();
|
const models = await client.listModels();
|
||||||
expect(models.some((model) => model.id.includes("gpt-5.1-codex"))).toBe(true);
|
expect(models.some((model) => model.id.includes("gpt-5.1-codex"))).toBe(true);
|
||||||
}, 30000);
|
}, 30000);
|
||||||
|
|
||||||
|
test.runIf(isCodexInstalled())("accepts image prompt blocks without request validation errors", async () => {
|
||||||
|
const cleanup = useTempCodexSessionDir();
|
||||||
|
const cwd = tmpCwd("codex-image-prompt-");
|
||||||
|
|
||||||
|
try {
|
||||||
|
const client = new CodexAppServerAgentClient(logger);
|
||||||
|
const session = await client.createSession({
|
||||||
|
provider: "codex",
|
||||||
|
cwd,
|
||||||
|
modeId: "auto",
|
||||||
|
model: CODEX_TEST_MODEL,
|
||||||
|
reasoningEffort: CODEX_TEST_REASONING_EFFORT,
|
||||||
|
});
|
||||||
|
|
||||||
|
const result = await session.run([
|
||||||
|
{ type: "text", text: "Reply with exactly: OK." },
|
||||||
|
{ type: "image", mimeType: "image/png", data: ONE_BY_ONE_PNG_BASE64 },
|
||||||
|
] satisfies AgentPromptContentBlock[]);
|
||||||
|
await session.close();
|
||||||
|
|
||||||
|
expect(result.finalText).toContain("OK");
|
||||||
|
} finally {
|
||||||
|
cleanup();
|
||||||
|
rmSync(cwd, { recursive: true, force: true });
|
||||||
|
}
|
||||||
|
}, 60000);
|
||||||
|
|
||||||
test.runIf(isCodexInstalled())("getRuntimeInfo reflects model + mode", async () => {
|
test.runIf(isCodexInstalled())("getRuntimeInfo reflects model + mode", async () => {
|
||||||
const cleanup = useTempCodexSessionDir();
|
const cleanup = useTempCodexSessionDir();
|
||||||
const cwd = tmpCwd("codex-runtime-");
|
const cwd = tmpCwd("codex-runtime-");
|
||||||
|
|||||||
@@ -27,6 +27,7 @@ import type { Logger } from "pino";
|
|||||||
|
|
||||||
import { execSync, spawn } from "node:child_process";
|
import { execSync, spawn } from "node:child_process";
|
||||||
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
import type { ChildProcessWithoutNullStreams } from "node:child_process";
|
||||||
|
import { randomUUID } from "node:crypto";
|
||||||
import fs from "node:fs/promises";
|
import fs from "node:fs/promises";
|
||||||
import { Dirent } from "node:fs";
|
import { Dirent } from "node:fs";
|
||||||
import os from "node:os";
|
import os from "node:os";
|
||||||
@@ -36,6 +37,7 @@ import readline from "node:readline";
|
|||||||
|
|
||||||
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
const DEFAULT_TIMEOUT_MS = 14 * 24 * 60 * 60 * 1000;
|
||||||
const CODEX_PROVIDER = "codex" as const;
|
const CODEX_PROVIDER = "codex" as const;
|
||||||
|
const CODEX_IMAGE_ATTACHMENT_DIR = "paseo-attachments";
|
||||||
|
|
||||||
const CODEX_APP_SERVER_CAPABILITIES: AgentCapabilityFlags = {
|
const CODEX_APP_SERVER_CAPABILITIES: AgentCapabilityFlags = {
|
||||||
supportsStreaming: true,
|
supportsStreaming: true,
|
||||||
@@ -790,6 +792,87 @@ function toSandboxPolicy(type: string, networkAccess?: boolean): Record<string,
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function getImageExtension(mimeType: string): string {
|
||||||
|
switch (mimeType) {
|
||||||
|
case "image/jpeg":
|
||||||
|
return "jpg";
|
||||||
|
case "image/png":
|
||||||
|
return "png";
|
||||||
|
case "image/webp":
|
||||||
|
return "webp";
|
||||||
|
case "image/gif":
|
||||||
|
return "gif";
|
||||||
|
case "image/bmp":
|
||||||
|
return "bmp";
|
||||||
|
case "image/tiff":
|
||||||
|
return "tiff";
|
||||||
|
default:
|
||||||
|
return "bin";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type ImageDataPayload = { mimeType: string; data: string };
|
||||||
|
|
||||||
|
function normalizeImageData(mimeType: string, data: string): ImageDataPayload {
|
||||||
|
if (data.startsWith("data:")) {
|
||||||
|
const match = data.match(/^data:([^;]+);base64,(.*)$/);
|
||||||
|
if (match) {
|
||||||
|
return { mimeType: match[1], data: match[2] };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return { mimeType, data };
|
||||||
|
}
|
||||||
|
|
||||||
|
async function writeImageAttachment(mimeType: string, data: string): Promise<string> {
|
||||||
|
const attachmentsDir = path.join(os.tmpdir(), CODEX_IMAGE_ATTACHMENT_DIR);
|
||||||
|
await fs.mkdir(attachmentsDir, { recursive: true });
|
||||||
|
const normalized = normalizeImageData(mimeType, data);
|
||||||
|
const extension = getImageExtension(normalized.mimeType);
|
||||||
|
const filename = `${randomUUID()}.${extension}`;
|
||||||
|
const filePath = path.join(attachmentsDir, filename);
|
||||||
|
await fs.writeFile(filePath, Buffer.from(normalized.data, "base64"));
|
||||||
|
return filePath;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function codexAppServerTurnInputFromPrompt(
|
||||||
|
prompt: AgentPromptInput,
|
||||||
|
logger: Logger
|
||||||
|
): Promise<unknown[]> {
|
||||||
|
if (typeof prompt === "string") {
|
||||||
|
return [{ type: "text", text: prompt }];
|
||||||
|
}
|
||||||
|
|
||||||
|
const blocks = prompt as Array<unknown>;
|
||||||
|
const output: unknown[] = [];
|
||||||
|
for (const block of blocks) {
|
||||||
|
if (!block || typeof block !== "object") {
|
||||||
|
output.push(block);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const record = block as { type?: unknown; mimeType?: unknown; data?: unknown };
|
||||||
|
if (
|
||||||
|
record.type === "image" &&
|
||||||
|
typeof record.mimeType === "string" &&
|
||||||
|
typeof record.data === "string"
|
||||||
|
) {
|
||||||
|
try {
|
||||||
|
const filePath = await writeImageAttachment(record.mimeType, record.data);
|
||||||
|
output.push({ type: "localImage", path: filePath });
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : String(error);
|
||||||
|
logger.warn({ message }, "Failed to write Codex image attachment");
|
||||||
|
output.push({
|
||||||
|
type: "text",
|
||||||
|
text: `User attached image (failed to write temp file): ${message}`,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
output.push(block);
|
||||||
|
}
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
class CodexAppServerAgentSession implements AgentSession {
|
class CodexAppServerAgentSession implements AgentSession {
|
||||||
readonly provider = CODEX_PROVIDER;
|
readonly provider = CODEX_PROVIDER;
|
||||||
readonly capabilities = CODEX_APP_SERVER_CAPABILITIES;
|
readonly capabilities = CODEX_APP_SERVER_CAPABILITIES;
|
||||||
@@ -818,7 +901,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
private pendingReasoning = new Map<string, string[]>();
|
private pendingReasoning = new Map<string, string[]>();
|
||||||
private latestUsage: AgentUsage | undefined;
|
private latestUsage: AgentUsage | undefined;
|
||||||
private connected = false;
|
private connected = false;
|
||||||
private paseoInstructionsInjected = false;
|
|
||||||
private collaborationModes: Array<{
|
private collaborationModes: Array<{
|
||||||
name: string;
|
name: string;
|
||||||
mode?: string | null;
|
mode?: string | null;
|
||||||
@@ -845,7 +927,6 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
if (this.resumeHandle?.sessionId) {
|
if (this.resumeHandle?.sessionId) {
|
||||||
this.currentThreadId = this.resumeHandle.sessionId;
|
this.currentThreadId = this.resumeHandle.sessionId;
|
||||||
this.historyPending = true;
|
this.historyPending = true;
|
||||||
this.paseoInstructionsInjected = true;
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1057,7 +1138,7 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
} else {
|
} else {
|
||||||
await this.ensureThread();
|
await this.ensureThread();
|
||||||
}
|
}
|
||||||
const input = this.buildUserInput(prompt);
|
const input = await this.buildUserInput(prompt);
|
||||||
const preset = MODE_PRESETS[this.currentMode] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
|
const preset = MODE_PRESETS[this.currentMode] ?? MODE_PRESETS[DEFAULT_CODEX_MODE_ID];
|
||||||
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
|
const approvalPolicy = this.config.approvalPolicy ?? preset.approvalPolicy;
|
||||||
const sandboxPolicyType = this.config.sandboxMode ?? preset.sandbox;
|
const sandboxPolicyType = this.config.sandboxMode ?? preset.sandbox;
|
||||||
@@ -1364,17 +1445,12 @@ class CodexAppServerAgentSession implements AgentSession {
|
|||||||
this.currentThreadId = threadId;
|
this.currentThreadId = threadId;
|
||||||
}
|
}
|
||||||
|
|
||||||
private buildUserInput(prompt: AgentPromptInput): unknown[] {
|
private async buildUserInput(prompt: AgentPromptInput): Promise<unknown[]> {
|
||||||
if (typeof prompt === "string") {
|
if (typeof prompt === "string") {
|
||||||
this.paseoInstructionsInjected = true;
|
|
||||||
return [{ type: "text", text: prompt }];
|
return [{ type: "text", text: prompt }];
|
||||||
}
|
}
|
||||||
const blocks = prompt as AgentPromptContentBlock[];
|
const blocks = prompt as AgentPromptContentBlock[];
|
||||||
if (this.paseoInstructionsInjected) {
|
return await codexAppServerTurnInputFromPrompt(blocks, this.logger);
|
||||||
return blocks;
|
|
||||||
}
|
|
||||||
this.paseoInstructionsInjected = true;
|
|
||||||
return blocks;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private emitEvent(event: AgentStreamEvent): void {
|
private emitEvent(event: AgentStreamEvent): void {
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
* Ad-hoc script to debug checkout_status_request timeouts.
|
* Ad-hoc script to debug checkout_status_request timeouts.
|
||||||
*
|
*
|
||||||
* Usage:
|
* Usage:
|
||||||
* npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts [agentId1] [agentId2]
|
* npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts [agentIdOrCwd1] [agentIdOrCwd2]
|
||||||
*
|
*
|
||||||
* To test against a different daemon:
|
* To test against a different daemon:
|
||||||
* PASEO_PORT=7777 npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts
|
* PASEO_PORT=7777 npx tsx packages/server/src/server/daemon-e2e/checkout-debug.ts
|
||||||
@@ -42,11 +42,13 @@ async function testMultiAgentSequence() {
|
|||||||
reconnect: { enabled: false },
|
reconnect: { enabled: false },
|
||||||
});
|
});
|
||||||
|
|
||||||
const agents: Array<{ id: string; title: string }> = [];
|
const agents: Array<{ id: string; title: string; cwd: string }> = [];
|
||||||
|
|
||||||
// Also log raw messages for debugging
|
// Also log raw messages for debugging
|
||||||
client.on("checkout_status_response", (msg: any) => {
|
client.on("checkout_status_response", (msg: any) => {
|
||||||
console.log(`[RAW checkout_status_response] requestId=${msg.payload.requestId} agentId=${msg.payload.agentId}`);
|
console.log(
|
||||||
|
`[RAW checkout_status_response] requestId=${msg.payload.requestId} cwd=${msg.payload.cwd}`
|
||||||
|
);
|
||||||
});
|
});
|
||||||
|
|
||||||
// Listen to connection state changes
|
// Listen to connection state changes
|
||||||
@@ -63,7 +65,7 @@ async function testMultiAgentSequence() {
|
|||||||
const agentsList = await client.fetchAgents();
|
const agentsList = await client.fetchAgents();
|
||||||
agents.length = 0;
|
agents.length = 0;
|
||||||
for (const a of agentsList) {
|
for (const a of agentsList) {
|
||||||
agents.push({ id: a.id, title: a.title ?? "(untitled)" });
|
agents.push({ id: a.id, title: a.title ?? "(untitled)", cwd: a.cwd });
|
||||||
}
|
}
|
||||||
|
|
||||||
if (agents.length === 0) {
|
if (agents.length === 0) {
|
||||||
@@ -80,53 +82,72 @@ async function testMultiAgentSequence() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Pick first two agents (or use command line args)
|
// Pick first two agents (or use command line args)
|
||||||
const agent1Id = process.argv[2] ?? agents[0]?.id;
|
const arg1 = process.argv[2];
|
||||||
const agent2Id = process.argv[3] ?? agents[1]?.id ?? agents[0]?.id;
|
const arg2 = process.argv[3];
|
||||||
|
const agent1 = (arg1 ? agents.find((a) => a.id === arg1) : null) ?? agents[0] ?? null;
|
||||||
|
const agent2 =
|
||||||
|
(arg2 ? agents.find((a) => a.id === arg2) : null) ??
|
||||||
|
agents[1] ??
|
||||||
|
agents[0] ??
|
||||||
|
null;
|
||||||
|
|
||||||
if (!agent1Id) {
|
const cwd1 = arg1 && !agent1 ? arg1 : agent1?.cwd;
|
||||||
console.log("No agents available to test");
|
const cwd2 = arg2 && !agent2 ? arg2 : agent2?.cwd;
|
||||||
|
|
||||||
|
if (!cwd1) {
|
||||||
|
console.log("No checkout cwd available to test");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n=== Test 1: Request checkout for agent1 (${agent1Id.slice(0, 8)}...) ===`);
|
console.log(`\n=== Test 1: Request checkout for cwd1 (${cwd1}) ===`);
|
||||||
const start1 = Date.now();
|
const start1 = Date.now();
|
||||||
try {
|
try {
|
||||||
const status1 = await client.getCheckoutStatus(agent1Id);
|
const status1 = await client.getCheckoutStatus(cwd1);
|
||||||
console.log(`✓ Agent1 completed in ${Date.now() - start1}ms - branch: ${status1.currentBranch}`);
|
console.log(
|
||||||
|
`✓ Cwd1 completed in ${Date.now() - start1}ms - branch: ${status1.currentBranch}`
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`✗ Agent1 failed after ${Date.now() - start1}ms:`, err);
|
console.log(`✗ Cwd1 failed after ${Date.now() - start1}ms:`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n=== Test 2: Request checkout for agent2 (${agent2Id.slice(0, 8)}...) ===`);
|
if (cwd2) {
|
||||||
const start2 = Date.now();
|
console.log(`\n=== Test 2: Request checkout for cwd2 (${cwd2}) ===`);
|
||||||
try {
|
const start2 = Date.now();
|
||||||
const status2 = await client.getCheckoutStatus(agent2Id);
|
try {
|
||||||
console.log(`✓ Agent2 completed in ${Date.now() - start2}ms - branch: ${status2.currentBranch}`);
|
const status2 = await client.getCheckoutStatus(cwd2);
|
||||||
} catch (err) {
|
console.log(
|
||||||
console.log(`✗ Agent2 failed after ${Date.now() - start2}ms:`, err);
|
`✓ Cwd2 completed in ${Date.now() - start2}ms - branch: ${status2.currentBranch}`
|
||||||
|
);
|
||||||
|
} catch (err) {
|
||||||
|
console.log(`✗ Cwd2 failed after ${Date.now() - start2}ms:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n=== Test 3: Request checkout for agent1 again ===`);
|
console.log(`\n=== Test 3: Request checkout for cwd1 again ===`);
|
||||||
const start3 = Date.now();
|
const start3 = Date.now();
|
||||||
try {
|
try {
|
||||||
const status3 = await client.getCheckoutStatus(agent1Id);
|
const status3 = await client.getCheckoutStatus(cwd1);
|
||||||
console.log(`✓ Agent1 (retry) completed in ${Date.now() - start3}ms - branch: ${status3.currentBranch}`);
|
console.log(
|
||||||
|
`✓ Cwd1 (retry) completed in ${Date.now() - start3}ms - branch: ${status3.currentBranch}`
|
||||||
|
);
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
console.log(`✗ Agent1 (retry) failed after ${Date.now() - start3}ms:`, err);
|
console.log(`✗ Cwd1 (retry) failed after ${Date.now() - start3}ms:`, err);
|
||||||
}
|
}
|
||||||
|
|
||||||
console.log(`\n=== Test 4: Request both agents in parallel ===`);
|
if (cwd2) {
|
||||||
const start4 = Date.now();
|
console.log(`\n=== Test 4: Request both cwds in parallel ===`);
|
||||||
try {
|
const start4 = Date.now();
|
||||||
const [p1, p2] = await Promise.all([
|
try {
|
||||||
client.getCheckoutStatus(agent1Id),
|
const [p1, p2] = await Promise.all([
|
||||||
client.getCheckoutStatus(agent2Id),
|
client.getCheckoutStatus(cwd1),
|
||||||
]);
|
client.getCheckoutStatus(cwd2),
|
||||||
console.log(`✓ Parallel completed in ${Date.now() - start4}ms`);
|
]);
|
||||||
console.log(` Agent1 branch: ${p1.currentBranch}`);
|
console.log(`✓ Parallel completed in ${Date.now() - start4}ms`);
|
||||||
console.log(` Agent2 branch: ${p2.currentBranch}`);
|
console.log(` Cwd1 branch: ${p1.currentBranch}`);
|
||||||
} catch (err) {
|
console.log(` Cwd2 branch: ${p2.currentBranch}`);
|
||||||
console.log(`✗ Parallel failed after ${Date.now() - start4}ms:`, err);
|
} catch (err) {
|
||||||
|
console.log(`✗ Parallel failed after ${Date.now() - start4}ms:`, err);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
|||||||
@@ -478,14 +478,6 @@ export const ListProviderModelsRequestMessageSchema = z.object({
|
|||||||
requestId: z.string(),
|
requestId: z.string(),
|
||||||
});
|
});
|
||||||
|
|
||||||
// Legacy alias used by older clients; keep for compatibility
|
|
||||||
|
|
||||||
export const GitRepoInfoRequestMessageSchema = z.object({
|
|
||||||
type: z.literal("git_repo_info_request"),
|
|
||||||
cwd: z.string(),
|
|
||||||
requestId: z.string(),
|
|
||||||
});
|
|
||||||
|
|
||||||
export const ResumeAgentRequestMessageSchema = z.object({
|
export const ResumeAgentRequestMessageSchema = z.object({
|
||||||
type: z.literal("resume_agent_request"),
|
type: z.literal("resume_agent_request"),
|
||||||
handle: AgentPersistenceHandleSchema,
|
handle: AgentPersistenceHandleSchema,
|
||||||
|
|||||||
Reference in New Issue
Block a user