Problem
listTitleWidth() is called once per visible row inside renderRowFull(), which is called for every row in every View() invocation. In stateList mode the method iterates over all sessions twice — once for titles, once for CWD strings — calling lipgloss.Width(display.Sanitize(...)) on each to find the natural maximum widths:
// picker/model.go listTitleWidth()
for _, s := range m.sessions {
if w := lipgloss.Width(display.Sanitize(s.Title)); w > maxNaturalTitle { ... }
}
for _, s := range m.sessions {
if w := lipgloss.Width(display.Sanitize(s.CWDDisplay)); w > maxDirW { ... }
}
lipgloss.Width calls ansi.stringWidth → FirstGraphemeCluster → uax29/graphemes for Unicode grapheme segmentation — not a trivial operation.
With V visible rows and N total sessions, each frame triggers V × 2N width computations. For V=30, N=200 that is 12,000 calls per View(), repeated every 120 ms tick.
Evidence
CPU profiling via sample(1) on a live aps process shows the hot path:
bubbletea.eventLoop (120 ms tick)
→ View() → renderList() → renderRowFull() ×V
→ listTitleWidth()
→ lipgloss.Width(Sanitize(title)) ×N
→ lipgloss.Width(Sanitize(cwd)) ×N
→ ansi.stringWidth → FirstGraphemeCluster
→ uax29/graphemes Unicode grapheme split ← CPU hot spot
listTitleWidth accounted for the majority of user-space samples in the profile. By contrast, idColW and msgColW are already computed once in newModel and never recalculated.
Fix
Add two cached fields to Model:
naturalTitleW int // max lipgloss.Width(Sanitize(s.Title)) across m.sessions
naturalDirW int // max lipgloss.Width(Sanitize(s.CWDDisplay)) across m.sessions
Compute them in a helper recomputeNaturalWidths() and call it from:
newModel (initial)
applyRefresh (sessions added or updated)
listTitleWidth() reads the cached values directly — no per-call iteration.
Non-goals
- Does not change visible layout or column widths
- Does not affect
stateListPreview path (already skips the natural-width bonus)
Plan
docs/agent/plan-issue-49-cache-list-title-width.md
Problem
listTitleWidth()is called once per visible row insiderenderRowFull(), which is called for every row in everyView()invocation. InstateListmode the method iterates over all sessions twice — once for titles, once for CWD strings — callinglipgloss.Width(display.Sanitize(...))on each to find the natural maximum widths:lipgloss.Widthcallsansi.stringWidth → FirstGraphemeCluster → uax29/graphemesfor Unicode grapheme segmentation — not a trivial operation.With V visible rows and N total sessions, each frame triggers V × 2N width computations. For V=30, N=200 that is 12,000 calls per
View(), repeated every 120 ms tick.Evidence
CPU profiling via
sample(1)on a live aps process shows the hot path:listTitleWidthaccounted for the majority of user-space samples in the profile. By contrast,idColWandmsgColWare already computed once innewModeland never recalculated.Fix
Add two cached fields to
Model:Compute them in a helper
recomputeNaturalWidths()and call it from:newModel(initial)applyRefresh(sessions added or updated)listTitleWidth()reads the cached values directly — no per-call iteration.Non-goals
stateListPreviewpath (already skips the natural-width bonus)Plan
docs/agent/plan-issue-49-cache-list-title-width.md