mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-05-07 02:51:50 +00:00
refactor(tui): tighten overlay helpers
- rename overlay help text component to match its role - share picker window math across model, session, and skills overlays
This commit is contained in:
parent
c6fdf48b79
commit
6e83d90eb4
5 changed files with 63 additions and 69 deletions
|
|
@ -9,7 +9,7 @@ import { $uiState } from '../app/uiStore.js'
|
|||
import { FloatBox } from './appChrome.js'
|
||||
import { MaskedPrompt } from './maskedPrompt.js'
|
||||
import { ModelPicker } from './modelPicker.js'
|
||||
import { OverlayControls } from './overlayControls.js'
|
||||
import { OverlayHint } from './overlayControls.js'
|
||||
import { ApprovalPrompt, ClarifyPrompt, ConfirmPrompt } from './prompts.js'
|
||||
import { SessionPicker } from './sessionPicker.js'
|
||||
import { SkillsHub } from './skillsHub.js'
|
||||
|
|
@ -163,11 +163,11 @@ export function FloatingOverlays({
|
|||
))}
|
||||
|
||||
<Box marginTop={1}>
|
||||
<OverlayControls t={ui.theme}>
|
||||
<OverlayHint t={ui.theme}>
|
||||
{overlay.pager.offset + pagerPageSize < overlay.pager.lines.length
|
||||
? `↑↓/jk line · Enter/Space/PgDn page · b/PgUp back · g/G top/bottom · Esc/q close (${Math.min(overlay.pager.offset + pagerPageSize, overlay.pager.lines.length)}/${overlay.pager.lines.length})`
|
||||
: `end · ↑↓/jk · b/PgUp back · g top · Esc/q close (${overlay.pager.lines.length} lines)`}
|
||||
</OverlayControls>
|
||||
</OverlayHint>
|
||||
</Box>
|
||||
</Box>
|
||||
</FloatBox>
|
||||
|
|
|
|||
|
|
@ -7,20 +7,12 @@ import type { ModelOptionProvider, ModelOptionsResponse } from '../gatewayTypes.
|
|||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayControls, useOverlayKeys } from './overlayControls.js'
|
||||
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.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))
|
||||
|
||||
const visibleItems = (items: string[], sel: number) => {
|
||||
const off = pageOffset(items.length, sel)
|
||||
|
||||
return { items: items.slice(off, off + VISIBLE), off }
|
||||
}
|
||||
|
||||
export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPickerProps) {
|
||||
const [providers, setProviders] = useState<ModelOptionProvider[]>([])
|
||||
const [currentModel, setCurrentModel] = useState('')
|
||||
|
|
@ -135,16 +127,16 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
const n = ch === '0' ? 10 : parseInt(ch, 10)
|
||||
|
||||
if (!Number.isNaN(n) && n >= 1 && n <= Math.min(10, count)) {
|
||||
const off = pageOffset(count, sel)
|
||||
const offset = windowOffset(count, sel, VISIBLE)
|
||||
|
||||
if (stage === 'provider') {
|
||||
const next = off + n - 1
|
||||
const next = offset + n - 1
|
||||
|
||||
if (providers[next]) {
|
||||
setProviderIdx(next)
|
||||
}
|
||||
} else if (provider && models[off + n - 1]) {
|
||||
onSelect(`${models[off + n - 1]} --provider ${provider.slug}${persistGlobal ? ' --global' : ''}`)
|
||||
} else if (provider && models[offset + n - 1]) {
|
||||
onSelect(`${models[offset + n - 1]} --provider ${provider.slug}${persistGlobal ? ' --global' : ''}`)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
@ -157,7 +149,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.label}>error: {err}</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -166,7 +158,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.dim}>no authenticated providers</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -176,7 +168,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
(p, i) => `${p.is_current ? '*' : ' '} ${names[i]} · ${p.total_models ?? p.models?.length ?? 0} models`
|
||||
)
|
||||
|
||||
const { items, off } = visibleItems(rows, providerIdx)
|
||||
const { items, offset } = windowItems(rows, providerIdx, VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
|
|
@ -191,12 +183,12 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
{provider?.warning ? `warning: ${provider.warning}` : ' '}
|
||||
</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off > 0 ? ` ↑ ${off} more` : ' '}
|
||||
{offset > 0 ? ` ↑ ${offset} more` : ' '}
|
||||
</Text>
|
||||
|
||||
{Array.from({ length: VISIBLE }, (_, i) => {
|
||||
const row = items[i]
|
||||
const idx = off + i
|
||||
const idx = offset + i
|
||||
|
||||
return row ? (
|
||||
<Text
|
||||
|
|
@ -217,18 +209,18 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
})}
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off + VISIBLE < rows.length ? ` ↓ ${rows.length - off - VISIBLE} more` : ' '}
|
||||
{offset + VISIBLE < rows.length ? ` ↓ ${rows.length - offset - VISIBLE} more` : ' '}
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
||||
</Text>
|
||||
<OverlayControls t={t}>↑/↓ select · Enter choose · 1-9,0 quick · Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>↑/↓ select · Enter choose · 1-9,0 quick · Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const { items, off } = visibleItems(models, modelIdx)
|
||||
const { items, offset } = windowItems(models, modelIdx, VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
|
|
@ -243,12 +235,12 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
{provider?.warning ? `warning: ${provider.warning}` : ' '}
|
||||
</Text>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off > 0 ? ` ↑ ${off} more` : ' '}
|
||||
{offset > 0 ? ` ↑ ${offset} more` : ' '}
|
||||
</Text>
|
||||
|
||||
{Array.from({ length: VISIBLE }, (_, i) => {
|
||||
const row = items[i]
|
||||
const idx = off + i
|
||||
const idx = offset + i
|
||||
|
||||
if (!row) {
|
||||
return !models.length && i === 0 ? (
|
||||
|
|
@ -277,15 +269,15 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke
|
|||
})}
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{off + VISIBLE < models.length ? ` ↓ ${models.length - off - VISIBLE} more` : ' '}
|
||||
{offset + VISIBLE < models.length ? ` ↓ ${models.length - offset - VISIBLE} more` : ' '}
|
||||
</Text>
|
||||
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
persist: {persistGlobal ? 'global' : 'session'} · g toggle
|
||||
</Text>
|
||||
<OverlayControls t={t}>
|
||||
<OverlayHint t={t}>
|
||||
{models.length ? '↑/↓ select · Enter switch · 1-9,0 quick · Esc back · q close' : 'Enter/Esc back · q close'}
|
||||
</OverlayControls>
|
||||
</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,8 +2,6 @@ import { Text, useInput } from '@hermes/ink'
|
|||
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
type TextWrap = 'end' | 'middle' | 'truncate' | 'truncate-end' | 'truncate-middle' | 'wrap' | 'wrap-char' | 'wrap-trim'
|
||||
|
||||
export function useOverlayKeys({ disabled = false, onBack, onClose }: OverlayKeysOptions) {
|
||||
useInput((ch, key) => {
|
||||
if (disabled) {
|
||||
|
|
@ -20,18 +18,29 @@ export function useOverlayKeys({ disabled = false, onBack, onClose }: OverlayKey
|
|||
})
|
||||
}
|
||||
|
||||
export function OverlayControls({ children, t, wrap = 'truncate-end' }: OverlayControlsProps) {
|
||||
export function OverlayHint({ children, t }: OverlayHintProps) {
|
||||
return (
|
||||
<Text color={t.color.dim} wrap={wrap}>
|
||||
<Text color={t.color.dim} wrap="truncate-end">
|
||||
{children}
|
||||
</Text>
|
||||
)
|
||||
}
|
||||
|
||||
interface OverlayControlsProps {
|
||||
export const windowOffset = (count: number, selected: number, visible: number) =>
|
||||
Math.max(0, Math.min(selected - Math.floor(visible / 2), count - visible))
|
||||
|
||||
export function windowItems<T>(items: T[], selected: number, visible: number) {
|
||||
const offset = windowOffset(items.length, selected, visible)
|
||||
|
||||
return {
|
||||
items: items.slice(offset, offset + visible),
|
||||
offset
|
||||
}
|
||||
}
|
||||
|
||||
interface OverlayHintProps {
|
||||
children: string
|
||||
t: Theme
|
||||
wrap?: TextWrap
|
||||
}
|
||||
|
||||
interface OverlayKeysOptions {
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import type { SessionListItem, SessionListResponse } from '../gatewayTypes.js'
|
|||
import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayControls, useOverlayKeys } from './overlayControls.js'
|
||||
import { OverlayHint, useOverlayKeys, windowOffset } from './overlayControls.js'
|
||||
|
||||
const VISIBLE = 15
|
||||
const MIN_WIDTH = 60
|
||||
|
|
@ -87,7 +87,7 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
|
|||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.label}>error: {err}</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -96,12 +96,12 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
|
|||
return (
|
||||
<Box flexDirection="column">
|
||||
<Text color={t.color.dim}>no previous sessions</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
const off = Math.max(0, Math.min(sel - Math.floor(VISIBLE / 2), items.length - VISIBLE))
|
||||
const offset = windowOffset(items.length, sel, VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
|
|
@ -109,10 +109,10 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
|
|||
Resume Session
|
||||
</Text>
|
||||
|
||||
{off > 0 && <Text color={t.color.dim}> ↑ {off} more</Text>}
|
||||
{offset > 0 && <Text color={t.color.dim}> ↑ {offset} more</Text>}
|
||||
|
||||
{items.slice(off, off + VISIBLE).map((s, vi) => {
|
||||
const i = off + vi
|
||||
{items.slice(offset, offset + VISIBLE).map((s, vi) => {
|
||||
const i = offset + vi
|
||||
const selected = sel === i
|
||||
|
||||
return (
|
||||
|
|
@ -140,8 +140,8 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps)
|
|||
)
|
||||
})}
|
||||
|
||||
{off + VISIBLE < items.length && <Text color={t.color.dim}> ↓ {items.length - off - VISIBLE} more</Text>}
|
||||
<OverlayControls t={t}>↑/↓ select · Enter resume · 1-9 quick · Esc/q cancel</OverlayControls>
|
||||
{offset + VISIBLE < items.length && <Text color={t.color.dim}> ↓ {items.length - offset - VISIBLE} more</Text>}
|
||||
<OverlayHint t={t}>↑/↓ select · Enter resume · 1-9 quick · Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -5,20 +5,12 @@ import type { GatewayClient } from '../gatewayClient.js'
|
|||
import { rpcErrorMessage } from '../lib/rpc.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayControls, useOverlayKeys } from './overlayControls.js'
|
||||
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.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))
|
||||
|
||||
const visibleItems = (items: string[], sel: number) => {
|
||||
const off = pageOffset(items.length, sel)
|
||||
|
||||
return { items: items.slice(off, off + VISIBLE), off }
|
||||
}
|
||||
|
||||
export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
||||
const [skillsByCat, setSkillsByCat] = useState<Record<string, string[]>>({})
|
||||
const [selectedCat, setSelectedCat] = useState('')
|
||||
|
|
@ -161,8 +153,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
const n = ch === '0' ? 10 : parseInt(ch, 10)
|
||||
|
||||
if (!Number.isNaN(n) && n >= 1 && n <= Math.min(10, count)) {
|
||||
const off = pageOffset(count, sel)
|
||||
const next = off + n - 1
|
||||
const next = windowOffset(count, sel, VISIBLE) + n - 1
|
||||
|
||||
if (stage === 'category') {
|
||||
const cat = cats[next]
|
||||
|
|
@ -195,7 +186,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text color={t.color.label}>error: {err}</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -204,14 +195,14 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
<Text color={t.color.dim}>no skills available</Text>
|
||||
<OverlayControls t={t}>Esc/q cancel</OverlayControls>
|
||||
<OverlayHint t={t}>Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (stage === 'category') {
|
||||
const rows = cats.map(c => `${c} · ${skillsByCat[c]?.length ?? 0} skills`)
|
||||
const { items, off } = visibleItems(rows, catIdx)
|
||||
const { items, offset } = windowItems(rows, catIdx, VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
|
|
@ -220,10 +211,10 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
</Text>
|
||||
|
||||
<Text color={t.color.dim}>select a category</Text>
|
||||
{off > 0 && <Text color={t.color.dim}> ↑ {off} more</Text>}
|
||||
{offset > 0 && <Text color={t.color.dim}> ↑ {offset} more</Text>}
|
||||
|
||||
{items.map((row, i) => {
|
||||
const idx = off + i
|
||||
const idx = offset + i
|
||||
|
||||
return (
|
||||
<Text
|
||||
|
|
@ -239,14 +230,14 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
)
|
||||
})}
|
||||
|
||||
{off + VISIBLE < rows.length && <Text color={t.color.dim}> ↓ {rows.length - off - VISIBLE} more</Text>}
|
||||
<OverlayControls t={t}>↑/↓ select · Enter open · 1-9,0 quick · Esc/q cancel</OverlayControls>
|
||||
{offset + VISIBLE < rows.length && <Text color={t.color.dim}> ↓ {rows.length - offset - VISIBLE} more</Text>}
|
||||
<OverlayHint t={t}>↑/↓ select · Enter open · 1-9,0 quick · Esc/q cancel</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
||||
if (stage === 'skill') {
|
||||
const { items, off } = visibleItems(skills, skillIdx)
|
||||
const { items, offset } = windowItems(skills, skillIdx, VISIBLE)
|
||||
|
||||
return (
|
||||
<Box flexDirection="column" width={width}>
|
||||
|
|
@ -256,10 +247,10 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
|
||||
<Text color={t.color.dim}>{skills.length} skill(s)</Text>
|
||||
{!skills.length ? <Text color={t.color.dim}>no skills in this category</Text> : null}
|
||||
{off > 0 && <Text color={t.color.dim}> ↑ {off} more</Text>}
|
||||
{offset > 0 && <Text color={t.color.dim}> ↑ {offset} more</Text>}
|
||||
|
||||
{items.map((row, i) => {
|
||||
const idx = off + i
|
||||
const idx = offset + i
|
||||
|
||||
return (
|
||||
<Text
|
||||
|
|
@ -275,10 +266,12 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
)
|
||||
})}
|
||||
|
||||
{off + VISIBLE < skills.length && <Text color={t.color.dim}> ↓ {skills.length - off - VISIBLE} more</Text>}
|
||||
<OverlayControls t={t}>
|
||||
{offset + VISIBLE < skills.length && (
|
||||
<Text color={t.color.dim}> ↓ {skills.length - offset - VISIBLE} more</Text>
|
||||
)}
|
||||
<OverlayHint t={t}>
|
||||
{skills.length ? '↑/↓ select · Enter open · 1-9,0 quick · Esc back · q close' : 'Esc back · q close'}
|
||||
</OverlayControls>
|
||||
</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
@ -296,7 +289,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) {
|
|||
{err ? <Text color={t.color.label}>error: {err}</Text> : null}
|
||||
{installing ? <Text color={t.color.amber}>installing…</Text> : null}
|
||||
|
||||
<OverlayControls t={t}>i reinspect · x reinstall · Enter/Esc back · q close</OverlayControls>
|
||||
<OverlayHint t={t}>i reinspect · x reinstall · Enter/Esc back · q close</OverlayHint>
|
||||
</Box>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue