mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-20 15:33:54 +00:00
fix(tui): redraw dashboard after new session (#65239)
This commit is contained in:
parent
3f2a389c7e
commit
a16ac37dd9
4 changed files with 74 additions and 4 deletions
|
|
@ -367,6 +367,25 @@ describe('createSlashHandler', () => {
|
|||
expect(ctx.gateway.rpc).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('routes the /reset catalog alias through the local fresh-session lifecycle', () => {
|
||||
const ctx = buildCtx({
|
||||
local: {
|
||||
catalog: {
|
||||
canon: {
|
||||
'/new': '/new',
|
||||
'/reset': '/new'
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
createSlashHandler(ctx)('/reset')
|
||||
getOverlayState().confirm?.onConfirm()
|
||||
|
||||
expect(ctx.session.newSession).toHaveBeenCalledWith('new session started', undefined)
|
||||
expect(ctx.gateway.gw.request).not.toHaveBeenCalled()
|
||||
})
|
||||
|
||||
it('keeps visible scrollback when branching a TUI session', async () => {
|
||||
patchUiState({ sid: 'sid-parent' })
|
||||
const rpc = vi.fn(() => Promise.resolve({ session_id: 'sid-branch', title: 'branch title' }))
|
||||
|
|
|
|||
|
|
@ -11,9 +11,24 @@ import {
|
|||
hydrateLiveSessionInflight,
|
||||
liveSessionInflightMessages,
|
||||
scheduleResumeScrollToBottom,
|
||||
signalFreshSessionBoundary,
|
||||
writeActiveSessionFile
|
||||
} from '../app/useSessionLifecycle.js'
|
||||
|
||||
describe('fresh session boundary', () => {
|
||||
it('signals only when a live session is replaced by a different session', () => {
|
||||
const onFreshSessionStarted = vi.fn()
|
||||
|
||||
expect(signalFreshSessionBoundary('old-session', 'new-session', onFreshSessionStarted)).toBe(true)
|
||||
expect(signalFreshSessionBoundary(null, 'first-session', onFreshSessionStarted)).toBe(false)
|
||||
expect(signalFreshSessionBoundary('same-session', 'same-session', onFreshSessionStarted)).toBe(false)
|
||||
expect(signalFreshSessionBoundary('old-session', null, onFreshSessionStarted)).toBe(false)
|
||||
expect(signalFreshSessionBoundary('old-session', 'new-session')).toBe(false)
|
||||
expect(onFreshSessionStarted).toHaveBeenCalledOnce()
|
||||
expect(onFreshSessionStarted).toHaveBeenCalledWith('new-session')
|
||||
})
|
||||
})
|
||||
|
||||
describe('writeActiveSessionFile', () => {
|
||||
let dir = ''
|
||||
|
||||
|
|
|
|||
|
|
@ -1,8 +1,16 @@
|
|||
import { type ScrollBoxHandle, useApp, useHasSelection, useSelection, useStdout, useTerminalTitle } from '@hermes/ink'
|
||||
import {
|
||||
forceRedraw,
|
||||
type ScrollBoxHandle,
|
||||
useApp,
|
||||
useHasSelection,
|
||||
useSelection,
|
||||
useStdout,
|
||||
useTerminalTitle
|
||||
} from '@hermes/ink'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
import { STARTUP_RESUME_ID } from '../config/env.js'
|
||||
import { DASHBOARD_TUI_MODE, STARTUP_RESUME_ID } from '../config/env.js'
|
||||
import { MAX_HISTORY, WHEEL_SCROLL_STEP } from '../config/limits.js'
|
||||
import { RESIZE_COALESCE_MS } from '../config/timing.js'
|
||||
import { hasLeadGap, prevRenderedMsg } from '../domain/blockLayout.js'
|
||||
|
|
@ -184,6 +192,7 @@ export function useMainApp(gw: GatewayClient) {
|
|||
const [voiceProcessing, setVoiceProcessing] = useState(false)
|
||||
const [voiceRecordKey, setVoiceRecordKey] = useState<ParsedVoiceRecordKey>(DEFAULT_VOICE_RECORD_KEY)
|
||||
const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now())
|
||||
const [dashboardFreshSessionId, setDashboardFreshSessionId] = useState<null | string>(null)
|
||||
const [turnStartedAt, setTurnStartedAt] = useState<null | number>(null)
|
||||
const [lastTurnEndedAt, setLastTurnEndedAt] = useState<null | number>(null)
|
||||
// Bumped by the gateway `reaction` event (core-detected affection).
|
||||
|
|
@ -496,6 +505,7 @@ export function useMainApp(gw: GatewayClient) {
|
|||
colsRef,
|
||||
composerActions,
|
||||
gw,
|
||||
onFreshSessionStarted: DASHBOARD_TUI_MODE ? setDashboardFreshSessionId : undefined,
|
||||
panel,
|
||||
rpc,
|
||||
scrollRef,
|
||||
|
|
@ -508,6 +518,12 @@ export function useMainApp(gw: GatewayClient) {
|
|||
sys
|
||||
})
|
||||
|
||||
useEffect(() => {
|
||||
if (dashboardFreshSessionId) {
|
||||
forceRedraw(stdout ?? process.stdout)
|
||||
}
|
||||
}, [dashboardFreshSessionId, stdout])
|
||||
|
||||
useEffect(() => {
|
||||
if (ui.busy) {
|
||||
setTurnStartedAt(prev => prev ?? Date.now())
|
||||
|
|
|
|||
|
|
@ -68,6 +68,20 @@ export const hydrateLiveSessionInflight = (inflight?: null | SessionInflightTurn
|
|||
turnController.hydrateStreamingText(assistant)
|
||||
}
|
||||
|
||||
export const signalFreshSessionBoundary = (
|
||||
previousSid: null | string,
|
||||
nextSid: null | string,
|
||||
onFreshSessionStarted?: (sessionId: string) => void
|
||||
) => {
|
||||
if (!previousSid || !nextSid || previousSid === nextSid || !onFreshSessionStarted) {
|
||||
return false
|
||||
}
|
||||
|
||||
onFreshSessionStarted(nextSid)
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export const scheduleResumeScrollToBottom = (
|
||||
scrollRef: RefObject<null | ScrollBoxHandle>,
|
||||
delays: readonly number[] = [0, 80, 240]
|
||||
|
|
@ -115,6 +129,7 @@ export interface UseSessionLifecycleOptions {
|
|||
colsRef: { current: number }
|
||||
composerActions: ComposerActions
|
||||
gw: GatewayClient
|
||||
onFreshSessionStarted?: (sessionId: string) => void
|
||||
panel: (title: string, sections: PanelSection[]) => void
|
||||
rpc: GatewayRpc
|
||||
scrollRef: RefObject<null | ScrollBoxHandle>
|
||||
|
|
@ -132,6 +147,7 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
|||
colsRef,
|
||||
composerActions,
|
||||
gw,
|
||||
onFreshSessionStarted,
|
||||
panel,
|
||||
rpc,
|
||||
scrollRef,
|
||||
|
|
@ -204,8 +220,10 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
|||
return null
|
||||
}
|
||||
|
||||
const previousSid = getUiState().sid
|
||||
|
||||
if (!keepCurrent) {
|
||||
await closeSession(getUiState().sid)
|
||||
await closeSession(previousSid)
|
||||
}
|
||||
|
||||
const r = await rpc<SessionCreateResponse>('session.create', { cols: colsRef.current })
|
||||
|
|
@ -270,9 +288,11 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
|||
})
|
||||
}
|
||||
|
||||
signalFreshSessionBoundary(previousSid, r.session_id, onFreshSessionStarted)
|
||||
|
||||
return r.session_id
|
||||
},
|
||||
[closeSession, colsRef, panel, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]
|
||||
[closeSession, colsRef, onFreshSessionStarted, panel, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]
|
||||
)
|
||||
|
||||
const newSession = useCallback(
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue