@@ -3,13 +3,13 @@ import { useState, useEffect, useMemo, useCallback } from 'react'
33import { useTranslation } from '@/lib/i18n'
44import { useAppStore } from '@/lib/store'
55import { Task } from '@/lib/types'
6- import { TaskCard } from '@/components/task-card'
6+ import { TaskCard , isTaskStale } from '@/components/task-card'
77import { TaskForm } from '@/components/task-form'
88import { TaskPipeline } from '@/components/task-pipeline'
99import { TaskQuality } from '@/components/task-quality'
1010import {
1111 CheckSquare , Plus , LayoutGrid , List , X ,
12- ChevronRight , User , FolderKanban , Clock , Trash2 , Edit3 , Tag , Shield
12+ ChevronRight , User , FolderKanban , Clock , Trash2 , Edit3 , Tag , Shield , AlertTriangle
1313} from 'lucide-react'
1414import { cn } from '@/lib/utils'
1515
@@ -64,6 +64,8 @@ export default function TasksPage() {
6464 const [ filterAgent , setFilterAgent ] = useState ( '' )
6565 const [ filterStatus , setFilterStatus ] = useState ( '' )
6666 const [ filterType , setFilterType ] = useState ( '' )
67+ const [ expandedCols , setExpandedCols ] = useState < Set < string > > ( new Set ( ) )
68+ const [ showTerminal , setShowTerminal ] = useState ( false )
6769
6870 useEffect ( ( ) => { fetchTasks ( ) } , [ fetchTasks ] )
6971 useEffect ( ( ) => { localStorage . setItem ( 'af-task-view' , view ) } , [ view ] )
@@ -89,6 +91,11 @@ export default function TasksPage() {
8991 return result
9092 } , [ tasks , filterProject , filterAgent , filterStatus , filterType ] )
9193
94+ const staleTasks = useMemo ( ( ) => filtered . filter ( isTaskStale ) , [ filtered ] )
95+ const terminalStatuses = useMemo ( ( ) => new Set ( [ 'completed' , 'failed' ] ) , [ ] )
96+ const activeTasks = useMemo ( ( ) => filtered . filter ( t => ! terminalStatuses . has ( t . status ) ) , [ filtered , terminalStatuses ] )
97+ const terminalTasks = useMemo ( ( ) => filtered . filter ( t => terminalStatuses . has ( t . status ) ) , [ filtered , terminalStatuses ] )
98+
9299 // Group for kanban by status
93100 const kanbanGroups = useMemo ( ( ) => {
94101 if ( groupBy === 'status' ) {
@@ -164,6 +171,40 @@ export default function TasksPage() {
164171 } catch { /* ignore */ }
165172 } , [ fetchTasks , t ] )
166173
174+ const handleStaleMoveTodo = useCallback ( async ( ) => {
175+ for ( const task of staleTasks ) {
176+ await fetch ( '/api/tasks' , {
177+ method : 'PUT' ,
178+ headers : { 'Content-Type' : 'application/json' } ,
179+ body : JSON . stringify ( { id : task . id , status : 'pending' } ) ,
180+ } )
181+ }
182+ fetchTasks ( )
183+ } , [ staleTasks , fetchTasks ] )
184+
185+ const handleStaleMarkFailed = useCallback ( async ( ) => {
186+ for ( const task of staleTasks ) {
187+ await fetch ( '/api/tasks' , {
188+ method : 'PUT' ,
189+ headers : { 'Content-Type' : 'application/json' } ,
190+ body : JSON . stringify ( { id : task . id , status : 'failed' } ) ,
191+ } )
192+ }
193+ fetchTasks ( )
194+ } , [ staleTasks , fetchTasks ] )
195+
196+ const handleClearCompleted = useCallback ( async ( ) => {
197+ if ( ! confirm ( t ( 'tasks.confirmClearCompleted' ) ) ) return
198+ try {
199+ await fetch ( '/api/tasks/batch' , {
200+ method : 'DELETE' ,
201+ headers : { 'Content-Type' : 'application/json' } ,
202+ body : JSON . stringify ( { statuses : [ 'completed' , 'failed' ] } ) ,
203+ } )
204+ fetchTasks ( )
205+ } catch { /* ignore */ }
206+ } , [ fetchTasks , t ] )
207+
167208 // Unique project names for filter
168209 const projectOptions = useMemo ( ( ) => {
169210 const ids = new Set ( tasks . map ( t => t . projectId ) . filter ( Boolean ) as string [ ] )
@@ -219,6 +260,15 @@ export default function TasksPage() {
219260 < List className = "w-4 h-4" />
220261 </ button >
221262 </ div >
263+ { terminalTasks . length > 0 && (
264+ < button
265+ onClick = { handleClearCompleted }
266+ className = "flex items-center gap-1.5 px-3 py-2 text-sm rounded-lg border border-border text-muted-foreground hover:text-foreground hover:border-red-500/30 transition-colors"
267+ >
268+ < Trash2 className = "w-4 h-4" />
269+ { t ( 'tasks.clearCompleted' ) }
270+ </ button >
271+ ) }
222272 < button
223273 onClick = { ( ) => { setEditTask ( null ) ; setShowForm ( true ) } }
224274 className = "flex items-center gap-1.5 px-3 py-2 bg-primary text-primary-foreground text-sm rounded-lg hover:bg-primary/90 transition-colors"
@@ -278,6 +328,30 @@ export default function TasksPage() {
278328 </ select >
279329 </ div >
280330
331+ { /* Stale tasks banner */ }
332+ { staleTasks . length > 0 && (
333+ < div className = "flex items-center justify-between gap-3 px-4 py-2.5 rounded-lg bg-amber-500/10 border border-amber-500/20" >
334+ < div className = "flex items-center gap-2 text-sm text-amber-400" >
335+ < AlertTriangle className = "w-4 h-4 shrink-0" />
336+ < span > { t ( 'tasks.staleTasks' ) . replace ( '{count}' , String ( staleTasks . length ) ) } </ span >
337+ </ div >
338+ < div className = "flex items-center gap-2" >
339+ < button
340+ onClick = { handleStaleMoveTodo }
341+ className = "px-2.5 py-1 text-xs rounded-md bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 transition-colors"
342+ >
343+ { t ( 'tasks.staleMoveTodo' ) }
344+ </ button >
345+ < button
346+ onClick = { handleStaleMarkFailed }
347+ className = "px-2.5 py-1 text-xs rounded-md bg-amber-500/20 text-amber-400 hover:bg-amber-500/30 transition-colors"
348+ >
349+ { t ( 'tasks.staleMarkFailed' ) }
350+ </ button >
351+ </ div >
352+ </ div >
353+ ) }
354+
281355 { /* Empty state */ }
282356 { tasks . length === 0 && (
283357 < div className = "text-center py-16 space-y-3" >
@@ -309,9 +383,38 @@ export default function TasksPage() {
309383 </ span >
310384 </ div >
311385 < div className = "space-y-2 min-h-[200px] p-1.5 rounded-lg bg-muted/30 border border-border/50" >
312- { ( kanbanGroups [ col . key ] || [ ] ) . map ( task => (
313- < TaskCard key = { task . id } task = { task } onClick = { ( ) => setDetailTask ( task ) } />
314- ) ) }
386+ { ( ( ) => {
387+ const colTasks = kanbanGroups [ col . key ] || [ ]
388+ const isTerminalCol = groupBy === 'status' && ( col . key === 'completed' || col . key === 'failed' )
389+ const maxCollapsed = 5
390+ const isExpanded = expandedCols . has ( col . key )
391+ const visible = isTerminalCol && ! isExpanded && colTasks . length > maxCollapsed
392+ ? colTasks . slice ( 0 , maxCollapsed )
393+ : colTasks
394+ const remaining = colTasks . length - visible . length
395+ return (
396+ < >
397+ { visible . map ( task => (
398+ < TaskCard key = { task . id } task = { task } onClick = { ( ) => setDetailTask ( task ) } />
399+ ) ) }
400+ { isTerminalCol && colTasks . length > maxCollapsed && (
401+ < button
402+ onClick = { ( ) => setExpandedCols ( prev => {
403+ const next = new Set ( prev )
404+ if ( next . has ( col . key ) ) next . delete ( col . key )
405+ else next . add ( col . key )
406+ return next
407+ } ) }
408+ className = "w-full text-center text-xs text-muted-foreground hover:text-foreground py-1.5 transition-colors"
409+ >
410+ { isExpanded
411+ ? t ( 'tasks.showLess' )
412+ : t ( 'tasks.showMore' ) . replace ( '{count}' , String ( remaining ) ) }
413+ </ button >
414+ ) }
415+ </ >
416+ )
417+ } ) ( ) }
315418 </ div >
316419 </ div >
317420 ) ) }
@@ -334,7 +437,7 @@ export default function TasksPage() {
334437 </ tr >
335438 </ thead >
336439 < tbody className = "divide-y divide-border" >
337- { filtered . map ( task => (
440+ { activeTasks . map ( task => (
338441 < tr
339442 key = { task . id }
340443 onClick = { ( ) => setDetailTask ( task ) }
@@ -376,6 +479,70 @@ export default function TasksPage() {
376479 </ tr >
377480 ) ) }
378481 </ tbody >
482+ { terminalTasks . length > 0 && (
483+ < >
484+ < tbody >
485+ < tr >
486+ < td colSpan = { 7 } className = "px-4 py-2" >
487+ < button
488+ onClick = { ( ) => setShowTerminal ( v => ! v ) }
489+ className = "flex items-center gap-1.5 text-xs text-muted-foreground hover:text-foreground transition-colors"
490+ >
491+ < ChevronRight className = { cn ( 'w-3.5 h-3.5 transition-transform' , showTerminal && 'rotate-90' ) } />
492+ { showTerminal
493+ ? t ( 'tasks.hideCompleted' )
494+ : t ( 'tasks.showCompleted' ) . replace ( '{count}' , String ( terminalTasks . length ) ) }
495+ </ button >
496+ </ td >
497+ </ tr >
498+ </ tbody >
499+ { showTerminal && (
500+ < tbody className = "divide-y divide-border opacity-60" >
501+ { terminalTasks . map ( task => (
502+ < tr
503+ key = { task . id }
504+ onClick = { ( ) => setDetailTask ( task ) }
505+ className = "hover:bg-muted/30 cursor-pointer transition-colors"
506+ >
507+ < td className = "px-4 py-2.5" >
508+ < span className = { cn ( 'inline-block w-2.5 h-2.5 rounded-full' , statusDot [ task . status ] || 'bg-zinc-400' ) } />
509+ </ td >
510+ < td className = "px-4 py-2.5 text-foreground font-medium truncate max-w-[200px]" > { task . name } </ td >
511+ < td className = "px-4 py-2.5 text-muted-foreground hidden md:table-cell" >
512+ { task . type ? (
513+ < span className = "text-[10px] px-1.5 py-0.5 rounded bg-primary/10 text-primary" > { task . type } </ span >
514+ ) : '-' }
515+ </ td >
516+ < td className = "px-4 py-2.5 text-muted-foreground hidden md:table-cell" >
517+ { task . assignees ?. join ( ', ' ) || '-' }
518+ </ td >
519+ < td className = "px-4 py-2.5 text-muted-foreground hidden lg:table-cell" >
520+ { task . projectId || t ( 'tasks.standalone' ) }
521+ </ td >
522+ < td className = "px-4 py-2.5" >
523+ < span className = { cn (
524+ 'text-[10px] font-bold px-1.5 py-0.5 rounded' ,
525+ task . priority === 'P0' ? 'bg-red-500/20 text-red-400'
526+ : task . priority === 'P1' ? 'bg-amber-500/20 text-amber-400'
527+ : 'bg-blue-500/20 text-blue-400'
528+ ) } >
529+ { task . priority }
530+ </ span >
531+ </ td >
532+ < td className = "px-4 py-2.5 hidden md:table-cell" >
533+ < div className = "flex items-center gap-2" >
534+ < div className = "w-16 h-1.5 rounded-full bg-muted overflow-hidden" >
535+ < div className = "h-full bg-primary rounded-full" style = { { width : `${ task . progress } %` } } />
536+ </ div >
537+ < span className = "text-xs text-muted-foreground" > { task . progress } %</ span >
538+ </ div >
539+ </ td >
540+ </ tr >
541+ ) ) }
542+ </ tbody >
543+ ) }
544+ </ >
545+ ) }
379546 </ table >
380547 </ div >
381548 ) }
0 commit comments