feat(cli,tui): show time since last final agent response on the status bar (#44265)
Adds an idle clock to the context/status bar in both the prompt_toolkit CLI and the Ink TUI: once a turn completes, a dim '✓ <elapsed>' segment shows how long the session has been idle since the last final agent response. Hidden while a turn is live (the per-prompt elapsed timer covers that) and before the first turn completes. - cli.py: track _last_turn_finished_at when the agent thread exits, surface it via _format_idle_since() in the snapshot, render in both the wide fragments path and the plain-text fallback. - ui-tui: stamp lastTurnEndedAt when busy flips false after a live turn, thread it through appStatus -> StatusRule, render via a ticking IdleSince segment sharing the duration breakpoint/width budget.
This commit is contained in:
@@ -341,6 +341,21 @@ function SessionDuration({ startedAt }: { startedAt: number }) {
|
||||
return fmtDuration(now - startedAt)
|
||||
}
|
||||
|
||||
function IdleSince({ endedAt }: { endedAt: number }) {
|
||||
// Time since the last final agent response. Re-ticks every second like
|
||||
// SessionDuration so the read-out stays live while the session idles.
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
setNow(Date.now())
|
||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [endedAt])
|
||||
|
||||
return `✓ ${fmtDuration(now - endedAt)}`
|
||||
}
|
||||
|
||||
const effortLabel = (effort?: string) => {
|
||||
const value = String(effort ?? '')
|
||||
.trim()
|
||||
@@ -400,6 +415,7 @@ export function StatusRule({
|
||||
notice,
|
||||
usage,
|
||||
bgCount,
|
||||
lastTurnEndedAt,
|
||||
liveSessionCount,
|
||||
sessionStartedAt,
|
||||
showCost,
|
||||
@@ -488,6 +504,10 @@ export function StatusRule({
|
||||
|
||||
const showBar = !!bar && fits(SEP + stringWidth(`[${bar}] ${pct != null ? `${pct}%` : ''}`))
|
||||
const showDuration = segs.duration && !!sessionStartedAt && fits(SEP + MAX_DURATION_WIDTH)
|
||||
// Idle clock — time since the last final agent response. Hidden while busy
|
||||
// (the FaceTicker's elapsed tail covers the live turn) and before the first
|
||||
// turn completes. Shares the duration breakpoint and width reservation.
|
||||
const showIdle = segs.duration && !busy && lastTurnEndedAt != null && fits(SEP + stringWidth('✓ ') + 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))
|
||||
@@ -567,6 +587,12 @@ export function StatusRule({
|
||||
<SessionDuration startedAt={sessionStartedAt!} />
|
||||
</Text>
|
||||
) : null}
|
||||
{showIdle ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
<IdleSince endedAt={lastTurnEndedAt!} />
|
||||
</Text>
|
||||
) : null}
|
||||
{showCompressions ? (
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
{' │ '}
|
||||
@@ -725,6 +751,7 @@ export function TranscriptScrollbar({ scrollRef, t }: TranscriptScrollbarProps)
|
||||
|
||||
interface StatusRuleProps {
|
||||
bgCount: number
|
||||
lastTurnEndedAt?: null | number
|
||||
liveSessionCount: number
|
||||
busy: boolean
|
||||
cols: number
|
||||
|
||||
@@ -366,6 +366,7 @@ const StatusRulePane = memo(function StatusRulePane({
|
||||
cols={composer.cols}
|
||||
cwdLabel={status.cwdLabel}
|
||||
indicatorStyle={ui.indicatorStyle}
|
||||
lastTurnEndedAt={status.lastTurnEndedAt}
|
||||
liveSessionCount={ui.liveSessionCount}
|
||||
model={ui.info?.model ?? ''}
|
||||
modelFast={ui.info?.fast || ui.info?.service_tier === 'priority'}
|
||||
|
||||
Reference in New Issue
Block a user