diff --git a/ui-tui/src/components/modelPicker.tsx b/ui-tui/src/components/modelPicker.tsx index 1c618c58ec..7927f3b736 100644 --- a/ui-tui/src/components/modelPicker.tsx +++ b/ui-tui/src/components/modelPicker.tsx @@ -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('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 ( - - + + Select Provider - Current model: {currentModel || '(unknown)'} + + Current model: {currentModel || '(unknown)'} + {provider?.warning ? `warning: ${provider.warning}` : ' '} - {off > 0 ? ` ↑ ${off} more` : ' '} + + {off > 0 ? ` ↑ ${off} more` : ' '} + {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} ) : ( - + {' '} ) })} - {off + VISIBLE < rows.length ? ` ↓ ${rows.length - off - VISIBLE} more` : ' '} + + {off + VISIBLE < rows.length ? ` ↓ ${rows.length - off - VISIBLE} more` : ' '} + - persist: {persistGlobal ? 'global' : 'session'} · g toggle - ↑/↓ select · Enter choose · 1-9,0 quick · Esc cancel + + persist: {persistGlobal ? 'global' : 'session'} · g toggle + + + ↑/↓ select · Enter choose · 1-9,0 quick · Esc cancel + ) } @@ -211,16 +231,20 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke const { items, off } = visibleItems(models, modelIdx) return ( - - + + Select Model - {names[providerIdx] || '(unknown provider)'} + + {names[providerIdx] || '(unknown provider)'} + {provider?.warning ? `warning: ${provider.warning}` : ' '} - {off > 0 ? ` ↑ ${off} more` : ' '} + + {off > 0 ? ` ↑ ${off} more` : ' '} + {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 ? ( - + no models listed for this provider ) : ( - + {' '} ) @@ -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 ) })} - + {off + VISIBLE < models.length ? ` ↓ ${models.length - off - VISIBLE} more` : ' '} - persist: {persistGlobal ? 'global' : 'session'} · g toggle - + + persist: {persistGlobal ? 'global' : 'session'} · g toggle + + {models.length ? '↑/↓ select · Enter switch · 1-9,0 quick · Esc back' : 'Enter/Esc back'} diff --git a/ui-tui/src/components/sessionPicker.tsx b/ui-tui/src/components/sessionPicker.tsx index 51bd451c39..c840782399 100644 --- a/ui-tui/src/components/sessionPicker.tsx +++ b/ui-tui/src/components/sessionPicker.tsx @@ -1,4 +1,4 @@ -import { Box, Text, useInput } from '@hermes/ink' +import { Box, Text, useInput, useStdout } from '@hermes/ink' import { useEffect, useState } from 'react' import type { GatewayClient } from '../gatewayClient.js' @@ -7,6 +7,8 @@ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js' import type { Theme } from '../theme.js' const VISIBLE = 15 +const MIN_WIDTH = 60 +const MAX_WIDTH = 120 const age = (ts: number) => { const d = (Date.now() / 1000 - ts) / 86400 @@ -28,6 +30,9 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps) const [sel, setSel] = useState(0) const [loading, setLoading] = useState(true) + const { stdout } = useStdout() + const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6)) + useEffect(() => { gw.request('session.list', { limit: 20 }) .then(raw => { @@ -99,7 +104,7 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps) const off = Math.max(0, Math.min(sel - Math.floor(VISIBLE / 2), items.length - VISIBLE)) return ( - + Resume Session @@ -128,7 +133,7 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps) - + {s.title || s.preview || '(untitled)'} diff --git a/ui-tui/src/components/skillsHub.tsx b/ui-tui/src/components/skillsHub.tsx index 48790eff6b..1bff92c0c8 100644 --- a/ui-tui/src/components/skillsHub.tsx +++ b/ui-tui/src/components/skillsHub.tsx @@ -1,4 +1,4 @@ -import { Box, Text, useInput } from '@hermes/ink' +import { Box, Text, useInput, useStdout } from '@hermes/ink' import { useEffect, useState } from 'react' import type { GatewayClient } from '../gatewayClient.js' @@ -6,6 +6,8 @@ import { 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)) @@ -26,6 +28,9 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { const [err, setErr] = useState('') const [loading, setLoading] = useState(true) + const { stdout } = useStdout() + const width = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6)) + useEffect(() => { gw.request<{ skills?: Record }>('skills.manage', { action: 'list' }) .then(r => { @@ -186,7 +191,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { if (err && stage === 'category') { return ( - + error: {err} Esc to cancel @@ -195,7 +200,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { if (!cats.length) { return ( - + no skills available Esc to cancel @@ -207,7 +212,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { const { items, off } = visibleItems(rows, catIdx) return ( - + Skills Hub @@ -224,6 +229,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { color={catIdx === idx ? t.color.amber : t.color.dim} inverse={catIdx === idx} key={row} + wrap="truncate-end" > {catIdx === idx ? '▸ ' : ' '} {i + 1}. {row} @@ -241,7 +247,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { const { items, off } = visibleItems(skills, skillIdx) return ( - + {selectedCat} @@ -259,6 +265,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { color={skillIdx === idx ? t.color.amber : t.color.dim} inverse={skillIdx === idx} key={row} + wrap="truncate-end" > {skillIdx === idx ? '▸ ' : ' '} {i + 1}. {row} @@ -275,7 +282,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { } return ( - + {info?.name ?? skillName}