@@ -883,6 +883,191 @@ that the TUI shows, either as formatted text or JSON for automation.`,
883883 boardCmd .Flags ().Int ("limit" , 5 , "Maximum entries to show per column" )
884884 rootCmd .AddCommand (boardCmd )
885885
886+ // Tail subcommand - live updating task view grouped by project and status
887+ tailCmd := & cobra.Command {
888+ Use : "tail" ,
889+ Short : "Live view of tasks organized by project and status" ,
890+ Long : `Show a continuously updating view of all active tasks,
891+ grouped by project and then by status. Refreshes automatically.
892+ Press Ctrl+C to stop.` ,
893+ Run : func (cmd * cobra.Command , args []string ) {
894+ interval , _ := cmd .Flags ().GetDuration ("interval" )
895+ showDone , _ := cmd .Flags ().GetBool ("done" )
896+
897+ dbPath := db .DefaultPath ()
898+ database , err := db .Open (dbPath )
899+ if err != nil {
900+ fmt .Fprintln (os .Stderr , errorStyle .Render ("Error: " + err .Error ()))
901+ os .Exit (1 )
902+ }
903+ defer database .Close ()
904+
905+ sigCh := make (chan os.Signal , 1 )
906+ signal .Notify (sigCh , syscall .SIGINT , syscall .SIGTERM )
907+
908+ ticker := time .NewTicker (interval )
909+ defer ticker .Stop ()
910+
911+ statusStyle := func (status string ) lipgloss.Style {
912+ switch status {
913+ case db .StatusQueued :
914+ return lipgloss .NewStyle ().Foreground (lipgloss .Color ("#F59E0B" ))
915+ case db .StatusProcessing :
916+ return lipgloss .NewStyle ().Foreground (lipgloss .Color ("#3B82F6" ))
917+ case db .StatusBlocked :
918+ return lipgloss .NewStyle ().Foreground (lipgloss .Color ("#EF4444" ))
919+ case db .StatusDone :
920+ return lipgloss .NewStyle ().Foreground (lipgloss .Color ("#10B981" ))
921+ default :
922+ return lipgloss .NewStyle ().Foreground (lipgloss .Color ("#6B7280" ))
923+ }
924+ }
925+
926+ projectStyle := lipgloss .NewStyle ().Bold (true ).Foreground (lipgloss .Color ("#A78BFA" ))
927+ headerStyle := lipgloss .NewStyle ().Bold (true ).Foreground (lipgloss .Color ("#E5E7EB" ))
928+
929+ statusOrder := []string {
930+ db .StatusProcessing ,
931+ db .StatusBlocked ,
932+ db .StatusQueued ,
933+ db .StatusBacklog ,
934+ }
935+ if showDone {
936+ statusOrder = append (statusOrder , db .StatusDone )
937+ }
938+
939+ statusLabels := map [string ]string {
940+ db .StatusProcessing : "In Progress" ,
941+ db .StatusBlocked : "Blocked" ,
942+ db .StatusQueued : "Queued" ,
943+ db .StatusBacklog : "Backlog" ,
944+ db .StatusDone : "Done" ,
945+ }
946+
947+ logStyle := lipgloss .NewStyle ().Foreground (lipgloss .Color ("#9CA3AF" )).Italic (true )
948+
949+ renderTail := func () {
950+ opts := db.ListTasksOptions {
951+ IncludeClosed : showDone ,
952+ Limit : 500 ,
953+ }
954+ tasks , err := database .ListTasks (opts )
955+ if err != nil {
956+ fmt .Fprintln (os .Stderr , errorStyle .Render ("Error: " + err .Error ()))
957+ return
958+ }
959+
960+ // Collect task IDs and fetch latest log per task
961+ var taskIDs []int64
962+ for _ , t := range tasks {
963+ taskIDs = append (taskIDs , t .ID )
964+ }
965+ latestLogs , _ := database .GetLatestLogPerTask (taskIDs )
966+ if latestLogs == nil {
967+ latestLogs = make (map [int64 ]* db.TaskLog )
968+ }
969+
970+ // Group by project, then by status
971+ type projectGroup struct {
972+ name string
973+ statuses map [string ][]* db.Task
974+ }
975+ projectMap := make (map [string ]* projectGroup )
976+ var projectOrder []string
977+
978+ for _ , t := range tasks {
979+ if t .Status == db .StatusArchived {
980+ continue
981+ }
982+ proj := t .Project
983+ if proj == "" {
984+ proj = "(no project)"
985+ }
986+ pg , exists := projectMap [proj ]
987+ if ! exists {
988+ pg = & projectGroup {name : proj , statuses : make (map [string ][]* db.Task )}
989+ projectMap [proj ] = pg
990+ projectOrder = append (projectOrder , proj )
991+ }
992+ pg .statuses [t .Status ] = append (pg .statuses [t .Status ], t )
993+ }
994+
995+ sort .Strings (projectOrder )
996+
997+ // Clear screen and move cursor to top
998+ fmt .Print ("\033 [2J\033 [H" )
999+
1000+ fmt .Println (headerStyle .Render ("TaskYou — Live Tail" ) + " " + dimStyle .Render (time .Now ().Format ("15:04:05" )))
1001+ fmt .Println (dimStyle .Render (strings .Repeat ("─" , 60 )))
1002+
1003+ if len (tasks ) == 0 {
1004+ fmt .Println (dimStyle .Render (" No tasks found" ))
1005+ }
1006+
1007+ for i , proj := range projectOrder {
1008+ pg := projectMap [proj ]
1009+ // Count total tasks in this project
1010+ total := 0
1011+ for _ , tl := range pg .statuses {
1012+ total += len (tl )
1013+ }
1014+ fmt .Printf ("\n %s %s\n " , projectStyle .Render (proj ), dimStyle .Render (fmt .Sprintf ("(%d)" , total )))
1015+
1016+ for _ , status := range statusOrder {
1017+ statusTasks := pg .statuses [status ]
1018+ if len (statusTasks ) == 0 {
1019+ continue
1020+ }
1021+ label := statusLabels [status ]
1022+ fmt .Printf (" %s\n " , statusStyle (status ).Render (fmt .Sprintf ("▸ %s (%d)" , label , len (statusTasks ))))
1023+ for _ , t := range statusTasks {
1024+ title := truncate (t .Title , 70 )
1025+ age := boardAgeHint (t )
1026+ line := fmt .Sprintf (" #%-4d %s" , t .ID , title )
1027+ if age != "" {
1028+ line += " " + dimStyle .Render (age )
1029+ }
1030+ fmt .Println (line )
1031+
1032+ // Show latest log line if available
1033+ if log , ok := latestLogs [t .ID ]; ok && log .Content != "" {
1034+ content := strings .TrimSpace (log .Content )
1035+ // Take only the first line of multi-line content
1036+ if idx := strings .IndexByte (content , '\n' ); idx != - 1 {
1037+ content = content [:idx ]
1038+ }
1039+ content = truncate (content , 80 )
1040+ fmt .Printf (" %s\n " , logStyle .Render (content ))
1041+ }
1042+ }
1043+ }
1044+
1045+ if i < len (projectOrder )- 1 {
1046+ fmt .Println ()
1047+ }
1048+ }
1049+
1050+ fmt .Println ()
1051+ fmt .Println (dimStyle .Render ("Press Ctrl+C to stop • refreshing every " + interval .String ()))
1052+ }
1053+
1054+ // Render immediately, then on each tick
1055+ renderTail ()
1056+ for {
1057+ select {
1058+ case <- sigCh :
1059+ fmt .Println ()
1060+ return
1061+ case <- ticker .C :
1062+ renderTail ()
1063+ }
1064+ }
1065+ },
1066+ }
1067+ tailCmd .Flags ().Duration ("interval" , 2 * time .Second , "Refresh interval (e.g. 1s, 500ms)" )
1068+ tailCmd .Flags ().Bool ("done" , false , "Include completed tasks" )
1069+ rootCmd .AddCommand (tailCmd )
1070+
8861071 // Show subcommand - show task details
8871072 showCmd := & cobra.Command {
8881073 Use : "show <task-id>" ,
0 commit comments