diff --git a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx
index 4fb96385f..4e468c200 100644
--- a/ui-tui/src/__tests__/appChromeStatusRule.test.tsx
+++ b/ui-tui/src/__tests__/appChromeStatusRule.test.tsx
@@ -81,4 +81,33 @@ describe('StatusRule session count click target', () => {
clickableSessionCount!.props.onClick({ stopImmediatePropagation: vi.fn() })
expect(openSwitcher).toHaveBeenCalledOnce()
})
+
+ it('keeps status + model and drops the low-value tail on a narrow terminal', () => {
+ const element = StatusRule({
+ bgCount: 0,
+ busy: false,
+ cols: 44,
+ cwdLabel: '~/src/hermes-agent/apps/desktop (bb/tui-statusbar-responsive)',
+ liveSessionCount: 3,
+ model: 'opus-4.8',
+ onSessionCountClick: vi.fn(),
+ sessionStartedAt: Date.now() - 60_000,
+ showCost: true,
+ status: 'ready',
+ statusColor: DEFAULT_THEME.color.ok,
+ t: DEFAULT_THEME,
+ turnStartedAt: null,
+ usage: { context_max: 200_000, context_percent: 25, context_used: 50_000, cost_usd: 0.5, total: 50_000 },
+ voiceLabel: 'voice off'
+ })
+
+ const rendered = textContent(element)
+
+ // Must-keep essentials survive intact …
+ expect(rendered).toContain('ready')
+ expect(rendered).toContain('opus 4.8')
+ // … while the low-value tail (session count, cost) is dropped, not truncated.
+ expect(rendered).not.toContain('3 sessions')
+ expect(rendered).not.toContain('$0.5000')
+ })
})
diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx
index dde6689f6..91278cd4c 100644
--- a/ui-tui/src/components/appChrome.tsx
+++ b/ui-tui/src/components/appChrome.tsx
@@ -390,89 +390,112 @@ export function StatusRule({
? `${fmtK(usage.total)} tok`
: ''
- const bar = segs.bar && usage.context_max ? ctxBar(pct) : ''
+ const bar = !segs.compactCtx && usage.context_max ? ctxBar(pct) : ''
const modelText = modelLabel(model, modelReasoningEffort, modelFast)
- // Reserve room for the must-keep left segments (indicator + model + context)
- // so the cwd/branch on the right truncates before they do. The busy face can
- // grow with its verb/duration tail, but only the glyph itself is essential.
- const minLeftContent =
+ // Width of the must-keep left segments (indicator + model + context). They
+ // are pinned (never shrink) and reserved so the cwd/branch on the right
+ // yields first. The busy face width depends on the active /indicator style
+ // (kaomoji is wide + verb; unicode is a bare 1-col spinner).
+ const essentialWidth =
stringWidth('─ ') +
- // The busy face width depends on the active /indicator style (kaomoji is
- // wide with a verb; unicode is a bare 1-col spinner) — reserve accordingly
- // so the model survives, without reserving the unbounded duration tail.
(busy ? busyIndicatorWidth(indicatorStyle, turnStartedAt != null) : stringWidth(status)) +
stringWidth(' │ ') +
stringWidth(modelText) +
(ctxLabel ? stringWidth(' │ ') + stringWidth(ctxLabel) : 0)
- const { leftWidth, rightWidth, separatorWidth } = statusRuleWidths(cols, cwdLabel, minLeftContent)
+ const { leftWidth, rightWidth, separatorWidth } = statusRuleWidths(cols, cwdLabel, essentialWidth)
+
+ // Whole-segment progressive disclosure for the tail: a segment renders only
+ // if it fits in the space left after the pinned essentials, evaluated in
+ // priority order. No mid-segment truncation, and the low-value tail (incl.
+ // the session count) drops first instead of crushing status/model/context.
+ const SEP = stringWidth(' │ ')
+ let tailBudget = Math.max(0, leftWidth - essentialWidth)
+ const fits = (w: number) => {
+ if (tailBudget >= w) {
+ tailBudget -= w
+
+ return true
+ }
+
+ return false
+ }
+
const sessionCountText = liveSessionCount > 0 ? statusSessionCountLabel(liveSessionCount) : ''
+ const compressions = typeof usage.compressions === 'number' ? usage.compressions : 0
+ const costText = typeof usage.cost_usd === 'number' ? `$${usage.cost_usd.toFixed(4)}` : ''
+
+ const showBar = !!bar && fits(SEP + stringWidth(`[${bar}] ${pct != null ? `${pct}%` : ''}`))
+ const showDuration = segs.duration && !!sessionStartedAt && fits(SEP + 6)
+ const showCompressions = segs.compressions && compressions > 0 && fits(SEP + stringWidth(`cmp ${compressions}`))
+ const showVoice = segs.voice && !!voiceLabel && fits(SEP + stringWidth(voiceLabel))
+ const showSessionCount = !!sessionCountText && fits(SEP + stringWidth(sessionCountText))
+ const showBg = segs.bg && bgCount > 0 && fits(SEP + stringWidth(`${bgCount} bg`))
+ const showCostSeg = segs.cost && showCost && !!costText && fits(SEP + stringWidth(costText))
+
const handleSessionCountClick = (event: { stopImmediatePropagation?: () => void }) => {
event.stopImmediatePropagation?.()
onSessionCountClick?.()
}
- const sessionCountNode = sessionCountText ? (
- onSessionCountClick ? (
-
- │ {sessionCountText}
-
- ) : (
- │ {sessionCountText}
- )
- ) : null
+ const sessionCountNode = onSessionCountClick ? (
+
+ │ {sessionCountText}
+
+ ) : (
+ │ {sessionCountText}
+ )
return (
-
- {'─ '}
-
- {busy ? (
-
- ) : (
-
- {status}
-
- )}
-
- {' │ '}
- {modelText}
-
- {ctxLabel ? (
+ {/* Pinned essentials — never shrink, always visible. */}
+
+ {'─ '}
+ {busy ? (
+
+ ) : (
+
+ {status}
+
+ )}
{' │ '}
- {ctxLabel}
+ {modelText}
- ) : null}
- {bar ? (
+ {ctxLabel ? (
+
+ {' │ '}
+ {ctxLabel}
+
+ ) : null}
+
+ {showBar ? (
{' │ '}
[{bar}] {pct != null ? `${pct}%` : ''}
) : null}
- {segs.duration && sessionStartedAt ? (
+ {showDuration ? (
{' │ '}
-
+
) : null}
- {segs.compressions && typeof usage.compressions === 'number' && usage.compressions > 0 ? (
+ {showCompressions ? (
{' │ '}
- = 10 ? t.color.error : usage.compressions >= 5 ? t.color.warn : t.color.muted}
- >
- cmp {usage.compressions}
+ = 10 ? t.color.error : compressions >= 5 ? t.color.warn : t.color.muted}>
+ cmp {compressions}
) : null}
- {segs.voice && voiceLabel ? (
+ {showVoice ? (
@@ -480,17 +503,17 @@ export function StatusRule({
{voiceLabel}
) : null}
- {sessionCountNode}
- {segs.bg && bgCount > 0 ? (
+ {showSessionCount ? sessionCountNode : null}
+ {showBg ? (
{' │ '}
{bgCount} bg
) : null}
- {segs.cost && showCost && typeof usage.cost_usd === 'number' ? (
+ {showCostSeg ? (
- {' │ $'}
- {usage.cost_usd.toFixed(4)}
+ {' │ '}
+ {costText}
) : null}
diff --git a/ui-tui/src/domain/paths.ts b/ui-tui/src/domain/paths.ts
index 43c023b6b..90483451c 100644
--- a/ui-tui/src/domain/paths.ts
+++ b/ui-tui/src/domain/paths.ts
@@ -5,7 +5,7 @@ export const shortCwd = (cwd: string, max = 28) => {
return p.length <= max ? p : `…${p.slice(-(max - 1))}`
}
-export const fmtCwdBranch = (cwd: string, branch: null | string, max = 40) => {
+export const fmtCwdBranch = (cwd: string, branch: null | string, max = 28) => {
if (!branch) {
return shortCwd(cwd, max)
}