Skip to content

Commit 65ee2be

Browse files
author
jariy17
committed
feat(eval): add read-only Eval TUI
Wire the interactive TUI over the existing eval command tree for the read-only surface: evaluator and online-eval menus, paginated lists, and resource detail hubs (with a raw JSON view). Mutating commands (create/update/delete/pause/resume) stay CLI-only for now. - New pickers: EvaluatorPicker, OnlineEvalPicker (over the existing listEvaluators / listOnlineEvaluationConfigs Core calls). - New screens: eval / evaluator / online-eval menus, list + get(+json). get hubs reuse the shared ResourceDetailScreen; evaluator kind is derived from the evaluatorConfig union (no type field on the get response), online-eval sampling reads rule.samplingConfig. - RouterScreen gains an `omit` prop so the eval menus hide the mutating subcommands. Without it those items would fall through to the HelpScreen catch-all, which exits the app. - eval routers swap createHelpDefault for withTuiOnEmptyFlagsAndArgs + renderTui, matching harness/runtime/memory. Bare read leaves now open the TUI instead of erroring on a missing required flag; existing leaf tests assert the headless path under --json, matching the memory read-only TUI convention.
1 parent d08ecca commit 65ee2be

18 files changed

Lines changed: 942 additions & 17 deletions

src/components/EvaluatorPicker.tsx

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
import type { EvaluatorSummary } from "@aws-sdk/client-bedrock-agentcore-control";
2+
import { useNavigate } from "react-router";
3+
import type { ScreenProps } from "../handlers/types";
4+
import { coreOptsFromCtx } from "../handlers/utils";
5+
import { formatTimestamp } from "./formatTimestamp";
6+
import { PaginatedTablePicker } from "./PaginatedTablePicker";
7+
import type { DataTableColumn } from "./ui/data-table";
8+
9+
// EvaluatorRow is the flat, display-ready shape the table renders. It also
10+
// satisfies DataTable's `T extends Record<string, unknown>` constraint, which the
11+
// SDK's EvaluatorSummary interface does not.
12+
interface EvaluatorRow extends Record<string, unknown> {
13+
evaluatorId: string;
14+
evaluatorName: string;
15+
evaluatorType: string;
16+
level: string;
17+
updatedAt: string;
18+
}
19+
20+
export const evaluatorColumns = [
21+
{ key: "evaluatorName", header: "name", flex: true },
22+
{ key: "evaluatorType", header: "type", width: 12 },
23+
{ key: "level", header: "level", width: 10 },
24+
{
25+
key: "updatedAt",
26+
header: "updated UTC",
27+
width: 16,
28+
render: formatTimestamp,
29+
},
30+
] satisfies DataTableColumn<EvaluatorRow>[];
31+
32+
function toRow(evaluator: EvaluatorSummary): EvaluatorRow {
33+
const id = evaluator.evaluatorId ?? "";
34+
return {
35+
evaluatorId: id,
36+
evaluatorName: evaluator.evaluatorName ?? id,
37+
evaluatorType: evaluator.evaluatorType ?? "-",
38+
level: evaluator.level ?? "-",
39+
updatedAt: evaluator.updatedAt?.toISOString() ?? "-",
40+
};
41+
}
42+
43+
export interface EvaluatorPickerProps extends ScreenProps {
44+
breadcrumb: string[];
45+
description?: string;
46+
onSelect: (evaluatorId: string) => void;
47+
onEscape?: () => void;
48+
}
49+
50+
/**
51+
* Fetches the caller's evaluators and renders them as a navigable table.
52+
*
53+
* The shared body of every "pick an evaluator" screen (list, and — in the write
54+
* TUI — update/delete). Esc returns to the parent menu derived from the
55+
* breadcrumb unless a host supplies its own onEscape.
56+
*/
57+
export function EvaluatorPicker({
58+
ctx,
59+
core,
60+
breadcrumb,
61+
description,
62+
onSelect,
63+
onEscape,
64+
}: EvaluatorPickerProps) {
65+
const opts = coreOptsFromCtx(ctx);
66+
const navigate = useNavigate();
67+
const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/")));
68+
69+
return (
70+
<PaginatedTablePicker
71+
breadcrumb={breadcrumb}
72+
description={description}
73+
queryKey={["evaluators", opts.region]}
74+
loadPage={async (token, pageSize) => {
75+
const response = await core.eval.listEvaluators(token, pageSize, opts);
76+
return {
77+
items: response.evaluators ?? [],
78+
nextToken: response.nextToken,
79+
};
80+
}}
81+
toRow={toRow}
82+
columns={evaluatorColumns}
83+
getValue={(row) => row.evaluatorId}
84+
onSelect={onSelect}
85+
onBack={goBack}
86+
loadingMessage="Loading evaluators…"
87+
errorMessage={(error) => `Error: ${error.message}`}
88+
emptyMessage="No evaluators found in this Region."
89+
emptyPageMessage="No evaluators on this page."
90+
/>
91+
);
92+
}
Lines changed: 93 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,93 @@
1+
import type { OnlineEvaluationConfigSummary } from "@aws-sdk/client-bedrock-agentcore-control";
2+
import { useNavigate } from "react-router";
3+
import type { ScreenProps } from "../handlers/types";
4+
import { coreOptsFromCtx } from "../handlers/utils";
5+
import { formatTimestamp } from "./formatTimestamp";
6+
import { PaginatedTablePicker } from "./PaginatedTablePicker";
7+
import type { DataTableColumn } from "./ui/data-table";
8+
9+
// OnlineEvalRow is the flat, display-ready shape the table renders. It also
10+
// satisfies DataTable's `T extends Record<string, unknown>` constraint, which the
11+
// SDK's OnlineEvaluationConfigSummary interface does not. The list API returns
12+
// only summary fields (name/status/executionStatus/timestamps); richer detail
13+
// like sampling rate and evaluators comes from GetOnlineEvaluationConfig.
14+
interface OnlineEvalRow extends Record<string, unknown> {
15+
configId: string;
16+
configName: string;
17+
status: string;
18+
executionStatus: string;
19+
updatedAt: string;
20+
}
21+
22+
export const onlineEvalColumns = [
23+
{ key: "configName", header: "name", flex: true },
24+
{ key: "status", header: "status", width: 12 },
25+
{ key: "executionStatus", header: "execution", width: 11 },
26+
{
27+
key: "updatedAt",
28+
header: "updated UTC",
29+
width: 16,
30+
render: formatTimestamp,
31+
},
32+
] satisfies DataTableColumn<OnlineEvalRow>[];
33+
34+
function toRow(config: OnlineEvaluationConfigSummary): OnlineEvalRow {
35+
const id = config.onlineEvaluationConfigId ?? "";
36+
return {
37+
configId: id,
38+
configName: config.onlineEvaluationConfigName ?? id,
39+
status: config.status ?? "-",
40+
executionStatus: config.executionStatus ?? "-",
41+
updatedAt: config.updatedAt?.toISOString() ?? "-",
42+
};
43+
}
44+
45+
export interface OnlineEvalPickerProps extends ScreenProps {
46+
breadcrumb: string[];
47+
description?: string;
48+
onSelect: (configId: string) => void;
49+
onEscape?: () => void;
50+
}
51+
52+
/**
53+
* Fetches the caller's online evaluation configs and renders them as a navigable
54+
* table. The shared body of every "pick a config" screen (list, and — in the
55+
* write TUI — update/pause/resume/delete). Esc returns to the parent menu derived
56+
* from the breadcrumb unless a host supplies its own onEscape.
57+
*/
58+
export function OnlineEvalPicker({
59+
ctx,
60+
core,
61+
breadcrumb,
62+
description,
63+
onSelect,
64+
onEscape,
65+
}: OnlineEvalPickerProps) {
66+
const opts = coreOptsFromCtx(ctx);
67+
const navigate = useNavigate();
68+
const goBack = onEscape ?? (() => navigate("/" + breadcrumb.slice(0, -1).join("/")));
69+
70+
return (
71+
<PaginatedTablePicker
72+
breadcrumb={breadcrumb}
73+
description={description}
74+
queryKey={["online-evals", opts.region]}
75+
loadPage={async (token, pageSize) => {
76+
const response = await core.eval.listOnlineEvaluationConfigs(token, pageSize, opts);
77+
return {
78+
items: response.onlineEvaluationConfigs ?? [],
79+
nextToken: response.nextToken,
80+
};
81+
}}
82+
toRow={toRow}
83+
columns={onlineEvalColumns}
84+
getValue={(row) => row.configId}
85+
onSelect={onSelect}
86+
onBack={goBack}
87+
loadingMessage="Loading online evaluation configs…"
88+
errorMessage={(error) => `Error: ${error.message}`}
89+
emptyMessage="No online evaluation configs found in this Region."
90+
emptyPageMessage="No online evaluation configs on this page."
91+
/>
92+
);
93+
}

src/components/Root.tsx

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,19 @@ import { MemoryScreen } from "../handlers/memory/screen.tsx";
3232
import { MemoryGetJsonScreen, MemoryGetScreen } from "../handlers/memory/get/screen.tsx";
3333
import { MemoryListScreen } from "../handlers/memory/list/screen.tsx";
3434
import { RuntimeInvokeScreen } from "../handlers/runtime/invoke/screen.tsx";
35+
import { EvalScreen } from "../handlers/eval/screen.tsx";
36+
import { EvaluatorScreen } from "../handlers/eval/evaluator/screen.tsx";
37+
import { EvaluatorListScreen } from "../handlers/eval/evaluator/list/screen.tsx";
38+
import {
39+
EvaluatorGetScreen,
40+
EvaluatorGetJsonScreen,
41+
} from "../handlers/eval/evaluator/get/screen.tsx";
42+
import { OnlineEvalScreen } from "../handlers/eval/online-eval/screen.tsx";
43+
import { OnlineEvalListScreen } from "../handlers/eval/online-eval/list/screen.tsx";
44+
import {
45+
OnlineEvalGetScreen,
46+
OnlineEvalGetJsonScreen,
47+
} from "../handlers/eval/online-eval/get/screen.tsx";
3548
import { RootScreen, HelpScreen } from "../handlers/screen.tsx";
3649
import type { Context } from "../router";
3750

@@ -288,6 +301,48 @@ export function Root({ path, ctx, core, queryClient }: RootProps) {
288301
path="agentcore/runtime/invoke/:runtimeId/:qualifier"
289302
element={<RuntimeInvokeScreen ctx={ctx} core={core} />}
290303
/>
304+
<Route path="agentcore/eval" element={<EvalScreen ctx={ctx} core={core} />} />
305+
<Route
306+
path="agentcore/eval/evaluator"
307+
element={<EvaluatorScreen ctx={ctx} core={core} />}
308+
/>
309+
<Route
310+
path="agentcore/eval/evaluator/list"
311+
element={<EvaluatorListScreen ctx={ctx} core={core} />}
312+
/>
313+
{/* Bare `get` (no id) has nothing to show — send the user to the list. */}
314+
<Route
315+
path="agentcore/eval/evaluator/get"
316+
element={<Navigate to="/agentcore/eval/evaluator/list" replace />}
317+
/>
318+
<Route
319+
path="agentcore/eval/evaluator/get/:evaluatorId"
320+
element={<EvaluatorGetScreen ctx={ctx} core={core} />}
321+
/>
322+
<Route
323+
path="agentcore/eval/evaluator/get/:evaluatorId/json"
324+
element={<EvaluatorGetJsonScreen ctx={ctx} core={core} />}
325+
/>
326+
<Route
327+
path="agentcore/eval/online-eval"
328+
element={<OnlineEvalScreen ctx={ctx} core={core} />}
329+
/>
330+
<Route
331+
path="agentcore/eval/online-eval/list"
332+
element={<OnlineEvalListScreen ctx={ctx} core={core} />}
333+
/>
334+
<Route
335+
path="agentcore/eval/online-eval/get"
336+
element={<Navigate to="/agentcore/eval/online-eval/list" replace />}
337+
/>
338+
<Route
339+
path="agentcore/eval/online-eval/get/:configId"
340+
element={<OnlineEvalGetScreen ctx={ctx} core={core} />}
341+
/>
342+
<Route
343+
path="agentcore/eval/online-eval/get/:configId/json"
344+
element={<OnlineEvalGetJsonScreen ctx={ctx} core={core} />}
345+
/>
291346
<Route path="*" element={<HelpScreen ctx={ctx} core={core} />} />
292347
</Routes>
293348
</MemoryRouter>

src/components/RouterScreen.tsx

Lines changed: 11 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -44,21 +44,29 @@ export interface RouterScreenProps extends ScreenProps {
4444
// segment is the app root; the last is the command whose subcommands are the
4545
// menu options.
4646
path: string[];
47+
// omit hides subcommands from the menu by name. The command tree still carries
48+
// them (they remain usable from the CLI), but they are not offered here — used
49+
// when the TUI intentionally does not route a command yet, so it can't fall
50+
// through to the HelpScreen catch-all (which exits the app).
51+
omit?: string[];
4752
}
4853

4954
// RouterScreen renders the interactive command menu for a Router node: a filter
5055
// input at the top and the node's subcommands (read straight off the Commander
5156
// Command) as navigable options below. Selecting an option routes to that
5257
// subcommand's screen.
53-
export function RouterScreen({ ctx, path }: RouterScreenProps) {
58+
export function RouterScreen({ ctx, path, omit }: RouterScreenProps) {
5459
const navigate = useNavigate();
5560
const { isRawModeSupported } = useStdin();
5661
const { exit } = useApp();
5762

5863
const command = resolveCommand(ctx.require(CommandKey), path);
5964
const options: Option[] = useMemo(
60-
() => command.commands.map((c) => ({ name: c.name(), description: c.description() })),
61-
[command],
65+
() =>
66+
command.commands
67+
.filter((c) => !omit?.includes(c.name()))
68+
.map((c) => ({ name: c.name(), description: c.description() })),
69+
[command, omit],
6270
);
6371

6472
const [query, setQuery] = useState("");

0 commit comments

Comments
 (0)