Merge pull request #31081 from NousResearch/bb/tui-skinny-status-rule
Some checks failed
Deploy Site / deploy-vercel (push) Has been cancelled
Deploy Site / deploy-docs (push) Has been cancelled
Docker / shell lint / Lint Dockerfile (hadolint) (push) Has been cancelled
Docker / shell lint / Lint docker/ shell scripts (shellcheck) (push) Has been cancelled
Docker Build and Publish / build-amd64 (push) Has been cancelled
Docker Build and Publish / build-arm64 (push) Has been cancelled
Lint (ruff + ty) / ruff + ty diff (push) Has been cancelled
Lint (ruff + ty) / ruff enforcement (blocking) (push) Has been cancelled
Lint (ruff + ty) / Windows footguns (blocking) (push) Has been cancelled
Nix / nix (macos-latest) (push) Has been cancelled
Nix / nix (ubuntu-latest) (push) Has been cancelled
Tests / test (1) (push) Has been cancelled
Tests / test (2) (push) Has been cancelled
Tests / test (3) (push) Has been cancelled
Tests / test (4) (push) Has been cancelled
Tests / test (5) (push) Has been cancelled
Tests / test (6) (push) Has been cancelled
Tests / e2e (push) Has been cancelled
Docker Build and Publish / merge (push) Has been cancelled
Docker Build and Publish / move-latest (push) Has been cancelled
Tests / save-durations (push) Has been cancelled

fix(tui): keep status rule one-line in skinny terminals
This commit is contained in:
ethernet 2026-05-25 01:24:29 -04:00 committed by GitHub
commit b288de8bf4
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 61 additions and 4 deletions

View file

@ -0,0 +1,32 @@
import { describe, expect, it } from 'vitest'
import { statusRuleWidths } from '../components/appChrome.js'
describe('statusRuleWidths', () => {
it('keeps the status rule within the terminal width', () => {
for (const cols of [8, 12, 20, 40, 100]) {
const widths = statusRuleWidths(cols, '~/src/hermes-agent/main (some-long-branch-name)')
expect(widths.leftWidth + widths.separatorWidth + widths.rightWidth).toBeLessThanOrEqual(cols)
expect(widths.leftWidth).toBeGreaterThan(0)
}
})
it('truncates the cwd segment before it can wrap in skinny terminals', () => {
const widths = statusRuleWidths(24, '~/src/hermes-agent/main (bb/some-extremely-long-branch)')
expect(widths.rightWidth).toBeLessThan('~/src/hermes-agent/main (bb/some-extremely-long-branch)'.length)
expect(widths.leftWidth).toBeGreaterThanOrEqual(8)
})
it('omits the cwd segment when there is no room for it', () => {
expect(statusRuleWidths(2, 'abcdef')).toEqual({ leftWidth: 2, rightWidth: 0, separatorWidth: 0 })
})
it('budgets the cwd segment by display width, not utf-16 length', () => {
const widths = statusRuleWidths(30, '目录/分支')
expect(widths.leftWidth + widths.separatorWidth + widths.rightWidth).toBeLessThanOrEqual(30)
expect(widths.rightWidth).toBeGreaterThan('目录/分支'.length)
})
})

View file

@ -1,4 +1,4 @@
import { Box, type ScrollBoxHandle, Text } from '@hermes/ink'
import { Box, type ScrollBoxHandle, stringWidth, Text } from '@hermes/ink'
import { useStore } from '@nanostores/react'
import { type ReactNode, type RefObject, useEffect, useMemo, useRef, useState } from 'react'
import unicodeSpinners from 'unicode-animations'
@ -150,6 +150,23 @@ function ctxBar(pct: number | undefined, w = 10) {
return '█'.repeat(filled) + '░'.repeat(w - filled)
}
export function statusRuleWidths(cols: number, cwdLabel: string) {
const width = Math.max(1, Math.floor(cols || 1))
const desiredSeparatorWidth = width >= 24 ? 3 : 1
const minLeftWidth = width >= 24 ? 8 : 1
const maxRightWidth = Math.max(0, width - desiredSeparatorWidth - minLeftWidth)
if (!cwdLabel || maxRightWidth <= 0) {
return { leftWidth: width, rightWidth: 0, separatorWidth: 0 }
}
const rightWidth = Math.max(0, Math.min(stringWidth(cwdLabel), maxRightWidth))
const separatorWidth = rightWidth > 0 ? desiredSeparatorWidth : 0
const leftWidth = Math.max(1, width - separatorWidth - rightWidth)
return { leftWidth, rightWidth, separatorWidth }
}
function SpawnHud({ t }: { t: Theme }) {
// Tight HUD that only appears when the session is actually fanning out.
// Colour escalates to warn/error as depth or concurrency approaches the cap.
@ -297,7 +314,7 @@ export function StatusRule({
: ''
const bar = usage.context_max ? ctxBar(pct) : ''
const leftWidth = Math.max(12, cols - cwdLabel.length - 3)
const { leftWidth, rightWidth, separatorWidth } = statusRuleWidths(cols, cwdLabel)
return (
<Box height={1}>
@ -349,8 +366,16 @@ export function StatusRule({
</Text>
</Box>
<Text color={t.color.border}> </Text>
<Text color={t.color.label}>{cwdLabel}</Text>
{rightWidth > 0 ? (
<>
<Text color={t.color.border}>{separatorWidth >= 3 ? ' ─ ' : ' '}</Text>
<Box flexShrink={0} width={rightWidth}>
<Text color={t.color.label} wrap="truncate-end">
{cwdLabel}
</Text>
</Box>
</>
) : null}
</Box>
)
}