feat(agent): add git diff viewer and improve permission handling

- 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 <noreply@anthropic.com>
This commit is contained in:
Mohamed Boudra
2025-10-26 22:19:16 +01:00
parent 70a1852e57
commit 086dde30cc
11 changed files with 627 additions and 29 deletions

View File

@@ -69,6 +69,7 @@ export default function RootLayout() {
<Stack.Screen name="agent/[id]" />
<Stack.Screen name="settings" />
<Stack.Screen name="audio-test" />
<Stack.Screen name="git-diff" />
</Stack>
<GlobalFooter />
</AppContainer>

View File

@@ -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<View>(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 (
<View style={styles.container}>
@@ -104,7 +139,16 @@ export default function AgentScreen() {
return (
<View style={styles.container}>
{/* Header */}
<BackHeader title={agent.title || "Agent"} />
<BackHeader
title={agent.title || "Agent"}
rightContent={
<View ref={menuButtonRef} collapsable={false}>
<Pressable onPress={handleOpenMenu} style={styles.menuButton}>
<MoreVertical size={20} color={theme.colors.foreground} />
</Pressable>
</View>
}
/>
{/* Content Area with Keyboard Animation */}
<View style={styles.contentContainer}>
@@ -127,6 +171,34 @@ export default function AgentScreen() {
)}
</ReanimatedAnimated.View>
</View>
{/* Dropdown Menu */}
<Modal
visible={menuVisible}
animationType="fade"
transparent={true}
onRequestClose={handleCloseMenu}
>
<View style={styles.menuOverlay}>
<Pressable style={styles.menuBackdrop} onPress={handleCloseMenu} />
<View
style={[
styles.dropdownMenu,
{
position: "absolute",
top: menuPosition.top,
left: menuPosition.left,
width: DROPDOWN_WIDTH,
},
]}
>
<Pressable onPress={handleViewChanges} style={styles.menuItem}>
<GitBranch size={20} color={theme.colors.foreground} />
<Text style={styles.menuItemText}>View Changes</Text>
</Pressable>
</View>
</View>
</Modal>
</View>
);
}
@@ -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,
},
}));

View File

@@ -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 (
<View style={styles.container}>
<BackHeader title="Changes" />
<View style={styles.errorContainer}>
<Text style={styles.errorText}>Agent not found</Text>
</View>
</View>
);
}
const isError = diffText?.startsWith("Error:");
const parsedFiles = isError || !diffText ? [] : parseDiff(diffText);
const hasChanges = parsedFiles.length > 0;
return (
<View style={styles.container}>
<BackHeader title="Changes" />
<ScrollView style={styles.scrollView} contentContainerStyle={styles.contentContainer}>
{isLoading ? (
<View style={styles.loadingContainer}>
<ActivityIndicator size="large" />
<Text style={styles.loadingText}>Loading changes...</Text>
</View>
) : isError ? (
<View style={styles.errorContainer}>
<Text style={styles.errorText}>{diffText}</Text>
</View>
) : !hasChanges ? (
<View style={styles.emptyContainer}>
<Text style={styles.emptyText}>No changes</Text>
</View>
) : (
parsedFiles.map((file, fileIndex) => (
<View key={fileIndex} style={styles.fileSection}>
<View style={styles.fileHeader}>
<Text style={styles.filePath}>{file.path}</Text>
</View>
<View style={styles.diffContent}>
<ScrollView
horizontal
nestedScrollEnabled
showsHorizontalScrollIndicator
bounces={false}
overScrollMode="never"
contentContainerStyle={styles.diffScrollContent}
>
<View style={styles.diffLinesContainer}>
{file.lines.map((line, lineIndex) => (
<Text
key={lineIndex}
style={[
styles.diffLine,
line.type === "add" && styles.addLine,
line.type === "remove" && styles.removeLine,
line.type === "header" && styles.headerLine,
line.type === "context" && styles.contextLine,
]}
>
{line.content}
</Text>
))}
</View>
</ScrollView>
</View>
</View>
))
)}
</ScrollView>
</View>
);
}
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,
},
}));

View File

@@ -267,28 +267,24 @@ export function AgentStreamView({
onMomentumScrollEnd={handleScrollEnd}
scrollEventThrottle={16}
ListEmptyComponent={
<View style={stylesheet.invertedWrapper}>
<View style={stylesheet.emptyState}>
<Text style={stylesheet.emptyStateText}>
Start chatting with this agent...
</Text>
</View>
<View style={stylesheet.emptyState}>
<Text style={stylesheet.emptyStateText}>
Start chatting with this agent...
</Text>
</View>
}
ListHeaderComponent={
<View style={stylesheet.invertedWrapper}>
{pendingPermissionItems.length > 0 ? (
<View style={stylesheet.permissionsContainer}>
{pendingPermissionItems.map((permission) => (
<PermissionRequestCard
key={permission.requestId}
permission={permission}
onResponse={onPermissionResponse}
/>
))}
</View>
) : null}
</View>
pendingPermissionItems.length > 0 ? (
<View style={stylesheet.permissionsContainer}>
{pendingPermissionItems.map((permission) => (
<PermissionRequestCard
key={permission.requestId}
permission={permission}
onResponse={onPermissionResponse}
/>
))}
</View>
) : null
}
extraData={pendingPermissionItems.length}
maintainVisibleContentPosition={{

View File

@@ -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) {
)}
</View>
{/* Right side - Empty for now */}
<View style={styles.headerRight} />
{/* Right side */}
<View style={styles.headerRight}>
{rightContent}
</View>
</View>
</View>
</View>

View File

@@ -115,6 +115,10 @@ interface SessionContextValue {
pendingPermissions: Map<string, PendingPermission>;
setPendingPermissions: (perms: Map<string, PendingPermission> | ((prev: Map<string, PendingPermission>) => Map<string, PendingPermission>)) => void;
// Git diffs
gitDiffs: Map<string, string>;
requestGitDiff: (agentId: string) => void;
// Helpers
initializeAgent: (params: { agentId: string; requestId?: string }) => void;
sendAgentMessage: (agentId: string, message: string, imageUris?: string[]) => Promise<void>;
@@ -167,6 +171,7 @@ export function SessionProvider({ children, serverUrl }: SessionProviderProps) {
const [commands, setCommands] = useState<Map<string, Command>>(new Map());
const [agentUpdates, setAgentUpdates] = useState<Map<string, AgentUpdate[]>>(new Map());
const [pendingPermissions, setPendingPermissions] = useState<Map<string, PendingPermission>>(new Map());
const [gitDiffs, setGitDiffs] = useState<Map<string, string>>(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,

View File

@@ -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();

View File

@@ -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 };

View File

@@ -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<typeof SessionInboundMessageSchema>;
@@ -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<typeof CreateAgentRequestMessage
export type InitializeAgentRequestMessage = z.infer<typeof InitializeAgentRequestMessageSchema>;
export type SetAgentModeMessage = z.infer<typeof SetAgentModeMessageSchema>;
export type AgentPermissionResponseMessage = z.infer<typeof AgentPermissionResponseMessageSchema>;
export type GitDiffRequest = z.infer<typeof GitDiffRequestSchema>;
export type GitDiffResponse = z.infer<typeof GitDiffResponseSchema>;
// ============================================================================
// WebSocket Level Messages (wraps session messages)

View File

@@ -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<void> {
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
*/

View File

@@ -6,5 +6,11 @@ export default defineConfig({
hookTimeout: 30000,
globals: true,
environment: "node",
pool: "threads",
poolOptions: {
threads: {
singleThread: true,
},
},
},
});