Compare commits

...

5 Commits

Author SHA1 Message Date
Brooklyn Nicholson
f7587dd445 chore: uptick 2026-04-20 13:02:32 -05:00
Brooklyn Nicholson
f1f438e7f9 refactor(tui): drop done-in sys line; FaceTicker counter only
The transcript line was noisy. Keep the one thing the issue really needs:
live elapsed next to the busy verb.
2026-04-20 11:40:12 -05:00
Brooklyn Nicholson
2de1aad028 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`).
2026-04-20 11:38:11 -05:00
Brooklyn Nicholson
9910681b85 refactor(tui): move last-msg elapsed from status bar to prompt right-edge
Status bar ticker was too hot in peripheral vision. The moment the elapsed
value matters is when the prompt returns — so surface it there. Dim
`fmtDuration` next to the GoodVibesHeart, idle-only (hidden while busy),
so quick turns and active streaming stay quiet.
2026-04-20 11:23:58 -05:00
Brooklyn Nicholson
1e7de177e8 feat(tui): show time-since-last-user-message alongside session total (#8541)
StatusRule now renders `{sinceLastMsg}/{sinceSession}` (e.g. `12s/3m 45s`)
when a user has submitted in the current session; falls back to the total
alone otherwise. Wires `lastUserAt` through the state/session lifecycle:
- useSubmission stamps `setLastUserAt(Date.now())` on send
- useSessionLifecycle nulls it in reset/resetVisibleHistory
- /branch slash nulls it on fork
2026-04-20 11:17:34 -05:00
4 changed files with 38 additions and 7 deletions

View File

@@ -303,6 +303,7 @@ export interface AppLayoutStatusProps {
showStickyPrompt: boolean
statusColor: string
stickyPrompt: string
turnStartedAt: null | number
voiceLabel: string
}

View File

@@ -1,4 +1,4 @@
import { type ScrollBoxHandle, useApp, useHasSelection, useSelection, useStdout, useTerminalTitle } from '@hermes/ink'
import { useApp, useHasSelection, useSelection, useStdout, useTerminalTitle, type ScrollBoxHandle } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
@@ -102,6 +102,7 @@ export function useMainApp(gw: GatewayClient) {
const [voiceRecording, setVoiceRecording] = useState(false)
const [voiceProcessing, setVoiceProcessing] = useState(false)
const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now())
const [turnStartedAt, setTurnStartedAt] = useState<null | number>(null)
const [goodVibesTick, setGoodVibesTick] = useState(0)
const [bellOnComplete, setBellOnComplete] = useState(false)
@@ -283,6 +284,14 @@ export function useMainApp(gw: GatewayClient) {
sys
})
useEffect(() => {
if (ui.busy) {
setTurnStartedAt(prev => prev ?? Date.now())
} else {
setTurnStartedAt(null)
}
}, [ui.busy])
useConfigSync({ gw, setBellOnComplete, setVoiceEnabled, sid: ui.sid })
// ── Terminal tab title ─────────────────────────────────────────────
@@ -635,9 +644,21 @@ export function useMainApp(gw: GatewayClient) {
showStickyPrompt: !!stickyPrompt,
statusColor: statusColorOf(ui.status, ui.theme.color),
stickyPrompt,
turnStartedAt: ui.sid ? turnStartedAt : null,
voiceLabel: voiceRecording ? 'REC' : voiceProcessing ? 'STT' : `voice ${voiceEnabled ? 'on' : 'off'}`
}),
[cwd, gitBranch, goodVibesTick, sessionStartedAt, stickyPrompt, ui, voiceEnabled, voiceProcessing, voiceRecording]
[
cwd,
gitBranch,
goodVibesTick,
sessionStartedAt,
stickyPrompt,
turnStartedAt,
ui,
voiceEnabled,
voiceProcessing,
voiceRecording
]
)
const appTranscript = useMemo(

View File

@@ -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>
)
}
@@ -100,6 +106,7 @@ export function StatusRule({
bgCount,
sessionStartedAt,
showCost,
turnStartedAt,
voiceLabel,
t
}: StatusRuleProps) {
@@ -120,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 ? (
@@ -288,11 +295,12 @@ interface StatusRuleProps {
cols: number
cwdLabel: string
model: string
sessionStartedAt?: number | null
sessionStartedAt?: null | number
showCost: boolean
status: string
statusColor: string
t: Theme
turnStartedAt?: null | number
usage: Usage
voiceLabel?: string
}

View File

@@ -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}
/>