Skip to content

Commit 1e1b091

Browse files
committed
refactor: simplify paged picker implementation
1 parent 88fc3da commit 1e1b091

19 files changed

Lines changed: 201 additions & 648 deletions

src/components/HarnessEndpointPicker.tsx

Lines changed: 7 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -67,30 +67,13 @@ export function HarnessEndpointPicker({
6767
};
6868
}}
6969
toRow={toRow}
70-
columns={(terminalColumns) => {
71-
const liveWidth = 8;
72-
const showTarget = terminalColumns >= 60;
73-
const showStatus = terminalColumns >= 70;
74-
const showUpdatedAt = terminalColumns >= 90;
75-
const targetWidth = showTarget ? 8 : 0;
76-
const statusWidth = showStatus ? 20 : 0;
77-
const updatedAtWidth = showUpdatedAt ? 30 : 0;
78-
const nameWidth = Math.max(
79-
12,
80-
terminalColumns - 2 - liveWidth - targetWidth - statusWidth - updatedAtWidth,
81-
);
82-
return [
83-
{ key: "endpointName", header: "name", width: nameWidth },
84-
{ key: "liveVersion", header: "live", width: liveWidth },
85-
...(showTarget
86-
? [{ key: "targetVersion" as const, header: "target", width: targetWidth }]
87-
: []),
88-
...(showStatus ? [{ key: "status" as const, header: "status", width: statusWidth }] : []),
89-
...(showUpdatedAt
90-
? [{ key: "updatedAt" as const, header: "updatedAt", width: updatedAtWidth }]
91-
: []),
92-
];
93-
}}
70+
columns={[
71+
{ key: "endpointName", header: "name" },
72+
{ key: "liveVersion", header: "live" },
73+
{ key: "targetVersion", header: "target" },
74+
{ key: "status", header: "status" },
75+
{ key: "updatedAt", header: "updatedAt" },
76+
]}
9477
getValue={(row) => row.endpointName}
9578
onSelect={onSelect}
9679
onBack={goBack}

src/components/HarnessPicker.tsx

Lines changed: 6 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -66,25 +66,12 @@ export function HarnessPicker({
6666
};
6767
}}
6868
toRow={toRow}
69-
columns={(terminalColumns) => {
70-
const versionWidth = 10;
71-
const showStatus = terminalColumns >= 70;
72-
const showUpdatedAt = terminalColumns >= 90;
73-
const statusWidth = showStatus ? 20 : 0;
74-
const updatedAtWidth = showUpdatedAt ? 30 : 0;
75-
const nameWidth = Math.max(
76-
12,
77-
terminalColumns - 2 - versionWidth - statusWidth - updatedAtWidth,
78-
);
79-
return [
80-
{ key: "harnessName", header: "name", width: nameWidth },
81-
{ key: "harnessVersion", header: "version", width: versionWidth },
82-
...(showStatus ? [{ key: "status" as const, header: "status", width: statusWidth }] : []),
83-
...(showUpdatedAt
84-
? [{ key: "updatedAt" as const, header: "updatedAt", width: updatedAtWidth }]
85-
: []),
86-
];
87-
}}
69+
columns={[
70+
{ key: "harnessName", header: "name" },
71+
{ key: "harnessVersion", header: "version" },
72+
{ key: "status", header: "status" },
73+
{ key: "updatedAt", header: "updatedAt" },
74+
]}
8875
getValue={(row) => row.harnessId}
8976
onSelect={onSelect}
9077
onBack={goBack}

src/components/HarnessVersionPicker.tsx

Lines changed: 5 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -59,20 +59,11 @@ export function HarnessVersionPicker({
5959
};
6060
}}
6161
toRow={toRow}
62-
columns={(terminalColumns) => {
63-
const showStatus = terminalColumns >= 60;
64-
const showCreatedAt = terminalColumns >= 90;
65-
const statusWidth = showStatus ? 20 : 0;
66-
const createdAtWidth = showCreatedAt ? 30 : 0;
67-
const versionWidth = Math.max(8, terminalColumns - 2 - statusWidth - createdAtWidth);
68-
return [
69-
{ key: "harnessVersion", header: "version", width: versionWidth },
70-
...(showStatus ? [{ key: "status" as const, header: "status", width: statusWidth }] : []),
71-
...(showCreatedAt
72-
? [{ key: "createdAt" as const, header: "createdAt", width: createdAtWidth }]
73-
: []),
74-
];
75-
}}
62+
columns={[
63+
{ key: "harnessVersion", header: "version" },
64+
{ key: "status", header: "status" },
65+
{ key: "createdAt", header: "createdAt" },
66+
]}
7667
sortRows={(rows) =>
7768
[...rows].sort((left, right) => Number(right.harnessVersion) - Number(left.harnessVersion))
7869
}
Lines changed: 1 addition & 235 deletions
Original file line numberDiff line numberDiff line change
@@ -1,156 +1,8 @@
11
import { test, expect, describe, afterEach } from "bun:test";
2-
import type {
3-
AgentRuntime,
4-
AgentRuntimeEndpoint,
5-
GetAgentRuntimeEndpointResponse,
6-
GetAgentRuntimeResponse,
7-
} from "@aws-sdk/client-bedrock-agentcore-control";
8-
import { createRootHandler } from "../handlers";
9-
import { DebugKey, EndpointKey, JsonKey, RegionKey } from "../handlers/keys";
10-
import { CommandKey, compile, type Context, ValueContext } from "../router";
11-
import {
12-
cleanupScreens,
13-
createSilentLogger,
14-
renderScreen,
15-
TestCoreClient,
16-
testIO,
17-
tick,
18-
waitFor,
19-
waitForText,
20-
} from "../testing";
21-
import { JsonRendererKey, renderTuiAt } from "../tui";
2+
import { cleanupScreens, renderScreen, tick, waitForText } from "../testing";
223

234
afterEach(cleanupScreens);
245

25-
const runtimeEndpointUrl = "https://runtime.test";
26-
27-
function runtime(overrides: Partial<AgentRuntime> = {}): AgentRuntime {
28-
return {
29-
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123",
30-
agentRuntimeId: "runtime-123",
31-
agentRuntimeVersion: "7",
32-
agentRuntimeName: "checkout",
33-
description: "Checkout Runtime",
34-
lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
35-
status: "READY",
36-
...overrides,
37-
};
38-
}
39-
40-
function getRuntimeResponse(
41-
overrides: Partial<GetAgentRuntimeResponse> = {},
42-
): GetAgentRuntimeResponse {
43-
return {
44-
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123",
45-
agentRuntimeName: "checkout",
46-
agentRuntimeId: "runtime-123",
47-
agentRuntimeVersion: "7",
48-
createdAt: new Date("2026-07-19T01:02:03.000Z"),
49-
lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
50-
roleArn: "arn:aws:iam::123456789012:role/runtime-role",
51-
networkConfiguration: { networkMode: "PUBLIC" },
52-
status: "READY",
53-
lifecycleConfiguration: {
54-
idleRuntimeSessionTimeout: 900,
55-
maxLifetime: 28_800,
56-
},
57-
...overrides,
58-
};
59-
}
60-
61-
function endpoint(overrides: Partial<AgentRuntimeEndpoint> = {}): AgentRuntimeEndpoint {
62-
return {
63-
name: "prod",
64-
liveVersion: "7",
65-
agentRuntimeEndpointArn:
66-
"arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123/endpoint/prod",
67-
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123",
68-
status: "READY",
69-
id: "prod",
70-
createdAt: new Date("2026-07-19T01:02:03.000Z"),
71-
lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
72-
...overrides,
73-
};
74-
}
75-
76-
function getEndpointResponse(
77-
overrides: Partial<GetAgentRuntimeEndpointResponse> = {},
78-
): GetAgentRuntimeEndpointResponse {
79-
return {
80-
liveVersion: "7",
81-
targetVersion: "8",
82-
agentRuntimeEndpointArn:
83-
"arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123/endpoint/prod",
84-
agentRuntimeArn: "arn:aws:bedrock-agentcore:us-east-1:123456789012:runtime/runtime-123",
85-
status: "READY",
86-
createdAt: new Date("2026-07-19T01:02:03.000Z"),
87-
lastUpdatedAt: new Date("2026-07-20T12:34:56.000Z"),
88-
name: "prod",
89-
id: "prod",
90-
...overrides,
91-
};
92-
}
93-
94-
function runtimeCore(): TestCoreClient {
95-
const core = new TestCoreClient();
96-
core.runtime.setListResponse({ agentRuntimes: [runtime()] });
97-
core.runtime.setGetResponse(getRuntimeResponse());
98-
core.runtime.setListEndpointsResponse({ runtimeEndpoints: [endpoint()] });
99-
core.runtime.setGetEndpointResponse(getEndpointResponse());
100-
return core;
101-
}
102-
103-
function runtimeContext(core: TestCoreClient): Context {
104-
const rootCommand = compile(
105-
createRootHandler(core, { io: testIO().io, logger: createSilentLogger() }),
106-
ValueContext.EmptyContext(),
107-
);
108-
109-
return ValueContext.EmptyContext()
110-
.withValue(CommandKey, rootCommand)
111-
.withValue(RegionKey, "us-east-1")
112-
.withValue(EndpointKey, runtimeEndpointUrl)
113-
.withValue(JsonKey, false)
114-
.withValue(DebugKey, false)
115-
.withValue(JsonRendererKey, { renderJson: () => {} });
116-
}
117-
118-
function expectEndpointPropagation(core: TestCoreClient, methods: readonly string[]): void {
119-
for (const method of methods) {
120-
const calls = core.runtime.calls.filter((call) => call.method === method);
121-
expect(calls.length).toBeGreaterThan(0);
122-
for (const call of calls) {
123-
expect(call.args.at(-1)).toEqual({
124-
region: "us-east-1",
125-
endpointUrl: runtimeEndpointUrl,
126-
});
127-
}
128-
}
129-
}
130-
131-
interface TtyInput extends NodeJS.ReadStream {
132-
write(chunk: string): boolean;
133-
}
134-
135-
function ttyTestIO(): { streams: ReturnType<typeof testIO>; stdin: TtyInput } {
136-
const streams = testIO({ isTTY: true });
137-
const stdin = streams.io.stdin as TtyInput;
138-
stdin.setRawMode = function () {
139-
return this;
140-
};
141-
stdin.ref = function () {
142-
return this;
143-
};
144-
stdin.unref = function () {
145-
return this;
146-
};
147-
Object.defineProperties(streams.io.stdout, {
148-
columns: { configurable: true, value: 100 },
149-
rows: { configurable: true, value: 40 },
150-
});
151-
return { streams, stdin };
152-
}
153-
1546
// RouterScreen is the interactive command menu. These tests mount it through the
1557
// real Root at a command path and drive it with key presses, asserting on the
1568
// rendered frames — behavior a user would see, not internal state.
@@ -282,90 +134,4 @@ describe("navigation", () => {
282134
expect(r.lastFrame()).toContain("❯ harness");
283135
r.unmount();
284136
});
285-
286-
test("navigates from Root through Runtime endpoints and escapes each boundary", async () => {
287-
const core = runtimeCore();
288-
const r = renderScreen("/agentcore", {
289-
core,
290-
ctx: runtimeContext(core),
291-
});
292-
293-
await waitForText(r.lastFrame, "❯ harness");
294-
await r.press("down");
295-
await r.press("return");
296-
await waitForText(r.lastFrame, "agentcore → runtime → inspect AgentCore Runtimes");
297-
298-
await r.press("down");
299-
await r.press("return");
300-
await waitForText(r.lastFrame, "agentcore → runtime → list");
301-
await waitForText(r.lastFrame, "checkout");
302-
303-
await r.press("return");
304-
await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123");
305-
await waitForText(r.lastFrame, "show the full JSON definition");
306-
307-
await r.press("down");
308-
await r.press("down");
309-
await r.press("return");
310-
await waitForText(r.lastFrame, "agentcore → runtime → endpoint → list → runtime-123");
311-
await waitForText(r.lastFrame, "prod");
312-
313-
await r.press("return");
314-
await waitForText(r.lastFrame, "agentcore → runtime → endpoint → get → runtime-123 → prod");
315-
await waitForText(r.lastFrame, '"agentRuntimeEndpointArn"');
316-
317-
await r.press("escape");
318-
await waitFor(() => {
319-
const frame = r.lastFrame() ?? "";
320-
return (
321-
frame.includes("agentcore → runtime → endpoint → list → runtime-123") &&
322-
!frame.includes("agentcore → runtime → endpoint → get")
323-
);
324-
});
325-
await r.press("escape");
326-
await waitForText(r.lastFrame, "agentcore → runtime → get → runtime-123");
327-
await r.press("escape");
328-
await waitForText(r.lastFrame, "agentcore → runtime → list");
329-
await r.press("escape");
330-
await waitForText(r.lastFrame, "agentcore → runtime → inspect AgentCore Runtimes");
331-
await r.press("escape");
332-
await waitForText(r.lastFrame, "the platform for production AI agents");
333-
334-
expectEndpointPropagation(core, [
335-
"listRuntimes",
336-
"getRuntime",
337-
"listRuntimeEndpoints",
338-
"getRuntimeEndpoint",
339-
]);
340-
});
341-
});
342-
343-
describe("Runtime TUI exit", () => {
344-
test("Ctrl+C exits a production Runtime list and ignores input after exit", async () => {
345-
const core = new TestCoreClient();
346-
core.runtime.setListResponse({
347-
agentRuntimes: [runtime()],
348-
nextToken: "page-2",
349-
});
350-
const { streams, stdin } = ttyTestIO();
351-
const renderPromise = renderTuiAt(
352-
"/agentcore/runtime/list",
353-
runtimeContext(core),
354-
core,
355-
streams.io,
356-
);
357-
const listCalls = () => core.runtime.calls.filter((call) => call.method === "listRuntimes");
358-
359-
await waitFor(() => listCalls().length > 0);
360-
await tick();
361-
const callsBeforeExit = listCalls().length;
362-
363-
stdin.write(String.fromCharCode(3));
364-
await expect(renderPromise).resolves.toBeUndefined();
365-
366-
stdin.write("l");
367-
await tick();
368-
expect(listCalls()).toHaveLength(callsBeforeExit);
369-
expect(listCalls().some((call) => call.args[0] === "page-2")).toBe(false);
370-
});
371137
});

src/components/RuntimeEndpointPicker.tsx

Lines changed: 7 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -54,40 +54,13 @@ export function RuntimeEndpointPicker({
5454
};
5555
}}
5656
toRow={toRow}
57-
columns={(terminalColumns) => {
58-
const showUpdatedAt = terminalColumns >= 90;
59-
const showStatus = terminalColumns >= 70;
60-
const showTarget = terminalColumns >= 60;
61-
const liveWidth = 8;
62-
const targetWidth = showTarget ? 8 : 0;
63-
const statusWidth = showStatus ? 20 : 0;
64-
const updatedAtWidth = showUpdatedAt ? 30 : 0;
65-
const qualifierWidth = Math.max(
66-
12,
67-
terminalColumns - 2 - liveWidth - targetWidth - statusWidth - updatedAtWidth,
68-
);
69-
return [
70-
{
71-
key: "qualifier",
72-
header: "qualifier",
73-
width: qualifierWidth,
74-
},
75-
{ key: "liveVersion", header: "live", width: liveWidth },
76-
...(showTarget
77-
? [{ key: "targetVersion" as const, header: "target", width: targetWidth }]
78-
: []),
79-
...(showStatus ? [{ key: "status" as const, header: "status", width: statusWidth }] : []),
80-
...(showUpdatedAt
81-
? [
82-
{
83-
key: "lastUpdatedAt" as const,
84-
header: "lastUpdatedAt",
85-
width: updatedAtWidth,
86-
},
87-
]
88-
: []),
89-
];
90-
}}
57+
columns={[
58+
{ key: "qualifier", header: "qualifier" },
59+
{ key: "liveVersion", header: "live" },
60+
{ key: "targetVersion", header: "target" },
61+
{ key: "status", header: "status" },
62+
{ key: "lastUpdatedAt", header: "lastUpdatedAt" },
63+
]}
9164
getValue={(row) => row.qualifier}
9265
onSelect={onSelect}
9366
onBack={goBack}

0 commit comments

Comments
 (0)