feat(tui-go): in-session right panel cycles events / changes / off (d)

Press d (or the "panel" palette command) in-session to cycle the right panel:
- events  — the live event stream (default, unchanged)
- changes — a token-usage line (tokens + turns, summed from the transcript's
            TurnMetrics) over a git-status-style list of files the session has
            written, each with summed +/− from its diff. ^x still opens the full
            diff.
- off     — hide the panel; the output transcript takes the full width.

changesRows derives everything from data already in the model (router-turn
metrics + the tool entries' unified diffs), so no new protocol. Footer + ? help
updated; "changes" preview kind added.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-22 14:47:23 +00:00
parent cde0c33031
commit 6c792b83e2
6 changed files with 95 additions and 3 deletions
+59 -3
View File
@@ -261,7 +261,7 @@ func (m Model) renderFooter() string {
} else {
// Trimmed to fit ~100 cols: tools/stats/model are all reachable via `p cmds`,
// so the footer keeps the high-traffic keys + the new transcript nav/copy.
hints = []string{hint("i", "message"), hint("^↑↓", "msgs"), hint("y", "copy"), hint("e", "events"), hint("t", "tools"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
hints = []string{hint("i", "message"), hint("^↑↓", "msgs"), hint("y", "copy"), hint("e", "events"), hint("d", "panel"), hint("l", "back"), hint("p", "cmds"), hint("q", "quit")}
}
if m.copiedFlash != 0 && m.frame-m.copiedFlash < copiedFlashFrames {
hints = append(hints, lipgloss.NewStyle().Foreground(t.P.OK).Background(bg).Bold(true).Render("✓ copied"))
@@ -295,12 +295,68 @@ func (m Model) renderMain(h int) string {
leftW := m.width * 63 / 100
rightW := m.width - leftW
// In-session / approval: output transcript over the event stream.
// In-session / approval. The right panel cycles (d): events, changes (token usage +
// changed files), or off (output takes the full width).
if m.rightPanel == 2 {
return m.theme.box("output", m.routerRows(m.width-4, h-2), m.width, h, true)
}
left := m.theme.box("output", m.routerRows(leftW-4, h-2), leftW, h, true)
right := m.theme.box("events", m.eventRows(rightW-4, h), rightW, h, false)
var right string
if m.rightPanel == 1 {
right = m.theme.box("changes", m.changesRows(rightW-4, h), rightW, h, false)
} else {
right = m.theme.box("events", m.eventRows(rightW-4, h), rightW, h, false)
}
return lipgloss.JoinHorizontal(lipgloss.Top, left, right)
}
// changesRows is the in-session "changes" panel: a token-usage line plus a git-status-style
// list of files the session has written (summed +/- from each write's diff). ^x opens the
// full diff. Drives the right panel when rightPanel == 1.
func (m Model) changesRows(w, h int) []string {
t := m.theme
turns, toks := 0, 0
for _, e := range m.routerMessages[m.selectedID] {
if e.Role == "router" && e.Metrics != nil {
turns++
toks += e.Metrics.TotalTokens
}
}
rows := []string{
t.span("tokens ", t.P.Faint) + t.span(itoa(toks), t.P.FgStrong) +
t.span(" · turns ", t.P.Faint) + t.span(itoa(turns), t.P.Dim),
"",
}
type chg struct{ add, del int }
var order []string
seen := map[string]*chg{}
for _, e := range m.routerMessages[m.selectedID] {
if e.Role != "tool" || !isUnifiedDiff(e.Content) {
continue
}
p, a, d := diffSummary(e.Content)
if seen[p] == nil {
seen[p] = &chg{}
order = append(order, p)
}
seen[p].add += a
seen[p].del += d
}
if len(order) == 0 {
return append(rows, t.span("no file changes yet", t.P.Faint))
}
rows = append(rows, t.span("changes (^x for full diff)", t.P.Faint))
for _, p := range order {
c := seen[p]
icon := lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.Bg).Render("✎ ")
delta := lipgloss.NewStyle().Foreground(t.P.OK).Background(t.P.Bg).Render("+"+itoa(c.add)) +
t.span(" ", t.P.Bg) + lipgloss.NewStyle().Foreground(t.P.Bad).Background(t.P.Bg).Render(""+itoa(c.del))
rows = append(rows, icon+t.span(truncate(p, w-16), t.P.Fg)+" "+delta)
}
return rows
}
// renderLauncher is the idle screen: a compact, opencode-style input centered in the main
// area, with a hideable right rail of status + quick keys. The session list isn't shown —
// it's a keypress away (R). The input's lower-right shows <workflow> · <model>, Tab-cycled.