mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-04-28 06:51:16 +08:00
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
This commit is contained in:
@@ -284,6 +284,7 @@ const buildSession = () => ({
|
||||
newSession: vi.fn(),
|
||||
resetVisibleHistory: vi.fn(),
|
||||
resumeById: vi.fn(),
|
||||
setLastUserAt: vi.fn(),
|
||||
setSessionStartedAt: vi.fn()
|
||||
})
|
||||
|
||||
|
||||
@@ -237,6 +237,7 @@ export interface SlashHandlerContext {
|
||||
newSession: (msg?: string) => void
|
||||
resetVisibleHistory: (info?: null | SessionInfo) => void
|
||||
resumeById: (id: string) => void
|
||||
setLastUserAt: StateSetter<null | number>
|
||||
setSessionStartedAt: StateSetter<number>
|
||||
}
|
||||
slashFlightRef: MutableRefObject<number>
|
||||
@@ -299,6 +300,7 @@ export interface AppLayoutProgressProps {
|
||||
export interface AppLayoutStatusProps {
|
||||
cwdLabel: string
|
||||
goodVibesTick: number
|
||||
lastUserAt: null | number
|
||||
sessionStartedAt: null | number
|
||||
showStickyPrompt: boolean
|
||||
statusColor: string
|
||||
|
||||
@@ -178,6 +178,7 @@ export const sessionCommands: SlashCommand[] = [
|
||||
void ctx.session.closeSession(prevSid)
|
||||
patchUiState({ sid: r.session_id })
|
||||
ctx.session.setSessionStartedAt(Date.now())
|
||||
ctx.session.setLastUserAt(null)
|
||||
ctx.transcript.setHistoryItems([])
|
||||
ctx.transcript.sys(`branched → ${r.title ?? ''}`)
|
||||
})
|
||||
|
||||
@@ -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 [lastUserAt, setLastUserAt] = useState<null | number>(null)
|
||||
const [goodVibesTick, setGoodVibesTick] = useState(0)
|
||||
const [bellOnComplete, setBellOnComplete] = useState(false)
|
||||
|
||||
@@ -275,6 +276,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||
rpc,
|
||||
scrollRef,
|
||||
setHistoryItems,
|
||||
setLastUserAt,
|
||||
setLastUserMsg,
|
||||
setSessionStartedAt,
|
||||
setStickyPrompt,
|
||||
@@ -374,6 +376,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||
composerState,
|
||||
gw,
|
||||
maybeGoodVibes,
|
||||
setLastUserAt,
|
||||
setLastUserMsg,
|
||||
slashRef,
|
||||
submitRef,
|
||||
@@ -497,6 +500,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||
newSession: session.newSession,
|
||||
resetVisibleHistory: session.resetVisibleHistory,
|
||||
resumeById: session.resumeById,
|
||||
setLastUserAt,
|
||||
setSessionStartedAt
|
||||
},
|
||||
slashFlightRef,
|
||||
@@ -631,13 +635,25 @@ export function useMainApp(gw: GatewayClient) {
|
||||
() => ({
|
||||
cwdLabel: fmtCwdBranch(cwd, gitBranch),
|
||||
goodVibesTick,
|
||||
lastUserAt: ui.sid ? lastUserAt : null,
|
||||
sessionStartedAt: ui.sid ? sessionStartedAt : null,
|
||||
showStickyPrompt: !!stickyPrompt,
|
||||
statusColor: statusColorOf(ui.status, ui.theme.color),
|
||||
stickyPrompt,
|
||||
voiceLabel: voiceRecording ? 'REC' : voiceProcessing ? 'STT' : `voice ${voiceEnabled ? 'on' : 'off'}`
|
||||
}),
|
||||
[cwd, gitBranch, goodVibesTick, sessionStartedAt, stickyPrompt, ui, voiceEnabled, voiceProcessing, voiceRecording]
|
||||
[
|
||||
cwd,
|
||||
gitBranch,
|
||||
goodVibesTick,
|
||||
lastUserAt,
|
||||
sessionStartedAt,
|
||||
stickyPrompt,
|
||||
ui,
|
||||
voiceEnabled,
|
||||
voiceProcessing,
|
||||
voiceRecording
|
||||
]
|
||||
)
|
||||
|
||||
const appTranscript = useMemo(
|
||||
|
||||
@@ -44,6 +44,7 @@ export interface UseSessionLifecycleOptions {
|
||||
rpc: GatewayRpc
|
||||
scrollRef: RefObject<null | ScrollBoxHandle>
|
||||
setHistoryItems: StateSetter<Msg[]>
|
||||
setLastUserAt: StateSetter<null | number>
|
||||
setLastUserMsg: StateSetter<string>
|
||||
setSessionStartedAt: StateSetter<number>
|
||||
setStickyPrompt: StateSetter<string>
|
||||
@@ -61,6 +62,7 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||
rpc,
|
||||
scrollRef,
|
||||
setHistoryItems,
|
||||
setLastUserAt,
|
||||
setLastUserMsg,
|
||||
setSessionStartedAt,
|
||||
setStickyPrompt,
|
||||
@@ -82,9 +84,18 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||
patchUiState({ bgTasks: new Set(), info: null, sid: null, usage: ZERO })
|
||||
setHistoryItems([])
|
||||
setLastUserMsg('')
|
||||
setLastUserAt(null)
|
||||
setStickyPrompt('')
|
||||
composerActions.setPasteSnips([])
|
||||
}, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording])
|
||||
}, [
|
||||
composerActions,
|
||||
setHistoryItems,
|
||||
setLastUserAt,
|
||||
setLastUserMsg,
|
||||
setStickyPrompt,
|
||||
setVoiceProcessing,
|
||||
setVoiceRecording
|
||||
])
|
||||
|
||||
const resetVisibleHistory = useCallback(
|
||||
(info: null | SessionInfo = null) => {
|
||||
@@ -96,11 +107,12 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||
setHistoryItems(info ? [introMsg(info)] : [])
|
||||
setStickyPrompt('')
|
||||
setLastUserMsg('')
|
||||
setLastUserAt(null)
|
||||
composerActions.setPasteSnips([])
|
||||
patchTurnState({ activity: [] })
|
||||
patchUiState({ info, usage: usageFrom(info) })
|
||||
},
|
||||
[composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt]
|
||||
[composerActions, setHistoryItems, setLastUserAt, setLastUserMsg, setStickyPrompt]
|
||||
)
|
||||
|
||||
const newSession = useCallback(
|
||||
|
||||
@@ -37,6 +37,7 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
||||
composerState,
|
||||
gw,
|
||||
maybeGoodVibes,
|
||||
setLastUserAt,
|
||||
setLastUserMsg,
|
||||
slashRef,
|
||||
submitRef,
|
||||
@@ -59,6 +60,7 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
||||
turnController.clearStatusTimer()
|
||||
maybeGoodVibes(submitText)
|
||||
setLastUserMsg(text)
|
||||
setLastUserAt(Date.now())
|
||||
appendMessage({ role: 'user', text: displayText })
|
||||
patchUiState({ busy: true, status: 'running…' })
|
||||
turnController.bufRef = ''
|
||||
@@ -94,7 +96,7 @@ export function useSubmission(opts: UseSubmissionOptions) {
|
||||
})
|
||||
.catch(() => startSubmit(text, expand(text)))
|
||||
},
|
||||
[appendMessage, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserMsg, sys]
|
||||
[appendMessage, composerState.pasteSnips, gw, maybeGoodVibes, setLastUserAt, setLastUserMsg, sys]
|
||||
)
|
||||
|
||||
const shellExec = useCallback(
|
||||
@@ -296,6 +298,7 @@ export interface UseSubmissionOptions {
|
||||
composerState: ComposerState
|
||||
gw: GatewayClient
|
||||
maybeGoodVibes: (text: string) => void
|
||||
setLastUserAt: (value: null | number) => void
|
||||
setLastUserMsg: (value: string) => void
|
||||
slashRef: MutableRefObject<(cmd: string) => boolean>
|
||||
submitRef: MutableRefObject<(value: string) => void>
|
||||
|
||||
@@ -55,7 +55,7 @@ function ctxBar(pct: number | undefined, w = 10) {
|
||||
return '█'.repeat(filled) + '░'.repeat(w - filled)
|
||||
}
|
||||
|
||||
function SessionDuration({ startedAt }: { startedAt: number }) {
|
||||
function SessionDuration({ lastUserAt, startedAt }: { lastUserAt?: null | number; startedAt: number }) {
|
||||
const [now, setNow] = useState(() => Date.now())
|
||||
|
||||
useEffect(() => {
|
||||
@@ -65,7 +65,9 @@ function SessionDuration({ startedAt }: { startedAt: number }) {
|
||||
return () => clearInterval(id)
|
||||
}, [startedAt])
|
||||
|
||||
return fmtDuration(now - startedAt)
|
||||
const total = fmtDuration(now - startedAt)
|
||||
|
||||
return lastUserAt ? `${fmtDuration(now - lastUserAt)}/${total}` : total
|
||||
}
|
||||
|
||||
export function GoodVibesHeart({ tick, t }: { tick: number; t: Theme }) {
|
||||
@@ -98,6 +100,7 @@ export function StatusRule({
|
||||
model,
|
||||
usage,
|
||||
bgCount,
|
||||
lastUserAt,
|
||||
sessionStartedAt,
|
||||
showCost,
|
||||
voiceLabel,
|
||||
@@ -132,7 +135,7 @@ export function StatusRule({
|
||||
{sessionStartedAt ? (
|
||||
<Text color={t.color.dim}>
|
||||
{' │ '}
|
||||
<SessionDuration startedAt={sessionStartedAt} />
|
||||
<SessionDuration lastUserAt={lastUserAt} startedAt={sessionStartedAt} />
|
||||
</Text>
|
||||
) : null}
|
||||
{voiceLabel ? <Text color={t.color.dim}> │ {voiceLabel}</Text> : null}
|
||||
@@ -287,8 +290,9 @@ interface StatusRuleProps {
|
||||
busy: boolean
|
||||
cols: number
|
||||
cwdLabel: string
|
||||
lastUserAt?: null | number
|
||||
model: string
|
||||
sessionStartedAt?: number | null
|
||||
sessionStartedAt?: null | number
|
||||
showCost: boolean
|
||||
status: string
|
||||
statusColor: string
|
||||
|
||||
@@ -188,6 +188,7 @@ const ComposerPane = memo(function ComposerPane({
|
||||
busy={ui.busy}
|
||||
cols={composer.cols}
|
||||
cwdLabel={status.cwdLabel}
|
||||
lastUserAt={status.lastUserAt}
|
||||
model={ui.info?.model?.split('/').pop() ?? ''}
|
||||
sessionStartedAt={status.sessionStartedAt}
|
||||
showCost={ui.showCost}
|
||||
|
||||
Reference in New Issue
Block a user