diff --git a/frontend/src/hooks/useBranching.ts b/frontend/src/hooks/useBranching.ts new file mode 100644 index 00000000..cf7bbd38 --- /dev/null +++ b/frontend/src/hooks/useBranching.ts @@ -0,0 +1,161 @@ +import { useState, useCallback } from 'react' +import { branchesApi } from '@/api/branches' +import { toast } from '@/lib/toast' +import type { + BranchResponse, + ForkCreateRequest, + ForkPointResponse, + BranchSwitchResponse, + ReviveRequest, + BranchMessageRequest, + BranchMessageResponse, +} from '@/types/branching' + +export interface UseBranching { + // State + branches: BranchResponse[] + activeBranchId: string | null + isLoading: boolean + isProcessing: boolean + + // Actions + loadBranches: (sessionId: string) => Promise + createFork: (sessionId: string, data: ForkCreateRequest) => Promise + switchBranch: (sessionId: string, branchId: string) => Promise + updateStatus: (sessionId: string, branchId: string, status: string, reason?: string) => Promise + reviveBranch: (sessionId: string, branchId: string, data: ReviveRequest) => Promise + sendMessage: (sessionId: string, branchId: string, data: BranchMessageRequest) => Promise +} + +export function useBranching(): UseBranching { + const [branches, setBranches] = useState([]) + const [activeBranchId, setActiveBranchId] = useState(null) + const [isLoading, setIsLoading] = useState(false) + const [isProcessing, setIsProcessing] = useState(false) + + const loadBranches = useCallback(async (sessionId: string) => { + setIsLoading(true) + try { + const result = await branchesApi.getBranches(sessionId) + setBranches(result.branches) + setActiveBranchId(result.active_branch_id) + } catch { + toast.error('Failed to load branches') + } finally { + setIsLoading(false) + } + }, []) + + const createFork = useCallback(async ( + sessionId: string, + data: ForkCreateRequest + ): Promise => { + setIsProcessing(true) + try { + const result = await branchesApi.createFork(sessionId, data) + // Reload branches to reflect the new fork + const updated = await branchesApi.getBranches(sessionId) + setBranches(updated.branches) + setActiveBranchId(updated.active_branch_id) + return result + } catch { + toast.error('Failed to create fork') + return null + } finally { + setIsProcessing(false) + } + }, []) + + const switchBranch = useCallback(async ( + sessionId: string, + branchId: string + ): Promise => { + setIsProcessing(true) + try { + const result = await branchesApi.switchBranch(sessionId, branchId) + setActiveBranchId(result.active_branch_id) + setBranches(prev => + prev.map(b => b.id === result.active_branch_id ? result.branch : b) + ) + return result + } catch { + toast.error('Failed to switch branch') + return null + } finally { + setIsProcessing(false) + } + }, []) + + const updateStatus = useCallback(async ( + sessionId: string, + branchId: string, + status: string, + reason?: string + ) => { + setIsProcessing(true) + try { + await branchesApi.updateBranchStatus(sessionId, branchId, status, reason) + setBranches(prev => + prev.map(b => + b.id === branchId + ? { ...b, status: status as BranchResponse['status'], status_reason: reason ?? b.status_reason } + : b + ) + ) + } catch { + toast.error('Failed to update branch status') + } finally { + setIsProcessing(false) + } + }, []) + + const reviveBranch = useCallback(async ( + sessionId: string, + branchId: string, + data: ReviveRequest + ) => { + setIsProcessing(true) + try { + await branchesApi.reviveBranch(sessionId, branchId, data) + // Reload to get updated statuses + const updated = await branchesApi.getBranches(sessionId) + setBranches(updated.branches) + setActiveBranchId(updated.active_branch_id) + toast.success('Branch revived') + } catch { + toast.error('Failed to revive branch') + } finally { + setIsProcessing(false) + } + }, []) + + const sendMessage = useCallback(async ( + sessionId: string, + branchId: string, + data: BranchMessageRequest + ): Promise => { + setIsProcessing(true) + try { + const result = await branchesApi.sendBranchMessage(sessionId, branchId, data) + return result + } catch { + toast.error('Failed to send message') + return null + } finally { + setIsProcessing(false) + } + }, []) + + return { + branches, + activeBranchId, + isLoading, + isProcessing, + loadBranches, + createFork, + switchBranch, + updateStatus, + reviveBranch, + sendMessage, + } +}