# Enhanced Download UI Implementation Plan > **For Claude:** REQUIRED SUB-SKILL: Use superpowers:executing-plans to implement this plan task-by-task. **Goal:** Enhance the download UI to display project aliases and classify APKs by environment (dev, sandbox, product, other) in a tabbed list view. **Architecture:** Shared utility functions for APK filename parsing + client-side tab organization. Project aliases stored in JSON config file. Server enriches APK data with environment metadata, client organizes into tabs. **Tech Stack:** Next.js App Router, TypeScript, SWR, React, Tailwind CSS, Jest --- ## Task 1: Create Project Aliases Configuration File **Files:** - Create: `lib/project-aliases.json` **Step 1: Create the project aliases JSON file** ```json { "FT": "外放构建", "FT_DEV": "开发构建" } ``` **Step 2: Commit** ```bash git add lib/project-aliases.json git commit -m "feat: add project aliases configuration" ``` --- ## Task 2: Create APK Parser Utilities **Files:** - Create: `lib/apk-parser.ts` - Test: `lib/__tests__/apk-parser.test.ts` **Step 1: Write the failing test** ```typescript // lib/__tests__/apk-parser.test.ts import { parseApkFilename, detectEnvironment } from '../apk-parser'; describe('parseApkFilename', () => { it('should parse APK with environment suffix', () => { const result = parseApkFilename('ft_timeshift_0_42_57ef3a60d.apk'); expect(result.environment).toBe('dev'); expect(result.commit).toBe('57ef3a60d'); expect(result.isValid).toBe(true); }); it('should parse APK without environment (product)', () => { const result = parseApkFilename('ft_0_42_ff9ff3441.apk'); expect(result.environment).toBe('product'); expect(result.commit).toBe('ff9ff3441'); expect(result.isValid).toBe(true); }); it('should parse sandbox APK', () => { const result = parseApkFilename('ft_sandbox_0_42_57ef3a60d.apk'); expect(result.environment).toBe('sandbox'); expect(result.commit).toBe('57ef3a60d'); expect(result.isValid).toBe(true); }); it('should handle invalid format', () => { const result = parseApkFilename('invalid_filename.apk'); expect(result.environment).toBe('other'); expect(result.commit).toBeNull(); expect(result.isValid).toBe(false); }); it('should handle missing commit ID', () => { const result = parseApkFilename('ft_dev_0_42.apk'); expect(result.environment).toBe('dev'); expect(result.commit).toBeNull(); expect(result.isValid).toBe(false); }); }); describe('detectEnvironment', () => { it('should detect dev environment', () => { expect(detectEnvironment('ft_timeshift_0_42_57ef3a60d.apk')).toBe('dev'); }); it('should detect sandbox environment', () => { expect(detectEnvironment('ft_sandbox_0_42_57ef3a60d.apk')).toBe('sandbox'); }); it('should detect product environment (no suffix)', () => { expect(detectEnvironment('ft_0_42_ff9ff3441.apk')).toBe('product'); }); it('should return other for unknown environments', () => { expect(detectEnvironment('ft_unknown_0_42_57ef3a60d.apk')).toBe('other'); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- lib/__tests__/apk-parser.test.ts` Expected: FAIL with "module not found" **Step 3: Write minimal implementation** ```typescript // lib/apk-parser.ts export type ApkEnvironment = 'dev' | 'sandbox' | 'product' | 'other'; export interface ParsedApkMetadata { environment: ApkEnvironment; commit: string | null; isValid: boolean; } // Environment mapping based on filename patterns const ENVIRONMENT_MAP: Record = { 'timeshift': 'dev', 'lan': 'dev', 'dev': 'dev', 'sandbox': 'sandbox', }; export function detectEnvironment(filename: string): ApkEnvironment { // Pattern: ft_[environment]_version_commit.apk // Extract the environment segment between first and second underscores const match = filename.match(/^ft_([^_]+)_/); if (!match) { // No environment suffix, default to product return 'product'; } const env = match[1].toLowerCase(); return ENVIRONMENT_MAP[env] || 'other'; } export function parseApkFilename(filename: string): ParsedApkMetadata { const environment = detectEnvironment(filename); const commitMatch = filename.match(/([a-f0-9]{7,})\.apk$/); const commit = commitMatch ? commitMatch[1] : null; const isValid = commit !== null; return { environment, commit, isValid, }; } ``` **Step 4: Run test to verify it passes** Run: `npm test -- lib/__tests__/apk-parser.test.ts` Expected: PASS **Step 5: Commit** ```bash git add lib/apk-parser.ts lib/__tests__/apk-parser.test.ts git commit -m "feat: add APK filename parser utilities" ``` --- ## Task 3: Add Project Alias Utility Function **Files:** - Modify: `lib/utils.ts` - Test: `lib/__tests__/utils.test.ts` **Step 1: Write the failing test** ```typescript // lib/__tests__/utils.test.ts (add to existing file) import { getProjectDisplayName } from '../utils'; describe('getProjectDisplayName', () => { const originalAliases = { 'FT': '外放构建', 'FT_DEV': '开发构建', }; beforeEach(() => { // Mock the aliases file jest.spyOn(require('fs'), 'readFileSync').mockReturnValue( JSON.stringify(originalAliases) ); }); afterEach(() => { jest.restoreAllMocks(); }); it('should return display name for aliased project', () => { expect(getProjectDisplayName('FT')).toBe('外放构建'); }); it('should return directory name when no alias exists', () => { expect(getProjectDisplayName('UNKNOWN')).toBe('UNKNOWN'); }); it('should return directory name when aliases file is empty', () => { jest.spyOn(require('fs'), 'readFileSync').mockReturnValue('{}'); expect(getProjectDisplayName('FT')).toBe('FT'); }); it('should return directory name when aliases file cannot be read', () => { jest.spyOn(require('fs'), 'readFileSync').mockImplementation(() => { throw new Error('File not found'); }); expect(getProjectDisplayName('FT')).toBe('FT'); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- lib/__tests__/utils.test.ts -t getProjectDisplayName` Expected: FAIL with "function not defined" **Step 3: Write minimal implementation** ```typescript // lib/utils.ts (add to existing file) import * as fs from 'fs'; import * as path from 'path'; // Client-safe utility functions /** * 格式化版本号显示 (0_42 -> 0.42) */ export function formatVersion(version: string): string { return version.replace(/_/g, '.'); } /** * 格式化文件大小 */ export function formatFileSize(bytes: number): string { if (bytes < 1024) return bytes + ' B'; if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(2) + ' KB'; if (bytes < 1024 * 1024 * 1024) return (bytes / (1024 * 1024)).toFixed(2) + ' MB'; return (bytes / (1024 * 1024 * 1024)).toFixed(2) + ' GB'; } /** * 格式化日期时间 */ export function formatDateTime(date: Date): string { const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const hours = String(date.getHours()).padStart(2, '0'); const minutes = String(date.getMinutes()).padStart(2, '0'); return `${year}-${month}-${day} ${hours}:${minutes}`; } /** * 获取项目显示名称(从别名配置) */ export function getProjectDisplayName(projectName: string): string { try { const aliasesPath = path.join(process.cwd(), 'lib', 'project-aliases.json'); const aliasesContent = fs.readFileSync(aliasesPath, 'utf-8'); const aliases = JSON.parse(aliasesContent); return aliases[projectName] || projectName; } catch { // If file doesn't exist or is invalid, return original name return projectName; } } ``` **Step 4: Run test to verify it passes** Run: `npm test -- lib/__tests__/utils.test.ts -t getProjectDisplayName` Expected: PASS **Step 5: Commit** ```bash git add lib/utils.ts lib/__tests__/utils.test.ts git commit -m "feat: add getProjectDisplayName utility" ``` --- ## Task 4: Update Projects API to Include Display Names **Files:** - Modify: `app/api/projects/route.ts` - Test: `app/api/__tests__/projects.test.ts` **Step 1: Write the failing test** ```typescript // app/api/__tests__/projects.test.ts import { GET } from '../projects/route'; import { getProjects } from '@/lib/fs-utils'; jest.mock('@/lib/fs-utils'); jest.mock('@/lib/utils', () => ({ getProjectDisplayName: jest.fn((name) => name === 'FT' ? '外放构建' : name), })); describe('/api/projects', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should return projects with display names', async () => { (getProjects as jest.Mock).mockResolvedValue(['FT', 'FT_DEV', 'UNKNOWN']); const response = await GET(); const data = await response.json(); expect(response.status).toBe(200); expect(data).toEqual([ { name: 'FT', displayName: '外放构建' }, { name: 'FT_DEV', displayName: 'FT_DEV' }, { name: 'UNKNOWN', displayName: 'UNKNOWN' }, ]); }); it('should handle errors gracefully', async () => { (getProjects as jest.Mock).mockRejectedValue(new Error('File system error')); const response = await GET(); const data = await response.json(); expect(response.status).toBe(500); expect(data.error).toBe('Failed to fetch projects'); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- app/api/__tests__/projects.test.ts` Expected: FAIL with response structure mismatch **Step 3: Write minimal implementation** ```typescript // app/api/projects/route.ts import { NextResponse } from 'next/server'; import { getProjects } from '@/lib/fs-utils'; import { getProjectDisplayName } from '@/lib/utils'; export async function GET() { try { const projects = await getProjects(); const projectsWithAliases = projects.map(project => ({ name: project, displayName: getProjectDisplayName(project), })); return NextResponse.json(projectsWithAliases); } catch (error) { return NextResponse.json( { error: 'Failed to fetch projects' }, { status: 500 } ); } } ``` **Step 4: Run test to verify it passes** Run: `npm test -- app/api/__tests__/projects.test.ts` Expected: PASS **Step 5: Commit** ```bash git add app/api/projects/route.ts app/api/__tests__/projects.test.ts git commit -m "feat: update projects API to include display names" ``` --- ## Task 5: Update APKs API to Include Environment Metadata **Files:** - Modify: `app/api/apks/route.ts` - Test: `app/api/__tests__/apks.test.ts` **Step 1: Write the failing test** ```typescript // app/api/__tests__/apks.test.ts import { GET } from '../apks/route'; import { getApks } from '@/lib/fs-utils'; import { parseApkFilename } from '@/lib/apk-parser'; jest.mock('@/lib/fs-utils'); jest.mock('@/lib/apk-parser'); describe('/api/apks', () => { beforeEach(() => { jest.clearAllMocks(); }); it('should return APKs with environment metadata', async () => { const mockApks = [ { name: 'ft_timeshift_0_42_57ef3a60d.apk', size: 15728640, modifiedAt: new Date('2026-03-04T14:30:00Z'), }, { name: 'ft_0_42_ff9ff3441.apk', size: 15728640, modifiedAt: new Date('2026-03-04T14:30:00Z'), }, ]; (getApks as jest.Mock).mockResolvedValue(mockApks); (parseApkFilename as jest.Mock) .mockReturnValueOnce({ environment: 'dev', commit: '57ef3a60d', isValid: true }) .mockReturnValueOnce({ environment: 'product', commit: 'ff9ff3441', isValid: true }); const request = new Request('http://localhost:3000/api/apks?project=FT&version=0_42'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(200); expect(data).toHaveLength(2); expect(data[0]).toMatchObject({ name: 'ft_timeshift_0_42_57ef3a60d.apk', environment: 'dev', commit: '57ef3a60d', }); expect(data[1]).toMatchObject({ name: 'ft_0_42_ff9ff3441.apk', environment: 'product', commit: 'ff9ff3441', }); }); it('should require project and version parameters', async () => { const request = new Request('http://localhost:3000/api/apks'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(400); expect(data.error).toBe('Project and version parameters are required'); }); it('should handle errors gracefully', async () => { (getApks as jest.Mock).mockRejectedValue(new Error('File system error')); const request = new Request('http://localhost:3000/api/apks?project=FT&version=0_42'); const response = await GET(request); const data = await response.json(); expect(response.status).toBe(500); expect(data.error).toBe('Failed to fetch APK files'); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- app/api/__tests__/apks.test.ts` Expected: FAIL with environment field missing **Step 3: Write minimal implementation** ```typescript // app/api/apks/route.ts import { NextResponse } from 'next/server'; import { getApks } from '@/lib/fs-utils'; import { parseApkFilename } from '@/lib/apk-parser'; export async function GET(request: Request) { const { searchParams } = new URL(request.url); const project = searchParams.get('project'); const version = searchParams.get('version'); const commitId = searchParams.get('commit') || undefined; if (!project || !version) { return NextResponse.json( { error: 'Project and version parameters are required' }, { status: 400 } ); } try { const apks = await getApks(project, version, commitId); const apksWithMetadata = apks.map(apk => { const parsed = parseApkFilename(apk.name); return { ...apk, environment: parsed.environment, commit: parsed.commit, modifiedAt: apk.modifiedAt.toISOString(), downloadUrl: `/api/download?project=${encodeURIComponent(project)}&version=${encodeURIComponent(version)}&filename=${encodeURIComponent(apk.name)}`, }; }); return NextResponse.json(apksWithMetadata); } catch (error) { return NextResponse.json( { error: 'Failed to fetch APK files' }, { status: 500 } ); } } ``` **Step 4: Run test to verify it passes** Run: `npm test -- app/api/__tests__/apks.test.ts` Expected: PASS **Step 5: Commit** ```bash git add app/api/apks/route.ts app/api/__tests__/apks.test.ts git commit -m "feat: update APKs API to include environment metadata" ``` --- ## Task 6: Create ApkTable Component **Files:** - Create: `app/download/ApkTable.tsx` - Test: `app/download/__tests__/ApkTable.test.tsx` **Step 1: Write the failing test** ```typescript // app/download/__tests__/ApkTable.test.tsx import React from 'react'; import { render, screen } from '@testing-library/react'; import ApkTable from '../ApkTable'; describe('ApkTable', () => { const mockApks = [ { name: 'ft_timeshift_0_42_57ef3a60d.apk', commit: '57ef3a60d', size: 15728640, modifiedAt: '2026-03-04T14:30:00.000Z', downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_timeshift_0_42_57ef3a60d.apk', }, ]; it('should render table with correct columns', () => { render(); expect(screen.getByText('Commit ID')).toBeInTheDocument(); expect(screen.getByText('文件大小')).toBeInTheDocument(); expect(screen.getByText('创建时间')).toBeInTheDocument(); }); it('should render APK data correctly', () => { render(); expect(screen.getByText('57ef3a60d')).toBeInTheDocument(); expect(screen.getByText('15.00 MB')).toBeInTheDocument(); expect(screen.getByText('2026-03-04 14:30')).toBeInTheDocument(); expect(screen.getByText('下载')).toBeInTheDocument(); }); it('should show empty state when no APKs', () => { render(); expect(screen.getByText('暂无 APK 文件')).toBeInTheDocument(); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- app/download/__tests__/ApkTable.test.tsx` Expected: FAIL with "module not found" **Step 3: Write minimal implementation** ```typescript // app/download/ApkTable.tsx 'use client'; import { formatFileSize, formatDateTime } from '@/lib/utils'; interface Apk { name: string; commit: string | null; size: number; modifiedAt: string; downloadUrl: string; } interface ApkTableProps { apks: Apk[]; } export default function ApkTable({ apks }: ApkTableProps) { if (!apks || apks.length === 0) { return (

暂无 APK 文件

); } return (
{apks.map((apk) => ( ))}
Commit ID 文件大小 创建时间 操作
{apk.commit || '-'} {formatFileSize(apk.size)} {formatDateTime(new Date(apk.modifiedAt))} 下载
); } ``` **Step 4: Run test to verify it passes** Run: `npm test -- app/download/__tests__/ApkTable.test.tsx` Expected: PASS **Step 5: Commit** ```bash git add app/download/ApkTable.tsx app/download/__tests__/ApkTable.test.tsx git commit -m "feat: add ApkTable component" ``` --- ## Task 7: Update SearchForm to Use Project Display Names **Files:** - Modify: `app/download/SearchForm.tsx` **Step 1: Update SearchForm to handle project display names** ```typescript // app/download/SearchForm.tsx (replace existing file) 'use client'; import { useState } from 'react'; import useSWR from 'swr'; import { formatVersion } from '@/lib/utils'; const fetcher = async (url: string) => { const res = await fetch(url); if (!res.ok) { throw new Error(`HTTP error: ${res.status}`); } return res.json(); }; interface Project { name: string; displayName: string; } interface SearchFormProps { onSearch: (project: string, version: string, commitId: string) => void; } export default function SearchForm({ onSearch }: SearchFormProps) { const [selectedProject, setSelectedProject] = useState(''); const [selectedVersion, setSelectedVersion] = useState(''); const [commitId, setCommitId] = useState(''); // 获取项目列表 const { data: projects, isLoading: loadingProjects } = useSWR( '/api/projects', fetcher ); // 获取版本列表 const { data: versions, isLoading: loadingVersions } = useSWR( selectedProject ? `/api/versions?project=${selectedProject}` : null, fetcher ); const handleSubmit = (e: React.FormEvent) => { e.preventDefault(); if (selectedProject && selectedVersion) { onSearch(selectedProject, selectedVersion, commitId); } }; return (
setCommitId(e.target.value)} placeholder="输入 commit id..." className="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500" />
); } ``` **Step 2: Run tests to verify no breaking changes** Run: `npm test` Expected: PASS **Step 3: Commit** ```bash git add app/download/SearchForm.tsx git commit -m "feat: update SearchForm to use project display names" ``` --- ## Task 8: Update ApkList to Use Tabbed View **Files:** - Modify: `app/download/ApkList.tsx` - Test: `app/download/__tests__/ApkList.test.tsx` **Step 1: Write the failing test** ```typescript // app/download/__tests__/ApkList.test.tsx import React from 'react'; import { render, screen, fireEvent } from '@testing-library/react'; import ApkList from '../ApkList'; import useSWR from 'swr'; jest.mock('swr'); describe('ApkList', () => { const mockApks = [ { name: 'ft_timeshift_0_42_57ef3a60d.apk', environment: 'dev', commit: '57ef3a60d', size: 15728640, modifiedAt: '2026-03-04T14:30:00.000Z', downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_timeshift_0_42_57ef3a60d.apk', }, { name: 'ft_0_42_ff9ff3441.apk', environment: 'product', commit: 'ff9ff3441', size: 15728640, modifiedAt: '2026-03-04T14:30:00.000Z', downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_0_42_ff9ff3441.apk', }, { name: 'ft_sandbox_0_42_57ef3a60d.apk', environment: 'sandbox', commit: '57ef3a60d', size: 15728640, modifiedAt: '2026-03-04T14:30:00.000Z', downloadUrl: '/api/download?project=FT&version=0_42&filename=ft_sandbox_0_42_57ef3a60d.apk', }, ]; beforeEach(() => { (useSWR as jest.Mock).mockReturnValue({ data: mockApks, isLoading: false, error: null, }); }); it('should render all 4 tabs', () => { render(); expect(screen.getByText('dev')).toBeInTheDocument(); expect(screen.getByText('product')).toBeInTheDocument(); expect(screen.getByText('sandbox')).toBeInTheDocument(); expect(screen.getByText('other')).toBeInTheDocument(); }); it('should switch tabs correctly', () => { render(); const devTab = screen.getByText('dev'); fireEvent.click(devTab); expect(screen.getByText('57ef3a60d')).toBeInTheDocument(); }); it('should show loading state', () => { (useSWR as jest.Mock).mockReturnValue({ data: null, isLoading: true, error: null, }); render(); expect(screen.getByText('加载中...')).toBeInTheDocument(); }); it('should show error state', () => { (useSWR as jest.Mock).mockReturnValue({ data: null, isLoading: false, error: new Error('API error'), }); render(); expect(screen.getByText('加载失败,请重试')).toBeInTheDocument(); }); }); ``` **Step 2: Run test to verify it fails** Run: `npm test -- app/download/__tests__/ApkList.test.tsx` Expected: FAIL with tab elements missing **Step 3: Write minimal implementation** ```typescript // app/download/ApkList.tsx (replace existing file) 'use client'; import { useState } from 'react'; import useSWR from 'swr'; import { formatVersion } from '@/lib/utils'; import { type ApkEnvironment } from '@/lib/apk-parser'; import ApkTable from './ApkTable'; const fetcher = (url: string) => fetch(url).then((res) => res.json()); interface Apk { name: string; environment: ApkEnvironment; commit: string | null; size: number; modifiedAt: string; downloadUrl: string; } interface ApkListProps { project: string; version: string; commitId: string; } const TABS: ApkEnvironment[] = ['dev', 'product', 'sandbox', 'other']; export default function ApkList({ project, version, commitId }: ApkListProps) { const [activeTab, setActiveTab] = useState('product'); const { data: apks, isLoading, error } = useSWR( `/api/apks?project=${project}&version=${version}&commit=${commitId}`, fetcher ); if (isLoading) { return (
加载中...
); } if (error) { return (
加载失败,请重试
); } if (!apks || apks.length === 0) { return (

未找到匹配的 APK 文件

); } // Group APKs by environment const groupedApks = apks.reduce((acc, apk) => { acc[apk.environment] = [...(acc[apk.environment] || []), apk]; return acc; }, {} as Record); return (
{/* Tab Navigation */}
{/* Tab Content */}
); } ``` **Step 4: Run test to verify it passes** Run: `npm test -- app/download/__tests__/ApkList.test.tsx` Expected: PASS **Step 5: Run all tests to ensure no regressions** Run: `npm test` Expected: PASS **Step 6: Commit** ```bash git add app/download/ApkList.tsx app/download/__tests__/ApkList.test.tsx git commit -m "feat: update ApkList to use tabbed view" ``` --- ## Task 9: Update Download Page Title to Use Display Name **Files:** - Modify: `app/download/page.tsx` **Step 1: Update page to show project display name** ```typescript // app/download/page.tsx (modify the existing file) 'use client'; import { useState } from 'react'; import useSWR from 'swr'; import SearchForm from './SearchForm'; import ApkList from './ApkList'; interface Project { name: string; displayName: string; } export default function DownloadPage() { const [searchParams, setSearchParams] = useState({ project: '', version: '', commitId: '', }); const { data: projects } = useSWR('/api/projects'); const handleSearch = (project: string, version: string, commitId: string) => { setSearchParams({ project, version, commitId }); }; const getProjectDisplayName = (projectName: string): string => { const project = projects?.find(p => p.name === projectName); return project?.displayName || projectName; }; return (

资源下载

搜索

{searchParams.project && searchParams.version ? ( <>

{getProjectDisplayName(searchParams.project)} - {searchParams.version.replace('_', '.')} {searchParams.commitId && ` (commit: ${searchParams.commitId})`}

) : (

请选择项目和版本开始搜索

)}
); } ``` **Step 2: Run tests to verify no breaking changes** Run: `npm test` Expected: PASS **Step 3: Commit** ```bash git add app/download/page.tsx git commit -m "feat: update download page title to use display name" ``` --- ## Task 10: Final Integration Testing **Files:** - Test: Integration testing across all components **Step 1: Run full test suite** Run: `npm test` Expected: All tests PASS **Step 2: Build the project to verify no TypeScript errors** Run: `npm run build` Expected: Build succeeds **Step 3: Start development server and manually test** Run: `npm run dev` Expected: Server starts without errors **Manual Testing Checklist:** - [ ] Navigate to download page - [ ] Select a project (verify alias display) - [ ] Select a version - [ ] Verify all 4 tabs appear (dev, product, sandbox, other) - [ ] Switch between tabs - [ ] Verify APK list shows correct columns - [ ] Test commit filter - [ ] Download an APK - [ ] Test with project that has no alias (verify directory name shows) **Step 4: Commit final cleanup if needed** ```bash git add . git commit -m "test: complete integration testing for enhanced download UI" ``` --- ## Summary This implementation plan creates a fully functional enhanced download UI with: 1. **Project aliases** - Stored in JSON config, fallback to directory names 2. **APK environment classification** - Parsed from filename, grouped into tabs 3. **Tabbed list view** - 4 tabs (dev, product, sandbox, other) 4. **List format** - Shows Commit ID, File Size, Creation Time 5. **Global commit filter** - Applies to all tabs 6. **Comprehensive tests** - Unit, integration, and component tests 7. **Type safety** - Full TypeScript support throughout All tasks follow TDD principles with bite-sized steps and frequent commits.