feat(tui-go): transcript free-scroll + help overlay + event filter
Three TUI-polish items. - OUTPUT free-scroll: PgUp/PgDn (and ^u/^d half-page) scroll the transcript back through history; esc snaps to tail-follow. outputScroll is clamped against outputViewport(), which mirrors View/renderMain geometry, so the edges land exactly (no dead presses unwinding an overshoot). routerRows split into buildTranscriptRows (full render) + windowing so the handler measures the same content. Tests cover the clamp + the fits-in-panel no-op. - Help overlay: `?` (or the "help" palette command) opens a keybinding cheat-sheet grouped by context (session / overlays / approval band) — the non-palette keys especially. Any key dismisses it. - Event-inspector filter: `/` filters the event list by a case-insensitive substring of type/detail (currentEvents applies it); `c` clears, esc stops editing then closes. Fresh per open. EVENTS side panel stays unfiltered. go build / vet / test green; rendered help + filtered inspector via cmd/preview (help, events-filter kinds). Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -132,6 +132,8 @@ func (m Model) renderOverlay(base string) string {
|
||||
return m.center(m.grantsModal())
|
||||
case OverlayGrantScope:
|
||||
return m.center(m.grantScopeModal())
|
||||
case OverlayHelp:
|
||||
return m.center(m.helpModal())
|
||||
}
|
||||
return base
|
||||
}
|
||||
@@ -437,8 +439,25 @@ func (m Model) eventInspectorModal() string {
|
||||
b.WriteString(mbg(t, " ("+itoa(m.overlayEventIdx+1)+"/"+itoa(len(evs))+")", t.P.Faint))
|
||||
}
|
||||
b.WriteString("\n\n")
|
||||
// Filter line (shown while editing or when a query is set): a `/` prompt over the query.
|
||||
if m.eventFilterTyping || m.eventFilter != "" {
|
||||
caret := mbg(t, "", t.P.BgPanel)
|
||||
if m.eventFilterTyping && m.caretVisible() {
|
||||
caret = lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("▏")
|
||||
}
|
||||
prompt := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render("/ ")
|
||||
if m.eventFilter == "" {
|
||||
b.WriteString(prompt + caret + mbg(t, "type to filter events…", t.P.Faint) + "\n\n")
|
||||
} else {
|
||||
b.WriteString(prompt + mbg(t, m.eventFilter, t.P.FgStrong) + caret + "\n\n")
|
||||
}
|
||||
}
|
||||
if len(evs) == 0 {
|
||||
b.WriteString(mbg(t, "no events", t.P.Faint))
|
||||
msg := "no events"
|
||||
if m.eventFilter != "" {
|
||||
msg = "no events match \"" + m.eventFilter + "\""
|
||||
}
|
||||
b.WriteString(mbg(t, msg, t.P.Faint))
|
||||
return m.center(t.Overlay.Width(w).Render(b.String()))
|
||||
}
|
||||
// Budget the plain detail so the styled row never needs ANSI-aware truncation
|
||||
@@ -482,7 +501,7 @@ func (m Model) eventInspectorModal() string {
|
||||
mbg(t, " "+truncate(e.Detail, detailBudget), t.P.Dim)
|
||||
b.WriteString(row + "\n")
|
||||
}
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"esc", "close"}}))
|
||||
b.WriteString("\n" + modalHints(t, [][2]string{{"↑↓", "select"}, {"/", "filter"}, {"c", "clear"}, {"esc", "close"}}))
|
||||
|
||||
modal := t.Overlay.Width(w).Render(b.String())
|
||||
return m.center(modal)
|
||||
@@ -688,6 +707,52 @@ func (m Model) grantScopeModal() string {
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
// helpModal is the keybinding cheat-sheet (?), grouped by context. Any key dismisses it.
|
||||
func (m Model) helpModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
type kb struct{ key, desc string }
|
||||
section := func(b *strings.Builder, title string, rows []kb) {
|
||||
b.WriteString(lipgloss.NewStyle().Foreground(t.P.Accent2).Background(t.P.BgPanel).Bold(true).Render(title) + "\n")
|
||||
for _, r := range rows {
|
||||
key := lipgloss.NewStyle().Foreground(t.P.Accent).Background(t.P.BgPanel).Render(padRaw(r.key, 16))
|
||||
b.WriteString(" " + key + mbg(t, r.desc, t.P.Fg) + "\n")
|
||||
}
|
||||
b.WriteString("\n")
|
||||
}
|
||||
|
||||
var b strings.Builder
|
||||
b.WriteString(m.titleLine("keybindings") + "\n\n")
|
||||
section(&b, "session", []kb{
|
||||
{"i", "compose a message"},
|
||||
{"^↑ / ^↓", "jump to your previous / next message"},
|
||||
{"y", "copy the selected message (OSC52)"},
|
||||
{"PgUp / PgDn", "scroll output (^u / ^d half-page)"},
|
||||
{"esc", "drop selection / scroll → follow newest"},
|
||||
{"^x", "open the latest diff"},
|
||||
{"l", "back to the session list"},
|
||||
})
|
||||
section(&b, "overlays", []kb{
|
||||
{"p", "command palette (shows all shortcuts)"},
|
||||
{"e", "event inspector (/ to filter)"},
|
||||
{"t / v", "tools / artifacts"},
|
||||
{"S / R", "session stats / resume sessions"},
|
||||
{"m / g", "swap model / edit config"},
|
||||
{"G / I", "grants / idea board"},
|
||||
})
|
||||
section(&b, "approval band", []kb{
|
||||
{"y / a / enter", "approve once"},
|
||||
{"n / r", "reject"},
|
||||
{"e / s", "steer (add a note)"},
|
||||
{"A", "approve-always — choose scope"},
|
||||
{"↑ / ↓", "walk the pending queue"},
|
||||
})
|
||||
b.WriteString(modalHints(t, [][2]string{{"any key", "close"}}))
|
||||
modal := t.Overlay.Width(w).Render(b.String())
|
||||
return m.center(modal)
|
||||
}
|
||||
|
||||
func (m Model) toolPaletteModal() string {
|
||||
t := m.theme
|
||||
w := m.modalWidth()
|
||||
|
||||
Reference in New Issue
Block a user