mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-27 17:58:07 +00:00
Merge pull request #69616 from NousResearch/bb/tui-input-theme-color
fix(ui-tui): input text goes invisible when a live skin flips the terminal's polarity
This commit is contained in:
commit
d6080d4cf7
7 changed files with 74 additions and 7 deletions
|
|
@ -1,6 +1,11 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { canFastAppendShape, canFastBackspaceShape, supportsFastEchoTerminal } from '../components/textInput.js'
|
||||
import {
|
||||
canFastAppendShape,
|
||||
canFastBackspaceShape,
|
||||
colorizeEcho,
|
||||
supportsFastEchoTerminal
|
||||
} from '../components/textInput.js'
|
||||
|
||||
// The fast-echo path bypasses Ink and writes characters directly to stdout
|
||||
// for the common case of typing plain English at the end of the line. These
|
||||
|
|
@ -173,6 +178,27 @@ describe('canFastBackspaceShape', () => {
|
|||
})
|
||||
})
|
||||
|
||||
describe('colorizeEcho', () => {
|
||||
// The fast-echo bypass writes raw cells past Ink, so a themed input must
|
||||
// carry the theme fg explicitly — a default-fg glyph goes invisible when a
|
||||
// skin repaints the background to the opposite polarity (dark skin on a
|
||||
// light terminal ⇒ black-on-black).
|
||||
|
||||
it('wraps the write in truecolor fg + reset for a hex theme color', () => {
|
||||
expect(colorizeEcho('x', '#ff2d95')).toBe('\x1b[38;2;255;45;149mx\x1b[39m')
|
||||
})
|
||||
|
||||
it('passes through untouched without a color (unthemed keeps terminal default)', () => {
|
||||
expect(colorizeEcho('x')).toBe('x')
|
||||
expect(colorizeEcho('x', undefined)).toBe('x')
|
||||
})
|
||||
|
||||
it('passes through on a non-hex color (never emit a garbage SGR)', () => {
|
||||
expect(colorizeEcho('x', 'red')).toBe('x')
|
||||
expect(colorizeEcho('x', '#fff')).toBe('x')
|
||||
})
|
||||
})
|
||||
|
||||
describe('supportsFastEchoTerminal', () => {
|
||||
it('disables fast-echo in Apple Terminal', () => {
|
||||
expect(supportsFastEchoTerminal({ TERM_PROGRAM: 'Apple_Terminal' } as NodeJS.ProcessEnv)).toBe(false)
|
||||
|
|
|
|||
|
|
@ -866,7 +866,13 @@ export function ActiveSessionSwitcher({
|
|||
<>
|
||||
<Box marginTop={1}>
|
||||
<Text color={t.color.label}>prompt › </Text>
|
||||
<TextInput columns={promptColumns} onChange={setDraft} onSubmit={submitDraft} value={draft} />
|
||||
<TextInput
|
||||
color={t.color.text}
|
||||
columns={promptColumns}
|
||||
onChange={setDraft}
|
||||
onSubmit={submitDraft}
|
||||
value={draft}
|
||||
/>
|
||||
</Box>
|
||||
<OrchestratorHintText segments={orchestratorContextHintSegments(true)} t={t} />
|
||||
<Text color={t.color.muted} wrap="truncate-end">
|
||||
|
|
|
|||
|
|
@ -417,6 +417,7 @@ const ComposerPane = memo(function ComposerPane({
|
|||
<Box flexGrow={0} flexShrink={0} height={inputHeight} width={inputColumns}>
|
||||
{/* Reserve the transcript scrollbar gutter too so typing never rewraps when the scrollbar column repaints. */}
|
||||
<TextInput
|
||||
color={ui.theme.color.text}
|
||||
columns={inputColumns}
|
||||
mouseApiRef={inputMouseRef}
|
||||
onChange={composer.updateInput}
|
||||
|
|
|
|||
|
|
@ -340,7 +340,7 @@ function BuyScreen({ ctx, onPatch, s, t }: ScreenProps) {
|
|||
<Text color={t.color.label}>Enter a custom amount:</Text>
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'$'}</Text>
|
||||
<TextInput columns={20} onChange={setCustom} onSubmit={submitCustom} value={custom} />
|
||||
<TextInput color={t.color.text} columns={20} onChange={setCustom} onSubmit={submitCustom} value={custom} />
|
||||
</Box>
|
||||
{error && <Text color={t.color.error}>{error}</Text>}
|
||||
<Text />
|
||||
|
|
@ -852,6 +852,7 @@ function AutoReloadScreen({ ctx, onClose, onPatch, s, t }: ScreenProps) {
|
|||
<Box borderColor={focused ? t.color.accent : t.color.border} borderStyle="round" paddingX={1}>
|
||||
<Text color={t.color.label}>{'$'}</Text>
|
||||
<TextInput
|
||||
color={t.color.text}
|
||||
columns={16}
|
||||
focus={focused}
|
||||
onChange={onChange}
|
||||
|
|
|
|||
|
|
@ -18,7 +18,14 @@ export function MaskedPrompt({ cols = 80, icon, label, onSubmit, sub, t }: Maske
|
|||
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'> '}</Text>
|
||||
<TextInput columns={Math.max(20, cols - 6)} mask="*" onChange={setValue} onSubmit={onSubmit} value={value} />
|
||||
<TextInput
|
||||
color={t.color.text}
|
||||
columns={Math.max(20, cols - 6)}
|
||||
mask="*"
|
||||
onChange={setValue}
|
||||
onSubmit={onSubmit}
|
||||
value={value}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
)
|
||||
|
|
|
|||
|
|
@ -192,7 +192,13 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
|
|||
|
||||
<Box>
|
||||
<Text color={t.color.label}>{'> '}</Text>
|
||||
<TextInput columns={Math.max(20, cols - 6)} onChange={setCustom} onSubmit={onAnswer} value={custom} />
|
||||
<TextInput
|
||||
color={t.color.text}
|
||||
columns={Math.max(20, cols - 6)}
|
||||
onChange={setCustom}
|
||||
onSubmit={onAnswer}
|
||||
value={custom}
|
||||
/>
|
||||
</Box>
|
||||
|
||||
<Text color={t.color.muted}>
|
||||
|
|
|
|||
|
|
@ -58,6 +58,14 @@ const colorizeHint = (s: string, hex?: string) => {
|
|||
return `${ESC}[38;2;${r};${g};${b}m${s}${ESC}[39m`
|
||||
}
|
||||
|
||||
// Typed-text fast-echo must carry the SAME explicit fg the Ink render uses:
|
||||
// the bypass writes raw cells, and a default-fg glyph goes invisible the
|
||||
// moment a skin repaints the background to the opposite polarity (a dark
|
||||
// skin on a light terminal ⇒ black-on-black). No color ⇒ passthrough, so
|
||||
// unthemed inputs keep the terminal default.
|
||||
export const colorizeEcho = (s: string, hex?: string) =>
|
||||
/^#[0-9a-f]{6}$/i.test(hex ?? '') ? `${ESC}[38;2;${hintRgb(hex).join(';')}m${s}${ESC}[39m` : s
|
||||
|
||||
/** Synthetic placeholder cursor: a hint-colored chip with luminance-picked
|
||||
* ink, standing in for the hidden hardware cursor (bubbles pattern). */
|
||||
const hintCursorCell = (ch: string, hex?: string) => {
|
||||
|
|
@ -565,6 +573,7 @@ export function TextInput({
|
|||
voiceRecordKey = DEFAULT_VOICE_RECORD_KEY,
|
||||
placeholder = '',
|
||||
placeholderColor,
|
||||
color,
|
||||
focus = true
|
||||
}: TextInputProps) {
|
||||
const [cur, setCur] = useState(value.length)
|
||||
|
|
@ -1306,7 +1315,9 @@ export function TextInput({
|
|||
|
||||
if (simpleAppend) {
|
||||
const effect = fastAppendEffect(preInsertValue, preInsertCursor, text)
|
||||
stdout!.write(effect.write)
|
||||
// Same explicit fg as the Ink render (see the <Text color>) —
|
||||
// the bypass cell must not flash the terminal-default color.
|
||||
stdout!.write(colorizeEcho(effect.write, color))
|
||||
// ASCII-printable text advances the physical cursor by exactly
|
||||
// text.length cells (canFastAppendShape rejects non-ASCII,
|
||||
// wide chars, newlines). Notify Ink so the cached displayCursor
|
||||
|
|
@ -1395,7 +1406,14 @@ export function TextInput({
|
|||
ref={boxRef}
|
||||
width={columns}
|
||||
>
|
||||
<Text wrap="wrap">{rendered}</Text>
|
||||
{/* Explicit theme color on the typed text — default fg tracks the HOST
|
||||
terminal's polarity, not the skin's, so a live dark-skin repaint on a
|
||||
light terminal would otherwise leave the input black-on-black. chalk
|
||||
re-opens the outer color after embedded [39m closes (placeholder
|
||||
chips), and INV cursor/selection cells don't touch fg. */}
|
||||
<Text color={color} wrap="wrap">
|
||||
{rendered}
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -1416,6 +1434,8 @@ export interface PasteEvent {
|
|||
}
|
||||
|
||||
interface TextInputProps {
|
||||
/** Hex color for typed text (theme text); terminal default when omitted. */
|
||||
color?: string
|
||||
columns?: number
|
||||
focus?: boolean
|
||||
mask?: string
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue