diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index 1f2f938a93..b39e40bad9 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -284,6 +284,7 @@ const buildSession = () => ({ newSession: vi.fn(), resetVisibleHistory: vi.fn(), resumeById: vi.fn(), + setLastUserAt: vi.fn(), setSessionStartedAt: vi.fn() }) diff --git a/ui-tui/src/app/interfaces.ts b/ui-tui/src/app/interfaces.ts index af13e047c7..e0541af153 100644 --- a/ui-tui/src/app/interfaces.ts +++ b/ui-tui/src/app/interfaces.ts @@ -237,6 +237,7 @@ export interface SlashHandlerContext { newSession: (msg?: string) => void resetVisibleHistory: (info?: null | SessionInfo) => void resumeById: (id: string) => void + setLastUserAt: StateSetter setSessionStartedAt: StateSetter } slashFlightRef: MutableRefObject @@ -299,6 +300,7 @@ export interface AppLayoutProgressProps { export interface AppLayoutStatusProps { cwdLabel: string goodVibesTick: number + lastUserAt: null | number sessionStartedAt: null | number showStickyPrompt: boolean statusColor: string diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 354d3c1975..ef5d72e206 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -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 ?? ''}`) }) diff --git a/ui-tui/src/app/useMainApp.ts b/ui-tui/src/app/useMainApp.ts index 27401b4188..5271246dda 100644 --- a/ui-tui/src/app/useMainApp.ts +++ b/ui-tui/src/app/useMainApp.ts @@ -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) 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( diff --git a/ui-tui/src/app/useSessionLifecycle.ts b/ui-tui/src/app/useSessionLifecycle.ts index acd10135e1..2738849e93 100644 --- a/ui-tui/src/app/useSessionLifecycle.ts +++ b/ui-tui/src/app/useSessionLifecycle.ts @@ -44,6 +44,7 @@ export interface UseSessionLifecycleOptions { rpc: GatewayRpc scrollRef: RefObject setHistoryItems: StateSetter + setLastUserAt: StateSetter setLastUserMsg: StateSetter setSessionStartedAt: StateSetter setStickyPrompt: StateSetter @@ -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( diff --git a/ui-tui/src/app/useSubmission.ts b/ui-tui/src/app/useSubmission.ts index f8a40f5a08..1e17ffdc72 100644 --- a/ui-tui/src/app/useSubmission.ts +++ b/ui-tui/src/app/useSubmission.ts @@ -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> diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 2f5f807dec..23c4a4e8e1 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -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 ? ( {' │ '} - + ) : null} {voiceLabel ? │ {voiceLabel} : 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 diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index f13adf1bbd..d711edca83 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -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}