mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
fix(tui): truncate long picker rows so the height stays stable
A6 added a fixed-height grid (Array.from({length: VISIBLE})), but the
row <Text> itself had no wrap prop so Ink defaulted to wrap="wrap".
A sufficiently long model or provider name would wrap to a second
visual line and bounce the overall picker height right back — which
is exactly what reappeared during the TUI v2 blitz retest on /model.
Pin every picker row (and the empty-state / padding rows) to
wrap="truncate-end" so each slot is guaranteed one line. Applies
across modelPicker, sessionPicker, and skillsHub.
This commit is contained in:
parent
fc6a27098e
commit
4ada76b6ed
3 changed files with 66 additions and 27 deletions
|
|
@ -1,4 +1,4 @@
|
|||
import { Box, Text, useInput } from '@hermes/ink'
|
||||
import { Box, Text, useInput, useStdout } from '@hermes/ink'
|
||||
import { useEffect, useMemo, useState } from 'react'
|
||||
|
||||
import { providerDisplayNames } from '../domain/providers.js'
|
||||
|
|
@ -8,6 +8,8 @@ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
|||
import type { Theme } from '../theme.js'
|
||||
|
||||
const VISIBLE = 12
|
||||
const MIN_WIDTH = 40
|
||||
const MAX_WIDTH = 90
|
||||
|
||||
const pageOffset = (count: number, sel: number) => Math.max(0, Math.min(sel - Math.floor(VISIBLE / 2), count - VISIBLE))
|
||||
|
||||
|
|
@ -27,6 +29,13 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
const [modelIdx, setModelIdx] = useState(0)
|
||||
const [stage, setStage] = useState<'model' | 'provider'>('provider')
|
||||
|
||||
const { stdout } = useStdout()
|
||||
// Pin the picker to a stable width so the FloatBox parent (which shrinks-
|
||||
// to-fit with alignSelf="flex-start") doesn't resize as long provider /
|
||||
// model names scroll into view, and so `wrap="truncate-end"` on each row
|
||||
// has an actual constraint to truncate against.
|
||||
const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6))
|
||||
|
||||
useEffect(() => {
|
||||
gw.request<ModelOptionsResponse>('model.options', sessionId ? { session_id: sessionId } : {})
|
||||
.then(raw => {
|
||||
|
|
@ -168,16 +177,20 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
const { items, off } = visibleItems(rows, providerIdx)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.amber}>
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text bold color={t.color.amber} wrap="truncate-end">
|
||||
Select Provider
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim}>Current model: {currentModel || '(unknown)'}</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
Current model: {currentModel || '(unknown)'}
|
||||
</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>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off > 0 ? ` ↑ ${off} more` : ' '}
|
||||
</Text>
|
||||
|
||||
{Array.from({ length: VISIBLE }, (_, i) => {
|
||||
const row = items[i]
|
||||
|
|
@ -189,21 +202,28 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
color={providerIdx === idx ? t.color.amber : t.color.dim}
|
||||
inverse={providerIdx === idx}
|
||||
key={providers[idx]?.slug ?? `row-${idx}`}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{providerIdx === idx ? '▸ ' : ' '}
|
||||
{i + 1}. {row}
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={t.color.dim} key={`pad-${i}`}>
|
||||
<Text color={t.color.dim} key={`pad-${i}`} wrap="truncate-end">
|
||||
{' '}
|
||||
</Text>
|
||||
)
|
||||
})}
|
||||
|
||||
<Text color={t.color.dim}>{off + VISIBLE < rows.length ? ` ↓ ${rows.length - off - VISIBLE} more` : ' '}</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{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>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
||||
</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
↑/↓ select · Enter choose · 1-9,0 quick · Esc cancel
|
||||
</Text>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -211,16 +231,20 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
const { items, off } = visibleItems(models, modelIdx)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text bold color={t.color.amber}>
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text bold color={t.color.amber} wrap="truncate-end">
|
||||
Select Model
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim}>{names[providerIdx] || '(unknown provider)'}</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{names[providerIdx] || '(unknown provider)'}
|
||||
</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>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off > 0 ? ` ↑ ${off} more` : ' '}
|
||||
</Text>
|
||||
|
||||
{Array.from({ length: VISIBLE }, (_, i) => {
|
||||
const row = items[i]
|
||||
|
|
@ -228,11 +252,11 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
|
||||
if (!row) {
|
||||
return !models.length && i === 0 ? (
|
||||
<Text color={t.color.dim} key="empty">
|
||||
<Text color={t.color.dim} key="empty" wrap="truncate-end">
|
||||
no models listed for this provider
|
||||
</Text>
|
||||
) : (
|
||||
<Text color={t.color.dim} key={`pad-${i}`}>
|
||||
<Text color={t.color.dim} key={`pad-${i}`} wrap="truncate-end">
|
||||
{' '}
|
||||
</Text>
|
||||
)
|
||||
|
|
@ -244,6 +268,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
color={modelIdx === idx ? t.color.amber : t.color.dim}
|
||||
inverse={modelIdx === idx}
|
||||
key={`${provider?.slug ?? 'prov'}:${idx}:${row}`}
|
||||
wrap="truncate-end"
|
||||
>
|
||||
{modelIdx === idx ? '▸ ' : ' '}
|
||||
{i + 1}. {row}
|
||||
|
|
@ -251,12 +276,14 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
)
|
||||
})}
|
||||
|
||||
<Text color={t.color.dim}>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off + VISIBLE < models.length ? ` ↓ ${models.length - off - VISIBLE} more` : ' '}
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim}>persist: {persistGlobal ? 'global' : 'session'} · g toggle</Text>
|
||||
<Text color={t.color.dim}>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
||||
</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{models.length ? '↑/↓ select · Enter switch · 1-9,0 quick · Esc back' : 'Enter/Esc back'}
|
||||
</Text>
|
||||
</Box>
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue