mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-11 03:31:55 +00:00
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.
106 lines
2.7 KiB
TypeScript
106 lines
2.7 KiB
TypeScript
import { type AnsiCode, ansiCodesToString, reduceAnsiCodes, tokenize, undoAnsiCodes } from '@alcalzone/ansi-tokenize'
|
|
|
|
import { lruEvict } from '../ink/lru.js'
|
|
import { stringWidth } from '../ink/stringWidth.js'
|
|
|
|
function isEndCode(code: AnsiCode): boolean {
|
|
return code.code === code.endCode
|
|
}
|
|
|
|
function filterStartCodes(codes: AnsiCode[]): AnsiCode[] {
|
|
return codes.filter(c => !isEndCode(c))
|
|
}
|
|
|
|
// LRU cache: same (string, start, end) → same output. Output.get() re-emits
|
|
// identical writes every frame for stable transcript content; this avoids
|
|
// re-tokenizing them. CPU profile (Apr 2026) showed sliceAnsi at 18% total
|
|
// time during scroll. Bounded at 4096 entries — entries are short clipped
|
|
// lines so memory cost is small.
|
|
const sliceCache = new Map<string, string>()
|
|
const SLICE_CACHE_LIMIT = 4096
|
|
|
|
export default function sliceAnsi(str: string, start: number, end?: number): string {
|
|
if (!str) {
|
|
return ''
|
|
}
|
|
|
|
// Hot-path: only cache when end is defined (the Output.get() use-case).
|
|
if (end !== undefined) {
|
|
const key = `${start}|${end}|${str}`
|
|
const cached = sliceCache.get(key)
|
|
|
|
if (cached !== undefined) {
|
|
sliceCache.delete(key)
|
|
sliceCache.set(key, cached)
|
|
|
|
return cached
|
|
}
|
|
|
|
const result = computeSlice(str, start, end)
|
|
|
|
if (sliceCache.size >= SLICE_CACHE_LIMIT) {
|
|
sliceCache.delete(sliceCache.keys().next().value!)
|
|
}
|
|
|
|
sliceCache.set(key, result)
|
|
|
|
return result
|
|
}
|
|
|
|
return computeSlice(str, start, end)
|
|
}
|
|
|
|
export function sliceCacheSize(): number {
|
|
return sliceCache.size
|
|
}
|
|
|
|
export function evictSliceCache(keepRatio = 0): void {
|
|
lruEvict(sliceCache, keepRatio)
|
|
}
|
|
|
|
function computeSlice(str: string, start: number, end?: number): string {
|
|
const tokens = tokenize(str)
|
|
let activeCodes: AnsiCode[] = []
|
|
let position = 0
|
|
let result = ''
|
|
let include = false
|
|
|
|
for (const token of tokens) {
|
|
const width = token.type === 'ansi' ? 0 : token.fullWidth ? 2 : stringWidth(token.value)
|
|
|
|
if (end !== undefined && position >= end) {
|
|
if (token.type === 'ansi' || width > 0 || !include) {
|
|
break
|
|
}
|
|
}
|
|
|
|
if (token.type === 'ansi') {
|
|
activeCodes.push(token)
|
|
|
|
if (include) {
|
|
result += token.code
|
|
}
|
|
} else {
|
|
if (!include && position >= start) {
|
|
if (start > 0 && width === 0) {
|
|
continue
|
|
}
|
|
|
|
include = true
|
|
activeCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
|
|
result = ansiCodesToString(activeCodes)
|
|
}
|
|
|
|
if (include) {
|
|
result += token.value
|
|
}
|
|
|
|
position += width
|
|
}
|
|
}
|
|
|
|
const activeStartCodes = filterStartCodes(reduceAnsiCodes(activeCodes))
|
|
result += ansiCodesToString(undoAnsiCodes(activeStartCodes))
|
|
|
|
return result
|
|
}
|