feat(tui-go): session list / resume view (E)

R opens a navigable overlay of recent sessions (GET /sessions: short id, status,
workflow/stage, relative age); enter resumes via POST /sessions/{id}/resume, which
replays onto the existing global /stream (no WS re-dial). Fetch/resume errors surface
in the overlay, never panic. stdlib-only (ws.Client.HTTPBase derives the base URL).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-20 16:22:27 +00:00
parent 46a71ee84c
commit 1f7a5d96a7
7 changed files with 483 additions and 11 deletions
+37
View File
@@ -51,12 +51,37 @@ func (m Model) Update(msg tea.Msg) (tea.Model, tea.Cmd) {
m.applyServerPhased(msg.m)
return m, readServer(m.client)
case sessionsLoadedMsg:
m.applySessionsLoaded(msg)
return m, nil
case sessionResumeMsg:
if msg.err != "" {
m.sessionListErr = "resume: " + msg.err
}
return m, nil
case tea.KeyMsg:
return m.handleKey(msg)
}
return m, nil
}
// applySessionsLoaded folds a GET /sessions result into the browser, clamping the
// cursor and surfacing any fetch error in place of a crash.
func (m *Model) applySessionsLoaded(msg sessionsLoadedMsg) {
m.sessionListLoading = false
if msg.err != "" {
m.sessionListErr = msg.err
return
}
m.sessionListErr = ""
m.sessionList = msg.sessions
if m.sessionListIndex >= len(m.sessionList) {
m.sessionListIndex = 0
}
}
func (m *Model) applyConn(s ws.Status) {
switch {
case s.Connected:
@@ -178,6 +203,10 @@ func (m Model) handleNormalKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
m.openArtifacts()
case "I":
m.openIdeas()
case "R":
// 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 "S":
m.openStats()
case "g":
@@ -359,6 +388,11 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
if m.overlay == OverlayConfig {
return m.handleConfigKey(k)
}
// Sessions overlay owns all its keys: enter resumes (emits a cmd), so it can't
// share the generic no-cmd esc path below.
if m.overlay == OverlaySessions {
return m.handleSessionsKey(k)
}
if k.Type == tea.KeyEsc {
m.overlay = OverlayNone
return m, nil
@@ -566,6 +600,7 @@ func paletteCommands() []paletteCmd {
{"events", "event inspector", "browse the event stream"},
{"artifacts", "view artifacts", "browse this session's artifacts"},
{"stats", "session stats", "metrics for the selected session"},
{"sessions", "resume session", "browse / resume recent sessions"},
{"config", "edit config", "view / change correx settings"},
{"mode", "toggle mode", "switch chat / steering"},
{"cancel", "cancel session", "stop the selected session"},
@@ -605,6 +640,8 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
m.openArtifacts()
case "stats":
m.openStats()
case "sessions":
return m, m.openSessions()
case "config":
m.openConfig()
case "mode":