refactor(tui): turn elapsed lives in FaceTicker; emit done-in sys line
Drops `lastUserAt` plumbing and the right-edge idle ticker. Matches the
claude-code / opencode convention: elapsed rides with the busy indicator
(spinner verb), nothing at idle.
- `turnStartedAt` driven by a useEffect on `ui.busy` — stamps on rising
edge, clears on falling edge. Covers agent turns and !shell alike.
- FaceTicker renders ` · {fmtDuration}` while busy; 1 s clock for the
counter, existing 2500 ms cycle for face/verb rotation.
- On busy → idle, if the block ran ≥ 1 s, emit a one-shot
`done in {fmtDuration}` sys line (≡ claude-code's `thought for Ns`).
This commit is contained in:
@@ -12,18 +12,24 @@ import type { Msg, Usage } from '../types.js'
|
||||
const FACE_TICK_MS = 2500
|
||||
const HEART_COLORS = ['#ff5fa2', '#ff4d6d']
|
||||
|
||||
function FaceTicker({ color }: { color: string }) {
|
||||
function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | number }) {
|
||||
const [tick, setTick] = useState(() => Math.floor(Math.random() * 1000))
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
const id = setInterval(() => setTick(n => n + 1), FACE_TICK_MS)
|
||||
const face = setInterval(() => setTick(n => n + 1), FACE_TICK_MS)
|
||||
const clock = setInterval(() => setNow(Date.now()), 1000)
|
||||
|
||||
return () => clearInterval(id)
|
||||
return () => {
|
||||
clearInterval(face)
|
||||
clearInterval(clock)
|
||||
}
|
||||
}, [])
|
||||
|
||||
return (
|
||||
<Text color={color}>
|
||||
{FACES[tick % FACES.length]} {VERBS[tick % VERBS.length]}…
|
||||
{startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
@@ -68,19 +74,6 @@ function SessionDuration({ startedAt }: { startedAt: number }) {
|
||||
return fmtDuration(now - startedAt)
|
||||
}
|
||||
|
||||
export function IdleSinceLastMsg({ lastUserAt, t }: { lastUserAt: number; t: Theme }) {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
setNow(Date.now())
|
||||
const id = setInterval(() => setNow(Date.now()), 1000)
|
||||
|
||||
return () => clearInterval(id)
|
||||
}, [lastUserAt])
|
||||
|
||||
return <Text color={t.color.dim}>{fmtDuration(now - lastUserAt)} </Text>
|
||||
}
|
||||
|
||||
export function GoodVibesHeart({ tick, t }: { tick: number; t: Theme }) {
|
||||
const [active, setActive] = useState(false)
|
||||
const [color, setColor] = useState(t.color.amber)
|
||||
@@ -113,6 +106,7 @@ export function StatusRule({
|
||||
bgCount,
|
||||
sessionStartedAt,
|
||||
showCost,
|
||||
turnStartedAt,
|
||||
voiceLabel,
|
||||
t
|
||||
}: StatusRuleProps) {
|
||||
@@ -133,7 +127,7 @@ export function StatusRule({
|
||||
<Box flexShrink={1} width={leftWidth}>
|
||||
<Text color={t.color.bronze} wrap="truncate-end">
|
||||
{'─ '}
|
||||
{busy ? <FaceTicker color={statusColor} /> : <Text color={statusColor}>{status}</Text>}
|
||||
{busy ? <FaceTicker color={statusColor} startedAt={turnStartedAt} /> : <Text color={statusColor}>{status}</Text>}
|
||||
<Text color={t.color.dim}> │ {model}</Text>
|
||||
{ctxLabel ? <Text color={t.color.dim}> │ {ctxLabel}</Text> : null}
|
||||
{bar ? (
|
||||
@@ -306,6 +300,7 @@ interface StatusRuleProps {
|
||||
status: string
|
||||
statusColor: string
|
||||
t: Theme
|
||||
turnStartedAt?: null | number
|
||||
usage: Usage
|
||||
voiceLabel?: string
|
||||
}
|
||||
|
||||
@@ -9,7 +9,7 @@ import { PLACEHOLDER } from '../content/placeholders.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
import type { DetailsMode } from '../types.js'
|
||||
|
||||
import { GoodVibesHeart, IdleSinceLastMsg, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
|
||||
import { GoodVibesHeart, StatusRule, StickyPromptTracker, TranscriptScrollbar } from './appChrome.js'
|
||||
import { FloatingOverlays, PromptZone } from './appOverlays.js'
|
||||
import { Banner, Panel, SessionPanel } from './branding.js'
|
||||
import { MessageLine } from './messageLine.js'
|
||||
@@ -194,6 +194,7 @@ const ComposerPane = memo(function ComposerPane({
|
||||
status={ui.status}
|
||||
statusColor={status.statusColor}
|
||||
t={ui.theme}
|
||||
turnStartedAt={status.turnStartedAt}
|
||||
usage={ui.usage}
|
||||
voiceLabel={status.voiceLabel}
|
||||
/>
|
||||
@@ -242,9 +243,7 @@ const ComposerPane = memo(function ComposerPane({
|
||||
value={composer.input}
|
||||
/>
|
||||
|
||||
<Box flexDirection="row" position="absolute" right={0}>
|
||||
{!ui.busy && status.lastUserAt ? <IdleSinceLastMsg lastUserAt={status.lastUserAt} t={ui.theme} /> : null}
|
||||
|
||||
<Box position="absolute" right={0}>
|
||||
<GoodVibesHeart t={ui.theme} tick={status.goodVibesTick} />
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
Reference in New Issue
Block a user