Show idle state in the top tab bar
Each agent tab now prefixes its label with the same one-rune idle indicator the sidebar uses (✕ error, ? permission, ◐ thinking, ○ idle, ● working), so the state of every open agent is visible without opening or focusing each tab. Tab redraws now fire on idle-state changes in addition to sidebar redraws.
This commit is contained in:
@@ -6,6 +6,12 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
### Added
|
||||
- The top tab bar now prefixes each agent tab's label with its
|
||||
idle-state glyph (✕ error, ? permission, ◐ thinking, ○ idle, ●
|
||||
working), matching the sidebar's vocabulary so the state of every
|
||||
open agent is visible without opening or focusing each tab.
|
||||
|
||||
### Changed
|
||||
- Built-in agent presets (`claude`, `codex`, `opencode`) now live in
|
||||
memory and user preset files merge over them by name instead of
|
||||
|
||||
@@ -829,11 +829,11 @@ func (st *uiState) OnChildSpawned(c *Child) {
|
||||
st.drawStatusLine()
|
||||
}
|
||||
|
||||
// OnChildStateChanged repaints the sidebar whenever a child's
|
||||
// idle-state badge flips. Cheap — the badge is the only chrome that
|
||||
// reflects state today, and drawSidebar bails when the cached frame
|
||||
// hasn't changed.
|
||||
// OnChildStateChanged repaints the sidebar and tab bar whenever a
|
||||
// child's idle-state badge flips. Cheap — both draws bail when the
|
||||
// cached frame hasn't changed.
|
||||
func (st *uiState) OnChildStateChanged(string, IdleState) {
|
||||
st.drawTabBar()
|
||||
st.drawSidebar()
|
||||
}
|
||||
|
||||
|
||||
@@ -62,6 +62,8 @@ func (st *uiState) drawTabBar() {
|
||||
startCol int
|
||||
width int
|
||||
label string
|
||||
glyph string
|
||||
glyphStyle string
|
||||
active bool
|
||||
}
|
||||
activeTab := -1
|
||||
@@ -115,9 +117,16 @@ func (st *uiState) drawTabBar() {
|
||||
if i < extra {
|
||||
w++
|
||||
}
|
||||
active := c.ID == focus
|
||||
glyph, glyphStyle := tabIdleGlyph(c.IdleState(), active)
|
||||
label := c.DisplayName()
|
||||
labelW := utf8.RuneCountInString(label)
|
||||
maxLabelW := w - 2 // one pad on each side
|
||||
// Reserve room for the glyph + its trailing space when present
|
||||
// (1 + 1 runes), on top of the one-cell pad on each side.
|
||||
maxLabelW := w - 2
|
||||
if glyph != "" {
|
||||
maxLabelW -= 2
|
||||
}
|
||||
if maxLabelW < 1 {
|
||||
maxLabelW = 1
|
||||
}
|
||||
@@ -133,7 +142,9 @@ func (st *uiState) drawTabBar() {
|
||||
startCol: col,
|
||||
width: w,
|
||||
label: label,
|
||||
active: c.ID == focus,
|
||||
glyph: glyph,
|
||||
glyphStyle: glyphStyle,
|
||||
active: active,
|
||||
})
|
||||
if tabs[len(tabs)-1].active {
|
||||
activeTab = len(tabs) - 1
|
||||
@@ -155,23 +166,37 @@ func (st *uiState) drawTabBar() {
|
||||
fmt.Fprintf(&b, "\x1b[3;1H\x1b[%dX", width)
|
||||
|
||||
for _, t := range tabs {
|
||||
// Row 1: centre-ish label inside the tab cell.
|
||||
// Row 1: centre-ish glyph+label inside the tab cell.
|
||||
labelW := utf8.RuneCountInString(t.label)
|
||||
leftPad := (t.width - labelW) / 2
|
||||
visibleW := labelW
|
||||
if t.glyph != "" {
|
||||
visibleW += 2 // glyph + separator space
|
||||
}
|
||||
leftPad := (t.width - visibleW) / 2
|
||||
if leftPad < 1 {
|
||||
leftPad = 1
|
||||
}
|
||||
rightPad := t.width - labelW - leftPad
|
||||
rightPad := t.width - visibleW - leftPad
|
||||
if rightPad < 0 {
|
||||
rightPad = 0
|
||||
}
|
||||
fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol)
|
||||
cellStyle := styleHint
|
||||
if t.active {
|
||||
b.WriteString(styleActive)
|
||||
} else {
|
||||
b.WriteString(styleHint)
|
||||
cellStyle = styleActive
|
||||
}
|
||||
fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol)
|
||||
b.WriteString(cellStyle)
|
||||
b.WriteString(strings.Repeat(" ", leftPad))
|
||||
if t.glyph != "" {
|
||||
// Glyph uses its own colour so error/permission states pop
|
||||
// regardless of tab focus, matching the sidebar's vocabulary.
|
||||
b.WriteString(styleReset)
|
||||
b.WriteString(t.glyphStyle)
|
||||
b.WriteString(t.glyph)
|
||||
b.WriteString(styleReset)
|
||||
b.WriteString(cellStyle)
|
||||
b.WriteString(" ")
|
||||
}
|
||||
b.WriteString(t.label)
|
||||
b.WriteString(strings.Repeat(" ", rightPad))
|
||||
b.WriteString(styleReset)
|
||||
@@ -226,3 +251,29 @@ func (st *uiState) drawTabBar() {
|
||||
defer st.outMu.Unlock()
|
||||
fmt.Fprintf(os.Stdout, "\x1b7%s\x1b8", frame)
|
||||
}
|
||||
|
||||
// tabIdleGlyph returns the one-rune state indicator (and its SGR style)
|
||||
// to render before a tab's label. Mirrors the sidebar's vocabulary so
|
||||
// users learn the symbols in one place: ✕ error, ? permission, ◐
|
||||
// thinking, ○ idle, ● working. Returns ("", "") for StateUnknown so the
|
||||
// first frame after spawn doesn't show a misleading badge.
|
||||
func tabIdleGlyph(state IdleState, active bool) (string, string) {
|
||||
base := styleHint
|
||||
if active {
|
||||
base = styleAccent
|
||||
}
|
||||
switch state {
|
||||
case StateError:
|
||||
return "✕", styleError
|
||||
case StatePermission:
|
||||
return "?", styleAccent
|
||||
case StateThinking:
|
||||
return "◐", base
|
||||
case StateIdle:
|
||||
return "○", base
|
||||
case StateWorking:
|
||||
return "●", base
|
||||
default:
|
||||
return "", ""
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user