feat: conversational branching, AI markers, TaskLane improvements, collapsible sidebar #120

Merged
chihlasm merged 58 commits from feat/conversational-branching into main 2026-03-27 13:16:44 +00:00
Showing only changes of commit f5056d2e84 - Show all commits

View File

@@ -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<void>
createFork: (sessionId: string, data: ForkCreateRequest) => Promise<ForkPointResponse | null>
switchBranch: (sessionId: string, branchId: string) => Promise<BranchSwitchResponse | null>
updateStatus: (sessionId: string, branchId: string, status: string, reason?: string) => Promise<void>
reviveBranch: (sessionId: string, branchId: string, data: ReviveRequest) => Promise<void>
sendMessage: (sessionId: string, branchId: string, data: BranchMessageRequest) => Promise<BranchMessageResponse | null>
}
export function useBranching(): UseBranching {
const [branches, setBranches] = useState<BranchResponse[]>([])
const [activeBranchId, setActiveBranchId] = useState<string | null>(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<ForkPointResponse | null> => {
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<BranchSwitchResponse | null> => {
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<BranchMessageResponse | null> => {
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,
}
}