1 Commits

Author SHA1 Message Date
412b1167a2 Cancel pending timers when a child is closed (#6)
Co-authored-by: Harry Bayliss <harry@hjb.dev>
Co-committed-by: Harry Bayliss <harry@hjb.dev>
2026-05-18 12:46:50 +01:00
4 changed files with 22 additions and 80 deletions

View File

@@ -6,14 +6,6 @@ loosely follows [Semantic Versioning](https://semver.org/spec/v2.0.0.html).
## [Unreleased] ## [Unreleased]
## [0.0.7] - 2026-05-18
### 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 ### Changed
- Built-in agent presets (`claude`, `codex`, `opencode`) now live in - Built-in agent presets (`claude`, `codex`, `opencode`) now live in
memory and user preset files merge over them by name instead of memory and user preset files merge over them by name instead of

View File

@@ -0,0 +1 @@
- [ ] We should show idle state in the top tab bar as well

View File

@@ -829,11 +829,11 @@ func (st *uiState) OnChildSpawned(c *Child) {
st.drawStatusLine() st.drawStatusLine()
} }
// OnChildStateChanged repaints the sidebar and tab bar whenever a // OnChildStateChanged repaints the sidebar whenever a child's
// child's idle-state badge flips. Cheap — both draws bail when the // idle-state badge flips. Cheap — the badge is the only chrome that
// cached frame hasn't changed. // reflects state today, and drawSidebar bails when the cached frame
// hasn't changed.
func (st *uiState) OnChildStateChanged(string, IdleState) { func (st *uiState) OnChildStateChanged(string, IdleState) {
st.drawTabBar()
st.drawSidebar() st.drawSidebar()
} }

View File

@@ -59,12 +59,10 @@ func (st *uiState) drawTabBar() {
newHintW := utf8.RuneCountInString(newHint) + 2 // " + new " framing newHintW := utf8.RuneCountInString(newHint) + 2 // " + new " framing
type tabRect struct { type tabRect struct {
startCol int startCol int
width int width int
label string label string
glyph string active bool
glyphStyle string
active bool
} }
activeTab := -1 activeTab := -1
@@ -117,16 +115,9 @@ func (st *uiState) drawTabBar() {
if i < extra { if i < extra {
w++ w++
} }
active := c.ID == focus
glyph, glyphStyle := tabIdleGlyph(c.IdleState(), active)
label := c.DisplayName() label := c.DisplayName()
labelW := utf8.RuneCountInString(label) labelW := utf8.RuneCountInString(label)
// Reserve room for the glyph + its trailing space when present maxLabelW := w - 2 // one pad on each side
// (1 + 1 runes), on top of the one-cell pad on each side.
maxLabelW := w - 2
if glyph != "" {
maxLabelW -= 2
}
if maxLabelW < 1 { if maxLabelW < 1 {
maxLabelW = 1 maxLabelW = 1
} }
@@ -139,12 +130,10 @@ func (st *uiState) drawTabBar() {
labelW = utf8.RuneCountInString(label) labelW = utf8.RuneCountInString(label)
} }
tabs = append(tabs, tabRect{ tabs = append(tabs, tabRect{
startCol: col, startCol: col,
width: w, width: w,
label: label, label: label,
glyph: glyph, active: c.ID == focus,
glyphStyle: glyphStyle,
active: active,
}) })
if tabs[len(tabs)-1].active { if tabs[len(tabs)-1].active {
activeTab = len(tabs) - 1 activeTab = len(tabs) - 1
@@ -166,37 +155,23 @@ func (st *uiState) drawTabBar() {
fmt.Fprintf(&b, "\x1b[3;1H\x1b[%dX", width) fmt.Fprintf(&b, "\x1b[3;1H\x1b[%dX", width)
for _, t := range tabs { for _, t := range tabs {
// Row 1: centre-ish glyph+label inside the tab cell. // Row 1: centre-ish label inside the tab cell.
labelW := utf8.RuneCountInString(t.label) labelW := utf8.RuneCountInString(t.label)
visibleW := labelW leftPad := (t.width - labelW) / 2
if t.glyph != "" {
visibleW += 2 // glyph + separator space
}
leftPad := (t.width - visibleW) / 2
if leftPad < 1 { if leftPad < 1 {
leftPad = 1 leftPad = 1
} }
rightPad := t.width - visibleW - leftPad rightPad := t.width - labelW - leftPad
if rightPad < 0 { if rightPad < 0 {
rightPad = 0 rightPad = 0
} }
cellStyle := styleHint
if t.active {
cellStyle = styleActive
}
fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol) fmt.Fprintf(&b, "\x1b[1;%dH", t.startCol)
b.WriteString(cellStyle) if t.active {
b.WriteString(strings.Repeat(" ", leftPad)) b.WriteString(styleActive)
if t.glyph != "" { } else {
// Glyph uses its own colour so error/permission states pop b.WriteString(styleHint)
// 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(strings.Repeat(" ", leftPad))
b.WriteString(t.label) b.WriteString(t.label)
b.WriteString(strings.Repeat(" ", rightPad)) b.WriteString(strings.Repeat(" ", rightPad))
b.WriteString(styleReset) b.WriteString(styleReset)
@@ -251,29 +226,3 @@ func (st *uiState) drawTabBar() {
defer st.outMu.Unlock() defer st.outMu.Unlock()
fmt.Fprintf(os.Stdout, "\x1b7%s\x1b8", frame) 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 "", ""
}
}