diff --git a/client/src/components/BoardUxRegression.test.ts b/client/src/components/BoardUxRegression.test.ts new file mode 100644 index 0000000..c756544 --- /dev/null +++ b/client/src/components/BoardUxRegression.test.ts @@ -0,0 +1,21 @@ +import { readFileSync } from 'node:fs' +import { resolve } from 'node:path' +import { describe, expect, it } from 'vitest' + +function readSource(path: string): string { + return readFileSync(resolve(process.cwd(), path), 'utf8') +} + +describe('Board UX regression checks', () => { + it('keeps the task board button always visible in the session title bar', () => { + const source = readSource('client/src/components/TerminalSession.tsx') + expect(source).toContain('title="Task board"') + expect(source).not.toContain('{tdStatus?.projectState?.enabled && (') + }) + + it('uses plain "Show more" text for closed-column progressive reveal', () => { + const source = readSource('client/src/components/TaskBoard.tsx') + expect(source).toContain('Show more') + expect(source).not.toContain('Show {issues.length - visibleIssues.length} older') + }) +}) diff --git a/client/src/components/TaskBoard.tsx b/client/src/components/TaskBoard.tsx index 897b36f..91c5929 100644 --- a/client/src/components/TaskBoard.tsx +++ b/client/src/components/TaskBoard.tsx @@ -18,12 +18,10 @@ import { PauseCircle, AlertCircle, Layers, - Tag, FolderGit2, - ChevronDown, - ChevronRight, RefreshCw, XCircle, + ChevronRight, } from 'lucide-react' // Status column configuration @@ -35,13 +33,6 @@ const STATUS_COLUMNS = [ { key: 'closed', label: 'Closed', icon: CheckCircle2, color: 'text-green-500', bg: 'bg-green-500/10' }, ] -const priorityColors: Record = { - P0: 'border-l-red-500', - P1: 'border-l-orange-500', - P2: 'border-l-border', - P3: 'border-l-border/50', -} - type ViewMode = 'board' | 'list' function normalizeBranchName(branch?: string): string { @@ -49,46 +40,51 @@ function normalizeBranchName(branch?: string): string { return branch.replace(/^refs\/heads\//, '').trim() } -function IssueCard({ issue, compact, indent, onSelect }: { issue: TdIssue; compact?: boolean; indent?: boolean; onSelect: (id: string) => void }) { - const labels = issue.labels ? issue.labels.split(',').filter(Boolean) : [] +function priorityBadgeClass(priority: string): string { + if (priority === 'P0') return 'bg-red-500/15 text-red-400' + if (priority === 'P1') return 'bg-orange-500/15 text-orange-400' + if (priority === 'P2') return 'bg-muted text-muted-foreground' + return 'bg-muted/50 text-muted-foreground/50' +} +function IssueCard({ issue, compact, indent, onSelect, childCount }: { issue: TdIssue; compact?: boolean; indent?: boolean; onSelect: (id: string) => void; childCount?: number }) { return ( ) @@ -126,70 +122,82 @@ function groupIssuesWithChildren(issues: TdIssue[]): Array<{ issue: TdIssue; chi return result } -function StatusColumn({ status, issues, onSelect }: { +function StatusColumn({ status, issues, onSelect, childCountByEpicId }: { status: typeof STATUS_COLUMNS[0] issues: TdIssue[] onSelect: (id: string) => void + childCountByEpicId: Map }) { const StatusIcon = status.icon - const grouped = useMemo(() => groupIssuesWithChildren(issues), [issues]) - const [collapsedEpics, setCollapsedEpics] = useState>(new Set()) - - const toggleEpic = (epicId: string) => { - setCollapsedEpics(prev => { - const next = new Set(prev) - if (next.has(epicId)) next.delete(epicId) - else next.add(epicId) - return next - }) + const [isCollapsed, setIsCollapsed] = useState(issues.length === 0) + const [showAllClosed, setShowAllClosed] = useState(false) + + useEffect(() => { + if (issues.length === 0) setIsCollapsed(true) + }, [issues.length]) + + const threeDaysAgo = new Date(Date.now() - 3 * 86_400_000).toISOString() + const visibleIssues = (status.key === 'closed' && !showAllClosed) + ? issues.filter(i => (i.closed_at ?? '') >= threeDaysAgo) + : issues + + if (isCollapsed) { + return ( +
setIsCollapsed(false)} + title={`${status.label} (${issues.length})`} + > + + + {status.label} + + {issues.length} +
+ ) } return ( -
+
{status.label} {issues.length} +
- {issues.length === 0 ? ( + {visibleIssues.length === 0 ? (

No issues

) : ( - grouped.map(({ issue, children }) => ( -
- {issue.type === 'epic' && children.length > 0 ? ( -
- - - {!collapsedEpics.has(issue.id) && children.map(child => ( -
- -
- ))} -
- ) : ( - - )} -
+ visibleIssues.map(issue => ( + )) )} + {status.key === 'closed' && !showAllClosed && issues.length > visibleIssues.length && ( + + )}
@@ -212,6 +220,16 @@ export function TaskBoard() { const [searchQuery, setSearchQuery] = useState('') const [selectedIssueId, setSelectedIssueId] = useState(null) + const childCountByEpicId = useMemo(() => { + const counts = new Map() + tdIssues.forEach(issue => { + if (issue.parent_id && issue.status !== 'closed') { + counts.set(issue.parent_id, (counts.get(issue.parent_id) ?? 0) + 1) + } + }) + return counts + }, [tdIssues]) + const projectWorktrees = useMemo(() => { if (!currentProject) return [] const projectName = currentProject.path.split('/').pop() || '' @@ -374,6 +392,7 @@ export function TaskBoard() { status={status} issues={(filteredBoard[status.key] || []).filter(i => !i.deleted_at)} onSelect={setSelectedIssueId} + childCountByEpicId={childCountByEpicId} /> ))}
@@ -393,6 +412,7 @@ export function TaskBoard() { issue={issue} compact onSelect={setSelectedIssueId} + childCount={issue.type === 'epic' ? childCountByEpicId.get(issue.id) : undefined} /> {children.map(child => (
diff --git a/client/src/components/TerminalSession.tsx b/client/src/components/TerminalSession.tsx index d272ee6..daebece 100644 --- a/client/src/components/TerminalSession.tsx +++ b/client/src/components/TerminalSession.tsx @@ -25,6 +25,7 @@ import { ArrowDown, Pencil, RotateCcw, + LayoutGrid, } from 'lucide-react'; import {cn} from '@/lib/utils'; import type {Session} from '@/lib/types'; @@ -130,6 +131,7 @@ export const TerminalSession = memo(function TerminalSession({ selectedSessions, worktrees, agents, + openTaskBoard, } = useAppStore(); const isMobile = useIsMobile(); const hasMultipleSessions = selectedSessions.length > 1; @@ -730,6 +732,17 @@ export const TerminalSession = memo(function TerminalSession({
+ {/* Task board button */} + + {/* Info button */}