diff --git a/ui-tui/packages/hermes-ink/src/ink/render-node-to-output.ts b/ui-tui/packages/hermes-ink/src/ink/render-node-to-output.ts index cb781f3e69..ffce94f11a 100644 --- a/ui-tui/packages/hermes-ink/src/ink/render-node-to-output.ts +++ b/ui-tui/packages/hermes-ink/src/ink/render-node-to-output.ts @@ -79,7 +79,6 @@ export type ScrollFastPathStats = { declined: { noPrevScreen: number heightDeltaMismatch: number - noHint: number other: number } lastDeclineReason?: string @@ -95,7 +94,6 @@ export const scrollFastPathStats: ScrollFastPathStats = { declined: { noPrevScreen: 0, heightDeltaMismatch: 0, - noHint: 0, other: 0 } } @@ -105,7 +103,6 @@ export function resetScrollFastPathStats(): void { scrollFastPathStats.taken = 0 scrollFastPathStats.declined.noPrevScreen = 0 scrollFastPathStats.declined.heightDeltaMismatch = 0 - scrollFastPathStats.declined.noHint = 0 scrollFastPathStats.declined.other = 0 scrollFastPathStats.lastDeclineReason = undefined scrollFastPathStats.lastHeightDelta = undefined diff --git a/ui-tui/packages/hermes-ink/src/ink/stringWidth.ts b/ui-tui/packages/hermes-ink/src/ink/stringWidth.ts index 840c11f7bf..41d00fd47c 100644 --- a/ui-tui/packages/hermes-ink/src/ink/stringWidth.ts +++ b/ui-tui/packages/hermes-ink/src/ink/stringWidth.ts @@ -311,13 +311,16 @@ export const stringWidth: (str: string) => number = str => { const cached = widthCache.get(str) if (cached !== undefined) { + // True LRU: refresh recency by re-inserting (Map iteration is insertion order). + widthCache.delete(str) + widthCache.set(str, cached) + return cached } const w = rawStringWidth(str) if (widthCache.size >= WIDTH_CACHE_LIMIT) { - // Drop oldest entry — Map iteration order is insertion order. widthCache.delete(widthCache.keys().next().value!) } diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index caba12bd30..d9f1c01810 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -1,6 +1,6 @@ import { useInput } from '@hermes/ink' import { useStore } from '@nanostores/react' -import { useRef } from 'react' +import { useEffect, useRef } from 'react' import { TYPING_IDLE_MS } from '../config/timing.js' import type { @@ -40,6 +40,16 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { // as the BASE — final rows = wheelStep × accelMult. const wheelAccelRef = useRef(initWheelAccelForHost()) + useEffect( + () => () => { + if (scrollIdleTimer.current) { + clearTimeout(scrollIdleTimer.current) + scrollIdleTimer.current = null + } + }, + [] + ) + const scrollTranscript = (delta: number) => { if (getUiState().busy) { turnController.boostStreamingForScroll() diff --git a/ui-tui/src/lib/perfPane.tsx b/ui-tui/src/lib/perfPane.tsx index ab512c108f..fbe86d7bad 100644 --- a/ui-tui/src/lib/perfPane.tsx +++ b/ui-tui/src/lib/perfPane.tsx @@ -132,7 +132,6 @@ export const logFrameEvent = ENABLED taken: scrollFastPathStats.taken, declined: { heightDeltaMismatch: scrollFastPathStats.declined.heightDeltaMismatch, - noHint: scrollFastPathStats.declined.noHint, noPrevScreen: scrollFastPathStats.declined.noPrevScreen, other: scrollFastPathStats.declined.other }, diff --git a/ui-tui/src/lib/virtualHeights.ts b/ui-tui/src/lib/virtualHeights.ts index 74583e9dd7..953ed43b20 100644 --- a/ui-tui/src/lib/virtualHeights.ts +++ b/ui-tui/src/lib/virtualHeights.ts @@ -12,8 +12,22 @@ export const hashText = (text: string) => { return (h >>> 0).toString(36) } -export const messageHeightKey = (msg: Msg) => - [msg.role, msg.kind ?? '', hashText([msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? ''].join('\0'))].join(':') +export const messageHeightKey = (msg: Msg) => { + const todoSig = msg.todos?.map(t => `${t.status}:${t.content}`).join('\u0001') ?? '' + const panelSig = + msg.panelData?.sections + .map(s => `${s.title ?? ''}:${s.text?.length ?? 0}:${s.items?.length ?? 0}:${s.rows?.length ?? 0}`) + .join('\u0001') ?? '' + const introSig = msg.kind === 'intro' ? (msg.info?.version ?? '') : '' + + return [ + msg.role, + msg.kind ?? '', + hashText( + [msg.text, msg.thinking ?? '', msg.tools?.join('\n') ?? '', todoSig, panelSig, introSig].join('\0') + ) + ].join(':') +} export const wrappedLines = (text: string, width: number) => { const w = Math.max(1, width) @@ -34,6 +48,14 @@ export const estimatedMsgHeight = ( return Math.max(3, (msg.panelData?.sections.length ?? 1) * 2 + 1) } + if (msg.kind === 'trail' && msg.todos?.length) { + if (msg.todoCollapsedByDefault) { + return 2 + } + + return Math.max(2, msg.todos.length + 2) + } + const bodyWidth = Math.max(20, cols - 5) const text = msg.role === 'assistant' && limitHistory ? boundedHistoryRenderText(msg.text) : msg.text let h = wrappedLines(text || ' ', bodyWidth) diff --git a/ui-tui/src/lib/wheelAccel.ts b/ui-tui/src/lib/wheelAccel.ts index d010c94131..78d8f56b6a 100644 --- a/ui-tui/src/lib/wheelAccel.ts +++ b/ui-tui/src/lib/wheelAccel.ts @@ -1,9 +1,8 @@ // Wheel-scroll acceleration state machine. // -// Ported from claude-code's src/components/ScrollKeybindingHandler.tsx -// (commit cb7cfba6 of their research snapshot at ~/claude-code). The -// algorithm is theirs; the tuning constants below are theirs; this file -// is a straight port adapted to our module structure. +// Algorithm and tuning constants adapted from a reference implementation +// of trackpad/wheel-event acceleration in TUI scroll handlers; this file +// is the port adapted to our module structure. // // Problem: one wheel event = 1 scrolled row feels sluggish on trackpads // (which can fire 200+ events/sec) and during deliberate mouse scrolls. diff --git a/ui-tui/src/types/hermes-ink.d.ts b/ui-tui/src/types/hermes-ink.d.ts index 769e7e9f19..a7e571db6c 100644 --- a/ui-tui/src/types/hermes-ink.d.ts +++ b/ui-tui/src/types/hermes-ink.d.ts @@ -112,7 +112,6 @@ declare module '@hermes/ink' { declined: { noPrevScreen: number heightDeltaMismatch: number - noHint: number other: number } lastDeclineReason?: string