Skip to content
2 changes: 1 addition & 1 deletion src/cli/tui/components/Panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@ export interface PanelProps {
fullWidth?: boolean;
}

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

return (
Expand Down
31 changes: 7 additions & 24 deletions src/cli/tui/components/__tests__/Panel.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,12 @@ import { Panel } from '../Panel.js';
import { Text } from 'ink';
import { render } from 'ink-testing-library';
import React from 'react';
import { afterEach, describe, expect, it, vi } from 'vitest';

const { mockContentWidth } = vi.hoisted(() => ({
mockContentWidth: { value: 60 },
}));
import { describe, expect, it, vi } from 'vitest';

vi.mock('../../context/index.js', () => ({
useLayout: () => ({ contentWidth: mockContentWidth.value }),
useLayout: () => ({ contentWidth: 80 }),
}));

afterEach(() => {
mockContentWidth.value = 60;
});

describe('Panel', () => {
it('renders children content inside a border', () => {
const { lastFrame } = render(
Expand All @@ -41,23 +33,14 @@ describe('Panel', () => {
expect(frame.indexOf('Settings')).toBeLessThan(frame.indexOf('body'));
});

it('adapts to different content widths from context', () => {
mockContentWidth.value = 30;
const { lastFrame: narrow } = render(
<Panel>
<Text>test</Text>
</Panel>
);

mockContentWidth.value = 100;
const { lastFrame: wide } = render(
it('defaults to full width', () => {
const { lastFrame } = render(
<Panel>
<Text>test</Text>
</Panel>
);

const narrowTopLine = narrow()!.split('\n')[0]!;
const wideTopLine = wide()!.split('\n')[0]!;
expect(narrowTopLine.length).toBeLessThan(wideTopLine.length);
const frame = lastFrame()!;
const topLine = frame.split('\n')[0]!;
expect(topLine.length).toBeGreaterThan(80);
});
});
30 changes: 3 additions & 27 deletions src/cli/tui/context/LayoutContext.tsx
Original file line number Diff line number Diff line change
@@ -1,52 +1,28 @@
import { useStdout } from 'ink';
import React, { type ReactNode, createContext, useContext } from 'react';

/** Maximum content width cap */
const MAX_CONTENT_WIDTH = 60;
const DEFAULT_WIDTH = 80;
Comment thread
jesseturner21 marked this conversation as resolved.

interface LayoutContextValue {
/** Global content width: min(terminalWidth, MAX_CONTENT_WIDTH) */
contentWidth: number;
}

const LayoutContext = createContext<LayoutContextValue>({
contentWidth: MAX_CONTENT_WIDTH,
contentWidth: DEFAULT_WIDTH,
});

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

/**
* Build the logo dynamically based on width.
* The logo has fixed text " >_ AgentCore" on left and version on right,
* with padding in between to fill the width.
*/
// eslint-disable-next-line react-refresh/only-export-components
export function buildLogo(width: number, version?: string): string {
const left = '│ >_ AgentCore';
const right = version ? `v${version} │` : '│';
// -2 for the border chars already in left/right
const innerWidth = width - 2;
const paddingNeeded = innerWidth - (left.length - 1) - (right.length - 1);
const padding = ' '.repeat(Math.max(0, paddingNeeded));

const topBorder = '┌' + '─'.repeat(innerWidth) + '┐';
const bottomBorder = '└' + '─'.repeat(innerWidth) + '┘';
const middle = left + padding + right;

return `\n${topBorder}\n${middle}\n${bottomBorder}`;
}

interface LayoutProviderProps {
children: ReactNode;
}

export function LayoutProvider({ children }: LayoutProviderProps) {
const { stdout } = useStdout();
const terminalWidth = stdout?.columns ?? MAX_CONTENT_WIDTH;
const contentWidth = Math.min(terminalWidth, MAX_CONTENT_WIDTH);
const contentWidth = stdout?.columns ?? DEFAULT_WIDTH;

return <LayoutContext.Provider value={{ contentWidth }}>{children}</LayoutContext.Provider>;
}
33 changes: 0 additions & 33 deletions src/cli/tui/context/__tests__/LayoutContext.test.ts

This file was deleted.

2 changes: 1 addition & 1 deletion src/cli/tui/context/index.ts
Original file line number Diff line number Diff line change
@@ -1 +1 @@
export { LayoutProvider, useLayout, buildLogo } from './LayoutContext';
export { LayoutProvider, useLayout } from './LayoutContext';
7 changes: 3 additions & 4 deletions src/cli/tui/screens/home/CommandListScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,4 +1,3 @@
import { buildLogo, useLayout } from '../../context';
import type { CommandMeta } from '../../utils/commands';
import { Box, Text, useApp, useStdout } from 'ink';
import React, { useEffect } from 'react';
Expand All @@ -18,11 +17,9 @@ interface CommandListScreenProps {
*/
export function CommandListScreen({ commands }: CommandListScreenProps) {
const { exit } = useApp();
const { contentWidth } = useLayout();
const { stdout } = useStdout();
const terminalWidth = stdout?.columns ?? 80;
const maxDescWidth = Math.max(20, terminalWidth - 18);
const logo = buildLogo(contentWidth);

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

return (
<Box flexDirection="column" paddingY={1}>
<Text>{logo}</Text>
<Box borderStyle="single" borderColor="cyan" width="100%" justifyContent="space-between" paddingX={1}>
<Text color="cyan">{'>_ AgentCore'}</Text>
</Box>
<Text> </Text>
<Text bold color="yellow">
Usage:
Expand Down
26 changes: 19 additions & 7 deletions src/cli/tui/screens/home/HelpScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,4 @@
import { Cursor, ScreenLayout } from '../../components';
import { useLayout } from '../../context';
import { HINTS } from '../../copy';
import { useTextInput } from '../../hooks';
import type { CommandMeta } from '../../utils/commands';
Expand Down Expand Up @@ -84,10 +83,8 @@ function HelpDisplay({
interactiveCount,
notice,
}: HelpDisplayProps) {
const { contentWidth } = useLayout();
const { stdout } = useStdout();
const terminalWidth = stdout?.columns ?? 80;
const bottomDivider = '─'.repeat(contentWidth);

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

{showCliSection && (
<>
<Box marginTop={1}>
<Text dimColor>CLI only {'─'.repeat(Math.max(0, contentWidth - 11))}</Text>
<Box
marginTop={1}
borderStyle="single"
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
width="100%"
>
<Text dimColor>CLI only</Text>
</Box>
{cliOnlyItems.map((item, idx) => (
<CommandRow
Expand All @@ -151,8 +156,15 @@ function HelpDisplay({

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

<Box marginTop={1} flexDirection="column">
<Text dimColor>{bottomDivider}</Text>
<Box
marginTop={1}
flexDirection="column"
borderStyle="single"
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
>
<Text dimColor>{hintText}</Text>
</Box>
</Box>
Expand Down
22 changes: 14 additions & 8 deletions src/cli/tui/screens/home/HomeScreen.tsx
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { findConfigRoot } from '../../../../lib';
import { Cursor, ScreenLayout } from '../../components';
import { buildLogo, useLayout } from '../../context';
import { HINTS } from '../../copy';
import { Box, Text, useApp, useInput } from 'ink';
import React from 'react';
Expand Down Expand Up @@ -53,10 +52,7 @@ interface HomeScreenProps {

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

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

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

{/* Divider and hint at bottom */}
<Box marginTop={1} flexDirection="column">
<Text dimColor>{divider}</Text>
<Box
marginTop={1}
flexDirection="column"
borderStyle="single"
borderTop
borderBottom={false}
borderLeft={false}
borderRight={false}
>
<Text dimColor>{HINTS.HOME}</Text>
</Box>
</Box>
Expand Down
Loading