mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-06-09 08:21:50 +00:00
Merge pull request #37115 from NousResearch/bb/tui-statusbar-responsive
fix(tui): prioritize status/model over cwd in the status bar on narrow terminals
This commit is contained in:
commit
7d51cd7516
5 changed files with 298 additions and 53 deletions
|
|
@ -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')
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { statusRuleWidths } from '../components/appChrome.js'
|
||||
import { busyIndicatorWidth, statusBarSegments, statusRuleWidths } from '../components/appChrome.js'
|
||||
|
||||
describe('statusRuleWidths', () => {
|
||||
it('keeps the status rule within the terminal width', () => {
|
||||
|
|
@ -29,4 +29,92 @@ describe('statusRuleWidths', () => {
|
|||
expect(widths.leftWidth + widths.separatorWidth + widths.rightWidth).toBeLessThanOrEqual(30)
|
||||
expect(widths.rightWidth).toBeGreaterThan('目录/分支'.length)
|
||||
})
|
||||
|
||||
it('reserves the high-priority left content so the cwd/branch yields first', () => {
|
||||
const cwd = '~/src/hermes-agent/apps/desktop (bb/tui-statusbar-responsive)'
|
||||
|
||||
const greedy = statusRuleWidths(70, cwd) // legacy behaviour: cwd hogs the row
|
||||
const reserved = statusRuleWidths(70, cwd, 40) // reserve indicator+model+ctx
|
||||
|
||||
expect(reserved.leftWidth).toBeGreaterThanOrEqual(40)
|
||||
expect(reserved.leftWidth).toBeGreaterThan(greedy.leftWidth)
|
||||
expect(reserved.rightWidth).toBeLessThan(greedy.rightWidth)
|
||||
expect(reserved.leftWidth + reserved.separatorWidth + reserved.rightWidth).toBeLessThanOrEqual(70)
|
||||
})
|
||||
|
||||
it('drops the cwd entirely when the essential left content needs the whole row', () => {
|
||||
expect(statusRuleWidths(40, '~/some/cwd (branch)', 60)).toEqual({
|
||||
leftWidth: 40,
|
||||
rightWidth: 0,
|
||||
separatorWidth: 0
|
||||
})
|
||||
})
|
||||
|
||||
it('keeps the default (no reservation) behaviour identical for legacy callers', () => {
|
||||
const cwd = '~/src/hermes-agent/main (some-long-branch-name)'
|
||||
|
||||
expect(statusRuleWidths(80, cwd, 0)).toEqual(statusRuleWidths(80, cwd))
|
||||
})
|
||||
})
|
||||
|
||||
describe('statusBarSegments', () => {
|
||||
it('shows every segment on a wide terminal', () => {
|
||||
const s = statusBarSegments(120)
|
||||
|
||||
expect(s).toEqual({
|
||||
compactCtx: false,
|
||||
bar: true,
|
||||
duration: true,
|
||||
compressions: true,
|
||||
voice: true,
|
||||
bg: true,
|
||||
cost: true
|
||||
})
|
||||
})
|
||||
|
||||
it('collapses the context bar to a token count on narrow terminals', () => {
|
||||
const s = statusBarSegments(60)
|
||||
|
||||
expect(s.compactCtx).toBe(true)
|
||||
expect(s.bar).toBe(false)
|
||||
expect(s.duration).toBe(false)
|
||||
expect(s.cost).toBe(false)
|
||||
})
|
||||
|
||||
it('sheds tail segments in priority order as the terminal narrows', () => {
|
||||
// cost is the first to go, the context bar the last of the tail.
|
||||
const order: (keyof ReturnType<typeof statusBarSegments>)[] = [
|
||||
'bar',
|
||||
'duration',
|
||||
'compressions',
|
||||
'voice',
|
||||
'bg',
|
||||
'cost'
|
||||
]
|
||||
|
||||
let prevCount = Infinity
|
||||
|
||||
for (const cols of [120, 95, 87, 83, 79, 75, 71]) {
|
||||
const s = statusBarSegments(cols)
|
||||
const visible = order.filter(k => s[k]).length
|
||||
|
||||
expect(visible).toBeLessThanOrEqual(prevCount)
|
||||
prevCount = visible
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
describe('busyIndicatorWidth', () => {
|
||||
it('reserves a bare spinner for the verb-less unicode style', () => {
|
||||
// unicode is a 1-col braille spinner with no verb; far slimmer than the
|
||||
// kaomoji face which carries a wide glyph + rotating verb.
|
||||
expect(busyIndicatorWidth('unicode', false)).toBeLessThan(busyIndicatorWidth('kaomoji', false))
|
||||
expect(busyIndicatorWidth('unicode', false)).toBe(1)
|
||||
})
|
||||
|
||||
it('reserves room for the elapsed-time tail only when a turn is timed', () => {
|
||||
for (const style of ['kaomoji', 'emoji', 'ascii', 'unicode'] as const) {
|
||||
expect(busyIndicatorWidth(style, true)).toBeGreaterThan(busyIndicatorWidth(style, false))
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1035,7 +1035,10 @@ export function useMainApp(gw: GatewayClient) {
|
|||
|
||||
const appStatus = useMemo(
|
||||
() => ({
|
||||
cwdLabel: fmtCwdBranch(cwd, gitBranch),
|
||||
// Cap the status-bar cwd/branch label tighter than the shared default so
|
||||
// it doesn't dominate the bar; the status rule reserves the left-side
|
||||
// essentials and truncates this further on narrow terminals.
|
||||
cwdLabel: fmtCwdBranch(cwd, gitBranch, 28),
|
||||
goodVibesTick,
|
||||
sessionStartedAt: ui.sid ? sessionStartedAt : null,
|
||||
showStickyPrompt: !!stickyPrompt,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,6 @@ import unicodeSpinners from 'unicode-animations'
|
|||
import { $delegationState } from '../app/delegationStore.js'
|
||||
import type { IndicatorStyle } from '../app/interfaces.js'
|
||||
import { useTurnSelector } from '../app/turnStore.js'
|
||||
import { $uiState } from '../app/uiStore.js'
|
||||
import { FACES } from '../content/faces.js'
|
||||
import { VERBS } from '../content/verbs.js'
|
||||
import { fmtDuration } from '../domain/messages.js'
|
||||
|
|
@ -75,9 +74,48 @@ const renderIndicator = (style: IndicatorStyle, tick: number): IndicatorRender =
|
|||
return { frame, intervalMs: Math.max(SPINNER_TICK_MS, spinner.interval), showVerb: false }
|
||||
}
|
||||
|
||||
function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | number }) {
|
||||
const ui = useStore($uiState)
|
||||
const style = ui.indicatorStyle
|
||||
// `FACES` / `EMOJI_FRAMES` are static, so measure their widest glyph once at
|
||||
// module load instead of rescanning on every status render.
|
||||
const KAOMOJI_FRAME_WIDTH = FACES.reduce((max, f) => Math.max(max, stringWidth(f)), 1)
|
||||
const EMOJI_FRAME_WIDTH = EMOJI_FRAMES.reduce((max, f) => Math.max(max, stringWidth(f)), 1)
|
||||
|
||||
const indicatorFrameWidth = (style: IndicatorStyle): number => {
|
||||
if (style === 'kaomoji') {
|
||||
return KAOMOJI_FRAME_WIDTH
|
||||
}
|
||||
|
||||
if (style === 'emoji') {
|
||||
return EMOJI_FRAME_WIDTH
|
||||
}
|
||||
|
||||
// 'ascii' and 'unicode' are single-column glyphs.
|
||||
return 1
|
||||
}
|
||||
|
||||
// Bounded width of the elapsed-time clock, derived from `fmtDuration` itself so
|
||||
// the reservation/budget stays consistent with what actually renders (it emits
|
||||
// a space between units, e.g. `59m 59s` / `99h 59m`). Durations beyond this
|
||||
// (100h+) are left to clip rather than reserving unbounded width.
|
||||
export const MAX_DURATION_WIDTH = Math.max(
|
||||
stringWidth(fmtDuration(59 * 60_000 + 59_000)), // "59m 59s"
|
||||
stringWidth(fmtDuration(99 * 3_600_000 + 59 * 60_000)) // "99h 59m"
|
||||
)
|
||||
|
||||
// Display width to reserve for the busy indicator so its verb + elapsed-time
|
||||
// tail can't shove the model off-screen on narrow terminals. Style-aware:
|
||||
// `unicode` is a bare 1-col braille spinner with no verb, while kaomoji/emoji/
|
||||
// ascii add a fixed-width verb; any style adds a bounded elapsed-time tail.
|
||||
// Mirrors FaceTicker's `frame + verbSegment + durationSegment` layout.
|
||||
export const busyIndicatorWidth = (style: IndicatorStyle, hasDuration: boolean): number => {
|
||||
const { showVerb } = renderIndicator(style, 0)
|
||||
const verb = showVerb ? 1 + VERB_PAD_LEN : 0
|
||||
// ` · ` plus the bounded clock (e.g. `59m 59s`).
|
||||
const duration = hasDuration ? stringWidth(' · ') + MAX_DURATION_WIDTH : 0
|
||||
|
||||
return indicatorFrameWidth(style) + verb + duration
|
||||
}
|
||||
|
||||
function FaceTicker({ color, startedAt, style }: { color: string; startedAt?: null | number; style: IndicatorStyle }) {
|
||||
const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000))
|
||||
const [verbTick, setVerbTick] = useState(() => Math.floor(Math.random() * VERBS.length))
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
|
@ -154,10 +192,18 @@ function ctxBar(pct: number | undefined, w = 10) {
|
|||
return '█'.repeat(filled) + '░'.repeat(w - filled)
|
||||
}
|
||||
|
||||
export function statusRuleWidths(cols: number, cwdLabel: string) {
|
||||
// `minLeftContent` is the display width of the high-priority left segments
|
||||
// (status indicator + model + context). Reserving it makes the cwd/branch
|
||||
// segment on the right yield FIRST on narrow terminals, instead of squeezing
|
||||
// the loading indicator and model down to nothing.
|
||||
export function statusRuleWidths(cols: number, cwdLabel: string, minLeftContent = 0) {
|
||||
const width = Math.max(1, Math.floor(cols || 1))
|
||||
const desiredSeparatorWidth = width >= 24 ? 3 : 1
|
||||
const minLeftWidth = width >= 24 ? 8 : 1
|
||||
const baseMinLeft = width >= 24 ? 8 : 1
|
||||
// Never reserve more than the terminal width; never less than the historical
|
||||
// floor. With the default `minLeftContent = 0` this is identical to the old
|
||||
// behaviour, so callers that don't pass content are unaffected.
|
||||
const minLeftWidth = Math.min(width, Math.max(baseMinLeft, Math.floor(minLeftContent)))
|
||||
const maxRightWidth = Math.max(0, width - desiredSeparatorWidth - minLeftWidth)
|
||||
|
||||
if (!cwdLabel || maxRightWidth <= 0) {
|
||||
|
|
@ -171,6 +217,35 @@ export function statusRuleWidths(cols: number, cwdLabel: string) {
|
|||
return { leftWidth, rightWidth, separatorWidth }
|
||||
}
|
||||
|
||||
// Progressive disclosure for the status rule's lower-priority tail segments.
|
||||
// As the terminal narrows we shed the least important pieces first (cost →
|
||||
// bg → voice → compressions → duration → context bar), and below the bar
|
||||
// breakpoint the context read-out collapses to a bare token count. Status and
|
||||
// model are never gated here — they're guaranteed room by `statusRuleWidths`.
|
||||
export interface StatusBarSegments {
|
||||
bar: boolean
|
||||
bg: boolean
|
||||
compactCtx: boolean
|
||||
compressions: boolean
|
||||
cost: boolean
|
||||
duration: boolean
|
||||
voice: boolean
|
||||
}
|
||||
|
||||
export function statusBarSegments(cols: number): StatusBarSegments {
|
||||
const w = Math.max(1, Math.floor(cols || 1))
|
||||
|
||||
return {
|
||||
compactCtx: w < 72,
|
||||
bar: w >= 72,
|
||||
duration: w >= 76,
|
||||
compressions: w >= 80,
|
||||
voice: w >= 84,
|
||||
bg: w >= 88,
|
||||
cost: w >= 96
|
||||
}
|
||||
}
|
||||
|
||||
function SpawnHud({ t }: { t: Theme }) {
|
||||
// Tight HUD that only appears when the session is actually fanning out.
|
||||
// Colour escalates to warn/error as depth or concurrency approaches the cap.
|
||||
|
|
@ -300,6 +375,7 @@ export function StatusRule({
|
|||
model,
|
||||
modelFast,
|
||||
modelReasoningEffort,
|
||||
indicatorStyle = 'kaomoji',
|
||||
usage,
|
||||
bgCount,
|
||||
liveSessionCount,
|
||||
|
|
@ -312,81 +388,124 @@ export function StatusRule({
|
|||
}: StatusRuleProps) {
|
||||
const pct = usage.context_percent
|
||||
const barColor = ctxBarColor(pct, t)
|
||||
const segs = statusBarSegments(cols)
|
||||
|
||||
// On narrow terminals the context read-out collapses to a bare token count
|
||||
// (`12k tok`) and the visual fill bar is dropped entirely.
|
||||
const ctxLabel = usage.context_max
|
||||
? `${fmtK(usage.context_used ?? 0)}/${fmtK(usage.context_max)}`
|
||||
? segs.compactCtx
|
||||
? `${fmtK(usage.context_used ?? 0)} tok`
|
||||
: `${fmtK(usage.context_used ?? 0)}/${fmtK(usage.context_max)}`
|
||||
: usage.total > 0
|
||||
? `${fmtK(usage.total)} tok`
|
||||
: ''
|
||||
|
||||
const bar = usage.context_max ? ctxBar(pct) : ''
|
||||
const { leftWidth, rightWidth, separatorWidth } = statusRuleWidths(cols, cwdLabel)
|
||||
const bar = !segs.compactCtx && usage.context_max ? ctxBar(pct) : ''
|
||||
const modelText = modelLabel(model, modelReasoningEffort, modelFast)
|
||||
|
||||
// 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('─ ') +
|
||||
(busy ? busyIndicatorWidth(indicatorStyle, turnStartedAt != null) : stringWidth(status)) +
|
||||
stringWidth(' │ ') +
|
||||
stringWidth(modelText) +
|
||||
(ctxLabel ? stringWidth(' │ ') + stringWidth(ctxLabel) : 0)
|
||||
|
||||
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
|
||||
// descending priority order — bar, duration, compressions, voice, session
|
||||
// count, bg, cost. Lower-priority segments drop first and nothing truncates
|
||||
// mid-segment, so status/model/context are never crushed.
|
||||
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 + MAX_DURATION_WIDTH)
|
||||
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 ? (
|
||||
<Box flexShrink={0} onClick={handleSessionCountClick}>
|
||||
<Text color={t.color.accent}> │ {sessionCountText}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={t.color.muted}> │ {sessionCountText}</Text>
|
||||
)
|
||||
) : null
|
||||
const sessionCountNode = onSessionCountClick ? (
|
||||
<Box flexShrink={0} onClick={handleSessionCountClick}>
|
||||
<Text color={t.color.accent}> │ {sessionCountText}</Text>
|
||||
</Box>
|
||||
) : (
|
||||
<Text color={t.color.muted}> │ {sessionCountText}</Text>
|
||||
)
|
||||
|
||||
return (
|
||||
<Box height={1}>
|
||||
<Box flexDirection="row" flexShrink={1} overflow="hidden" width={leftWidth}>
|
||||
<Text color={t.color.border} wrap="truncate-end">
|
||||
{'─ '}
|
||||
</Text>
|
||||
{busy ? (
|
||||
<FaceTicker color={statusColor} startedAt={turnStartedAt} />
|
||||
) : (
|
||||
<Text color={statusColor} wrap="truncate-end">
|
||||
{status}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
{modelLabel(model, modelReasoningEffort, modelFast)}
|
||||
</Text>
|
||||
{ctxLabel ? (
|
||||
{/* Pinned essentials — never shrink, always visible. */}
|
||||
<Box flexDirection="row" flexShrink={0}>
|
||||
<Text color={t.color.border}>{'─ '}</Text>
|
||||
{busy ? (
|
||||
<FaceTicker color={statusColor} startedAt={turnStartedAt} style={indicatorStyle} />
|
||||
) : (
|
||||
<Text color={statusColor} wrap="truncate-end">
|
||||
{status}
|
||||
</Text>
|
||||
)}
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
{ctxLabel}
|
||||
{modelText}
|
||||
</Text>
|
||||
) : null}
|
||||
{bar ? (
|
||||
{ctxLabel ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
{ctxLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
</Box>
|
||||
{showBar ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
<Text color={barColor}>[{bar}]</Text> <Text color={barColor}>{pct != null ? `${pct}%` : ''}</Text>
|
||||
</Text>
|
||||
) : null}
|
||||
{sessionStartedAt ? (
|
||||
{showDuration ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
<SessionDuration startedAt={sessionStartedAt} />
|
||||
<SessionDuration startedAt={sessionStartedAt!} />
|
||||
</Text>
|
||||
) : null}
|
||||
{typeof usage.compressions === 'number' && usage.compressions > 0 ? (
|
||||
{showCompressions ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
<Text
|
||||
color={usage.compressions >= 10 ? t.color.error : usage.compressions >= 5 ? t.color.warn : t.color.muted}
|
||||
>
|
||||
cmp {usage.compressions}
|
||||
<Text color={compressions >= 10 ? t.color.error : compressions >= 5 ? t.color.warn : t.color.muted}>
|
||||
cmp {compressions}
|
||||
</Text>
|
||||
</Text>
|
||||
) : null}
|
||||
<SpawnHud t={t} />
|
||||
{voiceLabel ? (
|
||||
{showVoice ? (
|
||||
<Text
|
||||
color={
|
||||
voiceLabel.startsWith('●') ? t.color.error : voiceLabel.startsWith('◉') ? t.color.warn : t.color.muted
|
||||
voiceLabel!.startsWith('●') ? t.color.error : voiceLabel!.startsWith('◉') ? t.color.warn : t.color.muted
|
||||
}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
|
|
@ -394,19 +513,23 @@ export function StatusRule({
|
|||
{voiceLabel}
|
||||
</Text>
|
||||
) : null}
|
||||
{sessionCountNode}
|
||||
{bgCount > 0 ? (
|
||||
{showSessionCount ? sessionCountNode : null}
|
||||
{showBg ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
{bgCount} bg
|
||||
</Text>
|
||||
) : null}
|
||||
{showCost && typeof usage.cost_usd === 'number' ? (
|
||||
{showCostSeg ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ $'}
|
||||
{usage.cost_usd.toFixed(4)}
|
||||
{' │ '}
|
||||
{costText}
|
||||
</Text>
|
||||
) : null}
|
||||
{/* SpawnHud isn't part of the tail budget (its width is dynamic), so it
|
||||
renders last — any overflow truncates the HUD itself rather than the
|
||||
budgeted segments before it. It self-hides when no delegation runs. */}
|
||||
<SpawnHud t={t} />
|
||||
</Box>
|
||||
|
||||
{rightWidth > 0 ? (
|
||||
|
|
@ -530,6 +653,7 @@ interface StatusRuleProps {
|
|||
model: string
|
||||
modelFast?: boolean
|
||||
modelReasoningEffort?: string
|
||||
indicatorStyle?: IndicatorStyle
|
||||
sessionStartedAt?: null | number
|
||||
showCost: boolean
|
||||
status: string
|
||||
|
|
|
|||
|
|
@ -358,6 +358,7 @@ const StatusRulePane = memo(function StatusRulePane({
|
|||
busy={ui.busy}
|
||||
cols={composer.cols}
|
||||
cwdLabel={status.cwdLabel}
|
||||
indicatorStyle={ui.indicatorStyle}
|
||||
liveSessionCount={ui.liveSessionCount}
|
||||
model={ui.info?.model ?? ''}
|
||||
modelFast={ui.info?.fast || ui.info?.service_tier === 'priority'}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue