feat(tasks): TUI task board

Add a cross-project task board to the TUI (T / palette): a roster fetched
from GET /tasks with vim-style nav, refresh, and an enter-to-open detail
pane (goal, criteria, paths, links, notes) built from the list payload — no
extra fetch. Server: TaskService.listAll() and a project-optional GET /tasks
(scoped with ?project=, whole board without; recent-first).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-23 20:32:48 +00:00
parent 70b4357316
commit 1d7fab4ee4
9 changed files with 593 additions and 6 deletions
+15
View File
@@ -116,6 +116,10 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
}
return m, nil
case tasksLoadedMsg:
m.applyTasksLoaded(msg)
return m, nil
case tea.KeyPressMsg:
next, cmd := m.handleKey(toKeyMsg(msg))
// A keypress may have entered a typing/active state (insert mode, steering, …);
@@ -300,6 +304,9 @@ func (m Model) handleNormalKey(k keyMsg) (tea.Model, tea.Cmd) {
// Resume browser: recent sessions over HTTP, reachable even after a server
// restart + TUI reopen (the live stream only carries running sessions).
return m, m.openSessions()
case "T":
// Task board: all tasks across projects over HTTP (GET /tasks).
return m, m.openTasks()
case "S":
m.openStats()
case "G":
@@ -641,6 +648,11 @@ func (m Model) handleOverlayKey(k keyMsg) (tea.Model, tea.Cmd) {
if m.overlay == OverlaySessions {
return m.handleSessionsKey(k)
}
// Tasks overlay owns all its keys: r refresh emits a cmd and enter toggles a detail
// pane, so it can't share the generic no-cmd esc path below.
if m.overlay == OverlayTasks {
return m.handleTasksKey(k)
}
if k.Type == keyEsc {
// In the event inspector, esc first stops an in-progress filter edit (keeping the
// query) rather than closing the overlay.
@@ -1095,6 +1107,7 @@ func paletteCommands() []paletteCmd {
{"artifacts", "v", "view artifacts", "browse this session's artifacts"},
{"stats", "S", "session stats", "metrics for the selected session"},
{"sessions", "R", "resume session", "browse / resume recent sessions"},
{"tasks", "T", "task board", "browse tasks across projects"},
{"config", "g", "edit config", "view / change correx settings"},
{"grants", "G", "grants", "view / revoke standing grants"},
{"statusbar", "", "status bar", "show / hide status-bar segments"},
@@ -1141,6 +1154,8 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
m.openStats()
case "sessions":
return m, m.openSessions()
case "tasks":
return m, m.openTasks()
case "config":
m.openConfig()
case "grants":