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
)
}