Skip to content
Closed
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
12 changes: 8 additions & 4 deletions src/components/FormCheckboxMultiSelect.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -31,10 +31,14 @@ export function FormCheckboxMultiSelect({

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 checkboxes. */}
{(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/FormRadioGroup.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,14 @@ export function FormRadioGroup({ name, helpText, options, selectedIndex }: FormR

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 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
26 changes: 15 additions & 11 deletions src/components/FormTextArea.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -54,17 +54,21 @@ export function FormTextArea({

return (
<Box flexDirection="column">
<Box
flexDirection="column"
borderStyle="single"
borderLeft={false}
borderRight={false}
borderTop={false}
borderColor={theme.colors.border}
>
<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 editor. */}
{(name !== "" || helpText !== "") && (
<Box
flexDirection="column"
borderStyle="single"
borderLeft={false}
borderRight={false}
borderTop={false}
borderColor={theme.colors.border}
>
{name !== "" && <Text color={theme.colors.text}>{name}</Text>}
{helpText !== "" && <Text color={theme.colors.muted}>{helpText}</Text>}
</Box>
)}
{hidden > 0 && <Text color={theme.colors.muted}>… (+{hidden} earlier lines)</Text>}
{visible.length === 0 ? (
<Text color={theme.colors.muted}>
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
90 changes: 87 additions & 3 deletions src/components/Root.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -110,13 +110,39 @@ import { GatewayInvokeScreen } from "../handlers/gateway/invoke/screen.tsx";
import { ProjectScreen, ProjectCommandNotImplementedScreen } from "../handlers/project/screen.tsx";
import { ProjectCreateScreen } from "../handlers/project/create/screen.tsx";
import { ProjectInvokePickerScreen } from "../handlers/project/invoke/screen.tsx";
import { AddScreen } from "../handlers/project/add/screen.tsx";
import { AddMemoryScreen } from "../handlers/project/add/memory/screen.tsx";
import { AddGatewayScreen } from "../handlers/project/add/gateway/screen.tsx";
import { AddPolicyEngineScreen } from "../handlers/project/add/policy-engine/screen.tsx";
import { AddConfigBundleScreen } from "../handlers/project/add/config-bundle/screen.tsx";
import { AddPolicyScreen } from "../handlers/project/add/policy/screen.tsx";
import { AddGatewayConnectorScreen } from "../handlers/project/add/gateway-connector/screen.tsx";
import { AddOnlineEvalScreen } from "../handlers/project/add/online-eval/screen.tsx";
import { AddOnlineInsightScreen } from "../handlers/project/add/online-insight/screen.tsx";
import {
AddEvaluatorScreen,
AddLlmAsAJudgeEvaluatorScreen,
} from "../handlers/project/add/evaluator/screen.tsx";
import { AddPaymentManagerScreen } from "../handlers/project/add/payment-manager/screen.tsx";
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
import type { Context } from "../router";

// PROJECT_COMMANDS are the `agentcore project` subcommands that are listed in
// the menu but have no screen of their own yet (`create` has the wizard). Each
// is routed explicitly so selecting it reports "not implemented" error
const PROJECT_COMMANDS = ["add", "export", "remove", "dev", "deploy", "status", "build"] as const;
// the menu but have no screen of their own yet (`create` has the wizard, `add`
// has its own menu). Each is routed explicitly so selecting it reports "not
// implemented" error
const PROJECT_COMMANDS = ["export", "remove", "dev", "deploy", "status", "build"] as const;

// ADD_COMMANDS are the `agentcore project add` resources that have no wizard
// yet. They stay in the menu — every one of them works from the command line —
// and route here so selecting one says so instead of falling through to help.
const ADD_COMMANDS = [
"harness",
"runtime",
"credentials",
"gateway-target",
"payment-connector",
] as const;

export interface RootProps {
// path is the command path to the executing node (e.g. "/agentcore").
Expand Down Expand Up @@ -762,6 +788,64 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
}
/>
))}
<Route path="agentcore/project/add" element={<AddScreen ctx={ctx} core={core} />} />
<Route
path="agentcore/project/add/memory"
element={<AddMemoryScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/gateway"
element={<AddGatewayScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/policy-engine"
element={<AddPolicyEngineScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/config-bundle"
element={<AddConfigBundleScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/policy"
element={<AddPolicyScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/gateway-connector"
element={<AddGatewayConnectorScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/online-eval"
element={<AddOnlineEvalScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/online-insight"
element={<AddOnlineInsightScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/evaluator"
element={<AddEvaluatorScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/evaluator/llm-as-a-judge"
element={<AddLlmAsAJudgeEvaluatorScreen ctx={ctx} core={core} />}
/>
<Route
path="agentcore/project/add/payment-manager"
element={<AddPaymentManagerScreen ctx={ctx} core={core} />}
/>
{ADD_COMMANDS.map((command) => (
<Route
key={command}
path={`agentcore/project/add/${command}`}
element={
<ProjectCommandNotImplementedScreen
ctx={ctx}
core={core}
command={`add ${command}`}
/>
}
/>
))}
<Route path="*" element={<HelpScreen ctx={ctx} core={core} />} />
</Routes>
</MemoryRouter>
Expand Down
47 changes: 47 additions & 0 deletions src/components/wizard/Prerequisite.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
import { Box, Text, useInput } from "ink";
import { Layout } from "../Layout";
import { darkTheme } from "../ui/_core.js";

const theme = darkTheme;

export interface PrerequisiteProps {
breadcrumb: string[];
description?: string;
// message says what the project lacks, e.g. "this project has no Gateways yet".
message: string;
// command is the CLI command that would add it, shown as the way forward.
command: string;
// onBack runs on esc; the screen the user came from.
onBack: () => void;
}

// Prerequisite stands in for a wizard whose first question has no possible
// answer — a Policy needs a Policy Engine, a connector needs a Gateway. It
// names what is missing and how to add it, instead of offering an empty picker.
export function Prerequisite({
breadcrumb,
description,
message,
command,
onBack,
}: PrerequisiteProps) {
useInput((_input, key) => {
if (key.escape) onBack();
});

return (
<Layout
breadcrumb={breadcrumb}
description={description}
keyHints={[
{ key: "esc", label: "back" },
{ key: "ctl+c", label: "quit" },
]}
>
<Box flexDirection="column" paddingX={1}>
<Text color={theme.colors.warning}>{message}</Text>
<Text color={theme.colors.muted}>add one first with `{command}`</Text>
</Box>
</Layout>
);
}
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 React 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: React.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>. React.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: React.ReactNode): child is React.ReactElement<StepProps> {
return React.isValidElement(child) && child.type === Step;
}
Loading
Loading