diff --git a/web-ui/src/components/git/BranchList.tsx b/web-ui/src/components/git/BranchList.tsx
new file mode 100644
index 00000000..48429475
--- /dev/null
+++ b/web-ui/src/components/git/BranchList.tsx
@@ -0,0 +1,178 @@
+/**
+ * BranchList Component
+ *
+ * Displays a list of git branches with status badges.
+ * Supports filtering by status and shows merge info for merged branches.
+ *
+ * Ticket: #272 - Git Visualization
+ */
+
+'use client';
+
+import { memo, useMemo } from 'react';
+import { GitCommitIcon, Loading03Icon, Alert02Icon } from '@hugeicons/react';
+import type { GitBranch, BranchStatus } from '@/types/git';
+
+export interface BranchListProps {
+ /** Array of branches to display */
+ branches: GitBranch[];
+ /** Filter to specific status (optional) */
+ filterStatus?: BranchStatus;
+ /** Loading state */
+ isLoading?: boolean;
+ /** Error message */
+ error?: string | null;
+ /** Optional callback for branch click */
+ onBranchClick?: (branch: GitBranch) => void;
+}
+
+/**
+ * Get badge styling based on branch status
+ */
+function getStatusStyles(status: BranchStatus): { bgClass: string; textClass: string } {
+ switch (status) {
+ case 'active':
+ return { bgClass: 'bg-primary/10', textClass: 'text-primary' };
+ case 'merged':
+ return { bgClass: 'bg-secondary/10', textClass: 'text-secondary-foreground' };
+ case 'abandoned':
+ return { bgClass: 'bg-muted', textClass: 'text-muted-foreground' };
+ }
+}
+
+/**
+ * Individual branch item
+ */
+interface BranchItemProps {
+ branch: GitBranch;
+ onClick?: () => void;
+}
+
+const BranchItem = memo(function BranchItem({ branch, onClick }: BranchItemProps) {
+ const statusStyles = getStatusStyles(branch.status);
+
+ return (
+
+
+
+
+ {branch.branch_name}
+
+
+
+ {branch.merge_commit && (
+
+ → {branch.merge_commit.slice(0, 7)}
+
+ )}
+
+ {branch.status}
+
+
+
+ );
+});
+
+BranchItem.displayName = 'BranchItem';
+
+/**
+ * BranchList - Shows list of git branches
+ */
+const BranchList = memo(function BranchList({
+ branches,
+ filterStatus,
+ isLoading = false,
+ error = null,
+ onBranchClick,
+}: BranchListProps) {
+ // Filter branches by status if provided
+ const displayedBranches = useMemo(() => {
+ if (!filterStatus) return branches;
+ return branches.filter((b) => b.status === filterStatus);
+ }, [branches, filterStatus]);
+
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+
+ Branches
+
+
+
+ Loading branches...
+
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+ );
+ }
+
+ // Empty state
+ if (displayedBranches.length === 0) {
+ return (
+
+
+
+ Branches
+
+
+
No branches created yet
+
Branches will appear here as issues are worked on
+
+
+ );
+ }
+
+ return (
+
+
+
+ Branches
+
+ ({displayedBranches.length})
+
+
+
+ {displayedBranches.map((branch) => (
+ onBranchClick(branch) : undefined}
+ />
+ ))}
+
+
+ );
+});
+
+BranchList.displayName = 'BranchList';
+
+export default BranchList;
diff --git a/web-ui/src/components/git/CommitHistory.tsx b/web-ui/src/components/git/CommitHistory.tsx
new file mode 100644
index 00000000..d145fd4c
--- /dev/null
+++ b/web-ui/src/components/git/CommitHistory.tsx
@@ -0,0 +1,189 @@
+/**
+ * CommitHistory Component
+ *
+ * Displays a list of recent git commits with message, hash,
+ * author, and file change count.
+ *
+ * Ticket: #272 - Git Visualization
+ */
+
+'use client';
+
+import { memo, useMemo } from 'react';
+import { GitCommitIcon, Loading03Icon, Alert02Icon } from '@hugeicons/react';
+import type { GitCommit } from '@/types/git';
+
+export interface CommitHistoryProps {
+ /** Array of commits to display */
+ commits: GitCommit[];
+ /** Maximum number of commits to show (default: 10) */
+ maxItems?: number;
+ /** Loading state */
+ isLoading?: boolean;
+ /** Error message */
+ error?: string | null;
+ /** Optional callback for commit click */
+ onCommitClick?: (commit: GitCommit) => void;
+}
+
+/**
+ * Format timestamp to relative time (e.g., "2 hours ago")
+ */
+function formatRelativeTime(timestamp: string): string {
+ const date = new Date(timestamp);
+ const now = new Date();
+ const diffMs = now.getTime() - date.getTime();
+ const diffMins = Math.floor(diffMs / 60000);
+ const diffHours = Math.floor(diffMins / 60);
+ const diffDays = Math.floor(diffHours / 24);
+
+ if (diffMins < 1) return 'just now';
+ if (diffMins < 60) return `${diffMins}m ago`;
+ if (diffHours < 24) return `${diffHours}h ago`;
+ if (diffDays < 7) return `${diffDays}d ago`;
+ return date.toLocaleDateString();
+}
+
+/**
+ * Individual commit item
+ */
+interface CommitItemProps {
+ commit: GitCommit;
+ onClick?: () => void;
+}
+
+const CommitItem = memo(function CommitItem({ commit, onClick }: CommitItemProps) {
+ return (
+
+
+
+
+
+ {commit.short_hash}
+
+ {commit.files_changed !== undefined && (
+
+ {commit.files_changed} file{commit.files_changed !== 1 ? 's' : ''}
+
+ )}
+
+
+ {commit.message}
+
+
+
+
+ );
+});
+
+CommitItem.displayName = 'CommitItem';
+
+/**
+ * CommitHistory - Shows list of recent commits
+ */
+const CommitHistory = memo(function CommitHistory({
+ commits,
+ maxItems = 10,
+ isLoading = false,
+ error = null,
+ onCommitClick,
+}: CommitHistoryProps) {
+ // Limit commits to maxItems
+ const displayedCommits = useMemo(
+ () => commits.slice(0, maxItems),
+ [commits, maxItems]
+ );
+
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+
+ Recent Commits
+
+
+
+ Loading commits...
+
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+
+
+ Recent Commits
+
+
+
+ );
+ }
+
+ // Empty state
+ if (displayedCommits.length === 0) {
+ return (
+
+
+
+ Recent Commits
+
+
+
No commits yet
+
Commits will appear here as work progresses
+
+
+ );
+ }
+
+ return (
+
+
+
+ Recent Commits
+
+ ({displayedCommits.length})
+
+
+
+ {displayedCommits.map((commit) => (
+ onCommitClick(commit) : undefined}
+ />
+ ))}
+
+ {commits.length > maxItems && (
+
+ Showing {maxItems} of {commits.length} commits
+
+ )}
+
+ );
+});
+
+CommitHistory.displayName = 'CommitHistory';
+
+export default CommitHistory;
diff --git a/web-ui/src/components/git/GitBranchIndicator.tsx b/web-ui/src/components/git/GitBranchIndicator.tsx
new file mode 100644
index 00000000..04830d5d
--- /dev/null
+++ b/web-ui/src/components/git/GitBranchIndicator.tsx
@@ -0,0 +1,104 @@
+/**
+ * GitBranchIndicator Component
+ *
+ * Displays the current git branch name and status in a compact badge format.
+ * Shows dirty state indicator when there are uncommitted changes.
+ *
+ * Ticket: #272 - Git Visualization
+ */
+
+'use client';
+
+import { memo } from 'react';
+import { GitCommitIcon, Loading03Icon, Alert02Icon } from '@hugeicons/react';
+import type { GitStatus } from '@/types/git';
+
+export interface GitBranchIndicatorProps {
+ /** Git status data (null when not loaded) */
+ status: GitStatus | null;
+ /** Loading state */
+ isLoading?: boolean;
+ /** Error message */
+ error?: string | null;
+}
+
+/**
+ * Calculate total file changes
+ */
+function getTotalChanges(status: GitStatus): number {
+ return (
+ status.modified_files.length +
+ status.untracked_files.length +
+ status.staged_files.length
+ );
+}
+
+/**
+ * GitBranchIndicator - Shows current branch with status
+ */
+const GitBranchIndicator = memo(function GitBranchIndicator({
+ status,
+ isLoading = false,
+ error = null,
+}: GitBranchIndicatorProps) {
+ // Loading state
+ if (isLoading) {
+ return (
+
+
+ Loading...
+
+ );
+ }
+
+ // Error state
+ if (error) {
+ return (
+
+ );
+ }
+
+ // No status
+ if (!status) {
+ return null;
+ }
+
+ const totalChanges = getTotalChanges(status);
+ const tooltipText = status.is_dirty
+ ? `${status.current_branch} (${totalChanges} uncommitted change${totalChanges !== 1 ? 's' : ''})`
+ : status.current_branch;
+
+ return (
+
+
+
+ {status.current_branch}
+
+ {status.is_dirty && (
+
+ )}
+
+ );
+});
+
+GitBranchIndicator.displayName = 'GitBranchIndicator';
+
+export default GitBranchIndicator;
diff --git a/web-ui/src/components/git/GitSection.tsx b/web-ui/src/components/git/GitSection.tsx
new file mode 100644
index 00000000..107c0080
--- /dev/null
+++ b/web-ui/src/components/git/GitSection.tsx
@@ -0,0 +1,155 @@
+/**
+ * GitSection Component
+ *
+ * Container component that combines Git visualization components:
+ * - GitBranchIndicator (current branch)
+ * - CommitHistory (recent commits)
+ * - BranchList (all branches)
+ *
+ * Uses SWR for data fetching with automatic refresh.
+ *
+ * Ticket: #272 - Git Visualization
+ */
+
+'use client';
+
+import { memo } from 'react';
+import useSWR from 'swr';
+import { GitCommitIcon, Loading03Icon, Alert02Icon } from '@hugeicons/react';
+import GitBranchIndicator from './GitBranchIndicator';
+import CommitHistory from './CommitHistory';
+import BranchList from './BranchList';
+import { getGitStatus, getCommits, getBranches } from '@/api/git';
+import type { GitStatus, GitCommit, GitBranch } from '@/types/git';
+
+export interface GitSectionProps {
+ /** Project ID to fetch Git data for */
+ projectId: number;
+ /** Maximum number of commits to show (default: 5) */
+ maxCommits?: number;
+ /** SWR refresh interval in ms (default: 30000) */
+ refreshInterval?: number;
+}
+
+/**
+ * GitSection - Dashboard section for Git visualization
+ */
+const GitSection = memo(function GitSection({
+ projectId,
+ maxCommits = 5,
+ refreshInterval = 30000,
+}: GitSectionProps) {
+ // Fetch git status
+ const {
+ data: status,
+ error: statusError,
+ isLoading: statusLoading,
+ } = useSWR
(
+ `git-status-${projectId}`,
+ () => getGitStatus(projectId),
+ { refreshInterval }
+ );
+
+ // Fetch recent commits (cache key includes maxCommits to invalidate on limit change)
+ const {
+ data: commits,
+ error: commitsError,
+ isLoading: commitsLoading,
+ } = useSWR(
+ `git-commits-${projectId}-${maxCommits}`,
+ () => getCommits(projectId, { limit: maxCommits }),
+ { refreshInterval }
+ );
+
+ // Fetch branches
+ const {
+ data: branches,
+ error: branchesError,
+ isLoading: branchesLoading,
+ } = useSWR(
+ `git-branches-${projectId}`,
+ () => getBranches(projectId),
+ { refreshInterval }
+ );
+
+ // Combined loading state
+ const isLoading = statusLoading || commitsLoading || branchesLoading;
+
+ // Combined error state
+ const hasError = statusError || commitsError || branchesError;
+ const errorMessage = statusError?.message || commitsError?.message || branchesError?.message;
+
+ // Loading state
+ if (isLoading && !status && !commits && !branches) {
+ return (
+
+
+
+ Loading Git data...
+
+
+ );
+ }
+
+ // Error state
+ if (hasError && !status && !commits && !branches) {
+ return (
+
+
+
+
+ {errorMessage || 'Failed to load Git data'}
+
+
+
+ );
+ }
+
+ return (
+
+ {/* Section Header */}
+
+
+ {/* Content */}
+
+ {/* Commits Section */}
+
+
+ {/* Branches Section */}
+
+
+
+ );
+});
+
+GitSection.displayName = 'GitSection';
+
+export default GitSection;
diff --git a/web-ui/src/components/git/index.ts b/web-ui/src/components/git/index.ts
new file mode 100644
index 00000000..616ccf27
--- /dev/null
+++ b/web-ui/src/components/git/index.ts
@@ -0,0 +1,17 @@
+/**
+ * Git Component Exports
+ *
+ * Central export point for all Git visualization components.
+ * Ticket: #272 - Git Visualization
+ */
+
+export { default as GitBranchIndicator } from './GitBranchIndicator';
+export { default as CommitHistory } from './CommitHistory';
+export { default as BranchList } from './BranchList';
+export { default as GitSection } from './GitSection';
+
+// Re-export prop types
+export type { GitBranchIndicatorProps } from './GitBranchIndicator';
+export type { CommitHistoryProps } from './CommitHistory';
+export type { BranchListProps } from './BranchList';
+export type { GitSectionProps } from './GitSection';
diff --git a/web-ui/src/lib/websocketMessageMapper.ts b/web-ui/src/lib/websocketMessageMapper.ts
index 7dda8203..185d3007 100644
--- a/web-ui/src/lib/websocketMessageMapper.ts
+++ b/web-ui/src/lib/websocketMessageMapper.ts
@@ -226,7 +226,6 @@ export function mapWebSocketMessageToAction(
}
case 'test_result':
- case 'commit_created':
case 'correction_attempt': {
// These are special activity message types
// Map them to activity updates
@@ -244,6 +243,66 @@ export function mapWebSocketMessageToAction(
};
}
+ // ========================================================================
+ // Git Messages (Ticket #272)
+ // ========================================================================
+
+ case 'commit_created': {
+ const msg = message as WebSocketMessage;
+
+ // Validate required fields - commits with empty hashes break downstream lookups
+ if (!msg.commit_hash || !msg.commit_message) {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('[WebSocketMapper] commit_created message missing required fields, skipping', msg);
+ }
+ return null;
+ }
+
+ return {
+ type: 'COMMIT_CREATED',
+ payload: {
+ commit: {
+ hash: msg.commit_hash,
+ short_hash: msg.commit_hash.slice(0, 7),
+ message: msg.commit_message,
+ author: msg.agent || 'Agent',
+ timestamp: message.timestamp.toString(),
+ files_changed: Array.isArray(msg.files_changed)
+ ? msg.files_changed.length
+ : undefined,
+ },
+ taskId: msg.task_id,
+ timestamp,
+ },
+ };
+ }
+
+ case 'branch_created': {
+ const msg = message as WebSocketMessage;
+
+ // Validate required fields - branches with id=0 or empty names break React keys
+ if (!msg.data?.branch_name || !msg.data?.id) {
+ if (process.env.NODE_ENV === 'development') {
+ console.warn('[WebSocketMapper] branch_created message missing required fields, skipping', msg);
+ }
+ return null;
+ }
+
+ return {
+ type: 'BRANCH_CREATED',
+ payload: {
+ branch: {
+ id: msg.data.id,
+ branch_name: msg.data.branch_name,
+ issue_id: msg.data.issue_id ?? 0,
+ status: 'active' as const,
+ created_at: message.timestamp.toString(),
+ },
+ timestamp,
+ },
+ };
+ }
+
case 'progress_update': {
const msg = message as WebSocketMessage;
return {
diff --git a/web-ui/src/reducers/agentReducer.ts b/web-ui/src/reducers/agentReducer.ts
index 3820378a..0500f0b5 100644
--- a/web-ui/src/reducers/agentReducer.ts
+++ b/web-ui/src/reducers/agentReducer.ts
@@ -10,6 +10,7 @@
*/
import type { AgentState, AgentAction } from '@/types/agentState';
+import { INITIAL_GIT_STATE } from '@/types/git';
// ============================================================================
// Initial State
@@ -27,6 +28,7 @@ export function getInitialState(): AgentState {
projectProgress: null,
wsConnected: false,
lastSyncTimestamp: 0,
+ gitState: null,
};
}
@@ -374,6 +376,121 @@ export function agentReducer(
break;
}
+ // ========================================================================
+ // Git Actions (Ticket #272)
+ // ========================================================================
+
+ case 'GIT_STATUS_LOADED': {
+ const { status } = action.payload;
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ status,
+ isLoading: false,
+ error: null,
+ },
+ };
+ break;
+ }
+
+ case 'GIT_COMMITS_LOADED': {
+ const { commits } = action.payload;
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ recentCommits: commits,
+ isLoading: false,
+ error: null,
+ },
+ };
+ break;
+ }
+
+ case 'GIT_BRANCHES_LOADED': {
+ const { branches } = action.payload;
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ branches,
+ isLoading: false,
+ error: null,
+ },
+ };
+ break;
+ }
+
+ case 'COMMIT_CREATED': {
+ const { commit } = action.payload;
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ // Defensive: ensure recentCommits is an array even if state was partially initialized
+ const currentCommits = currentGitState.recentCommits || [];
+ // Prepend new commit, keep only last 10 (FIFO)
+ const updatedCommits = [commit, ...currentCommits.slice(0, 9)];
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ recentCommits: updatedCommits,
+ },
+ };
+ break;
+ }
+
+ case 'BRANCH_CREATED': {
+ const { branch } = action.payload;
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ // Defensive: ensure branches is an array even if state was partially initialized
+ const currentBranches = currentGitState.branches || [];
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ branches: [...currentBranches, branch],
+ },
+ };
+ break;
+ }
+
+ case 'GIT_LOADING': {
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ isLoading: action.payload,
+ },
+ };
+ break;
+ }
+
+ case 'GIT_ERROR': {
+ const currentGitState = state.gitState ?? { ...INITIAL_GIT_STATE };
+
+ newState = {
+ ...state,
+ gitState: {
+ ...currentGitState,
+ error: action.payload,
+ isLoading: false,
+ },
+ };
+ break;
+ }
+
// ========================================================================
// Default case - unknown action type
// ========================================================================
diff --git a/web-ui/src/types/agentState.ts b/web-ui/src/types/agentState.ts
index 0c3e7209..54023c69 100644
--- a/web-ui/src/types/agentState.ts
+++ b/web-ui/src/types/agentState.ts
@@ -6,8 +6,11 @@
*
* Phase: 5.2 - Dashboard Multi-Agent State Management
* Date: 2025-11-06
+ * Updated: Git Visualization (Ticket #272)
*/
+import type { GitState, GitStatus, GitCommit, GitBranch } from './git';
+
// ============================================================================
// Core Entity Types
// ============================================================================
@@ -246,6 +249,7 @@ export interface AgentState {
projectProgress: ProjectProgress | null; // Overall project progress
wsConnected: boolean; // WebSocket connection status
lastSyncTimestamp: number; // Unix ms of last full sync
+ gitState: GitState | null; // Git visualization state (null before first load)
}
// ============================================================================
@@ -387,6 +391,82 @@ export interface FullResyncAction {
};
}
+// ============================================================================
+// Git Actions (Ticket #272)
+// ============================================================================
+
+/**
+ * Load Git status from API
+ */
+export interface GitStatusLoadedAction {
+ type: 'GIT_STATUS_LOADED';
+ payload: {
+ status: GitStatus;
+ timestamp: number;
+ };
+}
+
+/**
+ * Load Git commits from API
+ */
+export interface GitCommitsLoadedAction {
+ type: 'GIT_COMMITS_LOADED';
+ payload: {
+ commits: GitCommit[];
+ timestamp: number;
+ };
+}
+
+/**
+ * Load Git branches from API
+ */
+export interface GitBranchesLoadedAction {
+ type: 'GIT_BRANCHES_LOADED';
+ payload: {
+ branches: GitBranch[];
+ timestamp: number;
+ };
+}
+
+/**
+ * Handle new commit created via WebSocket
+ */
+export interface CommitCreatedAction {
+ type: 'COMMIT_CREATED';
+ payload: {
+ commit: GitCommit;
+ taskId?: number;
+ timestamp: number;
+ };
+}
+
+/**
+ * Handle branch created via WebSocket
+ */
+export interface BranchCreatedAction {
+ type: 'BRANCH_CREATED';
+ payload: {
+ branch: GitBranch;
+ timestamp: number;
+ };
+}
+
+/**
+ * Set Git loading state
+ */
+export interface GitLoadingAction {
+ type: 'GIT_LOADING';
+ payload: boolean;
+}
+
+/**
+ * Set Git error state
+ */
+export interface GitErrorAction {
+ type: 'GIT_ERROR';
+ payload: string | null;
+}
+
/**
* Discriminated union of all possible reducer actions
*/
@@ -403,7 +483,15 @@ export type AgentAction =
| ActivityAddedAction
| ProgressUpdatedAction
| WebSocketConnectedAction
- | FullResyncAction;
+ | FullResyncAction
+ // Git Actions (Ticket #272)
+ | GitStatusLoadedAction
+ | GitCommitsLoadedAction
+ | GitBranchesLoadedAction
+ | CommitCreatedAction
+ | BranchCreatedAction
+ | GitLoadingAction
+ | GitErrorAction;
// ============================================================================
// Utility Types
diff --git a/web-ui/src/types/git.ts b/web-ui/src/types/git.ts
new file mode 100644
index 00000000..5370cbad
--- /dev/null
+++ b/web-ui/src/types/git.ts
@@ -0,0 +1,133 @@
+/**
+ * Git Type Definitions for CodeFRAME UI
+ *
+ * TypeScript interfaces for Git visualization components.
+ * These types match the backend API responses from the Git router.
+ *
+ * @see codeframe/ui/routers/git.py for API response models
+ */
+
+// ============================================================================
+// Branch Types
+// ============================================================================
+
+/**
+ * Valid branch status values
+ */
+export type BranchStatus = 'active' | 'merged' | 'abandoned';
+
+/**
+ * Git branch entity matching BranchResponse from backend
+ */
+export interface GitBranch {
+ id: number;
+ branch_name: string;
+ issue_id: number;
+ status: BranchStatus;
+ created_at: string;
+ merged_at?: string;
+ merge_commit?: string;
+}
+
+// ============================================================================
+// Commit Types
+// ============================================================================
+
+/**
+ * Git commit entity matching CommitListItem from backend
+ */
+export interface GitCommit {
+ hash: string;
+ short_hash: string;
+ message: string;
+ author: string;
+ timestamp: string;
+ files_changed?: number;
+}
+
+// ============================================================================
+// Status Types
+// ============================================================================
+
+/**
+ * Git working tree status matching GitStatusResponse from backend
+ */
+export interface GitStatus {
+ current_branch: string;
+ is_dirty: boolean;
+ modified_files: string[];
+ untracked_files: string[];
+ staged_files: string[];
+}
+
+// ============================================================================
+// State Management Types
+// ============================================================================
+
+/**
+ * Git state for the Dashboard context
+ * Used by GitSection and related components
+ */
+export interface GitState {
+ /** Current git status (null when loading or on error) */
+ status: GitStatus | null;
+
+ /** Recent commits (limited to last 10) */
+ recentCommits: GitCommit[];
+
+ /** All branches for the project */
+ branches: GitBranch[];
+
+ /** Loading state indicator */
+ isLoading: boolean;
+
+ /** Error message (null when no error) */
+ error: string | null;
+}
+
+// ============================================================================
+// API Response Types
+// ============================================================================
+
+/**
+ * Response from GET /api/projects/{id}/git/branches
+ */
+export interface BranchListResponse {
+ branches: GitBranch[];
+}
+
+/**
+ * Response from GET /api/projects/{id}/git/commits
+ */
+export interface CommitListResponse {
+ commits: GitCommit[];
+}
+
+// ============================================================================
+// Utility Types
+// ============================================================================
+
+/**
+ * Initial/empty Git state for initialization
+ */
+export const INITIAL_GIT_STATE: GitState = {
+ status: null,
+ recentCommits: [],
+ branches: [],
+ isLoading: false,
+ error: null,
+};
+
+/**
+ * Type guard to check if branch is active
+ */
+export function isBranchActive(branch: GitBranch): boolean {
+ return branch.status === 'active';
+}
+
+/**
+ * Type guard to check if branch is merged
+ */
+export function isBranchMerged(branch: GitBranch): boolean {
+ return branch.status === 'merged';
+}
diff --git a/web-ui/src/types/index.ts b/web-ui/src/types/index.ts
index 5faff2e4..86ed0d2c 100644
--- a/web-ui/src/types/index.ts
+++ b/web-ui/src/types/index.ts
@@ -120,7 +120,8 @@ export type WebSocketMessageType =
| 'agent_retired' // Sprint 4
| 'task_assigned' // Sprint 4
| 'task_blocked' // Sprint 4
- | 'task_unblocked'; // Sprint 4
+ | 'task_unblocked' // Sprint 4
+ | 'branch_created'; // Ticket #272 - Git Visualization
export interface WebSocketMessage {
type: WebSocketMessageType;
@@ -242,4 +243,16 @@ export type {
GetAgentsForProjectParams,
GetProjectsForAgentParams,
UnassignAgentParams,
-} from './agentAssignment';
\ No newline at end of file
+} from './agentAssignment';
+
+// Re-export Git types (Git Visualization - Ticket #272)
+export type {
+ GitBranch,
+ GitCommit,
+ GitStatus,
+ GitState,
+ BranchStatus,
+ BranchListResponse,
+ CommitListResponse,
+} from './git';
+export { INITIAL_GIT_STATE, isBranchActive, isBranchMerged } from './git';
\ No newline at end of file
diff --git a/web-ui/test-utils/agentState.fixture.ts b/web-ui/test-utils/agentState.fixture.ts
index 2d22374d..ed36b8f6 100644
--- a/web-ui/test-utils/agentState.fixture.ts
+++ b/web-ui/test-utils/agentState.fixture.ts
@@ -85,6 +85,7 @@ export function createInitialAgentState(overrides?: Partial): AgentS
projectProgress: null,
wsConnected: false,
lastSyncTimestamp: 0,
+ gitState: null,
...overrides,
};
}
@@ -157,6 +158,7 @@ export function createPopulatedAgentState(): AgentState {
}),
wsConnected: true,
lastSyncTimestamp: Date.now(),
+ gitState: null,
};
}