Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 9 additions & 5 deletions src/components/FormRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ export interface FormRadioGroupProps {
// FormRadioGroup renders a column of radio rows. It is fully controlled: the
// parent owns the focused index and the key handling that moves it.
export function FormRadioGroup({
name,
name = "",
helpText,
options,
focusedIndex,
Expand All @@ -31,10 +31,14 @@ export function FormRadioGroup({

return (
<Box flexDirection="column">
<Box flexDirection="column">
{name && <Text color={theme.colors.text}>{name}</Text>}
<Text color={theme.colors.muted}>{helpText}</Text>
</Box>
{/* Either row is omitted when empty, so a caller whose surrounding
context already asks the question renders just the options. */}
{(name !== "" || helpText !== "") && (
<Box flexDirection="column">
{name !== "" && <Text color={theme.colors.text}>{name}</Text>}
{helpText !== "" && <Text color={theme.colors.muted}>{helpText}</Text>}
</Box>
)}
<Box
flexDirection="column"
paddingX={1}
Expand Down
12 changes: 8 additions & 4 deletions src/components/FormTextInput.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ export function FormTextInput({
}: FormTextInputProps) {
return (
<Box flexDirection="column">
<Box flexDirection="column">
<Text color={theme.colors.text}>{name}</Text>
<Text color={theme.colors.muted}>{helpText}</Text>
</Box>
{/* Either row is omitted when empty, so a caller whose surrounding
context already asks the question renders just the input. */}
{(name !== "" || helpText !== "") && (
<Box flexDirection="column">
{name !== "" && <Text color={theme.colors.text}>{name}</Text>}
{helpText !== "" && <Text color={theme.colors.muted}>{helpText}</Text>}
</Box>
)}
<Box borderStyle="round" borderColor={focused ? theme.colors.focus : theme.colors.border}>
<TextInput
value={value}
Expand Down
7 changes: 5 additions & 2 deletions src/components/KeyValueTable.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,10 @@ export interface KeyValueTableProps {
// legibly instead of squeezing it into the margin. The gap is inside the
// column, so a key that fills the cap still stands clear of its value. Capped
// by layout rather than by reading the terminal width: a resize re-lays out
// without re-rendering, so a width computed in render would go stale.
// without re-rendering, so a width computed in render would go stale. The table
// grows into the space it is given for the same reason — a percentage cap needs
// a parent whose width it can be a share of, and a table laid out as a row item
// would otherwise be sized from its own content and wrap its keys mid-word.
const MAX_KEY_SHARE = "50%";
const GAP = 2;

Expand All @@ -23,7 +26,7 @@ export function KeyValueTable({ items }: KeyValueTableProps) {
// Two boxes rather than one padded string, so a value that wraps continues
// under itself, not under the key.
return (
<Box flexDirection="column">
<Box flexDirection="column" flexGrow={1}>

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

flexGrow here makes the table take all free height in any growing column parent. The gateway policy generate screen now has 14 blank lines between the table and the input. Put flexDirection="column" on the two bordered wrapper Boxes instead and leave the shared table alone.

{Object.entries(items).map(([key, value]) => (
<Box key={key}>
<Box
Expand Down
5 changes: 5 additions & 0 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -118,6 +118,7 @@ import { BuildProjectScreen } from "../handlers/project/build/screen.tsx";
import { DeployProjectScreen } from "../handlers/project/deploy/screen.tsx";
import { ProjectCreateScreen } from "../handlers/project/create/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { AddRuntimeScreen } from "../handlers/project/add/runtime/screen.tsx";
import { ProjectStatusScreen } from "../handlers/project/status/screen.tsx";
import { HelpScreen, RootScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";
Expand Down Expand Up @@ -811,6 +812,10 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
path="agentcore/project/status"
element={<ProjectStatusScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/runtime"
element={<AddRuntimeScreen ctx={ctx} core={core} />}
/>
{/* Every known command without a screen of its own: a group opens its
menu and a leaf its interactive help. Unknown routes retain the
help-and-exit fallback. */}
Expand Down
43 changes: 43 additions & 0 deletions src/components/wizard/Step.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
import { isValidElement, type ReactElement, type ReactNode } from "react";
import { Box, Text } from "ink";
import { darkTheme } from "../ui/_core.js";

const theme = darkTheme;

export interface StepProps {
// name is the step's stable key. Position is tracked by key rather than by
// index because branches have different lengths: a conditional step that
// appears or disappears must not shift the user to a different question.
name: string;
// title labels the step in the Stepper; defaults to `name`.
title?: string;
// question is the one-line prompt shown under the Stepper. The Stepper
// already names the step, so the body opens with the question itself.
question?: string;
children: ReactNode;
}

// Step is one page of a <Wizard>: the stepper entry, the question line, and the
// field that collects the answer.
//
// One field per step. Every field registers its own useInput and answers enter,
// esc and the arrows itself; two fields mounted at once would both react to the
// same keystroke. The shell has no notion of focus and is not meant to grow
// one — a step that genuinely needs two related inputs should get a single
// compound field that owns one useInput and manages focus internally.
export function Step({ question, children }: StepProps) {
return (
<Box flexDirection="column" paddingX={1}>
{question !== undefined && <Text color={theme.colors.muted}>{question}</Text>}
{children}
</Box>
);
}

// isStepElement narrows a child to a <Step>. Children.toArray already
// drops the `false`/`null` that a `{condition && <Step/>}` branch produces, so
// filtering with this yields exactly the steps that apply to the current
// answers — which is how a wizard branches without a step-list useMemo.
export function isStepElement(child: ReactNode): child is ReactElement<StepProps> {
return isValidElement(child) && child.type === Step;
}
Loading
Loading