From 6e83d90eb4904677d530480d41b642362f514026 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 25 Apr 2026 14:23:45 -0500 Subject: [PATCH] refactor(tui): tighten overlay helpers - rename overlay help text component to match its role - share picker window math across model, session, and skills overlays --- ui-tui/src/components/appOverlays.tsx | 6 ++-- ui-tui/src/components/modelPicker.tsx | 44 ++++++++++------------- ui-tui/src/components/overlayControls.tsx | 21 +++++++---- ui-tui/src/components/sessionPicker.tsx | 18 +++++----- ui-tui/src/components/skillsHub.tsx | 43 ++++++++++------------ 5 files changed, 63 insertions(+), 69 deletions(-) diff --git a/ui-tui/src/components/appOverlays.tsx b/ui-tui/src/components/appOverlays.tsx index 9e1b6ded30..25342598b8 100644 --- a/ui-tui/src/components/appOverlays.tsx +++ b/ui-tui/src/components/appOverlays.tsx @@ -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({ ))} - + {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)`} - + diff --git a/ui-tui/src/components/modelPicker.tsx b/ui-tui/src/components/modelPicker.tsx index 3e5c8c3648..83c8abaab7 100644 --- a/ui-tui/src/components/modelPicker.tsx +++ b/ui-tui/src/components/modelPicker.tsx @@ -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([]) 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 ( error: {err} - Esc/q cancel + Esc/q cancel ) } @@ -166,7 +158,7 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke return ( no authenticated providers - Esc/q cancel + Esc/q cancel ) } @@ -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 ( @@ -191,12 +183,12 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke {provider?.warning ? `warning: ${provider.warning}` : ' '} - {off > 0 ? ` ↑ ${off} more` : ' '} + {offset > 0 ? ` ↑ ${offset} more` : ' '} {Array.from({ length: VISIBLE }, (_, i) => { const row = items[i] - const idx = off + i + const idx = offset + i return row ? ( - {off + VISIBLE < rows.length ? ` ↓ ${rows.length - off - VISIBLE} more` : ' '} + {offset + VISIBLE < rows.length ? ` ↓ ${rows.length - offset - VISIBLE} more` : ' '} persist: {persistGlobal ? 'global' : 'session'} · g toggle - ↑/↓ select · Enter choose · 1-9,0 quick · Esc/q cancel + ↑/↓ select · Enter choose · 1-9,0 quick · Esc/q cancel ) } - const { items, off } = visibleItems(models, modelIdx) + const { items, offset } = windowItems(models, modelIdx, VISIBLE) return ( @@ -243,12 +235,12 @@ export function ModelPicker({ gw, onCancel, onSelect, sessionId, t }: ModelPicke {provider?.warning ? `warning: ${provider.warning}` : ' '} - {off > 0 ? ` ↑ ${off} more` : ' '} + {offset > 0 ? ` ↑ ${offset} more` : ' '} {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 })} - {off + VISIBLE < models.length ? ` ↓ ${models.length - off - VISIBLE} more` : ' '} + {offset + VISIBLE < models.length ? ` ↓ ${models.length - offset - VISIBLE} more` : ' '} persist: {persistGlobal ? 'global' : 'session'} · g toggle - + {models.length ? '↑/↓ select · Enter switch · 1-9,0 quick · Esc back · q close' : 'Enter/Esc back · q close'} - + ) } diff --git a/ui-tui/src/components/overlayControls.tsx b/ui-tui/src/components/overlayControls.tsx index 03d28127ed..d6f885fdcd 100644 --- a/ui-tui/src/components/overlayControls.tsx +++ b/ui-tui/src/components/overlayControls.tsx @@ -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 ( - + {children} ) } -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(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 { diff --git a/ui-tui/src/components/sessionPicker.tsx b/ui-tui/src/components/sessionPicker.tsx index fa1529ef4c..8e936b989b 100644 --- a/ui-tui/src/components/sessionPicker.tsx +++ b/ui-tui/src/components/sessionPicker.tsx @@ -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 ( error: {err} - Esc/q cancel + Esc/q cancel ) } @@ -96,12 +96,12 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps) return ( no previous sessions - Esc/q cancel + Esc/q cancel ) } - const off = Math.max(0, Math.min(sel - Math.floor(VISIBLE / 2), items.length - VISIBLE)) + const offset = windowOffset(items.length, sel, VISIBLE) return ( @@ -109,10 +109,10 @@ export function SessionPicker({ gw, onCancel, onSelect, t }: SessionPickerProps) Resume Session - {off > 0 && ↑ {off} more} + {offset > 0 && ↑ {offset} more} - {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 && ↓ {items.length - off - VISIBLE} more} - ↑/↓ select · Enter resume · 1-9 quick · Esc/q cancel + {offset + VISIBLE < items.length && ↓ {items.length - offset - VISIBLE} more} + ↑/↓ select · Enter resume · 1-9 quick · Esc/q cancel ) } diff --git a/ui-tui/src/components/skillsHub.tsx b/ui-tui/src/components/skillsHub.tsx index 44710645a8..3284b145f5 100644 --- a/ui-tui/src/components/skillsHub.tsx +++ b/ui-tui/src/components/skillsHub.tsx @@ -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>({}) 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 ( error: {err} - Esc/q cancel + Esc/q cancel ) } @@ -204,14 +195,14 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { return ( no skills available - Esc/q cancel + Esc/q cancel ) } 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 ( @@ -220,10 +211,10 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { select a category - {off > 0 && ↑ {off} more} + {offset > 0 && ↑ {offset} more} {items.map((row, i) => { - const idx = off + i + const idx = offset + i return ( ↓ {rows.length - off - VISIBLE} more} - ↑/↓ select · Enter open · 1-9,0 quick · Esc/q cancel + {offset + VISIBLE < rows.length && ↓ {rows.length - offset - VISIBLE} more} + ↑/↓ select · Enter open · 1-9,0 quick · Esc/q cancel ) } if (stage === 'skill') { - const { items, off } = visibleItems(skills, skillIdx) + const { items, offset } = windowItems(skills, skillIdx, VISIBLE) return ( @@ -256,10 +247,10 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { {skills.length} skill(s) {!skills.length ? no skills in this category : null} - {off > 0 && ↑ {off} more} + {offset > 0 && ↑ {offset} more} {items.map((row, i) => { - const idx = off + i + const idx = offset + i return ( ↓ {skills.length - off - VISIBLE} more} - + {offset + VISIBLE < skills.length && ( + ↓ {skills.length - offset - VISIBLE} more + )} + {skills.length ? '↑/↓ select · Enter open · 1-9,0 quick · Esc back · q close' : 'Esc back · q close'} - + ) } @@ -296,7 +289,7 @@ export function SkillsHub({ gw, onClose, t }: SkillsHubProps) { {err ? error: {err} : null} {installing ? installing… : null} - i reinspect · x reinstall · Enter/Esc back · q close + i reinspect · x reinstall · Enter/Esc back · q close ) }