diff --git a/apps/tui-go/internal/app/tasks_overlay.go b/apps/tui-go/internal/app/tasks_overlay.go index 9c15952e..66c15047 100644 --- a/apps/tui-go/internal/app/tasks_overlay.go +++ b/apps/tui-go/internal/app/tasks_overlay.go @@ -27,6 +27,10 @@ type TaskSummary struct { Notes []TaskNoteWire `json:"notes"` CreatedAt string `json:"createdAt"` UpdatedAt string `json:"updatedAt"` + // Dependency-graph status from the server: Ready = workable now (TODO, no unmet deps); + // BlockedBy = ids of unfinished tasks this one waits on. Absent on older servers → zero values. + Ready bool `json:"ready"` + BlockedBy []string `json:"blockedBy"` } type TaskLinkWire struct { @@ -336,14 +340,29 @@ func (m Model) taskListRow(tasks []TaskSummary, i int) string { if tk.Claimant != "" { claim = " @" + shortSessionID(tk.Claimant) } - titleW := m.modalWidth() - 6 - 12 - 1 - 12 - 1 - len([]rune(claim)) + readyCell := taskReadyCell(t, tk) // 2-col dependency-readiness glyph + titleW := m.modalWidth() - 6 - 12 - 1 - 12 - 1 - 2 - 1 - len([]rune(claim)) if titleW < 8 { titleW = 8 } titleCell := mbg(t, padRaw(truncate(tk.Title, titleW), titleW), t.P.Fg) claimCell := mbg(t, claim, t.P.Faint) - return marker + idCell + " " + statusCell + " " + titleCell + claimCell + return marker + idCell + " " + statusCell + " " + readyCell + " " + titleCell + claimCell +} + +// taskReadyCell is a 2-column glyph showing dependency-graph readiness, so a decomposed graph reads +// at a glance: a filled dot when the task is workable now (TODO, no unmet deps), a hollow dot when it +// is waiting on unfinished blockers, blank for anything already in flight or finished. +func taskReadyCell(t Theme, tk TaskSummary) string { + switch { + case tk.Ready: + return lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.BgPanel).Render(padRaw("●", 2)) + case len(tk.BlockedBy) > 0: + return lipgloss.NewStyle().Foreground(t.P.Dim).Background(t.P.BgPanel).Render(padRaw("○", 2)) + default: + return mbg(t, " ", t.P.BgPanel) + } } func (m Model) taskDetailModal() string { @@ -407,6 +426,11 @@ func (m Model) taskDetailBody(width int) []string { if tk.Claimant != "" { out = append(out, mbg(t, "claimant: "+tk.Claimant, t.P.Faint)) } + if tk.Ready { + out = append(out, mbg(t, "● ready to work (no unmet dependencies)", t.P.OK)) + } else if len(tk.BlockedBy) > 0 { + out = append(out, mbg(t, "○ blocked — waiting on "+strings.Join(tk.BlockedBy, ", "), t.P.Dim)) + } out = append(out, "") if tk.Goal != "" { diff --git a/apps/tui-go/internal/app/tasks_overlay_test.go b/apps/tui-go/internal/app/tasks_overlay_test.go index 1fb93554..2ccba01b 100644 --- a/apps/tui-go/internal/app/tasks_overlay_test.go +++ b/apps/tui-go/internal/app/tasks_overlay_test.go @@ -148,6 +148,26 @@ func TestTasksFilterNarrowsList(t *testing.T) { } } +func TestTasksReadyAndBlockedSurfaced(t *testing.T) { + m := tasksModel() + m.overlay = OverlayTasks + m.taskList = []TaskSummary{ + {ID: "webui-2", Key: "webui-2", Status: "TODO", Title: "Scaffold app", Ready: true}, + {ID: "webui-3", Key: "webui-3", Status: "TODO", Title: "Auth view", BlockedBy: []string{"webui-2"}}, + } + + // A ready task's detail says so; a blocked task's detail names what it waits on. + m.taskDetail = true + m.taskListIndex = 0 + if out := m.tasksModal(); !strings.Contains(out, "ready to work") { + t.Fatalf("expected ready marker in detail:\n%s", out) + } + m.taskListIndex = 1 + if out := m.tasksModal(); !strings.Contains(out, "waiting on webui-2") { + t.Fatalf("expected blocked-by detail naming the blocker:\n%s", out) + } +} + func TestTasksFetchErrorShownNoPanic(t *testing.T) { m := tasksModel() m.overlay = OverlayTasks