From 87d1c5b0c3a4c58c22b233fd6b1d9064d030ce56 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Fri, 25 Sep 2026 16:03:37 -0500 Subject: [PATCH] refactor(tui): drop redundant native-mode guards --- hermes_cli/main.py | 2 +- ui-tui/scripts/bench-native-history.tsx | 20 ++++++++++++-- ui-tui/src/components/appLayout.tsx | 36 +++++++++---------------- ui-tui/src/lib/terminalModes.ts | 9 ++++--- 4 files changed, 37 insertions(+), 30 deletions(-) diff --git a/hermes_cli/main.py b/hermes_cli/main.py index f392c6f86f..980877cb58 100644 --- a/hermes_cli/main.py +++ b/hermes_cli/main.py @@ -1841,7 +1841,7 @@ def cmd_chat(args): _launch_tui( passthrough.pop("resume"), tui_dev=getattr(args, "tui_dev", False), - native_mode=True if getattr(args, "tui_native", False) else None, + native_mode=getattr(args, "tui_native", False) or None, model=getattr(args, "model", None), accept_hooks=getattr(args, "accept_hooks", False), **passthrough, diff --git a/ui-tui/scripts/bench-native-history.tsx b/ui-tui/scripts/bench-native-history.tsx index 0549e73cc7..63346458d2 100644 --- a/ui-tui/scripts/bench-native-history.tsx +++ b/ui-tui/scripts/bench-native-history.tsx @@ -91,7 +91,7 @@ function VirtualHarness({ items, scrollRef }: VirtualProps) { {virtual.topSpacer > 0 ? : null} {items.slice(virtual.start, virtual.end).map(item => ( - + {item.text} ))} @@ -108,6 +108,7 @@ async function runSample(mode: 'native' | 'virtual', itemCount: number): Promise const scrollRef = { current: null as ScrollBoxHandle | null } const items = makeItems(itemCount) + const renderHarness = (nextItems: readonly Item[]) => mode === 'native' ? ( @@ -117,23 +118,27 @@ async function runSample(mode: 'native' | 'virtual', itemCount: number): Promise const heapBefore = process.memoryUsage?.().heapUsed ?? null const mountStart = performance.now() + const instance = renderSync(renderHarness(items), { patchConsole: false, stderr: stderr as unknown as NodeJS.WriteStream, stdin: stdin as unknown as NodeJS.ReadStream, stdout: stdout as unknown as NodeJS.WriteStream }) + await settle() const mountMs = performance.now() - mountStart const rerenderItems = items.map((item, index) => index === items.length - 1 ? { ...item, text: `${item.text} rerender` } : item ) + const rerenderStart = performance.now() instance.rerender(renderHarness(rerenderItems)) await settle() const rerenderMs = performance.now() - rerenderStart const heapAfter = process.memoryUsage?.().heapUsed ?? null + const sample = { heapDeltaBytes: heapBefore === null || heapAfter === null ? null : heapAfter - heapBefore, mountMs, @@ -147,13 +152,16 @@ async function runSample(mode: 'native' | 'virtual', itemCount: number): Promise stdin.destroy() stdout.destroy() stderr.destroy() + return sample } function distribution(values: number[]) { const sorted = [...values].sort((a, b) => a - b) + const percentile = (p: number) => sorted[Math.min(sorted.length - 1, Math.max(0, Math.ceil(sorted.length * p) - 1))] ?? 0 + return { max: sorted.at(-1) ?? 0, mean: sorted.reduce((sum, value) => sum + value, 0) / Math.max(1, sorted.length), @@ -168,7 +176,9 @@ function numericArg(name: string, fallback: number) { .slice(2) .find(arg => arg.startsWith(`--${name}=`)) ?.split('=', 2)[1] + const parsed = Number(raw) + return Number.isSafeInteger(parsed) && parsed >= 0 ? parsed : fallback } @@ -177,11 +187,16 @@ function workloadsArg() { .slice(2) .find(arg => arg.startsWith('--items=')) ?.split('=', 2)[1] - if (!raw) return DEFAULT_WORKLOADS + + if (!raw) { + return DEFAULT_WORKLOADS + } const parsed = raw.split(',').map(Number) + if (parsed.some(value => !Number.isSafeInteger(value) || value <= 0)) { throw new Error(`invalid --items workload list: ${raw}`) } + return parsed } @@ -209,6 +224,7 @@ async function main() { const virtual: Sample[] = [] const native: Sample[] = [] + for (let sample = 0; sample < sampleCount; sample++) { virtual.push(await runSample('virtual', itemCount)) native.push(await runSample('native', itemCount)) diff --git a/ui-tui/src/components/appLayout.tsx b/ui-tui/src/components/appLayout.tsx index 5ceac38a64..d4591835ef 100644 --- a/ui-tui/src/components/appLayout.tsx +++ b/ui-tui/src/components/appLayout.tsx @@ -181,14 +181,18 @@ const TranscriptPane = memo(function TranscriptPane({ [transcript.historyItems] ) + const clearBlankSelection = (e: { cellIsBlank?: boolean }) => { + if (e.cellIsBlank) { + actions.clearSelection() + } + } + const transcriptContent = ( - {!nativeMode && transcript.virtualHistory.topSpacer > 0 ? ( - - ) : null} + {transcript.virtualHistory.topSpacer > 0 ? : null} {transcript.virtualRows - .slice(nativeMode ? 0 : transcript.virtualHistory.start, nativeMode ? undefined : transcript.virtualHistory.end) + .slice(transcript.virtualHistory.start, transcript.virtualHistory.end) .map(row => ( {row.msg.role === 'user' && firstUserIdx >= 0 && row.index > firstUserIdx && ( @@ -236,9 +240,7 @@ const TranscriptPane = memo(function TranscriptPane({ ))} - {!nativeMode && transcript.virtualHistory.bottomSpacer > 0 ? ( - - ) : null} + {transcript.virtualHistory.bottomSpacer > 0 ? : null} {nativeMode ? ( - { - if (e.cellIsBlank) { - actions.clearSelection() - } - }} - > + {transcriptContent} ) : ( @@ -274,11 +268,7 @@ const TranscriptPane = memo(function TranscriptPane({ flexDirection="column" flexGrow={1} flexShrink={1} - onClick={(e: { cellIsBlank?: boolean }) => { - if (e.cellIsBlank) { - actions.clearSelection() - } - }} + onClick={clearBlankSelection} ref={transcript.scrollRef} stickyScroll > @@ -421,10 +411,10 @@ const ComposerPane = memo(function ComposerPane({ - {!nativeMode && } + {!nativeMode && } {!nativeMode && composer.input === '?' && !composer.inputBuf.length && } diff --git a/ui-tui/src/lib/terminalModes.ts b/ui-tui/src/lib/terminalModes.ts index e6f7147370..a2c1371fbf 100644 --- a/ui-tui/src/lib/terminalModes.ts +++ b/ui-tui/src/lib/terminalModes.ts @@ -21,6 +21,11 @@ export const TERMINAL_MODE_RESET = '\x1b[0m' + // attributes '\x1b[?25h' // cursor visible +type ResettableStream = Pick & { + fd?: number +} + +/** Native mode leaves its frame in the primary buffer; wipe it on exit so the shell prompt starts clean. */ export function clearNativeTuiFrame(stream: ResettableStream = process.stdout): boolean { if (!stream.isTTY) { return false @@ -34,10 +39,6 @@ export function clearNativeTuiFrame(stream: ResettableStream = process.stdout): } } -type ResettableStream = Pick & { - fd?: number -} - // OSC 10/11 set the terminal's DEFAULT foreground/background — so every cell, // including text rendered with no explicit color (markdown body, borders, // third-party output), takes the skin instead of the host profile's defaults.