mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-08 03:01:47 +00:00
chore(tui): /clean recent perf work — KISS/DRY pass
24 files, -319 LoC. Behaviour preserved, 369/369 tests green. - hermes-ink caches: shared lruEvict helper for the four parallel LRU caches (stringWidth, wrapText, sliceAnsi, lineWidth); touch-on-read stays inlined per cache; tightened output.ts skip-slice fast path. - wheelAccel: trimmed provenance header, collapsed env parsing, ternary dispatch in computeWheelStep. - perfPane: folded ensureLogDir into once-flag, spread-with-overrides for fastPath/phases instead of full rebuilds. - env: extracted truthy() (used 4×). - virtualHeights: collapsed user/diff/slash height bumps; trail+todos estimate. - useInputHandlers: scrollIdleTimer cleanup on unmount, ?? undefined shorthand. - useMainApp: dropped dead liveTailVisible IIFE and liveProgress indirection. - appLayout, markdown, messageLine, entry: vertical rhythm, dropped narration comments, inlined one-shot vars. - fix: empty catch blocks → /* best-effort */ for no-empty lint.
This commit is contained in:
parent
527ac351b4
commit
b1c49d5e73
32 changed files with 259 additions and 547 deletions
|
|
@ -50,6 +50,7 @@ export const archiveTodosAtTurnEnd = () => {
|
|||
}
|
||||
|
||||
const done = isTodoDone(state.todos)
|
||||
|
||||
const msg: Msg = {
|
||||
kind: 'trail',
|
||||
role: 'system',
|
||||
|
|
|
|||
|
|
@ -29,35 +29,19 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
|||
const overlay = useStore($overlayState)
|
||||
const isBlocked = useStore($isBlocked)
|
||||
const pagerPageSize = Math.max(5, (terminal.stdout?.rows ?? 24) - 6)
|
||||
const scrollIdleTimer = useRef<ReturnType<typeof setTimeout> | null>(null)
|
||||
const scrollIdleTimer = useRef<null | ReturnType<typeof setTimeout>>(null)
|
||||
|
||||
// Wheel acceleration state machine (ported from claude-code). Adapts
|
||||
// step size per wheel event based on inter-event timing: fast flicks
|
||||
// ramp up, slow clicks stay at 1 row, direction flips reset. See
|
||||
// lib/wheelAccel.ts for the full tuning rationale. The accel state
|
||||
// mutates in place and is kept across renders via a ref. wheelStep
|
||||
// (passed from useMainApp / the WHEEL_SCROLL_STEP constant) is used
|
||||
// as the BASE — final rows = wheelStep × accelMult.
|
||||
// Wheel accel ported from claude-code: inter-event timing drives step size,
|
||||
// direction flips reset. wheelStep (WHEEL_SCROLL_STEP) is the base; final
|
||||
// rows = wheelStep × accelMult. State mutates in place across renders.
|
||||
const wheelAccelRef = useRef(initWheelAccelForHost())
|
||||
|
||||
useEffect(
|
||||
() => () => {
|
||||
if (scrollIdleTimer.current) {
|
||||
clearTimeout(scrollIdleTimer.current)
|
||||
scrollIdleTimer.current = null
|
||||
}
|
||||
},
|
||||
[]
|
||||
)
|
||||
useEffect(() => () => clearTimeout(scrollIdleTimer.current ?? undefined), [])
|
||||
|
||||
const scrollTranscript = (delta: number) => {
|
||||
if (getUiState().busy) {
|
||||
turnController.boostStreamingForScroll()
|
||||
|
||||
if (scrollIdleTimer.current) {
|
||||
clearTimeout(scrollIdleTimer.current)
|
||||
}
|
||||
|
||||
clearTimeout(scrollIdleTimer.current ?? undefined)
|
||||
scrollIdleTimer.current = setTimeout(() => {
|
||||
scrollIdleTimer.current = null
|
||||
turnController.relaxStreaming()
|
||||
|
|
@ -300,16 +284,10 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
|||
|
||||
if (key.wheelUp || key.wheelDown) {
|
||||
const dir: -1 | 1 = key.wheelUp ? -1 : 1
|
||||
const accelRows = computeWheelStep(wheelAccelRef.current, dir, Date.now())
|
||||
// 0 = direction-flip bounce deferred; skip the no-op scroll.
|
||||
const rows = computeWheelStep(wheelAccelRef.current, dir, Date.now())
|
||||
|
||||
// computeWheelStep returns 0 when a direction flip is deferred for
|
||||
// bounce detection — scrollBy(0) is a no-op; skip the call to avoid
|
||||
// needless render scheduling.
|
||||
if (accelRows === 0) {
|
||||
return
|
||||
}
|
||||
|
||||
return scrollTranscript(dir * accelRows * wheelStep)
|
||||
return rows ? scrollTranscript(dir * rows * wheelStep) : undefined
|
||||
}
|
||||
|
||||
if (key.shift && key.upArrow) {
|
||||
|
|
@ -321,14 +299,9 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult {
|
|||
}
|
||||
|
||||
if (key.pageUp || key.pageDown) {
|
||||
// Half-viewport keeps 50% continuity and stays under Ink's
|
||||
// `delta < innerHeight` DECSTBM fast-path threshold.
|
||||
const viewport = terminal.scrollRef.current?.getViewportHeight() ?? Math.max(6, (terminal.stdout?.rows ?? 24) - 8)
|
||||
// Half-viewport per keystroke. A whole-viewport jump (our old
|
||||
// `viewport - 2`) fully replaces what's on screen — no visual
|
||||
// continuity, the user can't scan — AND it lands right at Ink's
|
||||
// `delta < innerHeight` fast-path threshold, disqualifying the
|
||||
// DECSTBM blit on every press. Half-viewport keeps 50% continuity,
|
||||
// well under the threshold, and two presses still scroll the same
|
||||
// total distance.
|
||||
const step = Math.max(4, Math.floor(viewport / 2))
|
||||
|
||||
return scrollTranscript(key.pageUp ? -step : step)
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { useApp, useHasSelection, useSelection, useStdout, useTerminalTitle, type ScrollBoxHandle } from '@hermes/ink'
|
||||
import { type ScrollBoxHandle, useApp, useHasSelection, useSelection, useStdout, useTerminalTitle } from '@hermes/ink'
|
||||
import { useStore } from '@nanostores/react'
|
||||
import { useCallback, useEffect, useMemo, useRef, useState } from 'react'
|
||||
|
||||
|
|
@ -20,7 +20,6 @@ import { appendTranscriptMessage } from '../lib/messages.js'
|
|||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||
import { terminalParityHints } from '../lib/terminalParity.js'
|
||||
import { buildToolTrailLine, sameToolTrailGroup, toolTrailLabel } from '../lib/text.js'
|
||||
import { getViewportSnapshot } from '../lib/viewportStore.js'
|
||||
import { estimatedMsgHeight, messageHeightKey } from '../lib/virtualHeights.js'
|
||||
import type { Msg, PanelSection, SlashCatalog } from '../types.js'
|
||||
|
||||
|
|
@ -199,8 +198,10 @@ export function useMainApp(gw: GatewayClient) {
|
|||
|
||||
return `${thinking}:${tools}`
|
||||
}, [ui.detailsMode, ui.detailsModeCommandOverride, ui.sections])
|
||||
|
||||
const detailsVisible = detailsLayoutKey !== 'hidden:hidden'
|
||||
const heightCacheKey = `${ui.sid ?? 'draft'}:${cols}:${ui.compact ? '1' : '0'}:${detailsLayoutKey}`
|
||||
|
||||
const heightCache = useMemo(() => {
|
||||
let cache = heightCachesRef.current.get(heightCacheKey)
|
||||
|
||||
|
|
@ -215,6 +216,7 @@ export function useMainApp(gw: GatewayClient) {
|
|||
|
||||
return cache
|
||||
}, [heightCacheKey])
|
||||
|
||||
const initialHeights = useMemo(() => {
|
||||
const out = new Map<string, number>()
|
||||
|
||||
|
|
@ -232,6 +234,7 @@ export function useMainApp(gw: GatewayClient) {
|
|||
|
||||
return out
|
||||
}, [cols, detailsVisible, heightCache, ui.compact, virtualRows])
|
||||
|
||||
const syncHeightCache = useCallback(
|
||||
(heights: ReadonlyMap<string, number>) => {
|
||||
for (const row of virtualRows) {
|
||||
|
|
@ -719,26 +722,10 @@ export function useMainApp(gw: GatewayClient) {
|
|||
[cols, composerActions, composerState, empty, pagerPageSize, submit]
|
||||
)
|
||||
|
||||
const liveTailVisible = (() => {
|
||||
const s = scrollRef.current
|
||||
|
||||
if (!s) {
|
||||
return true
|
||||
}
|
||||
|
||||
const { bottom, scrollHeight } = getViewportSnapshot(s)
|
||||
|
||||
return bottom >= scrollHeight - 3
|
||||
})()
|
||||
|
||||
const liveProgress = useMemo(() => ({ showProgressArea }), [showProgressArea])
|
||||
|
||||
// Always pass current progress through. Freezing this while offscreen looked
|
||||
// like a nice scroll optimization, but it also froze the live tail's
|
||||
// thinking/tool state at arbitrary intermediate snapshots. Streaming update
|
||||
// throttling now handles interaction load; progress state should remain
|
||||
// truthful so panels don't randomly disappear.
|
||||
const appProgress = liveProgress
|
||||
// Pass current progress through unfrozen — streaming update throttling
|
||||
// handles interaction load; progress must stay truthful so panels don't
|
||||
// randomly disappear when the live tail scrolls offscreen.
|
||||
const appProgress = useMemo(() => ({ showProgressArea }), [showProgressArea])
|
||||
|
||||
const cwd = ui.info?.cwd || process.env.HERMES_CWD || process.cwd()
|
||||
const gitBranch = useGitBranch(cwd)
|
||||
|
|
|
|||
|
|
@ -29,10 +29,11 @@ export const writeActiveSessionFile = (sessionId: null | string, file = process.
|
|||
return
|
||||
}
|
||||
|
||||
// Best-effort shell-epilogue hint; never break live session changes.
|
||||
try {
|
||||
writeFileSync(file, JSON.stringify({ session_id: sessionId }), { mode: 0o600 })
|
||||
} catch {
|
||||
// Best-effort shell epilogue hint only; never break live session changes.
|
||||
/* best-effort */
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -98,8 +99,8 @@ export function useSessionLifecycle(opts: UseSessionLifecycleOptions) {
|
|||
setLastUserMsg('')
|
||||
setStickyPrompt('')
|
||||
composerActions.setPasteSnips([])
|
||||
// Half-prune Ink content caches: new session has new keys, but a partial
|
||||
// warm pool helps if the user resumes back to the prior session.
|
||||
// Half-prune: new session has new keys, but keep a warm pool in case
|
||||
// the user resumes back to the prior session.
|
||||
evictInkCaches('half')
|
||||
}, [composerActions, setHistoryItems, setLastUserMsg, setStickyPrompt, setVoiceProcessing, setVoiceRecording])
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue