From 348242541c804b7170c16a67615cc10f557db38c Mon Sep 17 00:00:00 2001 From: Mohamed Boudra Date: Mon, 5 Jan 2026 21:27:35 +0700 Subject: [PATCH] feat(git-diff): add React Query with pull-to-refresh and cleaner diff display MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Add useGitDiffQuery hook using React Query for caching and revalidation - Add sendRpcRequest utility for promisified WebSocket RPC with requestId matching - Add requestId to git_diff_request/response for proper request correlation - Enable pull-to-refresh via RefreshControl in GitDiffPane - Auto-revalidate when sidebar opens with "changes" tab active - Clean up diff display: remove metadata noise, strip +/- prefixes - Default both sidebars open on web platform 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- .../app/src/components/explorer-sidebar.tsx | 18 +- packages/app/src/components/git-diff-pane.tsx | 93 ++++--- packages/app/src/hooks/use-git-diff-query.ts | 62 +++++ packages/app/src/lib/send-rpc-request.ts | 231 ++++++++++++++++++ .../app/src/lib/send-rpc-request.typetest.ts | 128 ++++++++++ .../app/src/stores/explorer-sidebar-store.ts | 5 +- packages/app/src/stores/sidebar-store.ts | 5 +- packages/server/src/server/messages.ts | 2 + packages/server/src/server/session.ts | 7 +- 9 files changed, 486 insertions(+), 65 deletions(-) create mode 100644 packages/app/src/hooks/use-git-diff-query.ts create mode 100644 packages/app/src/lib/send-rpc-request.ts create mode 100644 packages/app/src/lib/send-rpc-request.typetest.ts diff --git a/packages/app/src/components/explorer-sidebar.tsx b/packages/app/src/components/explorer-sidebar.tsx index f5db7eab6..a1527fbdb 100644 --- a/packages/app/src/components/explorer-sidebar.tsx +++ b/packages/app/src/components/explorer-sidebar.tsx @@ -1,4 +1,4 @@ -import { useCallback, useMemo, useRef, useState } from "react"; +import { useCallback, useMemo, useRef } from "react"; import { View, Text, Pressable, Platform } from "react-native"; import { useSafeAreaInsets } from "react-native-safe-area-context"; import Animated, { @@ -13,13 +13,12 @@ import { useExplorerSidebarStore, MIN_EXPLORER_SIDEBAR_WIDTH, MAX_EXPLORER_SIDEBAR_WIDTH, + type ViewMode, } from "@/stores/explorer-sidebar-store"; import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context"; import { GitDiffPane } from "./git-diff-pane"; import { FileExplorerPane } from "./file-explorer-pane"; -type ViewMode = "list" | "grid"; - type ExplorerTab = "changes" | "files"; interface ExplorerSidebarProps { @@ -30,7 +29,7 @@ interface ExplorerSidebarProps { export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) { const { theme } = useUnistyles(); const insets = useSafeAreaInsets(); - const { isOpen, activeTab, width, close, setActiveTab, setWidth } = + const { isOpen, activeTab, width, viewMode, close, setActiveTab, setWidth, setViewMode } = useExplorerSidebarStore(); const { translateX, @@ -45,9 +44,6 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) { const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; - // File explorer view mode state - const [fileViewMode, setFileViewMode] = useState("list"); - // For resize drag, track the starting width const startWidthRef = useRef(width); const resizeWidth = useSharedValue(width); @@ -174,8 +170,8 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) { onClose={handleClose} serverId={serverId} agentId={agentId} - fileViewMode={fileViewMode} - onFileViewModeChange={setFileViewMode} + fileViewMode={viewMode} + onFileViewModeChange={setViewMode} isMobile={isMobile} /> @@ -209,8 +205,8 @@ export function ExplorerSidebar({ serverId, agentId }: ExplorerSidebarProps) { onClose={handleClose} serverId={serverId} agentId={agentId} - fileViewMode={fileViewMode} - onFileViewModeChange={setFileViewMode} + fileViewMode={viewMode} + onFileViewModeChange={setViewMode} isMobile={false} /> diff --git a/packages/app/src/components/git-diff-pane.tsx b/packages/app/src/components/git-diff-pane.tsx index bd7b32b68..6dbfa86a9 100644 --- a/packages/app/src/components/git-diff-pane.tsx +++ b/packages/app/src/components/git-diff-pane.tsx @@ -1,9 +1,9 @@ -import { useEffect, useRef, useState } from "react"; -import { View, Text, ActivityIndicator, Platform } from "react-native"; +import { View, Text, ActivityIndicator, Platform, RefreshControl } from "react-native"; import { Gesture, GestureDetector, ScrollView } from "react-native-gesture-handler"; -import { StyleSheet, UnistylesRuntime } from "react-native-unistyles"; +import { StyleSheet, UnistylesRuntime, useUnistyles } from "react-native-unistyles"; import { useExplorerSidebarAnimation } from "@/contexts/explorer-sidebar-animation-context"; import { useSessionStore } from "@/stores/session-store"; +import { useGitDiffQuery } from "@/hooks/use-git-diff-query"; interface ParsedDiffFile { path: string; @@ -33,13 +33,28 @@ function parseDiff(diffText: string): ParsedDiffFile[] { 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 }); + // Skip metadata lines - they're noise in the UI + // - First line (a/... b/...) - already shown in file header + // - index ... - git hash info, not useful + // - --- a/... and +++ b/... - file markers, redundant + if (i === 0) continue; + if (line.startsWith("index ")) continue; + if (line.startsWith("--- ")) continue; + if (line.startsWith("+++ ")) continue; + + if (line.startsWith("@@")) { + // Extract just the line numbers portion from @@ -x,y +x,y @@ context + const hunkMatch = line.match(/^(@@ .+? @@)/); + const hunkHeader = hunkMatch ? hunkMatch[1] : line; + parsedLines.push({ type: "header", content: hunkHeader }); } else if (line.startsWith("+")) { - parsedLines.push({ type: "add", content: line }); + parsedLines.push({ type: "add", content: line.slice(1) }); } else if (line.startsWith("-")) { - parsedLines.push({ type: "remove", content: line }); - } else { + parsedLines.push({ type: "remove", content: line.slice(1) }); + } else if (line.startsWith(" ")) { + parsedLines.push({ type: "context", content: line.slice(1) }); + } else if (line.length > 0) { + // Non-empty lines without prefix (rare, but handle gracefully) parsedLines.push({ type: "context", content: line }); } } @@ -56,9 +71,12 @@ interface GitDiffPaneProps { } export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { - const [isLoading, setIsLoading] = useState(true); - const hasRequestedRef = useRef(null); + const { theme } = useUnistyles(); const { closeGestureRef } = useExplorerSidebarAnimation(); + const { diff, isLoading, isFetching, isError, error, refresh } = useGitDiffQuery({ + serverId, + agentId, + }); const isMobile = UnistylesRuntime.breakpoint === "xs" || UnistylesRuntime.breakpoint === "sm"; @@ -75,42 +93,6 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { state.sessions[serverId]?.agents?.get(agentId) ); - const diffText = useSessionStore((state) => - state.sessions[serverId]?.gitDiffs?.get(agentId) - ); - - const requestGitDiff = useSessionStore((state) => - state.sessions[serverId]?.methods?.requestGitDiff - ); - - useEffect(() => { - if (!agentId || !requestGitDiff) { - setIsLoading(false); - return; - } - - // Prevent duplicate requests for the same agentId - if (hasRequestedRef.current === agentId) { - return; - } - hasRequestedRef.current = agentId; - - setIsLoading(true); - requestGitDiff(agentId); - - const timeout = setTimeout(() => { - setIsLoading(false); - }, 5000); - - return () => clearTimeout(timeout); - }, [agentId, requestGitDiff]); - - useEffect(() => { - if (diffText !== undefined) { - setIsLoading(false); - } - }, [diffText]); - if (!agent) { return ( @@ -119,12 +101,23 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { ); } - const isError = diffText?.startsWith("Error:"); - const parsedFiles = isError || !diffText ? [] : parseDiff(diffText); + const parsedFiles = isError || !diff ? [] : parseDiff(diff); const hasChanges = parsedFiles.length > 0; + const errorMessage = isError && error instanceof Error ? error.message : null; return ( - + + } + > {isLoading ? ( @@ -132,7 +125,7 @@ export function GitDiffPane({ serverId, agentId }: GitDiffPaneProps) { ) : isError ? ( - {diffText} + {errorMessage ?? "Failed to load changes"} ) : !hasChanges ? ( diff --git a/packages/app/src/hooks/use-git-diff-query.ts b/packages/app/src/hooks/use-git-diff-query.ts new file mode 100644 index 000000000..9ce2c5e0d --- /dev/null +++ b/packages/app/src/hooks/use-git-diff-query.ts @@ -0,0 +1,62 @@ +import { useQuery, useQueryClient } from "@tanstack/react-query"; +import { useCallback, useEffect } from "react"; +import { useSessionStore } from "@/stores/session-store"; +import { sendRpcRequest } from "@/lib/send-rpc-request"; +import { useExplorerSidebarStore } from "@/stores/explorer-sidebar-store"; + +const GIT_DIFF_STALE_TIME = 30_000; + +function gitDiffQueryKey(serverId: string, agentId: string) { + return ["gitDiff", serverId, agentId] as const; +} + +interface UseGitDiffQueryOptions { + serverId: string; + agentId: string; +} + +export function useGitDiffQuery({ serverId, agentId }: UseGitDiffQueryOptions) { + const queryClient = useQueryClient(); + const ws = useSessionStore((state) => state.sessions[serverId]?.ws); + const { isOpen, activeTab } = useExplorerSidebarStore(); + + const query = useQuery({ + queryKey: gitDiffQueryKey(serverId, agentId), + queryFn: async () => { + if (!ws) { + throw new Error("WebSocket not available"); + } + const response = await sendRpcRequest(ws, { + type: "git_diff_request", + agentId, + }); + return response.diff; + }, + enabled: !!ws && ws.isConnected && !!agentId, + staleTime: GIT_DIFF_STALE_TIME, + }); + + // Revalidate when sidebar opens with "changes" tab active + useEffect(() => { + if (!isOpen || activeTab !== "changes" || !agentId) { + return; + } + // Invalidate to trigger background refetch (shows stale data while fetching) + queryClient.invalidateQueries({ + queryKey: gitDiffQueryKey(serverId, agentId), + }); + }, [isOpen, activeTab, serverId, agentId, queryClient]); + + const refresh = useCallback(() => { + return query.refetch(); + }, [query]); + + return { + diff: query.data ?? null, + isLoading: query.isLoading, + isFetching: query.isFetching, + isError: query.isError, + error: query.error, + refresh, + }; +} diff --git a/packages/app/src/lib/send-rpc-request.ts b/packages/app/src/lib/send-rpc-request.ts new file mode 100644 index 000000000..c84018c9e --- /dev/null +++ b/packages/app/src/lib/send-rpc-request.ts @@ -0,0 +1,231 @@ +import type { + SessionInboundMessage, + SessionOutboundMessage, +} from "@server/server/messages"; +import type { UseWebSocketReturn } from "@/hooks/use-websocket"; +import { generateMessageId } from "@/types/stream"; + +// ============================================================================ +// Type-level utilities for automatic request→response type inference +// ============================================================================ + +/** + * Extract the payload type from a message. + */ +type PayloadOf = TMessage extends { payload: infer P } ? P : never; + +/** + * All request message types that can be used with sendRpcRequest. + * These are inbound messages that end with `_request`. + */ +type RpcRequestMessage = Extract; + +/** + * Extract the type literal from a request message. + */ +type RpcRequestType = RpcRequestMessage["type"]; + +/** + * Override mapping for requests that don't follow the standard `_request` → `_response` pattern. + */ +interface ResponseTypeOverrides { + create_agent_request: "agent_state"; + refresh_agent_request: "agent_state"; + initialize_agent_request: "initialize_agent_request"; +} + +/** + * Convert request type string to response type string. + * - First checks override mapping for non-standard patterns + * - Falls back to standard `*_request` → `*_response` conversion + */ +type RequestToResponseType = T extends keyof ResponseTypeOverrides + ? ResponseTypeOverrides[T] + : T extends `${infer Base}_request` + ? `${Base}_response` + : never; + +/** + * Given a request type string, get the response message type. + */ +type ResponseMessageFor = Extract< + SessionOutboundMessage, + { type: RequestToResponseType } +>; + +/** + * Given a request type string, get the payload type of the response. + */ +type ResponsePayloadFor = PayloadOf>; + +/** + * Given a request type string, get the request message type (without requestId). + */ +type RequestInputFor = Omit< + Extract, + "requestId" +>; + +/** + * Infer the request type from a request object. + * This enables TypeScript to narrow based on the `type` property. + */ +type InferRequestType = TRequest extends { type: infer T extends RpcRequestType } + ? T + : never; + +// ============================================================================ +// Runtime configuration +// ============================================================================ + +interface SendRpcRequestOptions { + timeoutMs?: number; +} + +const DEFAULT_TIMEOUT_MS = 15000; + +class RpcError extends Error { + constructor( + message: string, + public readonly code: "timeout" | "response_error" | "disconnected" + ) { + super(message); + this.name = "RpcError"; + } +} + +/** + * Maps request types to their corresponding response types at runtime. + */ +const RESPONSE_TYPE_MAP: Record = { + git_diff_request: "git_diff_response", + file_explorer_request: "file_explorer_response", + file_download_token_request: "file_download_token_response", + git_repo_info_request: "git_repo_info_response", + list_provider_models_request: "list_provider_models_response", + list_conversations_request: "list_conversations_response", + list_persisted_agents_request: "list_persisted_agents_response", + create_agent_request: "agent_state", + refresh_agent_request: "agent_state", + initialize_agent_request: "initialize_agent_request", +}; + +// ============================================================================ +// Main function +// ============================================================================ + +/** + * Send an RPC request over WebSocket and wait for the matching response. + * + * Features: + * - Auto-generates and injects requestId into the request + * - Matches response by requestId in payload + * - Fully typed: response type is inferred from request type + * - Throws on timeout or if response contains an error field + * + * @example + * ```ts + * const response = await sendRpcRequest(ws, { + * type: "git_diff_request", + * agentId: "abc123", + * }); + * // response is typed as { agentId: string; diff: string; error: string | null } + * ``` + */ +export function sendRpcRequest< + const TRequest extends RequestInputFor +>( + ws: UseWebSocketReturn, + request: TRequest, + options?: SendRpcRequestOptions +): Promise>> { + const timeoutMs = options?.timeoutMs ?? DEFAULT_TIMEOUT_MS; + const requestId = generateMessageId(); + const requestType = (request as { type: string }).type; + const responseType = RESPONSE_TYPE_MAP[requestType]; + + if (!responseType) { + return Promise.reject( + new RpcError(`Unknown request type: ${requestType}`, "response_error") + ); + } + + return new Promise((resolve, reject) => { + let timeoutHandle: ReturnType | null = null; + let unsubscribe: (() => void) | null = null; + let settled = false; + + const cleanup = () => { + if (timeoutHandle) { + clearTimeout(timeoutHandle); + timeoutHandle = null; + } + if (unsubscribe) { + unsubscribe(); + unsubscribe = null; + } + }; + + const settle = (fn: () => U): U | undefined => { + if (settled) { + return undefined; + } + settled = true; + cleanup(); + return fn(); + }; + + // Subscribe to the response type + unsubscribe = ws.on(responseType, (message) => { + const payload = (message as { payload?: unknown }).payload; + if (!payload || typeof payload !== "object") { + return; + } + + const payloadRecord = payload as Record; + + // Match by requestId + if (payloadRecord.requestId !== requestId) { + return; + } + + // Check for error in response + if ( + typeof payloadRecord.error === "string" && + payloadRecord.error.length > 0 + ) { + settle(() => + reject(new RpcError(payloadRecord.error as string, "response_error")) + ); + return; + } + + settle(() => resolve(payload as ResponsePayloadFor>)); + }); + + // Set up timeout + if (timeoutMs > 0) { + timeoutHandle = setTimeout(() => { + settle(() => + reject( + new RpcError(`RPC request timed out after ${timeoutMs}ms`, "timeout") + ) + ); + }, timeoutMs); + } + + // Send the request with the generated requestId + const fullRequest = { + ...request, + requestId, + } as SessionInboundMessage; + + ws.send({ + type: "session", + message: fullRequest, + }); + }); +} + +export { RpcError }; +export type { RpcRequestType, ResponsePayloadFor, RequestInputFor }; diff --git a/packages/app/src/lib/send-rpc-request.typetest.ts b/packages/app/src/lib/send-rpc-request.typetest.ts new file mode 100644 index 000000000..b1d9d21fa --- /dev/null +++ b/packages/app/src/lib/send-rpc-request.typetest.ts @@ -0,0 +1,128 @@ +/** + * Type verification tests for sendRpcRequest + * Run `npm run typecheck` - this file should compile with the expected errors marked by @ts-expect-error + */ +import { sendRpcRequest } from "./send-rpc-request"; +import type { UseWebSocketReturn } from "@/hooks/use-websocket"; + +declare const ws: UseWebSocketReturn; + +// ============================================================================ +// Test 1: Git diff request - response should be fully typed +// ============================================================================ +async function testGitDiff() { + const response = await sendRpcRequest(ws, { + type: "git_diff_request", + agentId: "test-agent", + }); + + // ✅ These should work - fields exist on response + const agentId: string = response.agentId; + const diff: string = response.diff; + const error: string | null = response.error; + console.log(agentId, diff, error); + + // ❌ These should error + // @ts-expect-error - 'nonExistent' does not exist on git_diff_response payload + response.nonExistent; + + // @ts-expect-error - diff is string, not number + const wrongType: number = response.diff; + console.log(wrongType); +} + +// ============================================================================ +// Test 2: File explorer request - response should be fully typed +// ============================================================================ +async function testFileExplorer() { + const response = await sendRpcRequest(ws, { + type: "file_explorer_request", + agentId: "test-agent", + path: ".", + mode: "list", + }); + + // ✅ These should work + const agentId: string = response.agentId; + const mode: "list" | "file" = response.mode; + const path: string = response.path; + console.log(agentId, mode, path); + + // ✅ Directory is optional/nullable + if (response.directory) { + const entries = response.directory.entries; + console.log(entries); + } + + // ❌ Should error + // @ts-expect-error - 'fakeField' does not exist + response.fakeField; +} + +// ============================================================================ +// Test 3: Request must include required fields +// ============================================================================ +async function testRequiredFields() { + // ✅ Valid calls - all required fields present + await sendRpcRequest(ws, { type: "git_diff_request", agentId: "test" }); + await sendRpcRequest(ws, { type: "file_explorer_request", agentId: "x", path: ".", mode: "list" }); + + // Note: Missing fields would cause compile errors, but we can't use @ts-expect-error + // on the call itself because the `const` generic infers the literal object type. + // The type system validates at the constraint level, not the call level. +} + +// ============================================================================ +// Test 4: File download token request +// ============================================================================ +async function testFileDownloadToken() { + const response = await sendRpcRequest(ws, { + type: "file_download_token_request", + agentId: "test-agent", + path: "/file.txt", + }); + + // ✅ These should work + const token: string | null = response.token; + const agentId: string = response.agentId; + console.log(token, agentId); + + // ❌ Should error + // @ts-expect-error - invalid field + response.notAField; +} + +// ============================================================================ +// Test 5: Verify template literal type derivation works +// ============================================================================ +async function testTemplateLiteralDerivation() { + // The response type should be automatically derived from request type + // "git_diff_request" → "git_diff_response" → payload type + + const gitDiff = await sendRpcRequest(ws, { type: "git_diff_request", agentId: "a" }); + const fileExplorer = await sendRpcRequest(ws, { type: "file_explorer_request", agentId: "a", path: ".", mode: "list" }); + const downloadToken = await sendRpcRequest(ws, { type: "file_download_token_request", agentId: "a", path: "." }); + + // Each response should have its own distinct type + console.log(gitDiff.diff); // string + console.log(fileExplorer.directory); // object | null + console.log(downloadToken.token); // string | null + + // ❌ Cross-type access should fail + // @ts-expect-error - gitDiff doesn't have 'directory' + gitDiff.directory; + + // @ts-expect-error - fileExplorer doesn't have 'diff' + fileExplorer.diff; + + // @ts-expect-error - downloadToken doesn't have 'diff' + downloadToken.diff; +} + +export { + testGitDiff, + testFileExplorer, + testRequiredFields, + testFileDownloadToken, + testTemplateLiteralDerivation, +}; diff --git a/packages/app/src/stores/explorer-sidebar-store.ts b/packages/app/src/stores/explorer-sidebar-store.ts index 660a36247..098c074f0 100644 --- a/packages/app/src/stores/explorer-sidebar-store.ts +++ b/packages/app/src/stores/explorer-sidebar-store.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { Platform } from "react-native"; type ExplorerTab = "changes" | "files"; export type ViewMode = "list" | "grid"; @@ -10,6 +11,8 @@ export const DEFAULT_EXPLORER_SIDEBAR_WIDTH = 400; export const MIN_EXPLORER_SIDEBAR_WIDTH = 280; export const MAX_EXPLORER_SIDEBAR_WIDTH = 800; +const DEFAULT_OPEN = Platform.OS === "web"; + interface ExplorerSidebarState { isOpen: boolean; activeTab: ExplorerTab; @@ -32,7 +35,7 @@ function clampWidth(width: number): number { export const useExplorerSidebarStore = create()( persist( (set) => ({ - isOpen: false, + isOpen: DEFAULT_OPEN, activeTab: "changes", width: DEFAULT_EXPLORER_SIDEBAR_WIDTH, viewMode: "list", diff --git a/packages/app/src/stores/sidebar-store.ts b/packages/app/src/stores/sidebar-store.ts index f693f04c2..bb926f386 100644 --- a/packages/app/src/stores/sidebar-store.ts +++ b/packages/app/src/stores/sidebar-store.ts @@ -1,6 +1,7 @@ import { create } from "zustand"; import { persist, createJSONStorage } from "zustand/middleware"; import AsyncStorage from "@react-native-async-storage/async-storage"; +import { Platform } from "react-native"; interface SidebarState { isOpen: boolean; @@ -9,10 +10,12 @@ interface SidebarState { close: () => void; } +const DEFAULT_OPEN = Platform.OS === "web"; + export const useSidebarStore = create()( persist( (set) => ({ - isOpen: false, + isOpen: DEFAULT_OPEN, toggle: () => set((state) => ({ isOpen: !state.isOpen })), open: () => set({ isOpen: true }), close: () => set({ isOpen: false }), diff --git a/packages/server/src/server/messages.ts b/packages/server/src/server/messages.ts index 48db3040e..784e36d3e 100644 --- a/packages/server/src/server/messages.ts +++ b/packages/server/src/server/messages.ts @@ -450,6 +450,7 @@ export const AgentPermissionResponseMessageSchema = z.object({ export const GitDiffRequestSchema = z.object({ type: z.literal("git_diff_request"), agentId: z.string(), + requestId: z.string().optional(), }); const FileExplorerEntrySchema = z.object({ @@ -736,6 +737,7 @@ export const GitDiffResponseSchema = z.object({ agentId: z.string(), diff: z.string(), error: z.string().nullable(), + requestId: z.string().optional(), }), }); diff --git a/packages/server/src/server/session.ts b/packages/server/src/server/session.ts index 274b5e700..7c028a2f1 100644 --- a/packages/server/src/server/session.ts +++ b/packages/server/src/server/session.ts @@ -840,7 +840,7 @@ export class Session { break; case "git_diff_request": - await this.handleGitDiffRequest(msg.agentId); + await this.handleGitDiffRequest(msg.agentId, msg.requestId); break; case "file_explorer_request": @@ -1967,7 +1967,7 @@ export class Session { /** * Handle git diff request for an agent */ - private async handleGitDiffRequest(agentId: string): Promise { + private async handleGitDiffRequest(agentId: string, requestId?: string): Promise { console.log( `[Session ${this.clientId}] Handling git diff request for agent ${agentId}` ); @@ -1983,6 +1983,7 @@ export class Session { agentId, diff: "", error: `Agent not found: ${agentId}`, + requestId, }, }); return; @@ -1998,6 +1999,7 @@ export class Session { agentId, diff: stdout, error: null, + requestId, }, }); @@ -2015,6 +2017,7 @@ export class Session { agentId, diff: "", error: error.message, + requestId, }, }); }