Merge origin/main into bb/tui-parity-mutating-commands

Resolve session command merge conflict and keep the branch current with main so PR #16656 is mergeable.
This commit is contained in:
Brooklyn Nicholson
2026-04-27 12:51:11 -05:00
8 changed files with 27 additions and 23 deletions

View File

@@ -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'

View File

@@ -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'

View File

@@ -73,6 +73,16 @@ export type Root = {
waitUntilExit: () => Promise<void>
}
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.
*/

View File

@@ -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')

View File

@@ -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'
})
})

View File

@@ -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[] = [

View File

@@ -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)) {

View File

@@ -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'],