Skip to content

Commit 1fdf29e

Browse files
Tighten TUI spacing for real terminals
1 parent e93193e commit 1fdf29e

10 files changed

Lines changed: 220 additions & 177 deletions

src/tui/layouts/AuthLayout.tsx

Lines changed: 24 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { Panel } from "../components/Panel.js";
66
import { KVRow, TuiFooter, TuiHeader } from "../components/TuiFrame.js";
77
import { useFocusNavigation, type FocusItem } from "../hooks/useFocusNavigation.js";
88
import { useTerminalSize } from "../hooks/useTerminalSize.js";
9-
import { colors } from "../theme.js";
9+
import { colors, layout as tuiLayout } from "../theme.js";
1010

1111
const LABELS: Record<AIProvider, string> = {
1212
openai: "OpenAI",
@@ -71,7 +71,7 @@ export function AuthLayout() {
7171
<Box key={`${terminal.width}x${terminal.height}`} flexDirection="column" width={terminal.width} height={terminal.height}>
7272
<TuiHeader command="setupr auth" title="global auth" status={activeModel.id} statusColor={colors.success} right="masked keys only" width={terminal.width} />
7373

74-
<Box flexDirection={layout.stacked ? "column" : "row"} width="100%" height={layout.contentHeight} flexShrink={1} overflow="hidden">
74+
<Box flexDirection={layout.stacked ? "column" : "row"} width="100%" height={layout.contentHeight} flexShrink={1} overflow="hidden" gap={tuiLayout.panelGap}>
7575
<Panel title="Providers" focusState={focus.focusState("providers")} width={layout.stacked ? "100%" : layout.providers.width} height={layout.providers.height}>
7676
<Box flexDirection="column">
7777
{rows.slice(0, layout.providerLimit).map((row) => (
@@ -83,18 +83,19 @@ export function AuthLayout() {
8383
</Box>
8484
</Panel>
8585

86-
<Panel title="Active Model" focusState={focus.focusState("model")} width={layout.stacked ? "100%" : layout.model.width} height={layout.stacked ? layout.model.height : Math.max(8, Math.floor(layout.model.height * 0.36))}>
87-
<ModelPanel activeModel={activeModel} />
88-
</Panel>
89-
90-
{!layout.stacked && (
91-
<Panel title="Model Catalog" focusState={focus.focusState("catalog")} width={layout.model.width} height={layout.model.height - Math.max(8, Math.floor(layout.model.height * 0.36))}>
92-
<ModelCatalog availableModels={availableModels} activeModelId={activeModel.id} limit={layout.modelLimit} />
86+
<Box flexDirection="column" width={layout.stacked ? "100%" : layout.model.width} height={layout.model.height} gap={tuiLayout.panelGap}>
87+
<Panel title="Active Model" focusState={focus.focusState("model")} width="100%" height={layout.stacked ? layout.model.height : Math.max(8, Math.floor((layout.model.height - tuiLayout.panelGap) * 0.36))}>
88+
<ModelPanel activeModel={activeModel} />
9389
</Panel>
94-
)}
90+
{!layout.stacked && (
91+
<Panel title="Model Catalog" focusState={focus.focusState("catalog")} width="100%" flexGrow={1} minHeight={8}>
92+
<ModelCatalog availableModels={availableModels} activeModelId={activeModel.id} limit={layout.modelLimit} />
93+
</Panel>
94+
)}
95+
</Box>
9596

96-
<Box flexDirection="column" width={layout.stacked ? "100%" : layout.actions.width} height={layout.actions.height}>
97-
<Panel title="Test Results" focusState={focus.focusState("tests")} width="100%" height={layout.stacked ? Math.max(6, Math.floor(layout.actions.height * 0.46)) : Math.max(8, Math.floor(layout.actions.height * 0.48))}>
97+
<Box flexDirection="column" width={layout.stacked ? "100%" : layout.actions.width} height={layout.actions.height} gap={tuiLayout.panelGap}>
98+
<Panel title="Test Results" focusState={focus.focusState("tests")} width="100%" height={layout.stacked ? Math.max(6, Math.floor(layout.actions.height * 0.46)) : Math.max(8, Math.floor((layout.actions.height - tuiLayout.panelGap) * 0.48))}>
9899
<TestResults rows={rows} activeModelProvider={activeModel.provider} />
99100
</Panel>
100101
<Panel title="Secure Storage" focusState={focus.focusState("storage")} width="100%" flexGrow={1} minHeight={6}>
@@ -192,6 +193,7 @@ interface AuthLayoutGeometry {
192193

193194
function buildAuthLayout(width: number, height: number): AuthLayoutGeometry {
194195
const contentHeight = Math.max(8, height - 2);
196+
const gap = tuiLayout.panelGap;
195197
const compact = width < 96 || contentHeight < 18;
196198
if (compact) {
197199
return {
@@ -212,7 +214,7 @@ function buildAuthLayout(width: number, height: number): AuthLayoutGeometry {
212214
if (stacked) {
213215
const providerHeight = clamp(Math.floor(contentHeight * 0.42), 6, Math.max(6, contentHeight - 8));
214216
const modelHeight = clamp(Math.floor(contentHeight * 0.30), 5, Math.max(5, contentHeight - providerHeight - 4));
215-
const actionsHeight = Math.max(4, contentHeight - providerHeight - modelHeight);
217+
const actionsHeight = Math.max(4, contentHeight - providerHeight - modelHeight - gap * 2);
216218
return {
217219
width,
218220
height,
@@ -222,12 +224,12 @@ function buildAuthLayout(width: number, height: number): AuthLayoutGeometry {
222224
providerLimit: Math.max(1, providerHeight - 3),
223225
modelLimit: Math.max(1, modelHeight - 6),
224226
providers: { x: 1, y: 2, width, height: providerHeight },
225-
model: { x: 1, y: providerHeight + 2, width, height: modelHeight },
226-
actions: { x: 1, y: providerHeight + modelHeight + 2, width, height: actionsHeight },
227+
model: { x: 1, y: providerHeight + gap + 2, width, height: modelHeight },
228+
actions: { x: 1, y: providerHeight + modelHeight + gap * 2 + 2, width, height: actionsHeight },
227229
};
228230
}
229231

230-
const [providersWidth, modelWidth, actionsWidth] = distributeWidths(width, [0.28, 0.48, 0.24], [30, 40, 28]);
232+
const [providersWidth, modelWidth, actionsWidth] = distributeWidths(Math.max(1, width - gap * 2), [0.28, 0.48, 0.24], [30, 40, 28]);
231233
return {
232234
width,
233235
height,
@@ -237,8 +239,8 @@ function buildAuthLayout(width: number, height: number): AuthLayoutGeometry {
237239
providerLimit: Math.max(1, contentHeight - 3),
238240
modelLimit: Math.max(1, contentHeight - 7),
239241
providers: { x: 1, y: 2, width: providersWidth, height: contentHeight },
240-
model: { x: providersWidth + 1, y: 2, width: modelWidth, height: contentHeight },
241-
actions: { x: providersWidth + modelWidth + 1, y: 2, width: actionsWidth, height: contentHeight },
242+
model: { x: providersWidth + gap + 1, y: 2, width: modelWidth, height: contentHeight },
243+
actions: { x: providersWidth + modelWidth + gap * 2 + 1, y: 2, width: actionsWidth, height: contentHeight },
242244
};
243245
}
244246

@@ -258,10 +260,10 @@ function buildFocusItems(layout: AuthLayoutGeometry): FocusItem[] {
258260
}
259261
return [
260262
{ id: "providers", row: 0, column: 0, bounds: layout.providers },
261-
{ id: "model", row: 0, column: 1, bounds: layout.model },
262-
{ id: "catalog", row: 1, column: 1, bounds: layout.model },
263-
{ id: "tests", row: 0, column: 2, bounds: layout.actions },
264-
{ id: "storage", row: 1, column: 2, bounds: layout.actions },
263+
{ id: "model", row: 0, column: 1, bounds: { ...layout.model, height: Math.max(8, Math.floor((layout.model.height - tuiLayout.panelGap) * 0.36)) } },
264+
{ id: "catalog", row: 1, column: 1, bounds: { ...layout.model, y: layout.model.y + Math.max(8, Math.floor((layout.model.height - tuiLayout.panelGap) * 0.36)) + tuiLayout.panelGap, height: Math.max(8, layout.model.height - Math.max(8, Math.floor((layout.model.height - tuiLayout.panelGap) * 0.36)) - tuiLayout.panelGap) } },
265+
{ id: "tests", row: 0, column: 2, bounds: { ...layout.actions, height: Math.max(8, Math.floor((layout.actions.height - tuiLayout.panelGap) * 0.48)) } },
266+
{ id: "storage", row: 1, column: 2, bounds: { ...layout.actions, y: layout.actions.y + Math.max(8, Math.floor((layout.actions.height - tuiLayout.panelGap) * 0.48)) + tuiLayout.panelGap, height: Math.max(6, layout.actions.height - Math.max(8, Math.floor((layout.actions.height - tuiLayout.panelGap) * 0.48)) - tuiLayout.panelGap) } },
265267
];
266268
}
267269

src/tui/layouts/ChatLayout.tsx

Lines changed: 11 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -17,7 +17,7 @@ import { useFocusNavigation, type FocusBounds, type FocusItem } from "../hooks/u
1717
import { useAppStore } from "../hooks/useStore.js";
1818
import { useTerminalSize } from "../hooks/useTerminalSize.js";
1919
import { parseSgrMouse } from "../terminalInput.js";
20-
import { colors, icons } from "../theme.js";
20+
import { colors, icons, layout as tuiLayout } from "../theme.js";
2121

2222
interface ChatLayoutProps {
2323
cwd: string;
@@ -287,10 +287,13 @@ export function ChatLayout({ cwd, store, initialMessage, startNew = false }: Cha
287287
export function buildChatLayout(width: number, height: number): ChatLayoutModel {
288288
const stacked = width < 96 || height < 22;
289289
const bodyHeight = Math.max(8, height - 2);
290+
const gap = tuiLayout.panelGap;
291+
const pairTotal = stacked ? width : Math.max(1, width - gap);
290292
const rightWidth = stacked ? width : clamp(Math.floor(width * 0.26), 28, 42);
291-
const leftWidth = stacked ? width : width - rightWidth;
292-
const planHeight = stacked ? clamp(Math.floor(bodyHeight * 0.24), 4, 8) : clamp(Math.floor(bodyHeight * 0.52), 8, bodyHeight - 6);
293-
const statusHeight = stacked ? clamp(Math.floor(bodyHeight * 0.2), 4, 7) : bodyHeight - planHeight;
293+
const leftWidth = stacked ? width : pairTotal - rightWidth;
294+
const sideHeight = stacked ? bodyHeight : Math.max(8, bodyHeight - gap);
295+
const planHeight = stacked ? clamp(Math.floor(bodyHeight * 0.24), 4, 8) : clamp(Math.floor(sideHeight * 0.52), 8, sideHeight - 6);
296+
const statusHeight = stacked ? clamp(Math.floor(bodyHeight * 0.2), 4, 7) : sideHeight - planHeight;
294297
const inputMaxLines = Math.max(1, Math.min(6, Math.floor(bodyHeight / 5)));
295298
const inputHeight = inputMaxLines + 2;
296299
const transcriptHeight = stacked
@@ -327,8 +330,8 @@ export function buildChatFocusItems(layout: ChatLayoutModel): FocusItem[] {
327330
return [
328331
{ id: "conversation", row: 0, column: 0, redirectTo: "input", bounds: { x: 1, y: 2, width: layout.leftWidth, height: layout.bodyHeight } },
329332
{ id: "input", row: 1, column: 0, parentIds: ["conversation"], bounds: layout.inputBounds },
330-
{ id: "plan", row: 0, column: 1, bounds: { x: layout.leftWidth + 1, y: 2, width: layout.rightWidth, height: layout.planHeight } },
331-
{ id: "status", row: 1, column: 1, bounds: { x: layout.leftWidth + 1, y: 2 + layout.planHeight, width: layout.rightWidth, height: layout.statusHeight } },
333+
{ id: "plan", row: 0, column: 1, bounds: { x: layout.leftWidth + tuiLayout.panelGap + 1, y: 2, width: layout.rightWidth, height: layout.planHeight } },
334+
{ id: "status", row: 1, column: 1, bounds: { x: layout.leftWidth + tuiLayout.panelGap + 1, y: 2 + layout.planHeight + tuiLayout.panelGap, width: layout.rightWidth, height: layout.statusHeight } },
332335
];
333336
}
334337

@@ -349,9 +352,9 @@ function Header({ cwd, projectName, status, ready, width }: { cwd: string; proje
349352

350353
function WideChat(props: ChatViewProps) {
351354
return (
352-
<Box flexDirection="row" width={props.layout.width} height={props.layout.bodyHeight}>
355+
<Box flexDirection="row" width={props.layout.width} height={props.layout.bodyHeight} gap={tuiLayout.panelGap}>
353356
<ConversationPanel {...props} width={props.layout.leftWidth} />
354-
<Box flexDirection="column" width={props.layout.rightWidth} height="100%">
357+
<Box flexDirection="column" width={props.layout.rightWidth} height="100%" gap={tuiLayout.panelGap}>
355358
<PlanPanel steps={props.steps} focusState={props.focus("plan")} height={props.layout.planHeight} />
356359
<StatusPanel {...props} height={props.layout.statusHeight} />
357360
</Box>

src/tui/layouts/CleanLayout.tsx

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ import { Spinner } from "../components/Spinner.js";
1010
import { KVRow, MetricText, TuiFooter, TuiHeader, statusColor } from "../components/TuiFrame.js";
1111
import { useFocusNavigation, type FocusBounds, type FocusItem } from "../hooks/useFocusNavigation.js";
1212
import { useTerminalSize } from "../hooks/useTerminalSize.js";
13-
import { colors, icons } from "../theme.js";
13+
import { colors, icons, layout as tuiLayout } from "../theme.js";
1414

1515
interface CleanTarget {
1616
path: string;
@@ -152,10 +152,10 @@ export function CleanLayout({ cwd, mode, force = false }: CleanLayoutProps) {
152152
}
153153

154154
function WideClean(props: CleanViewProps) {
155-
const panelHeight = Math.max(6, props.layout.bodyHeight - props.layout.inputHeight);
155+
const panelHeight = Math.max(6, props.layout.bodyHeight - props.layout.inputHeight - tuiLayout.panelGap);
156156
return (
157-
<Box flexDirection="column" width={props.layout.width} height={props.layout.bodyHeight}>
158-
<Box flexDirection="row" width="100%" height={panelHeight}>
157+
<Box flexDirection="column" width={props.layout.width} height={props.layout.bodyHeight} gap={tuiLayout.panelGap}>
158+
<Box flexDirection="row" width="100%" height={panelHeight} gap={tuiLayout.panelGap}>
159159
<Panel title="Targets" focusState={props.focus("targets")} width={props.layout.targetsWidth} height="100%">
160160
<TargetsPanel {...props} limit={panelHeight - 3} />
161161
</Panel>
@@ -172,12 +172,12 @@ function WideClean(props: CleanViewProps) {
172172
}
173173

174174
function StackedClean(props: CleanViewProps) {
175-
const panelHeight = Math.max(8, props.layout.bodyHeight - props.layout.inputHeight);
175+
const panelHeight = Math.max(8, props.layout.bodyHeight - props.layout.inputHeight - tuiLayout.panelGap * 3);
176176
const targetsHeight = Math.max(8, Math.floor(panelHeight * 0.45));
177177
const reviewHeight = Math.max(7, Math.floor(panelHeight * 0.32));
178178
const riskHeight = Math.max(5, panelHeight - targetsHeight - reviewHeight);
179179
return (
180-
<Box flexDirection="column" width={props.layout.width} height={props.layout.bodyHeight}>
180+
<Box flexDirection="column" width={props.layout.width} height={props.layout.bodyHeight} gap={tuiLayout.panelGap}>
181181
<Panel title="Targets" focusState={props.focus("targets")} width="100%" height={targetsHeight}>
182182
<TargetsPanel {...props} limit={targetsHeight - 3} />
183183
</Panel>
@@ -271,9 +271,10 @@ function RiskPanel({ phase, risk, failed, totalBytes, targets, compact = false }
271271
export function buildCleanLayout(width: number, height: number): CleanLayoutGeometry {
272272
const bodyHeight = Math.max(8, height - 2);
273273
const stacked = width < 106 || bodyHeight < 22;
274+
const gap = tuiLayout.panelGap;
274275
const targetsWidth = stacked ? width : clamp(Math.floor(width * 0.30), 28, 42);
275276
const riskWidth = stacked ? width : clamp(Math.floor(width * 0.27), 28, 40);
276-
const reviewWidth = stacked ? width : width - targetsWidth - riskWidth;
277+
const reviewWidth = stacked ? width : Math.max(8, width - targetsWidth - riskWidth - gap * 2);
277278
const inputMaxLines = Math.max(1, Math.min(4, Math.floor(bodyHeight / 6)));
278279
const inputHeight = inputMaxLines + 2;
279280
const inputBounds = {
@@ -287,22 +288,22 @@ export function buildCleanLayout(width: number, height: number): CleanLayoutGeom
287288

288289
export function buildCleanFocusItems(layout: CleanLayoutGeometry): FocusItem[] {
289290
if (layout.stacked) {
290-
const panelHeight = Math.max(8, layout.bodyHeight - layout.inputHeight);
291+
const panelHeight = Math.max(8, layout.bodyHeight - layout.inputHeight - tuiLayout.panelGap * 3);
291292
const targetsHeight = Math.max(8, Math.floor(panelHeight * 0.45));
292293
const reviewHeight = Math.max(7, Math.floor(panelHeight * 0.32));
293294
const riskHeight = Math.max(5, panelHeight - targetsHeight - reviewHeight);
294295
return [
295296
{ id: "targets", row: 0, column: 0, bounds: { x: 1, y: 2, width: layout.width, height: targetsHeight } },
296-
{ id: "review", row: 1, column: 0, redirectTo: "input", bounds: { x: 1, y: 2 + targetsHeight, width: layout.width, height: reviewHeight } },
297+
{ id: "review", row: 1, column: 0, redirectTo: "input", bounds: { x: 1, y: 2 + targetsHeight + tuiLayout.panelGap, width: layout.width, height: reviewHeight } },
297298
{ id: "input", row: 2, column: 0, parentIds: ["review"], bounds: layout.inputBounds },
298-
{ id: "risk", row: 3, column: 0, bounds: { x: 1, y: 2 + targetsHeight + reviewHeight, width: layout.width, height: riskHeight } },
299+
{ id: "risk", row: 3, column: 0, bounds: { x: 1, y: 2 + targetsHeight + reviewHeight + tuiLayout.panelGap * 2, width: layout.width, height: riskHeight } },
299300
];
300301
}
301302
return [
302-
{ id: "targets", row: 0, column: 0, bounds: { x: 1, y: 2, width: layout.targetsWidth, height: layout.bodyHeight - layout.inputHeight } },
303-
{ id: "review", row: 0, column: 1, redirectTo: "input", bounds: { x: layout.targetsWidth + 1, y: 2, width: layout.reviewWidth, height: layout.bodyHeight - layout.inputHeight } },
303+
{ id: "targets", row: 0, column: 0, bounds: { x: 1, y: 2, width: layout.targetsWidth, height: layout.bodyHeight - layout.inputHeight - tuiLayout.panelGap } },
304+
{ id: "review", row: 0, column: 1, redirectTo: "input", bounds: { x: layout.targetsWidth + tuiLayout.panelGap + 1, y: 2, width: layout.reviewWidth, height: layout.bodyHeight - layout.inputHeight - tuiLayout.panelGap } },
304305
{ id: "input", row: 1, column: 1, parentIds: ["review"], bounds: layout.inputBounds },
305-
{ id: "risk", row: 0, column: 2, bounds: { x: layout.targetsWidth + layout.reviewWidth + 1, y: 2, width: layout.riskWidth, height: layout.bodyHeight - layout.inputHeight } },
306+
{ id: "risk", row: 0, column: 2, bounds: { x: layout.targetsWidth + layout.reviewWidth + tuiLayout.panelGap * 2 + 1, y: 2, width: layout.riskWidth, height: layout.bodyHeight - layout.inputHeight - tuiLayout.panelGap } },
306307
];
307308
}
308309

0 commit comments

Comments
 (0)