diff --git a/apps/tui-go/internal/app/demo.go b/apps/tui-go/internal/app/demo.go index 4da069c6..1474bcb9 100644 --- a/apps/tui-go/internal/app/demo.go +++ b/apps/tui-go/internal/app/demo.go @@ -22,6 +22,16 @@ func PreviewFrame(kind string, w, h int) string { m.bgUpdates = 3 m.selectedID = "04a546aa" + case "resume": + m.connected = true + m.currentModel = "llama-cpp:default" + m.overlay = OverlaySessions + m.sessionList = []SessionSummary{ + {SessionID: "04a546aa8b2c", Status: "ACTIVE", WorkflowID: "healthcheck", StageCount: 3, LastActivityAt: "2026-06-22T14:30:05Z"}, + {SessionID: "0d7097bb1f3e", Status: "COMPLETED", WorkflowID: "healthcheck", StageCount: 5, LastActivityAt: "2026-06-21T09:15:00Z"}, + {SessionID: "1dae17cc77aa", Status: "FAILED", WorkflowID: "chat", LastActivityAt: "2025-12-30T18:02:00Z"}, + } + case "workflows": m.connected = true m.currentModel = "llama-cpp:default" diff --git a/apps/tui-go/internal/app/sessions_overlay.go b/apps/tui-go/internal/app/sessions_overlay.go index 6a4f73d1..bb07ece6 100644 --- a/apps/tui-go/internal/app/sessions_overlay.go +++ b/apps/tui-go/internal/app/sessions_overlay.go @@ -228,10 +228,25 @@ func (m Model) sessionListRow(i int) string { } stageCell := mbg(t, padRaw(stage, 22), t.P.Accent2) - age := relativeAge(s.LastActivityAt) - ageCell := mbg(t, age, t.P.Faint) + dateCell := mbg(t, padRaw(absDateISO(s.LastActivityAt), 12), t.P.Dim) + ageCell := mbg(t, "· "+relativeAge(s.LastActivityAt), t.P.Faint) - return marker + idCell + " " + statusCell + " " + stageCell + " " + ageCell + return marker + idCell + " " + statusCell + " " + stageCell + " " + dateCell + " " + ageCell +} + +// absDateISO renders an ISO-8601 instant (the server's LastActivityAt) as a compact local +// date via shortDateTime, or "—" when absent/unparseable. +func absDateISO(iso string) string { + if iso == "" { + return "—" + } + ts, err := time.Parse(time.RFC3339Nano, iso) + if err != nil { + if ts, err = time.Parse(time.RFC3339, iso); err != nil { + return "—" + } + } + return shortDateTime(ts.UnixMilli()) } // shortSessionID trims a long session id to a recognizable prefix for the list. diff --git a/apps/tui-go/internal/app/view.go b/apps/tui-go/internal/app/view.go index ca67a7e6..6446534c 100644 --- a/apps/tui-go/internal/app/view.go +++ b/apps/tui-go/internal/app/view.go @@ -405,15 +405,8 @@ func (m Model) activeSessionCount() int { // — the strip is too valuable to drop, the constraint is width not height). func (m Model) renderMainNarrow(h int) string { w := m.width - if m.displayState() == StateIdle { - title := "sessions" - body := m.sessionRows(w - 4) - if m.wfVisible { - title, body = "workflows", m.workflowRows(w-4) - } - return m.theme.box(title, body, w, h, true) - } - // in-session / approval: stack output over events. + // Idle is handled by renderLauncher (which drops the rail when narrow); this only + // runs for in-session / approval: stack output over events. outH := h * 55 / 100 if outH < 3 { outH = 3 @@ -495,54 +488,6 @@ func (m Model) renderInput() string { // --- body row builders --- -func (m Model) sessionRows(w int) []string { - t := m.theme - list := m.filteredSessions() - if len(list) == 0 { - return []string{t.span("no sessions yet", t.P.Faint), "", t.span("type a name below to begin", t.P.Faint)} - } - rows := make([]string, 0, len(list)) - for _, s := range list { - sel := s.ID == m.selectedID && !m.wfVisible - short := s.ID - if len(short) > 6 { - short = short[:6] - } - nameFg := t.P.Fg - bar := t.span(" ", t.P.Bg) - if sel { - bar = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▌ ") - nameFg = t.P.FgStrong - } - idCell := t.span("["+short+"] ", t.P.Faint) - statusCell := lipgloss.NewStyle().Foreground(statusColor(t, s.Status)).Background(t.P.Bg).Bold(true). - Render(padRaw(statusLabel(s.Status), 9)) - stageStr := "" - if s.CurrentStage != "" { - stageStr = " [" + s.CurrentStage + "]" - } - dateCell := t.span(shortDateTime(s.LastEventAt), t.P.Faint) - - // Right-align the last-activity date; truncate the name first so the date column - // always survives the box's width clamp (padTo) even on a slim panel. - fixed := lipgloss.Width(bar) + lipgloss.Width(idCell) + lipgloss.Width(statusCell) + 1 + len([]rune(stageStr)) - name := s.Name - if budget := w - fixed - lipgloss.Width(dateCell) - 1; budget >= 1 && len([]rune(name)) > budget { - name = truncate(name, budget) - } - nameCell := lipgloss.NewStyle().Foreground(nameFg).Background(t.P.Bg).Render(" " + name) - stageCell := t.span(stageStr, t.P.Dim) - - left := bar + idCell + statusCell + nameCell + stageCell - gap := w - lipgloss.Width(left) - lipgloss.Width(dateCell) - if gap < 1 { - gap = 1 - } - rows = append(rows, left+t.span(strings.Repeat(" ", gap), t.P.Bg)+dateCell) - } - return rows -} - // shortDateTime formats a millis-since-epoch instant as a compact local timestamp for the // session list: "Jan 02 15:04" within the current year, "Jan 02 2006" for older years. // Empty for a zero/absent time (renders as no date rather than a bogus epoch). @@ -557,56 +502,6 @@ func shortDateTime(ms int64) string { return ts.Format("Jan 02 15:04") } -func (m Model) workflowRows(w int) []string { - t := m.theme - if len(m.workflows) == 0 { - return []string{t.span("no workflows advertised", t.P.Faint)} - } - rows := make([]string, 0, len(m.workflows)) - for i, wf := range m.workflows { - marker := t.span(" ", t.P.Bg) - fg := t.P.Fg - if i == m.wfIndex { - marker = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Render("▸ ") - fg = t.P.FgStrong - } - rows = append(rows, marker+lipgloss.NewStyle().Foreground(fg).Background(t.P.Bg).Render(wf.ID)+t.span(" "+wf.Description, t.P.Faint)) - } - return rows -} - -func (m Model) welcomeRows(w int) []string { - t := m.theme - title := lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.Bg).Bold(true).Render("Corre") + - lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.Bg).Bold(true).Render("x") + - lipgloss.NewStyle().Foreground(t.P.FgStrong).Background(t.P.Bg).Bold(true).Render(" Agent Harness") - - var status string - if m.connected { - status = lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render("Connected") + - t.span(" · ", t.P.Faint) + t.span(plural(len(m.sessions), "session"), t.P.Dim) - } else { - status = lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.Bg).Render("Disconnected") - } - - rows := []string{title, status, "", - t.span("Select a session from the left", t.P.Dim), - t.span("or type a name to start a new one.", t.P.Dim), ""} - - if n := len(m.sessions); n > 0 { - rows = append(rows, t.span("recent:", t.P.Faint)) - start := 0 - if n > 5 { - start = n - 5 - } - for _, s := range m.sessions[start:] { - mark := statusGlyph(t, s.Status) - rows = append(rows, t.span(" "+s.Name+" ", t.P.Fg)+t.span(shortDateTime(s.LastEventAt), t.P.Faint)+" "+mark) - } - } - return rows -} - func (m Model) routerRows(w, h int) []string { t := m.theme if len(m.routerMessages[m.selectedID]) == 0 {