From 11b0d9ed2f89124c1fdd8b2cb39eb4d88830f2f6 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 23 May 2026 13:47:35 -0500 Subject: [PATCH 1/3] fix(tui): keep status rule one-line in skinny terminals Clamp and truncate the cwd/branch segment so narrow status bars cannot wrap into the composer input row. --- ui-tui/src/__tests__/statusRule.test.ts | 25 +++++++++++++++++++++++++ ui-tui/src/components/appChrome.tsx | 25 ++++++++++++++++++++++--- 2 files changed, 47 insertions(+), 3 deletions(-) create mode 100644 ui-tui/src/__tests__/statusRule.test.ts diff --git a/ui-tui/src/__tests__/statusRule.test.ts b/ui-tui/src/__tests__/statusRule.test.ts new file mode 100644 index 00000000000..bf34e37750c --- /dev/null +++ b/ui-tui/src/__tests__/statusRule.test.ts @@ -0,0 +1,25 @@ +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: 1, rightWidth: 0, separatorWidth: 1 }) + }) +}) diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index c961f4c2731..8c386200ad6 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -150,6 +150,17 @@ 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 separatorWidth = width >= 24 ? 3 : 1 + const minLeftWidth = width >= 24 ? 8 : 1 + const maxRightWidth = Math.max(0, width - separatorWidth - minLeftWidth) + const rightWidth = Math.max(0, Math.min(cwdLabel.length, maxRightWidth)) + 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 +308,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 ( @@ -349,8 +360,16 @@ export function StatusRule({ - - {cwdLabel} + {rightWidth > 0 ? ( + <> + {separatorWidth >= 3 ? ' ─ ' : ' '} + + + {cwdLabel} + + + + ) : null} ) } From 4d9791c551bdba614ef6f7f0096ae59e06d6823b Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 23 May 2026 14:06:08 -0500 Subject: [PATCH 2/3] fix(tui): reclaim status width when cwd is hidden Make the cwd separator width conditional so the computed status layout matches the rendered row on ultra-narrow terminals. --- ui-tui/src/__tests__/statusRule.test.ts | 2 +- ui-tui/src/components/appChrome.tsx | 10 ++++++++-- 2 files changed, 9 insertions(+), 3 deletions(-) diff --git a/ui-tui/src/__tests__/statusRule.test.ts b/ui-tui/src/__tests__/statusRule.test.ts index bf34e37750c..0249c7e19e5 100644 --- a/ui-tui/src/__tests__/statusRule.test.ts +++ b/ui-tui/src/__tests__/statusRule.test.ts @@ -20,6 +20,6 @@ describe('statusRuleWidths', () => { }) it('omits the cwd segment when there is no room for it', () => { - expect(statusRuleWidths(2, 'abcdef')).toEqual({ leftWidth: 1, rightWidth: 0, separatorWidth: 1 }) + expect(statusRuleWidths(2, 'abcdef')).toEqual({ leftWidth: 2, rightWidth: 0, separatorWidth: 0 }) }) }) diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 8c386200ad6..01bbd92c204 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -152,10 +152,16 @@ function ctxBar(pct: number | undefined, w = 10) { export function statusRuleWidths(cols: number, cwdLabel: string) { const width = Math.max(1, Math.floor(cols || 1)) - const separatorWidth = width >= 24 ? 3 : 1 + const desiredSeparatorWidth = width >= 24 ? 3 : 1 const minLeftWidth = width >= 24 ? 8 : 1 - const maxRightWidth = Math.max(0, width - separatorWidth - minLeftWidth) + 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(cwdLabel.length, maxRightWidth)) + const separatorWidth = rightWidth > 0 ? desiredSeparatorWidth : 0 const leftWidth = Math.max(1, width - separatorWidth - rightWidth) return { leftWidth, rightWidth, separatorWidth } From a4c27af6973974b88894020afc1fcb22072a9cc4 Mon Sep 17 00:00:00 2001 From: Brooklyn Nicholson Date: Sat, 23 May 2026 14:11:44 -0500 Subject: [PATCH 3/3] fix(tui): measure status cwd by display width Budget the right-hand status label by terminal display width so wide Unicode paths cannot wrap skinny status bars. --- ui-tui/src/__tests__/statusRule.test.ts | 7 +++++++ ui-tui/src/components/appChrome.tsx | 4 ++-- 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/ui-tui/src/__tests__/statusRule.test.ts b/ui-tui/src/__tests__/statusRule.test.ts index 0249c7e19e5..635b35db996 100644 --- a/ui-tui/src/__tests__/statusRule.test.ts +++ b/ui-tui/src/__tests__/statusRule.test.ts @@ -22,4 +22,11 @@ describe('statusRuleWidths', () => { 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) + }) }) diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx index 01bbd92c204..771c917691f 100644 --- a/ui-tui/src/components/appChrome.tsx +++ b/ui-tui/src/components/appChrome.tsx @@ -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' @@ -160,7 +160,7 @@ export function statusRuleWidths(cols: number, cwdLabel: string) { return { leftWidth: width, rightWidth: 0, separatorWidth: 0 } } - const rightWidth = Math.max(0, Math.min(cwdLabel.length, maxRightWidth)) + const rightWidth = Math.max(0, Math.min(stringWidth(cwdLabel), maxRightWidth)) const separatorWidth = rightWidth > 0 ? desiredSeparatorWidth : 0 const leftWidth = Math.max(1, width - separatorWidth - rightWidth)