From c0dfa7c2309e5c83e87d39d898489dd35f226efe Mon Sep 17 00:00:00 2001 From: chihlasm Date: Tue, 24 Mar 2026 09:51:30 +0000 Subject: [PATCH] feat: add BranchMap sidebar with BranchNode tree visualization BranchNode renders a branch button with depth-based indent, status icon, and status badge using the design system color palette. BranchMap builds a tree from the flat branch list via buildTree/flattenTree and renders all nodes with a GitBranch header. Co-Authored-By: Claude Opus 4.6 (1M context) --- frontend/src/components/session/BranchMap.tsx | 81 +++++++++++++++++ .../src/components/session/BranchNode.tsx | 91 +++++++++++++++++++ 2 files changed, 172 insertions(+) create mode 100644 frontend/src/components/session/BranchMap.tsx create mode 100644 frontend/src/components/session/BranchNode.tsx diff --git a/frontend/src/components/session/BranchMap.tsx b/frontend/src/components/session/BranchMap.tsx new file mode 100644 index 00000000..7a39041c --- /dev/null +++ b/frontend/src/components/session/BranchMap.tsx @@ -0,0 +1,81 @@ +import { GitBranch } from 'lucide-react' +import { BranchNode } from './BranchNode' +import type { BranchResponse } from '@/types/branching' + +interface BranchTreeNode { + branch: BranchResponse + depth: number + children: BranchTreeNode[] +} + +function buildTree(branches: BranchResponse[]): BranchTreeNode[] { + const map = new Map() + const roots: BranchTreeNode[] = [] + + // First pass: create nodes + for (const branch of branches) { + map.set(branch.id, { branch, depth: 0, children: [] }) + } + + // Second pass: assign parents / roots + for (const branch of branches) { + const node = map.get(branch.id)! + if (branch.parent_branch_id && map.has(branch.parent_branch_id)) { + const parent = map.get(branch.parent_branch_id)! + node.depth = parent.depth + 1 + parent.children.push(node) + } else { + roots.push(node) + } + } + + return roots +} + +function flattenTree(nodes: BranchTreeNode[]): Array<{ branch: BranchResponse; depth: number }> { + const result: Array<{ branch: BranchResponse; depth: number }> = [] + for (const node of nodes) { + result.push({ branch: node.branch, depth: node.depth }) + if (node.children.length > 0) { + result.push(...flattenTree(node.children)) + } + } + return result +} + +interface BranchMapProps { + branches: BranchResponse[] + activeBranchId: string | null + onSelectBranch: (branchId: string) => void +} + +export function BranchMap({ branches, activeBranchId, onSelectBranch }: BranchMapProps) { + const roots = buildTree(branches) + const flat = flattenTree(roots) + + return ( +
+
+ + + Branch Map + + {branches.length} +
+ + {flat.length === 0 ? ( +

No branches yet.

+ ) : ( + flat.map(({ branch, depth }) => ( + + )) + )} +
+ ) +} diff --git a/frontend/src/components/session/BranchNode.tsx b/frontend/src/components/session/BranchNode.tsx new file mode 100644 index 00000000..192f1056 --- /dev/null +++ b/frontend/src/components/session/BranchNode.tsx @@ -0,0 +1,91 @@ +import { CircleDot, CheckCircle2, XCircle, Circle, RotateCcw } from 'lucide-react' +import { cn } from '@/lib/utils' +import type { BranchResponse } from '@/types/branching' + +type BranchStatus = BranchResponse['status'] + +interface StatusConfig { + icon: React.ElementType + textClass: string + badgeClass: string + label: string +} + +const STATUS_CONFIG: Record = { + active: { + icon: CircleDot, + textClass: 'text-accent', + badgeClass: 'bg-accent-dim text-accent-text', + label: 'Active', + }, + solved: { + icon: CheckCircle2, + textClass: 'text-[#34d399]', + badgeClass: 'bg-[rgba(52,211,153,0.10)] text-[#34d399]', + label: 'Solved', + }, + dead_end: { + icon: XCircle, + textClass: 'text-[#f87171]', + badgeClass: 'bg-[rgba(248,113,113,0.10)] text-[#f87171]', + label: 'Dead End', + }, + untried: { + icon: Circle, + textClass: 'text-muted', + badgeClass: 'bg-[rgba(79,86,102,0.20)] text-muted', + label: 'Untried', + }, + revived: { + icon: RotateCcw, + textClass: 'text-[#eab308]', + badgeClass: 'bg-[rgba(234,179,8,0.10)] text-[#eab308]', + label: 'Revived', + }, +} + +interface BranchNodeProps { + branch: BranchResponse + depth: number + isActive: boolean + onClick: (branchId: string) => void +} + +export function BranchNode({ branch, depth, isActive, onClick }: BranchNodeProps) { + const config = STATUS_CONFIG[branch.status] + const Icon = config.icon + + return ( + + ) +}