feat(tui-go): command-palette keybinds + configurable status bar

Palette: each command now shows its bare-key shortcut in a key column (and the filter
matches on it), so the palette teaches the shortcuts instead of hiding them.

Status bar: a new "status bar" palette command opens a toggle overlay to show/hide the
non-essential segments (current stage, session status, workspace path, model, last-event
clock, resource gauge); correx/connection/session-name/spinner stay. Choices persist
TUI-local in tui-prefs.json (JSON in the config dir — display state, kept out of the
shared/replayed server config) and reload on launch.

Also trims the in-session footer (drops stats/model — both reachable via `p cmds`) so it
no longer overflows + truncates `q quit` at ~100 cols after the transcript-nav additions.
Tests cover toggle→render gating and prefs persistence; verified via cmd/preview renders.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-21 10:50:28 +00:00
parent c616982b7b
commit 54f94a549f
6 changed files with 261 additions and 37 deletions
+54 -14
View File
@@ -731,10 +731,43 @@ func (m Model) handleOverlayKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
m.fileFilter += string(k.Runes)
m.fileIndex = 0
}
case OverlayStatusbar:
switch {
case k.Type == tea.KeyUp || runeIs(k, "k"):
if m.sbIndex > 0 {
m.sbIndex--
}
case k.Type == tea.KeyDown || runeIs(k, "j"):
if m.sbIndex < len(statusSegments)-1 {
m.sbIndex++
}
case k.Type == tea.KeySpace || k.Type == tea.KeyEnter || runeIs(k, "x"):
if m.sbIndex >= 0 && m.sbIndex < len(statusSegments) {
m.toggleStatusSegment(statusSegments[m.sbIndex].id)
}
case runeIs(k, "g"):
m.overlay = OverlayNone
}
}
return m, nil
}
// toggleStatusSegment flips a status-bar segment's visibility and persists the change.
func (m *Model) toggleStatusSegment(id string) {
if m.sbHidden == nil {
m.sbHidden = map[string]bool{}
}
if m.sbHidden[id] {
delete(m.sbHidden, id)
} else {
m.sbHidden[id] = true
}
saveStatusbarHidden(m.sbHidden)
}
// sbShow reports whether a status-bar segment is currently visible.
func (m Model) sbShow(id string) bool { return !m.sbHidden[id] }
// openArtifacts opens the artifact viewer for the selected session and requests its
// artifacts from the server. No-op when no session is selected.
func (m *Model) openArtifacts() {
@@ -821,22 +854,26 @@ func (m Model) handlePaletteKey(k tea.KeyMsg) (tea.Model, tea.Cmd) {
return m, nil
}
type paletteCmd struct{ id, title, hint string }
// paletteCmd is one command-palette row. key is the bare-key shortcut that runs the same
// action directly (shown in the palette so the shortcuts are discoverable); empty when the
// command has no direct key.
type paletteCmd struct{ id, key, title, hint string }
func paletteCommands() []paletteCmd {
return []paletteCmd{
{"workflows", "start workflow", "open the workflow picker"},
{"tools", "tool palette", "tools for the current stage"},
{"models", "swap model", "pick / pin the local model"},
{"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"},
{"back", "back to list", "leave the current session"},
{"quit", "quit", "exit correx"},
{"workflows", "w", "start workflow", "open the workflow picker"},
{"tools", "t", "tool palette", "tools for the current stage"},
{"models", "m", "swap model", "pick / pin the local model"},
{"events", "e", "event inspector", "browse the event stream"},
{"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"},
{"config", "g", "edit config", "view / change correx settings"},
{"statusbar", "", "status bar", "show / hide status-bar segments"},
{"mode", "s", "toggle mode", "switch chat / steering"},
{"cancel", "c", "cancel session", "stop the selected session"},
{"back", "l", "back to list", "leave the current session"},
{"quit", "q", "quit", "exit correx"},
}
}
@@ -847,7 +884,7 @@ func (m Model) filteredPalette() []paletteCmd {
}
var out []paletteCmd
for _, c := range paletteCommands() {
if strings.Contains(strings.ToLower(c.title+" "+c.hint), f) {
if strings.Contains(strings.ToLower(c.key+" "+c.title+" "+c.hint), f) {
out = append(out, c)
}
}
@@ -875,6 +912,9 @@ func (m Model) execPalette(id string) (tea.Model, tea.Cmd) {
return m, m.openSessions()
case "config":
m.openConfig()
case "statusbar":
m.overlay = OverlayStatusbar
m.sbIndex = 0
case "mode":
m.cycleChatMode()
case "cancel":