diff --git a/ui-tui/src/__tests__/statusRule.test.ts b/ui-tui/src/__tests__/statusRule.test.ts
new file mode 100644
index 00000000000..635b35db996
--- /dev/null
+++ b/ui-tui/src/__tests__/statusRule.test.ts
@@ -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)
+ })
+})
diff --git a/ui-tui/src/components/appChrome.tsx b/ui-tui/src/components/appChrome.tsx
index c961f4c2731..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'
@@ -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 (
@@ -349,8 +366,16 @@ export function StatusRule({
- ─
- {cwdLabel}
+ {rightWidth > 0 ? (
+ <>
+ {separatorWidth >= 3 ? ' ─ ' : ' '}
+
+
+ {cwdLabel}
+
+
+ >
+ ) : null}
)
}