mirror of
https://github.com/NousResearch/hermes-agent.git
synced 2026-07-31 19:16:29 +00:00
fix(ui-tui): overlay width caps are absolute — clampOverlayWidth (Copilot review)
Every picker/hub forced width >= 24 AFTER applying the caller's maxWidth, so a grid cell narrower than 24 (or a FloatBox cell under 28 with its 4 cols of chrome) overflowed and clipped at the terminal edge. One shared clampOverlayWidth(preferred, maxWidth, min=24): the caller's cap is ABSOLUTE (a cell knows its budget), the usability floor applies only when the cap allows it, uncapped keeps the old floor semantics. Five call sites (model/pet pickers, skills/plugins hubs, session switcher) route through it; the grid-test FloatBox drops its own 24 floor. Contract-tested including the sub-floor cap case from the review.
This commit is contained in:
parent
5bfffcd445
commit
11f2e54f0c
8 changed files with 47 additions and 11 deletions
22
ui-tui/src/__tests__/overlayPrimitives.test.ts
Normal file
22
ui-tui/src/__tests__/overlayPrimitives.test.ts
Normal file
|
|
@ -0,0 +1,22 @@
|
|||
import { describe, expect, it } from 'vitest'
|
||||
|
||||
import { clampOverlayWidth } from '../components/overlayPrimitives.js'
|
||||
|
||||
describe('clampOverlayWidth', () => {
|
||||
it('prefers preferred, capped by maxWidth', () => {
|
||||
expect(clampOverlayWidth(60)).toBe(60)
|
||||
expect(clampOverlayWidth(60, 40)).toBe(40)
|
||||
expect(clampOverlayWidth(30, 80)).toBe(30)
|
||||
})
|
||||
|
||||
it('honors caps BELOW the usability floor instead of overflowing the cell', () => {
|
||||
// Copilot review on #20379: a 20-col grid cell must get 20, not 24.
|
||||
expect(clampOverlayWidth(60, 20)).toBe(20)
|
||||
expect(clampOverlayWidth(60, 1)).toBe(1)
|
||||
})
|
||||
|
||||
it('keeps the floor when the cap allows it', () => {
|
||||
expect(clampOverlayWidth(10, 80)).toBe(24)
|
||||
expect(clampOverlayWidth(10)).toBe(24)
|
||||
})
|
||||
})
|
||||
|
|
@ -16,7 +16,7 @@ import type { Theme } from '../theme.js'
|
|||
|
||||
import { ModelPicker } from './modelPicker.js'
|
||||
import { windowOffset } from './overlayControls.js'
|
||||
import { listRowStyle } from './overlayPrimitives.js'
|
||||
import { clampOverlayWidth, listRowStyle } from './overlayPrimitives.js'
|
||||
import { TextInput } from './textInput.js'
|
||||
|
||||
const VISIBLE = 12
|
||||
|
|
@ -325,7 +325,7 @@ export function ActiveSessionSwitcher({
|
|||
const { stdout } = useStdout()
|
||||
// Optional maxWidth lets grid layouts hand the switcher its cell budget.
|
||||
const preferredWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6))
|
||||
const width = Math.max(24, Math.min(preferredWidth, Math.trunc(maxWidth ?? preferredWidth)))
|
||||
const width = clampOverlayWidth(preferredWidth, maxWidth)
|
||||
const promptColumns = Math.max(20, width - 11)
|
||||
|
||||
// Rows are [new][live…][history…]: the "+ new" row is pinned first (index 0,
|
||||
|
|
|
|||
|
|
@ -227,7 +227,10 @@ export function FloatingOverlays({
|
|||
id: 'grid-test',
|
||||
render: () => (
|
||||
<FloatBox color={theme.color.border}>
|
||||
<GridTestOverlay cols={Math.max(24, cols - 6)} state={gridTest} t={theme} />
|
||||
{/* cols-6 = FloatBox chrome (4) + margin (2); no 24-col floor —
|
||||
forcing one would overflow cells narrower than 28 and clip at
|
||||
the terminal edge. */}
|
||||
<GridTestOverlay cols={Math.max(1, cols - 6)} state={gridTest} t={theme} />
|
||||
</FloatBox>
|
||||
)
|
||||
})
|
||||
|
|
|
|||
|
|
@ -10,7 +10,7 @@ import { asRpcResult, rpcErrorMessage } from '../lib/rpc.js'
|
|||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayHint, useOverlayKeys, windowItems } from './overlayControls.js'
|
||||
import { chipRowProps } from './overlayPrimitives.js'
|
||||
import { chipRowProps, clampOverlayWidth } from './overlayPrimitives.js'
|
||||
|
||||
const VISIBLE = 12
|
||||
const MIN_WIDTH = 40
|
||||
|
|
@ -62,7 +62,7 @@ export function ModelPicker({
|
|||
// has an actual constraint to truncate against. Optional maxWidth lets
|
||||
// grid layouts hand the picker its cell budget.
|
||||
const preferredWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6))
|
||||
const width = Math.max(24, Math.min(preferredWidth, Math.trunc(maxWidth ?? preferredWidth)))
|
||||
const width = clampOverlayWidth(preferredWidth, maxWidth)
|
||||
|
||||
useEffect(() => {
|
||||
gw.request<ModelOptionsResponse>('model.options', {
|
||||
|
|
|
|||
|
|
@ -6,6 +6,17 @@ import type { UsageModelData } from '../gatewayTypes.js'
|
|||
import { liftForContrast, mix } from '../lib/color.js'
|
||||
import type { Theme } from '../theme.js'
|
||||
|
||||
/**
|
||||
* Overlay width clamp: prefer `preferred`, honor the caller's `maxWidth`
|
||||
* ABSOLUTELY (a grid cell knows its budget — overflowing it clips at the
|
||||
* terminal edge), and keep a usability floor only when the cap allows it.
|
||||
*/
|
||||
export function clampOverlayWidth(preferred: number, maxWidth?: number, min = 24): number {
|
||||
const cap = maxWidth === undefined ? Number.MAX_SAFE_INTEGER : Math.max(1, Math.trunc(maxWidth))
|
||||
|
||||
return Math.max(Math.min(min, cap), Math.min(preferred, cap))
|
||||
}
|
||||
|
||||
/**
|
||||
* THE scrollbar treatment (transcript + overlays): thumb rides the theme
|
||||
* base, accent while interacting; track recedes via an explicit blend toward
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
|
|||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayHint, windowItems } from './overlayControls.js'
|
||||
import { chipRowProps } from './overlayPrimitives.js'
|
||||
import { chipRowProps, clampOverlayWidth } from './overlayPrimitives.js'
|
||||
|
||||
const VISIBLE = 10
|
||||
const MIN_WIDTH = 40
|
||||
|
|
@ -42,7 +42,7 @@ export function PetPicker({ gw, maxWidth, onClose, t }: PetPickerProps) {
|
|||
const { stdout } = useStdout()
|
||||
// Optional maxWidth lets grid layouts hand the picker its cell budget.
|
||||
const preferredWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6))
|
||||
const width = Math.max(24, Math.min(preferredWidth, Math.trunc(maxWidth ?? preferredWidth)))
|
||||
const width = clampOverlayWidth(preferredWidth, maxWidth)
|
||||
|
||||
useEffect(() => {
|
||||
gw.request<Gallery>('pet.gallery')
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ import { rpcErrorMessage } from '../lib/rpc.js'
|
|||
import type { Theme } from '../theme.js'
|
||||
|
||||
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.js'
|
||||
import { chipRowProps } from './overlayPrimitives.js'
|
||||
import { chipRowProps, clampOverlayWidth } from './overlayPrimitives.js'
|
||||
|
||||
const VISIBLE = 12
|
||||
const MIN_WIDTH = 44
|
||||
|
|
@ -53,7 +53,7 @@ export function PluginsHub({ gw, maxWidth, onClose, t }: PluginsHubProps) {
|
|||
const { stdout } = useStdout()
|
||||
// Optional maxWidth lets grid layouts hand the hub its cell budget.
|
||||
const preferredWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, (stdout?.columns ?? 80) - 6))
|
||||
const width = Math.max(24, Math.min(preferredWidth, Math.trunc(maxWidth ?? preferredWidth)))
|
||||
const width = clampOverlayWidth(preferredWidth, maxWidth)
|
||||
|
||||
const load = () => {
|
||||
gw.request<PluginsListResponse>('plugins.manage', { action: 'list' })
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import type { Theme } from '../theme.js'
|
|||
|
||||
import { OverlayHint, useOverlayKeys, windowItems, windowOffset } from './overlayControls.js'
|
||||
import { chipRowProps } from './overlayPrimitives.js'
|
||||
import { clampOverlayWidth } from './overlayPrimitives.js'
|
||||
|
||||
const VISIBLE = 12
|
||||
const MIN_WIDTH = 40
|
||||
|
|
@ -26,8 +27,7 @@ export function SkillsHub({ gw, maxWidth, onClose, t }: SkillsHubProps) {
|
|||
const { stdout } = useStdout()
|
||||
const terminalWidth = Math.max(1, (stdout?.columns ?? 80) - 6)
|
||||
const preferredWidth = Math.max(MIN_WIDTH, Math.min(MAX_WIDTH, terminalWidth))
|
||||
const widthCap = Math.max(24, Math.trunc(maxWidth ?? preferredWidth))
|
||||
const width = Math.max(24, Math.min(preferredWidth, widthCap))
|
||||
const width = clampOverlayWidth(preferredWidth, maxWidth)
|
||||
|
||||
useEffect(() => {
|
||||
gw.request<{ skills?: Record<string, string[]> }>('skills.manage', { action: 'list' })
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue