From b3e7a412e24e8a2ee97c28b511440808e161628c Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Mon, 27 Apr 2026 12:44:24 -0500 Subject: [PATCH] fix(tui): wire Ctrl+L to Ink forceRedraw path Expose a small forceRedraw API from @hermes/ink and use it for Ctrl/Cmd+L so the hotkey performs a real terminal clear + full repaint instead of a no-op state patch. --- ui-tui/packages/hermes-ink/index.d.ts | 2 +- ui-tui/packages/hermes-ink/src/entry-exports.ts | 2 +- ui-tui/packages/hermes-ink/src/ink/root.ts | 10 ++++++++++ ui-tui/src/app/useInputHandlers.ts | 8 ++++---- 4 files changed, 16 insertions(+), 6 deletions(-) diff --git a/ui-tui/packages/hermes-ink/index.d.ts b/ui-tui/packages/hermes-ink/index.d.ts index 9375cb4b3a..92aeb0a4f2 100644 --- a/ui-tui/packages/hermes-ink/index.d.ts +++ b/ui-tui/packages/hermes-ink/index.d.ts @@ -30,7 +30,7 @@ export { useTerminalFocus } from './src/ink/hooks/use-terminal-focus.ts' export { useTerminalTitle } from './src/ink/hooks/use-terminal-title.ts' export { useTerminalViewport } from './src/ink/hooks/use-terminal-viewport.ts' export { default as measureElement } from './src/ink/measure-element.ts' -export { createRoot, default as render, renderSync } from './src/ink/root.ts' +export { createRoot, default as render, forceRedraw, renderSync } from './src/ink/root.ts' export type { Instance, RenderOptions, Root } from './src/ink/root.ts' export { stringWidth } from './src/ink/stringWidth.ts' export { default as TextInput, UncontrolledTextInput } from 'ink-text-input' diff --git a/ui-tui/packages/hermes-ink/src/entry-exports.ts b/ui-tui/packages/hermes-ink/src/entry-exports.ts index d56387dd5b..6bfb0e4955 100644 --- a/ui-tui/packages/hermes-ink/src/entry-exports.ts +++ b/ui-tui/packages/hermes-ink/src/entry-exports.ts @@ -23,7 +23,7 @@ export { useTerminalTitle } from './ink/hooks/use-terminal-title.js' export { useTerminalViewport } from './ink/hooks/use-terminal-viewport.js' export { default as measureElement } from './ink/measure-element.js' export { scrollFastPathStats, type ScrollFastPathStats } from './ink/render-node-to-output.js' -export { createRoot, default as render, renderSync } from './ink/root.js' +export { createRoot, default as render, forceRedraw, renderSync } from './ink/root.js' export { stringWidth } from './ink/stringWidth.js' export { isXtermJs } from './ink/terminal.js' export { default as TextInput, UncontrolledTextInput } from 'ink-text-input' diff --git a/ui-tui/packages/hermes-ink/src/ink/root.ts b/ui-tui/packages/hermes-ink/src/ink/root.ts index 27ace59a6b..fdfb97a6e7 100644 --- a/ui-tui/packages/hermes-ink/src/ink/root.ts +++ b/ui-tui/packages/hermes-ink/src/ink/root.ts @@ -73,6 +73,16 @@ export type Root = { waitUntilExit: () => Promise } +export const forceRedraw = (stdout: NodeJS.WriteStream = process.stdout): boolean => { + const instance = instances.get(stdout) + if (!instance) { + return false + } + + instance.forceRedraw() + return true +} + /** * Mount a component and render the output. */ diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index fe143ee36a..5aab1d1bf8 100644 --- a/ui-tui/src/app/useInputHandlers.ts +++ b/ui-tui/src/app/useInputHandlers.ts @@ -1,4 +1,4 @@ -import { useInput } from '@hermes/ink' +import { forceRedraw, useInput } from '@hermes/ink' import { useStore } from '@nanostores/react' import { useEffect, useRef } from 'react' @@ -18,7 +18,7 @@ import type { InputHandlerContext, InputHandlerResult } from './interfaces.js' import { $isBlocked, $overlayState, patchOverlayState } from './overlayStore.js' import { turnController } from './turnController.js' import { patchTurnState } from './turnStore.js' -import { getUiState, patchUiState } from './uiStore.js' +import { getUiState } from './uiStore.js' const isCtrl = (key: { ctrl: boolean }, ch: string, target: string) => key.ctrl && ch.toLowerCase() === target @@ -380,8 +380,8 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { if (isAction(key, ch, 'l')) { clearSelection() - - return patchUiState({}) + forceRedraw(terminal.stdout ?? process.stdout) + return } if (isVoiceToggleKey(key, ch)) {