From 086dde30cc8f22dbfde0e4c149dca2638c026a1d Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Sun, 26 Oct 2025 22:19:16 +0100 Subject: [PATCH] feat(agent): add git diff viewer and improve permission handling MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add git diff viewer screen accessible via agent dropdown menu - Implement git_diff_request/response WebSocket messages - Add dropdown menu to agent screen header with "View Changes" option - Clear pending permissions when starting new agent turn to prevent conflicts - Treat agent refusals as completed rather than failed state - Add test coverage for permission handling edge cases - Configure vitest to run in single-threaded mode for stability - Remove redundant wrapper in agent stream view 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude --- packages/app/src/app/_layout.tsx | 1 + packages/app/src/app/agent/[id].tsx | 113 +++++++- packages/app/src/app/git-diff.tsx | 258 ++++++++++++++++++ .../app/src/components/agent-stream-view.tsx | 34 +-- .../src/components/headers/back-header.tsx | 9 +- packages/app/src/contexts/session-context.tsx | 34 +++ .../src/server/acp/agent-manager.test.ts | 77 ++++++ .../server/src/server/acp/agent-manager.ts | 46 +++- packages/server/src/server/messages.ts | 18 ++ packages/server/src/server/session.ts | 60 ++++ packages/server/vitest.config.ts | 6 + 11 files changed, 627 insertions(+), 29 deletions(-) create mode 100644 packages/app/src/app/git-diff.tsx diff --git a/packages/app/src/app/_layout.tsx b/packages/app/src/app/_layout.tsx index 683a5e7da..dd8139171 100644 --- a/packages/app/src/app/_layout.tsx +++ b/packages/app/src/app/_layout.tsx @@ -69,6 +69,7 @@ export default function RootLayout() { + diff --git a/packages/app/src/app/agent/[id].tsx b/packages/app/src/app/agent/[id].tsx index 23e866ac1..f8a34f253 100644 --- a/packages/app/src/app/agent/[id].tsx +++ b/packages/app/src/app/agent/[id].tsx @@ -1,19 +1,23 @@ -import { useEffect, useMemo } from "react"; -import { View, Text, ActivityIndicator } from "react-native"; -import { useLocalSearchParams } from "expo-router"; +import { useEffect, useMemo, useRef, useCallback, useState } from "react"; +import { View, Text, ActivityIndicator, Pressable, Modal, Dimensions } from "react-native"; +import { useLocalSearchParams, useRouter } from "expo-router"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import { useReanimatedKeyboardAnimation } from "react-native-keyboard-controller"; import ReanimatedAnimated, { useAnimatedStyle, useSharedValue } from "react-native-reanimated"; import { StyleSheet, useUnistyles } from "react-native-unistyles"; +import { MoreVertical, GitBranch } from "lucide-react-native"; import { BackHeader } from "@/components/headers/back-header"; import { AgentStreamView } from "@/components/agent-stream-view"; import { AgentInputArea } from "@/components/agent-input-area"; import { useSession } from "@/contexts/session-context"; import { useFooterControls } from "@/contexts/footer-controls-context"; +const DROPDOWN_WIDTH = 220; + export default function AgentScreen() { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); + const router = useRouter(); const { id } = useLocalSearchParams<{ id: string }>(); const { agents, @@ -24,6 +28,9 @@ export default function AgentScreen() { initializeAgent, } = useSession(); const { registerFooterControls, unregisterFooterControls } = useFooterControls(); + const [menuVisible, setMenuVisible] = useState(false); + const [menuPosition, setMenuPosition] = useState({ top: 0, left: 0 }); + const menuButtonRef = useRef(null); // Keyboard animation const { height: keyboardHeight } = useReanimatedKeyboardAnimation(); @@ -90,6 +97,34 @@ export default function AgentScreen() { }; }, [agentControls, agent, isInitializing, registerFooterControls, unregisterFooterControls]); + const handleOpenMenu = useCallback(() => { + menuButtonRef.current?.measureInWindow((x, y, width, height) => { + const screenWidth = Dimensions.get("window").width; + const verticalOffset = 6; + const horizontalMargin = 16; + const desiredLeft = x + width - DROPDOWN_WIDTH; + const maxLeft = screenWidth - DROPDOWN_WIDTH - horizontalMargin; + const clampedLeft = Math.min(Math.max(desiredLeft, horizontalMargin), maxLeft); + + setMenuPosition({ + top: y + height + verticalOffset, + left: clampedLeft, + }); + setMenuVisible(true); + }); + }, []); + + const handleCloseMenu = useCallback(() => { + setMenuVisible(false); + }, []); + + const handleViewChanges = useCallback(() => { + handleCloseMenu(); + if (id) { + router.push(`/git-diff?agentId=${id}`); + } + }, [id, router, handleCloseMenu]); + if (!agent) { return ( @@ -104,7 +139,16 @@ export default function AgentScreen() { return ( {/* Header */} - + + + + + + } + /> {/* Content Area with Keyboard Animation */} @@ -127,6 +171,34 @@ export default function AgentScreen() { )} + + {/* Dropdown Menu */} + + + + + + + View Changes + + + + ); } @@ -162,4 +234,37 @@ const styles = StyleSheet.create((theme) => ({ fontSize: theme.fontSize.lg, color: theme.colors.mutedForeground, }, + menuButton: { + padding: theme.spacing[3], + borderRadius: theme.borderRadius.lg, + }, + menuOverlay: { + flex: 1, + }, + menuBackdrop: { + ...StyleSheet.absoluteFillObject, + }, + dropdownMenu: { + backgroundColor: theme.colors.card, + borderRadius: theme.borderRadius.lg, + padding: theme.spacing[2], + shadowColor: "#000", + shadowOffset: { width: 0, height: 2 }, + shadowOpacity: 0.25, + shadowRadius: 8, + elevation: 5, + }, + menuItem: { + flexDirection: "row", + alignItems: "center", + gap: theme.spacing[3], + paddingVertical: theme.spacing[3], + paddingHorizontal: theme.spacing[3], + borderRadius: theme.borderRadius.md, + }, + menuItemText: { + fontSize: theme.fontSize.base, + color: theme.colors.foreground, + fontWeight: theme.fontWeight.normal, + }, })); diff --git a/packages/app/src/app/git-diff.tsx b/packages/app/src/app/git-diff.tsx new file mode 100644 index 000000000..e60ae139f --- /dev/null +++ b/packages/app/src/app/git-diff.tsx @@ -0,0 +1,258 @@ +import { useEffect, useState } from "react"; +import { View, Text, ScrollView, ActivityIndicator } from "react-native"; +import { useLocalSearchParams } from "expo-router"; +import { StyleSheet } from "react-native-unistyles"; +import { BackHeader } from "@/components/headers/back-header"; +import { useSession } from "@/contexts/session-context"; + +interface ParsedDiffFile { + path: string; + lines: Array<{ + type: "add" | "remove" | "context" | "header"; + content: string; + }>; +} + +function parseDiff(diffText: string): ParsedDiffFile[] { + if (!diffText || diffText.trim().length === 0) { + return []; + } + + const files: ParsedDiffFile[] = []; + const sections = diffText.split(/^diff --git /m).filter(Boolean); + + for (const section of sections) { + const lines = section.split("\n"); + const firstLine = lines[0]; + + const pathMatch = firstLine.match(/a\/(.*?) b\//); + const path = pathMatch ? pathMatch[1] : "unknown"; + + const parsedLines: ParsedDiffFile["lines"] = []; + + for (let i = 0; i < lines.length; i++) { + const line = lines[i]; + + if (line.startsWith("+++") || line.startsWith("---") || line.startsWith("@@") || line.startsWith("index ")) { + parsedLines.push({ type: "header", content: line }); + } else if (line.startsWith("+")) { + parsedLines.push({ type: "add", content: line }); + } else if (line.startsWith("-")) { + parsedLines.push({ type: "remove", content: line }); + } else { + parsedLines.push({ type: "context", content: line }); + } + } + + files.push({ path, lines: parsedLines }); + } + + return files; +} + +export default function GitDiffScreen() { + const { agentId } = useLocalSearchParams<{ agentId: string }>(); + const { agents, gitDiffs, requestGitDiff } = useSession(); + const [isLoading, setIsLoading] = useState(true); + + const agent = agentId ? agents.get(agentId) : undefined; + const diffText = agentId ? gitDiffs.get(agentId) : undefined; + + useEffect(() => { + if (!agentId) { + setIsLoading(false); + return; + } + + if (diffText !== undefined) { + setIsLoading(false); + return; + } + + requestGitDiff(agentId); + + const timeout = setTimeout(() => { + setIsLoading(false); + }, 5000); + + return () => clearTimeout(timeout); + }, [agentId, diffText, requestGitDiff]); + + useEffect(() => { + if (diffText !== undefined) { + setIsLoading(false); + } + }, [diffText]); + + if (!agent) { + return ( + + + + Agent not found + + + ); + } + + const isError = diffText?.startsWith("Error:"); + const parsedFiles = isError || !diffText ? [] : parseDiff(diffText); + const hasChanges = parsedFiles.length > 0; + + return ( + + + + + {isLoading ? ( + + + Loading changes... + + ) : isError ? ( + + {diffText} + + ) : !hasChanges ? ( + + No changes + + ) : ( + parsedFiles.map((file, fileIndex) => ( + + + {file.path} + + + + + {file.lines.map((line, lineIndex) => ( + + {line.content} + + ))} + + + + + )) + )} + + + ); +} + +const styles = StyleSheet.create((theme) => ({ + container: { + flex: 1, + backgroundColor: theme.colors.background, + }, + scrollView: { + flex: 1, + }, + contentContainer: { + padding: theme.spacing[4], + }, + loadingContainer: { + flex: 1, + alignItems: "center", + justifyContent: "center", + paddingTop: theme.spacing[16], + gap: theme.spacing[4], + }, + loadingText: { + fontSize: theme.fontSize.base, + color: theme.colors.mutedForeground, + }, + errorContainer: { + flex: 1, + alignItems: "center", + justifyContent: "center", + paddingTop: theme.spacing[16], + paddingHorizontal: theme.spacing[6], + }, + errorText: { + fontSize: theme.fontSize.base, + color: theme.colors.destructive, + textAlign: "center", + }, + emptyContainer: { + flex: 1, + alignItems: "center", + justifyContent: "center", + paddingTop: theme.spacing[16], + }, + emptyText: { + fontSize: theme.fontSize.lg, + color: theme.colors.mutedForeground, + }, + fileSection: { + marginBottom: theme.spacing[6], + borderRadius: theme.borderRadius.lg, + overflow: "hidden", + borderWidth: theme.borderWidth[1], + borderColor: theme.colors.border, + }, + fileHeader: { + backgroundColor: theme.colors.muted, + padding: theme.spacing[3], + borderBottomWidth: theme.borderWidth[1], + borderBottomColor: theme.colors.border, + }, + filePath: { + fontSize: theme.fontSize.sm, + fontWeight: theme.fontWeight.semibold, + color: theme.colors.foreground, + fontFamily: "monospace", + }, + diffContent: { + backgroundColor: theme.colors.card, + }, + diffScrollContent: { + flexDirection: "column", + alignItems: "flex-start", + paddingBottom: theme.spacing[2], + }, + diffLinesContainer: { + alignSelf: "flex-start", + }, + diffLine: { + fontSize: theme.fontSize.xs, + fontFamily: "monospace", + paddingHorizontal: theme.spacing[3], + paddingVertical: theme.spacing[1], + flexShrink: 0, + minWidth: "100%", + }, + addLine: { + backgroundColor: theme.colors.palette.green[900], + color: theme.colors.palette.green[200], + }, + removeLine: { + backgroundColor: theme.colors.palette.red[900], + color: theme.colors.palette.red[200], + }, + headerLine: { + color: theme.colors.mutedForeground, + backgroundColor: theme.colors.muted, + }, + contextLine: { + color: theme.colors.mutedForeground, + }, +})); diff --git a/packages/app/src/components/agent-stream-view.tsx b/packages/app/src/components/agent-stream-view.tsx index 7ae78886b..ab49013b9 100644 --- a/packages/app/src/components/agent-stream-view.tsx +++ b/packages/app/src/components/agent-stream-view.tsx @@ -267,28 +267,24 @@ export function AgentStreamView({ onMomentumScrollEnd={handleScrollEnd} scrollEventThrottle={16} ListEmptyComponent={ - - - - Start chatting with this agent... - - + + + Start chatting with this agent... + } ListHeaderComponent={ - - {pendingPermissionItems.length > 0 ? ( - - {pendingPermissionItems.map((permission) => ( - - ))} - - ) : null} - + pendingPermissionItems.length > 0 ? ( + + {pendingPermissionItems.map((permission) => ( + + ))} + + ) : null } extraData={pendingPermissionItems.length} maintainVisibleContentPosition={{ diff --git a/packages/app/src/components/headers/back-header.tsx b/packages/app/src/components/headers/back-header.tsx index 38160ff5f..9817e90bc 100644 --- a/packages/app/src/components/headers/back-header.tsx +++ b/packages/app/src/components/headers/back-header.tsx @@ -6,9 +6,10 @@ import { ArrowLeft } from "lucide-react-native"; interface BackHeaderProps { title?: string; + rightContent?: React.ReactNode; } -export function BackHeader({ title }: BackHeaderProps) { +export function BackHeader({ title, rightContent }: BackHeaderProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); @@ -31,8 +32,10 @@ export function BackHeader({ title }: BackHeaderProps) { )} - {/* Right side - Empty for now */} - + {/* Right side */} + + {rightContent} + diff --git a/packages/app/src/contexts/session-context.tsx b/packages/app/src/contexts/session-context.tsx index 0a765f84c..59aa0bd97 100644 --- a/packages/app/src/contexts/session-context.tsx +++ b/packages/app/src/contexts/session-context.tsx @@ -115,6 +115,10 @@ interface SessionContextValue { pendingPermissions: Map; setPendingPermissions: (perms: Map | ((prev: Map) => Map)) => void; + // Git diffs + gitDiffs: Map; + requestGitDiff: (agentId: string) => void; + // Helpers initializeAgent: (params: { agentId: string; requestId?: string }) => void; sendAgentMessage: (agentId: string, message: string, imageUris?: string[]) => Promise; @@ -167,6 +171,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { const [commands, setCommands] = useState>(new Map()); const [agentUpdates, setAgentUpdates] = useState>(new Map()); const [pendingPermissions, setPendingPermissions] = useState>(new Map()); + const [gitDiffs, setGitDiffs] = useState>(new Map()); // WebSocket message handlers useEffect(() => { @@ -593,6 +598,21 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { } }); + // Git diff response handler + const unsubGitDiff = ws.on("git_diff_response", (message) => { + if (message.type !== "git_diff_response") return; + const { agentId, diff, error } = message.payload; + + console.log("[Session] Git diff response for agent:", agentId, error ? `(error: ${error})` : ""); + + if (error) { + console.error("[Session] Git diff error:", error); + setGitDiffs((prev) => new Map(prev).set(agentId, `Error: ${error}`)); + } else { + setGitDiffs((prev) => new Map(prev).set(agentId, diff || "")); + } + }); + return () => { unsubSessionState(); unsubAgentCreated(); @@ -605,6 +625,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { unsubActivity(); unsubChunk(); unsubTranscription(); + unsubGitDiff(); }; }, [ws, audioPlayer, setIsPlayingAudio]); @@ -776,6 +797,17 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { isSpeakingRef.current = isSpeaking; }, []); + const requestGitDiff = useCallback((agentId: string) => { + const msg: WSInboundMessage = { + type: "session", + message: { + type: "git_diff_request", + agentId, + }, + }; + ws.send(msg); + }, [ws]); + const value: SessionContextValue = { ws, audioPlayer, @@ -797,6 +829,8 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) { setAgentUpdates, pendingPermissions, setPendingPermissions, + gitDiffs, + requestGitDiff, initializeAgent, sendAgentMessage, sendAgentAudio, diff --git a/packages/server/src/server/acp/agent-manager.test.ts b/packages/server/src/server/acp/agent-manager.test.ts index fb6d0e31d..93413ad47 100644 --- a/packages/server/src/server/acp/agent-manager.test.ts +++ b/packages/server/src/server/acp/agent-manager.test.ts @@ -96,6 +96,83 @@ describe("AgentManager", () => { } }, 120000); + it("should not fail when sending '.' after plan permission request", async () => { + const manager = new AgentManager(); + let permissionRequest: RequestPermissionRequest | null = null; + let requestId: string | null = null; + + const agentId = await manager.createAgent({ + cwd: tmpDir, + type: "claude", + initialMode: "plan", + }); + createdAgents.push({ manager, agentId }); + + const unsubscribe = manager.subscribeToUpdates( + agentId, + (update: AgentUpdate) => { + const notification: AgentNotification = update.notification; + if (notification.type === "permission") { + permissionRequest = notification.request; + requestId = notification.requestId; + } + } + ); + + try { + await manager.sendPrompt( + agentId, + "Create a file called test.txt with the content 'hello world'" + ); + + let attempts = 0; + while (!permissionRequest && attempts < 40) { + await new Promise((resolve) => setTimeout(resolve, 500)); + attempts++; + } + + expect(permissionRequest).toBeDefined(); + expect(requestId).toBeDefined(); + + console.log("Permission request received, now sending '.' message instead of responding"); + + // Instead of responding to permission, send a "." message + await manager.sendPrompt(agentId, "."); + + // Wait for agent to finish processing + let status = manager.getAgentStatus(agentId); + let waitAttempts = 0; + while (status === "processing" && waitAttempts < 60) { + await new Promise((resolve) => setTimeout(resolve, 1000)); + status = manager.getAgentStatus(agentId); + waitAttempts++; + } + + const agentInfo = manager.listAgents().find((a) => a.id === agentId); + const updates = manager.getAgentUpdates(agentId); + const sessionUpdates = updates.filter(u => u.notification.type === "session"); + + console.log("Agent status after '.' message:", status); + console.log("Agent error:", agentInfo?.error); + console.log("Session updates count:", sessionUpdates.length); + console.log("Last few updates:", updates.slice(-5).map(u => ({ + type: u.notification.type, + timestamp: u.timestamp, + }))); + + // The agent should NOT be in failed state + expect(status).not.toBe("failed"); + + // The agent should be in a usable state (ready, completed, or processing) + expect(["ready", "completed", "processing"]).toContain(status); + + // There should be no error + expect(agentInfo?.error).toBeNull(); + } finally { + unsubscribe(); + } + }, 120000); + describe("persistence", () => { it("should load persisted agent and send new prompt", async () => { const manager = new AgentManager(); diff --git a/packages/server/src/server/acp/agent-manager.ts b/packages/server/src/server/acp/agent-manager.ts index bbd0bcfea..30bfd2a8e 100644 --- a/packages/server/src/server/acp/agent-manager.ts +++ b/packages/server/src/server/acp/agent-manager.ts @@ -416,6 +416,46 @@ export class AgentManager { } } + // Clear any pending permissions since we're starting a new turn + if (agent.pendingPermissions.size > 0) { + console.log( + `[Agent ${agentId}] Clearing ${agent.pendingPermissions.size} pending permission(s)` + ); + + // Reject all pending permission promises with cancellation + for (const [requestId, permission] of agent.pendingPermissions) { + permission.resolve({ + outcome: { + outcome: "cancelled" as const, + }, + }); + + // Emit permission_resolved notification so UI updates + const agentUpdate: AgentUpdate = { + agentId, + timestamp: new Date(), + notification: { + type: "permission_resolved", + requestId, + agentId, + optionId: "cancelled", + }, + }; + + agent.updates.push(agentUpdate); + + for (const subscriber of agent.subscribers) { + try { + subscriber(agentUpdate); + } catch (error) { + console.error(`[Agent ${agentId}] Subscriber error:`, error); + } + } + } + + agent.pendingPermissions.clear(); + } + // Get runtime (guaranteed to exist after ensureInitialized) if ( agent.state.type !== "ready" && @@ -506,14 +546,14 @@ export class AgentManager { stopReason: response.stopReason, }; } else if (response.stopReason === "refusal") { - console.error( + console.warn( `[Agent ${agentId}] Agent refused to process the prompt`, response ); agent.state = { - type: "failed", - lastError: "Agent refused to process the prompt", + type: "completed", runtime: agent.state.runtime, + stopReason: response.stopReason, }; } else if (response.stopReason === "cancelled") { agent.state = { type: "ready", runtime: agent.state.runtime }; diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 6c8afddbf..7e3f081db 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -122,6 +122,11 @@ export const AgentPermissionResponseMessageSchema = z.object({ optionId: z.string(), }); +export const GitDiffRequestSchema = z.object({ + type: z.literal("git_diff_request"), + agentId: z.string(), +}); + export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ UserTextMessageSchema, RealtimeAudioChunkMessageSchema, @@ -137,6 +142,7 @@ export const SessionInboundMessageSchema = z.discriminatedUnion("type", [ InitializeAgentRequestMessageSchema, SetAgentModeMessageSchema, AgentPermissionResponseMessageSchema, + GitDiffRequestSchema, ]); export type SessionInboundMessage = z.infer; @@ -321,6 +327,15 @@ export const AgentPermissionResolvedMessageSchema = z.object({ }), }); +export const GitDiffResponseSchema = z.object({ + type: z.literal("git_diff_response"), + payload: z.object({ + agentId: z.string(), + diff: z.string(), + error: z.string().nullable(), + }), +}); + export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ ActivityLogMessageSchema, AssistantChunkMessageSchema, @@ -338,6 +353,7 @@ export const SessionOutboundMessageSchema = z.discriminatedUnion("type", [ DeleteConversationResponseMessageSchema, AgentPermissionRequestMessageSchema, AgentPermissionResolvedMessageSchema, + GitDiffResponseSchema, ]); export type SessionOutboundMessage = z.infer< @@ -374,6 +390,8 @@ export type CreateAgentRequestMessage = z.infer; export type SetAgentModeMessage = z.infer; export type AgentPermissionResponseMessage = z.infer; +export type GitDiffRequest = z.infer; +export type GitDiffResponse = z.infer; // ============================================================================ // WebSocket Level Messages (wraps session messages) diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 938464277..758f82931 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -518,6 +518,10 @@ export class Session { msg.optionId ); break; + + case "git_diff_request": + await this.handleGitDiffRequest(msg.agentId); + break; } } catch (error: any) { console.error( @@ -1016,6 +1020,62 @@ export class Session { } } + /** + * Handle git diff request for an agent + */ + private async handleGitDiffRequest(agentId: string): Promise { + console.log( + `[Session ${this.clientId}] Handling git diff request for agent ${agentId}` + ); + + try { + const agents = this.agentManager.listAgents(); + const agent = agents.find((a) => a.id === agentId); + + if (!agent) { + this.emit({ + type: "git_diff_response", + payload: { + agentId, + diff: "", + error: `Agent not found: ${agentId}`, + }, + }); + return; + } + + const { stdout } = await execAsync("git diff HEAD", { + cwd: agent.cwd, + }); + + this.emit({ + type: "git_diff_response", + payload: { + agentId, + diff: stdout, + error: null, + }, + }); + + console.log( + `[Session ${this.clientId}] Git diff for agent ${agentId} completed (${stdout.length} bytes)` + ); + } catch (error: any) { + console.error( + `[Session ${this.clientId}] Failed to get git diff for agent ${agentId}:`, + error + ); + this.emit({ + type: "git_diff_response", + payload: { + agentId, + diff: "", + error: error.message, + }, + }); + } + } + /** * Send current session state (live agents and commands) to client */ diff --git a/packages/server/vitest.config.ts b/packages/server/vitest.config.ts index c546ce57f..750941aeb 100644 --- a/packages/server/vitest.config.ts +++ b/packages/server/vitest.config.ts @@ -6,5 +6,11 @@ export default defineConfig({ hookTimeout: 30000, globals: true, environment: "node", + pool: "threads", + poolOptions: { + threads: { + singleThread: true, + }, + }, }, });