fix(tui): redraw dashboard after new session (#65239)
parent
3f2a389c7e
commit
a16ac37dd9
|
|
@ -367,6 +367,25 @@ describe('createSlashHandler', () => {
|
||||||
expect(ctx.gateway.rpc).not.toHaveBeenCalled()
|
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 () => {
|
it('keeps visible scrollback when branching a TUI session', async () => {
|
||||||
patchUiState({ sid: 'sid-parent' })
|
patchUiState({ sid: 'sid-parent' })
|
||||||
const rpc = vi.fn(() => Promise.resolve({ session_id: 'sid-branch', title: 'branch title' }))
|
const rpc = vi.fn(() => Promise.resolve({ session_id: 'sid-branch', title: 'branch title' }))
|
||||||
|
|
|
||||||
|
|
@ -11,9 +11,24 @@ import {
|
||||||
hydrateLiveSessionInflight,
|
hydrateLiveSessionInflight,
|
||||||
liveSessionInflightMessages,
|
liveSessionInflightMessages,
|
||||||
scheduleResumeScrollToBottom,
|
scheduleResumeScrollToBottom,
|
||||||
|
signalFreshSessionBoundary,
|
||||||
writeActiveSessionFile
|
writeActiveSessionFile
|
||||||
} from '../app/useSessionLifecycle.js'
|
} 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', () => {
|
describe('writeActiveSessionFile', () => {
|
||||||
let dir = ''
|
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 { useStore } from '@nanostores/react'
|
||||||
import { useCallback, useEffect, useMemo, useRef, useState } from '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 { MAX_HISTORY, WHEEL_SCROLL_STEP } from '../config/limits.js'
|
||||||
import { RESIZE_COALESCE_MS } from '../config/timing.js'
|
import { RESIZE_COALESCE_MS } from '../config/timing.js'
|
||||||
import { hasLeadGap, prevRenderedMsg } from '../domain/blockLayout.js'
|
import { hasLeadGap, prevRenderedMsg } from '../domain/blockLayout.js'
|
||||||
|
|
@ -184,6 +192,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||||
const [voiceProcessing, setVoiceProcessing] = useState(false)
|
const [voiceProcessing, setVoiceProcessing] = useState(false)
|
||||||
const [voiceRecordKey, setVoiceRecordKey] = useState<ParsedVoiceRecordKey>(DEFAULT_VOICE_RECORD_KEY)
|
const [voiceRecordKey, setVoiceRecordKey] = useState<ParsedVoiceRecordKey>(DEFAULT_VOICE_RECORD_KEY)
|
||||||
const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now())
|
const [sessionStartedAt, setSessionStartedAt] = useState(() => Date.now())
|
||||||
|
const [dashboardFreshSessionId, setDashboardFreshSessionId] = useState<null | string>(null)
|
||||||
const [turnStartedAt, setTurnStartedAt] = useState<null | number>(null)
|
const [turnStartedAt, setTurnStartedAt] = useState<null | number>(null)
|
||||||
const [lastTurnEndedAt, setLastTurnEndedAt] = useState<null | number>(null)
|
const [lastTurnEndedAt, setLastTurnEndedAt] = useState<null | number>(null)
|
||||||
// Bumped by the gateway `reaction` event (core-detected affection).
|
// Bumped by the gateway `reaction` event (core-detected affection).
|
||||||
|
|
@ -496,6 +505,7 @@ export function useMainApp(gw: GatewayClient) {
|
||||||
colsRef,
|
colsRef,
|
||||||
composerActions,
|
composerActions,
|
||||||
gw,
|
gw,
|
||||||
|
onFreshSessionStarted: DASHBOARD_TUI_MODE ? setDashboardFreshSessionId : undefined,
|
||||||
panel,
|
panel,
|
||||||
rpc,
|
rpc,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
|
@ -508,6 +518,12 @@ export function useMainApp(gw: GatewayClient) {
|
||||||
sys
|
sys
|
||||||
})
|
})
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (dashboardFreshSessionId) {
|
||||||
|
forceRedraw(stdout ?? process.stdout)
|
||||||
|
}
|
||||||
|
}, [dashboardFreshSessionId, stdout])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (ui.busy) {
|
if (ui.busy) {
|
||||||
setTurnStartedAt(prev => prev ?? Date.now())
|
setTurnStartedAt(prev => prev ?? Date.now())
|
||||||
|
|
|
||||||
|
|
@ -68,6 +68,20 @@ export const hydrateLiveSessionInflight = (inflight?: null | SessionInflightTurn
|
||||||
turnController.hydrateStreamingText(assistant)
|
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 = (
|
export const scheduleResumeScrollToBottom = (
|
||||||
scrollRef: RefObject<null | ScrollBoxHandle>,
|
scrollRef: RefObject<null | ScrollBoxHandle>,
|
||||||
delays: readonly number[] = [0, 80, 240]
|
delays: readonly number[] = [0, 80, 240]
|
||||||
|
|
@ -115,6 +129,7 @@ export interface UseSessionLifecycleOptions {
|
||||||
colsRef: { current: number }
|
colsRef: { current: number }
|
||||||
composerActions: ComposerActions
|
composerActions: ComposerActions
|
||||||
gw: GatewayClient
|
gw: GatewayClient
|
||||||
|
onFreshSessionStarted?: (sessionId: string) => void
|
||||||
panel: (title: string, sections: PanelSection[]) => void
|
panel: (title: string, sections: PanelSection[]) => void
|
||||||
rpc: GatewayRpc
|
rpc: GatewayRpc
|
||||||
scrollRef: RefObject<null | ScrollBoxHandle>
|
scrollRef: RefObject<null | ScrollBoxHandle>
|
||||||
|
|
@ -132,6 +147,7 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||||
colsRef,
|
colsRef,
|
||||||
composerActions,
|
composerActions,
|
||||||
gw,
|
gw,
|
||||||
|
onFreshSessionStarted,
|
||||||
panel,
|
panel,
|
||||||
rpc,
|
rpc,
|
||||||
scrollRef,
|
scrollRef,
|
||||||
|
|
@ -204,8 +220,10 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
||||||
return null
|
return null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const previousSid = getUiState().sid
|
||||||
|
|
||||||
if (!keepCurrent) {
|
if (!keepCurrent) {
|
||||||
await closeSession(getUiState().sid)
|
await closeSession(previousSid)
|
||||||
}
|
}
|
||||||
|
|
||||||
const r = await rpc<SessionCreateResponse>('session.create', { cols: colsRef.current })
|
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
|
return r.session_id
|
||||||
},
|
},
|
||||||
[closeSession, colsRef, panel, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]
|
[closeSession, colsRef, onFreshSessionStarted, panel, resetSession, rpc, setHistoryItems, setSessionStartedAt, sys]
|
||||||
)
|
)
|
||||||
|
|
||||||
const newSession = useCallback(
|
const newSession = useCallback(
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue