Skip to content

Commit 6ee1141

Browse files
fix: widen TUI panel to prevent text truncation (#1191) (#1193)
* fix: widen TUI panel to prevent text truncation in advanced settings The panel was hard-capped at 60 characters, causing option descriptions to be cut off with ellipses on wide terminals. Raised the cap to 100 and switched MultiSelectList from truncate to wrap for graceful handling on narrow terminals. Closes #1191 * docs: add screenshot for PR * chore: remove screenshot from repo * fix: default Panel to fullWidth so it fills the terminal Addresses reviewer feedback — panels now expand to fill available terminal width by default instead of capping at a fixed column count. * fix: revert wrap change, keep truncate for multi-select list Since panels now default to fullWidth, the truncation issue is solved by the wider panel itself. Keeping truncate avoids visual ambiguity on narrow terminals where wrapped text could overlap adjacent rows. * test: replace obsolete contentWidth test with fullWidth assertion * fix: remove content width cap, use full terminal width everywhere * fix: replace manual width strings with Ink layout components Remove buildLogo and manual '─'.repeat(contentWidth) dividers. Use Ink's Box with borderStyle and width="100%" instead, letting the layout engine handle fitting within parent padding automatically.
1 parent 52a24ce commit 6ee1141

8 files changed

Lines changed: 48 additions & 105 deletions

File tree

src/cli/tui/components/Panel.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@ export interface PanelProps {
1616
fullWidth?: boolean;
1717
}
1818

19-
export function Panel({ title, children, borderColor, height, flexGrow, flexBasis, fullWidth = false }: PanelProps) {
19+
export function Panel({ title, children, borderColor, height, flexGrow, flexBasis, fullWidth = true }: PanelProps) {
2020
const { contentWidth } = useLayout();
2121

2222
return (

src/cli/tui/components/__tests__/Panel.test.tsx

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -2,20 +2,12 @@ import { Panel } from '../Panel.js';
22
import { Text } from 'ink';
33
import { render } from 'ink-testing-library';
44
import React from 'react';
5-
import { afterEach, describe, expect, it, vi } from 'vitest';
6-
7-
const { mockContentWidth } = vi.hoisted(() => ({
8-
mockContentWidth: { value: 60 },
9-
}));
5+
import { describe, expect, it, vi } from 'vitest';
106

117
vi.mock('../../context/index.js', () => ({
12-
useLayout: () => ({ contentWidth: mockContentWidth.value }),
8+
useLayout: () => ({ contentWidth: 80 }),
139
}));
1410

15-
afterEach(() => {
16-
mockContentWidth.value = 60;
17-
});
18-
1911
describe('Panel', () => {
2012
it('renders children content inside a border', () => {
2113
const { lastFrame } = render(
@@ -41,23 +33,14 @@ describe('Panel', () => {
4133
expect(frame.indexOf('Settings')).toBeLessThan(frame.indexOf('body'));
4234
});
4335

44-
it('adapts to different content widths from context', () => {
45-
mockContentWidth.value = 30;
46-
const { lastFrame: narrow } = render(
47-
<Panel>
48-
<Text>test</Text>
49-
</Panel>
50-
);
51-
52-
mockContentWidth.value = 100;
53-
const { lastFrame: wide } = render(
36+
it('defaults to full width', () => {
37+
const { lastFrame } = render(
5438
<Panel>
5539
<Text>test</Text>
5640
</Panel>
5741
);
58-
59-
const narrowTopLine = narrow()!.split('\n')[0]!;
60-
const wideTopLine = wide()!.split('\n')[0]!;
61-
expect(narrowTopLine.length).toBeLessThan(wideTopLine.length);
42+
const frame = lastFrame()!;
43+
const topLine = frame.split('\n')[0]!;
44+
expect(topLine.length).toBeGreaterThan(80);
6245
});
6346
});
Lines changed: 3 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -1,52 +1,28 @@
11
import { useStdout } from 'ink';
22
import React, { type ReactNode, createContext, useContext } from 'react';
33

4-
/** Maximum content width cap */
5-
const MAX_CONTENT_WIDTH = 60;
4+
const DEFAULT_WIDTH = 80;
65

76
interface LayoutContextValue {
8-
/** Global content width: min(terminalWidth, MAX_CONTENT_WIDTH) */
97
contentWidth: number;
108
}
119

1210
const LayoutContext = createContext<LayoutContextValue>({
13-
contentWidth: MAX_CONTENT_WIDTH,
11+
contentWidth: DEFAULT_WIDTH,
1412
});
1513

1614
// eslint-disable-next-line react-refresh/only-export-components
1715
export function useLayout(): LayoutContextValue {
1816
return useContext(LayoutContext);
1917
}
2018

21-
/**
22-
* Build the logo dynamically based on width.
23-
* The logo has fixed text " >_ AgentCore" on left and version on right,
24-
* with padding in between to fill the width.
25-
*/
26-
// eslint-disable-next-line react-refresh/only-export-components
27-
export function buildLogo(width: number, version?: string): string {
28-
const left = '│ >_ AgentCore';
29-
const right = version ? `v${version} │` : '│';
30-
// -2 for the border chars already in left/right
31-
const innerWidth = width - 2;
32-
const paddingNeeded = innerWidth - (left.length - 1) - (right.length - 1);
33-
const padding = ' '.repeat(Math.max(0, paddingNeeded));
34-
35-
const topBorder = '┌' + '─'.repeat(innerWidth) + '┐';
36-
const bottomBorder = '└' + '─'.repeat(innerWidth) + '┘';
37-
const middle = left + padding + right;
38-
39-
return `\n${topBorder}\n${middle}\n${bottomBorder}`;
40-
}
41-
4219
interface LayoutProviderProps {
4320
children: ReactNode;
4421
}
4522

4623
export function LayoutProvider({ children }: LayoutProviderProps) {
4724
const { stdout } = useStdout();
48-
const terminalWidth = stdout?.columns ?? MAX_CONTENT_WIDTH;
49-
const contentWidth = Math.min(terminalWidth, MAX_CONTENT_WIDTH);
25+
const contentWidth = stdout?.columns ?? DEFAULT_WIDTH;
5026

5127
return <LayoutContext.Provider value={{ contentWidth }}>{children}</LayoutContext.Provider>;
5228
}

src/cli/tui/context/__tests__/LayoutContext.test.ts

Lines changed: 0 additions & 33 deletions
This file was deleted.

src/cli/tui/context/index.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
export { LayoutProvider, useLayout, buildLogo } from './LayoutContext';
1+
export { LayoutProvider, useLayout } from './LayoutContext';

src/cli/tui/screens/home/CommandListScreen.tsx

Lines changed: 3 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
import { buildLogo, useLayout } from '../../context';
21
import type { CommandMeta } from '../../utils/commands';
32
import { Box, Text, useApp, useStdout } from 'ink';
43
import React, { useEffect } from 'react';
@@ -18,11 +17,9 @@ interface CommandListScreenProps {
1817
*/
1918
export function CommandListScreen({ commands }: CommandListScreenProps) {
2019
const { exit } = useApp();
21-
const { contentWidth } = useLayout();
2220
const { stdout } = useStdout();
2321
const terminalWidth = stdout?.columns ?? 80;
2422
const maxDescWidth = Math.max(20, terminalWidth - 18);
25-
const logo = buildLogo(contentWidth);
2623

2724
// Exit after render
2825
useEffect(() => {
@@ -34,7 +31,9 @@ export function CommandListScreen({ commands }: CommandListScreenProps) {
3431

3532
return (
3633
<Box flexDirection="column" paddingY={1}>
37-
<Text>{logo}</Text>
34+
<Box borderStyle="single" borderColor="cyan" width="100%" justifyContent="space-between" paddingX={1}>
35+
<Text color="cyan">{'>_ AgentCore'}</Text>
36+
</Box>
3837
<Text> </Text>
3938
<Text bold color="yellow">
4039
Usage:

src/cli/tui/screens/home/HelpScreen.tsx

Lines changed: 19 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,4 @@
11
import { Cursor, ScreenLayout } from '../../components';
2-
import { useLayout } from '../../context';
32
import { HINTS } from '../../copy';
43
import { useTextInput } from '../../hooks';
54
import type { CommandMeta } from '../../utils/commands';
@@ -84,10 +83,8 @@ function HelpDisplay({
8483
interactiveCount,
8584
notice,
8685
}: HelpDisplayProps) {
87-
const { contentWidth } = useLayout();
8886
const { stdout } = useStdout();
8987
const terminalWidth = stdout?.columns ?? 80;
90-
const bottomDivider = '─'.repeat(contentWidth);
9188

9289
const allItems = [...interactiveItems, ...cliOnlyItems];
9390
const maxLabelLen = getMaxLabelLen(allItems);
@@ -131,8 +128,16 @@ function HelpDisplay({
131128

132129
{showCliSection && (
133130
<>
134-
<Box marginTop={1}>
135-
<Text dimColor>CLI only {'─'.repeat(Math.max(0, contentWidth - 11))}</Text>
131+
<Box
132+
marginTop={1}
133+
borderStyle="single"
134+
borderTop
135+
borderBottom={false}
136+
borderLeft={false}
137+
borderRight={false}
138+
width="100%"
139+
>
140+
<Text dimColor>CLI only</Text>
136141
</Box>
137142
{cliOnlyItems.map((item, idx) => (
138143
<CommandRow
@@ -151,8 +156,15 @@ function HelpDisplay({
151156

152157
{notice && <Box marginTop={1}>{notice}</Box>}
153158

154-
<Box marginTop={1} flexDirection="column">
155-
<Text dimColor>{bottomDivider}</Text>
159+
<Box
160+
marginTop={1}
161+
flexDirection="column"
162+
borderStyle="single"
163+
borderTop
164+
borderBottom={false}
165+
borderLeft={false}
166+
borderRight={false}
167+
>
156168
<Text dimColor>{hintText}</Text>
157169
</Box>
158170
</Box>

src/cli/tui/screens/home/HomeScreen.tsx

Lines changed: 14 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,5 @@
11
import { findConfigRoot } from '../../../../lib';
22
import { Cursor, ScreenLayout } from '../../components';
3-
import { buildLogo, useLayout } from '../../context';
43
import { HINTS } from '../../copy';
54
import { Box, Text, useApp, useInput } from 'ink';
65
import React from 'react';
@@ -53,10 +52,7 @@ interface HomeScreenProps {
5352

5453
export function HomeScreen({ cwd: _cwd, version, onShowHelp, onSelectCreate }: HomeScreenProps) {
5554
const { exit } = useApp();
56-
const { contentWidth } = useLayout();
5755
const showQuickStart = !hasProject();
58-
const logo = buildLogo(contentWidth, version);
59-
const divider = '─'.repeat(contentWidth);
6056

6157
useInput((input, key) => {
6258
if (key.escape) {
@@ -82,8 +78,11 @@ export function HomeScreen({ cwd: _cwd, version, onShowHelp, onSelectCreate }: H
8278
return (
8379
<ScreenLayout>
8480
<Box flexDirection="column">
85-
{/* Logo with version - always at top */}
86-
<Text color="cyan">{logo}</Text>
81+
{/* Logo with version */}
82+
<Box borderStyle="single" borderColor="cyan" width="100%" justifyContent="space-between" paddingX={1}>
83+
<Text color="cyan">{'>_ AgentCore'}</Text>
84+
{version && <Text color="cyan">v{version}</Text>}
85+
</Box>
8786

8887
{/* Input - directly under logo */}
8988
<Box marginTop={1}>
@@ -97,8 +96,15 @@ export function HomeScreen({ cwd: _cwd, version, onShowHelp, onSelectCreate }: H
9796
{showQuickStart ? <QuickStart /> : <Box height={QUICK_START_LINES} />}
9897

9998
{/* Divider and hint at bottom */}
100-
<Box marginTop={1} flexDirection="column">
101-
<Text dimColor>{divider}</Text>
99+
<Box
100+
marginTop={1}
101+
flexDirection="column"
102+
borderStyle="single"
103+
borderTop
104+
borderBottom={false}
105+
borderLeft={false}
106+
borderRight={false}
107+
>
102108
<Text dimColor>{HINTS.HOME}</Text>
103109
</Box>
104110
</Box>

0 commit comments

Comments
 (0)