mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-06 02:41:48 +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
|
|
@ -30,14 +30,18 @@ const TranscriptPane = memo(function TranscriptPane({
|
|||
}: Pick<AppLayoutProps, 'actions' | 'composer' | 'progress' | 'transcript'>) {
|
||||
const ui = useStore($uiState)
|
||||
|
||||
// Index of the latest user message — LiveTodoPanel is rendered as a child
|
||||
// of that row so it visually belongs to the user's prompt and follows it
|
||||
// during scroll. Falls back to -1 when no user message exists yet (empty
|
||||
// session); LiveTodoPanel then doesn't render at all.
|
||||
// LiveTodoPanel rides as a child of the latest user-message row so it
|
||||
// visually belongs to the prompt and follows it during scroll. -1 when
|
||||
// empty → row.index === -1 is always false → no render.
|
||||
const lastUserIdx = useMemo(() => {
|
||||
for (let i = transcript.historyItems.length - 1; i >= 0; i--) {
|
||||
if (transcript.historyItems[i].role === 'user') return i
|
||||
const items = transcript.historyItems
|
||||
|
||||
for (let i = items.length - 1; i >= 0; i--) {
|
||||
if (items[i].role === 'user') {
|
||||
return i
|
||||
}
|
||||
}
|
||||
|
||||
return -1
|
||||
}, [transcript.historyItems])
|
||||
|
||||
|
|
@ -259,18 +263,9 @@ export const AppLayout = memo(function AppLayout({
|
|||
}: AppLayoutProps) {
|
||||
const overlay = useStore($overlayState)
|
||||
|
||||
// Inline mode: skip <AlternateScreen> so the TUI renders into the
|
||||
// primary buffer and the terminal's native scrollback can capture rows
|
||||
// that scroll off the top. Mouse tracking is still enabled via
|
||||
// AlternateScreen when the wrapper is on; in inline mode we leave it
|
||||
// to the host terminal, which typically does wheel → scrollback.
|
||||
//
|
||||
// `Fragment` (via alias so the JSX stays legible) drops the alt-screen
|
||||
// constraint while keeping the inner layout identical. Content height
|
||||
// will then follow flex-column growth, which means the ScrollBox below
|
||||
// grows beyond the viewport — the terminal's primary buffer scrolls
|
||||
// old rows off the top into native scrollback. Composer + progress
|
||||
// stay at the bottom via normal flow (they're the last siblings).
|
||||
// Inline mode skips AlternateScreen so the host terminal's native
|
||||
// scrollback captures rows scrolled off the top; composer + progress
|
||||
// stay anchored via normal flex-column flow.
|
||||
const Shell = INLINE_MODE ? Fragment : AlternateScreen
|
||||
const shellProps = INLINE_MODE ? {} : { mouseTracking }
|
||||
|
||||
|
|
@ -305,9 +300,6 @@ export const AppLayout = memo(function AppLayout({
|
|||
<ComposerPane actions={actions} composer={composer} status={status} />
|
||||
</PerfPane>
|
||||
|
||||
{/* FPS counter overlay: pinned to the bottom row, right
|
||||
aligned, gated on HERMES_TUI_FPS. Returns null + skips
|
||||
this subtree when disabled (zero cost). */}
|
||||
{SHOW_FPS && (
|
||||
<Box flexShrink={0} justifyContent="flex-end" paddingRight={1}>
|
||||
<FpsOverlay />
|
||||
|
|
|
|||
|
|
@ -1,10 +1,4 @@
|
|||
// FPS counter overlay — renders in the bottom-right corner when
|
||||
// HERMES_TUI_FPS=1. Zero-cost when disabled (returns null at the
|
||||
// top of the component; React skips the whole subtree).
|
||||
//
|
||||
// Subscribes to $fpsState via nanostores. The store is only updated
|
||||
// when the env flag is on (trackFrame is undefined otherwise), so we
|
||||
// also gate the subscription on SHOW_FPS to avoid a useless listener.
|
||||
// FPS counter overlay (HERMES_TUI_FPS=1). Zero-cost when disabled.
|
||||
|
||||
import { Text } from '@hermes/ink'
|
||||
import { useStore } from '@nanostores/react'
|
||||
|
|
@ -12,17 +6,7 @@ import { useStore } from '@nanostores/react'
|
|||
import { SHOW_FPS } from '../config/env.js'
|
||||
import { $fpsState } from '../lib/fpsStore.js'
|
||||
|
||||
const fpsColor = (fps: number) => {
|
||||
if (fps >= 50) {
|
||||
return 'green'
|
||||
}
|
||||
|
||||
if (fps >= 30) {
|
||||
return 'yellow'
|
||||
}
|
||||
|
||||
return 'red'
|
||||
}
|
||||
const fpsColor = (fps: number) => (fps >= 50 ? 'green' : fps >= 30 ? 'yellow' : 'red')
|
||||
|
||||
export function FpsOverlay() {
|
||||
if (!SHOW_FPS) {
|
||||
|
|
@ -35,14 +19,10 @@ export function FpsOverlay() {
|
|||
function FpsOverlayInner() {
|
||||
const { fps, lastDurationMs, totalFrames } = useStore($fpsState)
|
||||
|
||||
// Zero-pad to stable width so the corner doesn't jitter as digits
|
||||
// come and go. Format: " 62fps 0.3ms #12345"
|
||||
const fpsStr = fps.toFixed(1).padStart(5)
|
||||
const durStr = lastDurationMs.toFixed(1).padStart(5)
|
||||
|
||||
// Zero-pad widths so digit churn doesn't jitter the corner.
|
||||
return (
|
||||
<Text color={fpsColor(fps)}>
|
||||
{fpsStr}fps · {durStr}ms · #{totalFrames}
|
||||
{fps.toFixed(1).padStart(5)}fps · {lastDurationMs.toFixed(1).padStart(5)}ms · #{totalFrames}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,5 @@
|
|||
import { Box, Link, Text } from '@hermes/ink'
|
||||
import { memo, useMemo, type ReactNode } from 'react'
|
||||
import { memo, type ReactNode, useMemo } from 'react'
|
||||
|
||||
import { ensureEmojiPresentation } from '../lib/emoji.js'
|
||||
import { highlightLine, isHighlightable } from '../lib/syntax.js'
|
||||
|
|
@ -213,26 +213,23 @@ function MdInline({ t, text }: { t: Theme; text: string }) {
|
|||
return <Text>{parts.length ? parts : <Text>{text}</Text>}</Text>
|
||||
}
|
||||
|
||||
// Cross-instance parsed-children cache. `Md` is mounted fresh whenever a
|
||||
// virtualized row enters the mount window — useMemo's per-instance cache
|
||||
// doesn't survive remounts, so PageUp into cold/resumed history reparses
|
||||
// every row (markdown scan + per-line syntax highlight).
|
||||
//
|
||||
// Outer WeakMap keyed by theme so palette swaps drop stale baked-in colors
|
||||
// without code intervention. Inner Map is LRU-bounded; key folds `compact`
|
||||
// in so the two layout modes don't poison each other.
|
||||
// Cross-instance parsed-children cache: useMemo's per-instance cache dies
|
||||
// on remount, so virtualization re-parses every row that scrolls back into
|
||||
// view. Theme-keyed WeakMap drops stale palettes; inner Map is LRU-bounded.
|
||||
const MD_CACHE_LIMIT = 512
|
||||
const mdCache = new WeakMap<Theme, Map<string, ReactNode[]>>()
|
||||
|
||||
const cacheBucket = (t: Theme) => {
|
||||
let b = mdCache.get(t)
|
||||
const b = mdCache.get(t)
|
||||
|
||||
if (!b) {
|
||||
b = new Map()
|
||||
mdCache.set(t, b)
|
||||
if (b) {
|
||||
return b
|
||||
}
|
||||
|
||||
return b
|
||||
const fresh = new Map<string, ReactNode[]>()
|
||||
mdCache.set(t, fresh)
|
||||
|
||||
return fresh
|
||||
}
|
||||
|
||||
const cacheGet = (b: Map<string, ReactNode[]>, key: string) => {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue