fix(tui): raise picker selection contrast with inverse + bold

Selected rows in the model/session/skills pickers and approval/clarify
prompts only changed from dim gray to cornsilk, which reads as low
contrast on lighter themes and LCDs (reported during TUI v2 blitz).

Switch the selected row to `inverse bold` with the brand accent color
across modelPicker, sessionPicker, skillsHub, and prompts so the
highlight is terminal-portable and unambiguous. Unselected rows stay
dim. Also extends the sessionPicker middle meta column (which was
always dim) to inherit the row's selection state.
This commit is contained in:
Brooklyn Nicholson 2026-04-21 10:47:31 -05:00
parent c3b8c8e42c
commit fc6a27098e
24 changed files with 248 additions and 93 deletions

View file

@ -28,8 +28,7 @@ function FaceTicker({ color, startedAt }: { color: string; startedAt?: null | nu
return (
<Text color={color}>
{FACES[tick % FACES.length]} {VERBS[tick % VERBS.length]}
{startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''}
{FACES[tick % FACES.length]} {VERBS[tick % VERBS.length]}{startedAt ? ` · ${fmtDuration(now - startedAt)}` : ''}
</Text>
)
}
@ -127,7 +126,11 @@ export function StatusRule({
<Box flexShrink={1} width={leftWidth}>
<Text color={t.color.bronze} wrap="truncate-end">
{'─ '}
{busy ? <FaceTicker color={statusColor} startedAt={turnStartedAt} /> : <Text color={statusColor}>{status}</Text>}
{busy ? (
<FaceTicker color={statusColor} startedAt={turnStartedAt} />
) : (
<Text color={statusColor}>{status}</Text>
)}
<Text color={t.color.dim}> {model}</Text>
{ctxLabel ? <Text color={t.color.dim}> {ctxLabel}</Text> : null}
{bar ? (

View file

@ -174,7 +174,9 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
</Text>
<Text color={t.color.dim}>Current model: {currentModel || '(unknown)'}</Text>
<Text color={t.color.label}>{provider?.warning ? `warning: ${provider.warning}` : ' '}</Text>
<Text color={t.color.label} wrap="truncate-end">
{provider?.warning ? `warning: ${provider.warning}` : ' '}
</Text>
<Text color={t.color.dim}>{off > 0 ? `${off} more` : ' '}</Text>
{Array.from({ length: VISIBLE }, (_, i) => {
@ -183,20 +185,22 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
return row ? (
<Text
color={providerIdx === idx ? t.color.cornsilk : t.color.dim}
bold={providerIdx === idx}
color={providerIdx === idx ? t.color.amber : t.color.dim}
inverse={providerIdx === idx}
key={providers[idx]?.slug ?? `row-${idx}`}
>
{providerIdx === idx ? '▸ ' : ' '}
{i + 1}. {row}
</Text>
) : (
<Text key={`pad-${i}`}> </Text>
<Text color={t.color.dim} key={`pad-${i}`}>
{' '}
</Text>
)
})}
<Text color={t.color.dim}>
{off + VISIBLE < rows.length ? `${rows.length - off - VISIBLE} more` : ' '}
</Text>
<Text color={t.color.dim}>{off + VISIBLE < rows.length ? `${rows.length - off - VISIBLE} more` : ' '}</Text>
<Text color={t.color.dim}>persist: {persistGlobal ? 'global' : 'session'} · g toggle</Text>
<Text color={t.color.dim}>/ select · Enter choose · 1-9,0 quick · Esc cancel</Text>
@ -213,7 +217,9 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
</Text>
<Text color={t.color.dim}>{names[providerIdx] || '(unknown provider)'}</Text>
<Text color={t.color.label}>{provider?.warning ? `warning: ${provider.warning}` : ' '}</Text>
<Text color={t.color.label} wrap="truncate-end">
{provider?.warning ? `warning: ${provider.warning}` : ' '}
</Text>
<Text color={t.color.dim}>{off > 0 ? `${off} more` : ' '}</Text>
{Array.from({ length: VISIBLE }, (_, i) => {
@ -226,13 +232,17 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
no models listed for this provider
</Text>
) : (
<Text key={`pad-${i}`}> </Text>
<Text color={t.color.dim} key={`pad-${i}`}>
{' '}
</Text>
)
}
return (
<Text
color={modelIdx === idx ? t.color.cornsilk : t.color.dim}
bold={modelIdx === idx}
color={modelIdx === idx ? t.color.amber : t.color.dim}
inverse={modelIdx === idx}
key={`${provider?.slug ?? 'prov'}:${idx}:${row}`}
>
{modelIdx === idx ? '▸ ' : ' '}

View file

@ -1,11 +1,11 @@
import { Box, Text, useInput } from '@hermes/ink'
import { useState } from 'react'
import { isMac } from '../lib/platform.js'
import type { Theme } from '../theme.js'
import type { ApprovalReq, ClarifyReq, ConfirmReq } from '../types.js'
import { TextInput } from './textInput.js'
import { isMac } from '../lib/platform.js'
const OPTS = ['once', 'session', 'always', 'deny'] as const
const LABELS = { always: 'Always allow', deny: 'Deny', once: 'Allow once', session: 'Allow this session' } as const
@ -64,8 +64,8 @@ export function ApprovalPrompt({ onChoice, req, t }: ApprovalPromptProps) {
{OPTS.map((o, i) => (
<Text key={o}>
<Text color={sel === i ? t.color.warn : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>
<Text bold={sel === i} color={sel === i ? t.color.warn : t.color.dim} inverse={sel === i}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {LABELS[o]}
</Text>
</Text>
@ -130,7 +130,8 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
</Box>
<Text color={t.color.dim}>
Enter send · Esc {choices.length ? 'back' : 'cancel'} · {isMac ? 'Cmd+C copy · Cmd+V paste · Ctrl+C cancel' : 'Ctrl+C cancel'}
Enter send · Esc {choices.length ? 'back' : 'cancel'} ·{' '}
{isMac ? 'Cmd+C copy · Cmd+V paste · Ctrl+C cancel' : 'Ctrl+C cancel'}
</Text>
</Box>
)
@ -142,8 +143,8 @@ export function ClarifyPrompt({ cols = 80, onAnswer, onCancel, req, t }: Clarify
{[...choices, 'Other (type your answer)'].map((c, i) => (
<Text key={i}>
<Text color={sel === i ? t.color.label : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>
<Text bold={sel === i} color={sel === i ? t.color.label : t.color.dim} inverse={sel === i}>
{sel === i ? '▸ ' : ' '}
{i + 1}. {c}
</Text>
</Text>

View file

@ -108,24 +108,29 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
{items.slice(off, off + VISIBLE).map((s, vi) => {
const i = off + vi
const selected = sel === i
return (
<Box key={s.id}>
<Text color={sel === i ? t.color.label : t.color.dim}>{sel === i ? '▸ ' : ' '}</Text>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
{selected ? '▸ ' : ' '}
</Text>
<Box width={30}>
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
{String(i + 1).padStart(2)}. [{s.id}]
</Text>
</Box>
<Box width={30}>
<Text color={t.color.dim}>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
({s.message_count} msgs, {age(s.started_at)}, {s.source || 'tui'})
</Text>
</Box>
<Text color={sel === i ? t.color.cornsilk : t.color.dim}>{s.title || s.preview || '(untitled)'}</Text>
<Text bold={selected} color={selected ? t.color.amber : t.color.dim} inverse={selected}>
{s.title || s.preview || '(untitled)'}
</Text>
</Box>
)
})}

View file

@ -219,7 +219,12 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
const idx = off + i
return (
<Text color={catIdx === idx ? t.color.cornsilk : t.color.dim} key={row}>
<Text
bold={catIdx === idx}
color={catIdx === idx ? t.color.amber : t.color.dim}
inverse={catIdx === idx}
key={row}
>
{catIdx === idx ? '▸ ' : ' '}
{i + 1}. {row}
</Text>
@ -249,7 +254,12 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
const idx = off + i
return (
<Text color={skillIdx === idx ? t.color.cornsilk : t.color.dim} key={row}>
<Text
bold={skillIdx === idx}
color={skillIdx === idx ? t.color.amber : t.color.dim}
inverse={skillIdx === idx}
key={row}
>
{skillIdx === idx ? '▸ ' : ' '}
{i + 1}. {row}
</Text>

View file

@ -277,8 +277,9 @@ function useFwdDelete(active: boolean) {
type PasteResult = { cursor: number; value: string } | null
const isPasteResultPromise = (value: PasteResult | Promise<PasteResult> | null | undefined): value is Promise<PasteResult> =>
!!value && typeof (value as PromiseLike<PasteResult>).then === 'function'
const isPasteResultPromise = (
value: PasteResult | Promise<PasteResult> | null | undefined
): value is Promise<PasteResult> => !!value && typeof (value as PromiseLike<PasteResult>).then === 'function'
export function TextInput({
columns = 80,
@ -522,9 +523,11 @@ export function TextInput({
}
const range = selRange()
const nextValue = range
? vRef.current.slice(0, range.start) + cleaned + vRef.current.slice(range.end)
: vRef.current.slice(0, curRef.current) + cleaned + vRef.current.slice(curRef.current)
const nextCursor = range ? range.start + cleaned.length : curRef.current + cleaned.length
commit(nextValue, nextCursor)
@ -778,7 +781,9 @@ interface TextInputProps {
focus?: boolean
mask?: string
onChange: (v: string) => void
onPaste?: (e: PasteEvent) => { cursor: number; value: string } | Promise<{ cursor: number; value: string } | null> | null
onPaste?: (
e: PasteEvent
) => { cursor: number; value: string } | Promise<{ cursor: number; value: string } | null> | null
onSubmit?: (v: string) => void
placeholder?: string
value: string