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/__tests__/constants.test.ts b/ui-tui/src/__tests__/constants.test.ts index d069d24c2d..5f95078787 100644 --- a/ui-tui/src/__tests__/constants.test.ts +++ b/ui-tui/src/__tests__/constants.test.ts @@ -26,6 +26,12 @@ describe('constants', () => { }) }) + it('documents Ctrl/Cmd+L as non-destructive redraw', () => { + const hotkey = HOTKEYS.find(([k]) => k.endsWith('+L')) + expect(hotkey).toBeDefined() + expect(hotkey?.[1]).toBe('redraw / repaint') + }) + it('TOOL_VERBS maps known tools (verb-only, no emoji)', () => { expect(TOOL_VERBS.terminal).toBe('terminal') expect(TOOL_VERBS.read_file).toBe('reading') diff --git a/ui-tui/src/__tests__/createSlashHandler.test.ts b/ui-tui/src/__tests__/createSlashHandler.test.ts index ebacf2286c..de48c4bb48 100644 --- a/ui-tui/src/__tests__/createSlashHandler.test.ts +++ b/ui-tui/src/__tests__/createSlashHandler.test.ts @@ -26,7 +26,7 @@ describe('createSlashHandler', () => { expect(ctx.gateway.gw.request).not.toHaveBeenCalled() }) - it('persists typed /model switches by default', async () => { + it('keeps typed /model switches session-scoped by default', async () => { patchUiState({ sid: 'sid-abc' }) const ctx = buildCtx({ @@ -40,7 +40,7 @@ describe('createSlashHandler', () => { expect(ctx.gateway.rpc).toHaveBeenCalledWith('config.set', { key: 'model', session_id: 'sid-abc', - value: 'x-model --global' + value: 'x-model' }) }) diff --git a/ui-tui/src/app/slash/commands/session.ts b/ui-tui/src/app/slash/commands/session.ts index 0f201d480e..58b389b3b3 100644 --- a/ui-tui/src/app/slash/commands/session.ts +++ b/ui-tui/src/app/slash/commands/session.ts @@ -16,17 +16,9 @@ import { patchOverlayState } from '../../overlayStore.js' import { patchUiState } from '../../uiStore.js' import type { SlashCommand } from '../types.js' -const GLOBAL_MODEL_FLAG_RE = /(?:^|\s)--global(?:\s|$)/ - const TUI_SESSION_MODEL_RE = new RegExp(`(?:^|\\s)${TUI_SESSION_MODEL_FLAG}(?:\\s|$)`) const TUI_SESSION_STRIP_RE = new RegExp(`\\s*${TUI_SESSION_MODEL_FLAG}\\b\\s*`, 'g') -const persistedModelArg = (arg: string) => { - const trimmed = arg.trim() - - return !trimmed || GLOBAL_MODEL_FLAG_RE.test(trimmed) ? trimmed : `${trimmed} --global` -} - const stripTuiSessionFlag = (trimmed: string) => trimmed.replace(TUI_SESSION_STRIP_RE, ' ').replace(/\s+/g, ' ').trim() const modelValueForConfigSet = (arg: string) => { @@ -40,7 +32,7 @@ const modelValueForConfigSet = (arg: string) => { return stripTuiSessionFlag(trimmed) } - return persistedModelArg(trimmed) + return trimmed } export const sessionCommands: SlashCommand[] = [ diff --git a/ui-tui/src/app/useInputHandlers.ts b/ui-tui/src/app/useInputHandlers.ts index 0441c1d2c7..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 @@ -379,13 +379,9 @@ export function useInputHandlers(ctx: InputHandlerContext): InputHandlerResult { } if (isAction(key, ch, 'l')) { - if (actions.guardBusySessionSwitch()) { - return - } - - patchUiState({ status: 'forging session…' }) - - return actions.newSession() + clearSelection() + forceRedraw(terminal.stdout ?? process.stdout) + return } if (isVoiceToggleKey(key, ch)) { diff --git a/ui-tui/src/content/hotkeys.ts b/ui-tui/src/content/hotkeys.ts index 9a079fd2c6..50c293acd8 100644 --- a/ui-tui/src/content/hotkeys.ts +++ b/ui-tui/src/content/hotkeys.ts @@ -19,7 +19,7 @@ export const HOTKEYS: [string, string][] = [ ...copyHotkeys, [action + '+D', 'exit'], [action + '+G / Alt+G', 'open $EDITOR (Alt+G fallback for VSCode/Cursor)'], - [action + '+L', 'new session (clear)'], + [action + '+L', 'redraw / repaint'], [paste + '+V / /paste', 'paste text; /paste attaches clipboard image'], ['Tab', 'apply completion'], ['↑/↓', 'completions / queue edit / history'],