diff --git a/src/components/FormRadioGroup.tsx b/src/components/FormRadioGroup.tsx
index d3b9506ba..abe8cfd6a 100644
--- a/src/components/FormRadioGroup.tsx
+++ b/src/components/FormRadioGroup.tsx
@@ -12,12 +12,21 @@ export interface FormRadioGroupProps {
name: string;
helpText: string;
options: FormRadioOption[];
- selectedIndex: number;
+ // highlighted/hovered row
+ focusedIndex: number;
+ // row that user selects / hits ENTER on
+ selectedIndex?: number;
}
// FormRadioGroup renders a column of radio rows. It is fully controlled: the
-// parent owns the selected index and the key handling that moves it.
-export function FormRadioGroup({ name, helpText, options, selectedIndex }: FormRadioGroupProps) {
+// parent owns the focused index and the key handling that moves it.
+export function FormRadioGroup({
+ name,
+ helpText,
+ options,
+ focusedIndex,
+ selectedIndex,
+}: FormRadioGroupProps) {
const columnWidth = options.reduce((max, option) => Math.max(max, option.label.length), 0) + 2;
return (
@@ -33,16 +42,23 @@ export function FormRadioGroup({ name, helpText, options, selectedIndex }: FormR
borderColor={theme.colors.border}
>
{options.map((option, i) => {
+ const focused = i === focusedIndex;
const selected = i === selectedIndex;
+ // A selected row uses the selection color; a hovered (focused) row
+ // uses the brighter focus color; everything else is neutral.
+ const accentColor = selected
+ ? theme.colors.selection
+ : focused
+ ? theme.colors.focus
+ : undefined;
+ const highlighted = focused || selected;
return (
-
- {selected ? "●" : "○"}
-
+ {highlighted ? "●" : "○"}
-
+
{option.label}
diff --git a/src/components/HarnessWizard.tsx b/src/components/HarnessWizard.tsx
index a86573ffe..fc63a4dd9 100644
--- a/src/components/HarnessWizard.tsx
+++ b/src/components/HarnessWizard.tsx
@@ -755,23 +755,25 @@ function ModelStep({
name="choose a model"
helpText="the provider and model that will power the harness"
options={rows}
- selectedIndex={index}
+ focusedIndex={index}
+ selectedIndex={focusedField !== null ? index : undefined}
/>
- {provider.fields.map((field, i) => (
- {
- onChange({ ...value, [field.key]: next });
- setError(null);
- }}
- focused={focusedField === i}
- />
- ))}
+ {focusedField !== null &&
+ provider.fields.map((field, i) => (
+ {
+ onChange({ ...value, [field.key]: next });
+ setError(null);
+ }}
+ focused={focusedField === i}
+ />
+ ))}
{error && {error}}
{index !== 0 && (
@@ -861,7 +863,7 @@ function MemoryStep({
name="choose a memory configuration"
helpText="how should the harness remember conversations?"
options={MEMORY_OPTIONS}
- selectedIndex={index}
+ focusedIndex={index}
/>
{value.kind === "byo" && (
{
r.unmount();
});
+ test("reveals model fields only after enter and hides them again on escape", async () => {
+ const r = renderScreen("/agentcore/harness/create", { core: coreForCreate() });
+
+ await waitForText(r.lastFrame, "the name of your harness");
+ await r.write("my_agent");
+ await r.press("return");
+
+ await waitForText(r.lastFrame, "choose a model");
+ await r.press("down"); // bedrock
+ await waitForText(r.lastFrame, "● bedrock");
+ expect(r.lastFrame()).not.toContain("model id");
+
+ await r.press("return");
+ await waitForText(r.lastFrame, "model id");
+
+ await r.press("escape");
+ await waitFor(() => !(r.lastFrame() ?? "").includes("model id"));
+ expect(r.lastFrame()).toContain("● bedrock");
+ r.unmount();
+ });
+
test("selecting gemini collects the model id and api key arn", async () => {
const core = coreForCreate();
const r = renderScreen("/agentcore/harness/create", { core });
diff --git a/src/handlers/harness/update/update.screen.test.tsx b/src/handlers/harness/update/update.screen.test.tsx
index 3e8e27b9b..e99402be8 100644
--- a/src/handlers/harness/update/update.screen.test.tsx
+++ b/src/handlers/harness/update/update.screen.test.tsx
@@ -104,10 +104,12 @@ describe("harness update wizard", () => {
core.harness.setGetResponse(current);
const r = renderScreen("/agentcore/harness/update/MyHarness-abc123", { core });
- // The harness's bedrock provider is preselected with its model id prefilled.
+ // The harness's bedrock provider is preselected. Its persisted model id is
+ // revealed after confirming the provider.
await waitForText(r.lastFrame, "● bedrock");
- expect(r.lastFrame()).toContain("us.anthropic.claude-opus-4-8");
+ expect(r.lastFrame()).not.toContain("us.anthropic.claude-opus-4-8");
await r.press("return"); // into the model id field
+ await waitForText(r.lastFrame, "us.anthropic.claude-opus-4-8");
await r.press("return"); // accept it unchanged
await waitForText(r.lastFrame, "● managed");
await r.press("return");
diff --git a/src/handlers/memory/record/list/screen.tsx b/src/handlers/memory/record/list/screen.tsx
index 11def27c6..c720ea36c 100644
--- a/src/handlers/memory/record/list/screen.tsx
+++ b/src/handlers/memory/record/list/screen.tsx
@@ -66,33 +66,48 @@ interface MemoryRecordScopeScreenProps {
function MemoryRecordScopeScreen({ memoryId }: MemoryRecordScopeScreenProps) {
const navigate = useNavigate();
- const [selectedIndex, setSelectedIndex] = useState(0);
+ const [focusedIndex, setFocusedIndex] = useState(0);
const [scope, setScope] = useState("");
const [submitted, setSubmitted] = useState(false);
-
- const submit = (value: string) => {
- if (value.trim() === "") {
- setSubmitted(true);
- return;
- }
-
- const kind: RecordScopeKind = selectedIndex === 0 ? "namespace" : "namespace-path";
- navigate(
- `/agentcore/memory/record/list/${encodeURIComponent(memoryId)}/${kind}/${encodeURIComponent(value)}`,
- );
- };
+ // editing is true while the scope text field has focus; the radio list has
+ // focus otherwise.
+ const [editing, setEditing] = useState(false);
useInput((_input, key) => {
- if (key.escape) {
- navigate(-1);
+ if (!editing) {
+ if (key.escape) {
+ navigate(-1);
+ return;
+ }
+ if (key.upArrow) {
+ setFocusedIndex(0);
+ return;
+ }
+ if (key.downArrow) {
+ setFocusedIndex(1);
+ return;
+ }
+ if (key.return) {
+ setEditing(true);
+ }
return;
}
- if (key.upArrow) {
- setSelectedIndex(0);
+
+ // The scope field is focused; its TextInput owns text editing.
+ if (key.escape || key.upArrow) {
+ setEditing(false);
+ setSubmitted(false);
return;
}
- if (key.downArrow) {
- setSelectedIndex(1);
+ if (key.return) {
+ if (scope.trim() === "") {
+ setSubmitted(true);
+ return;
+ }
+ const kind: RecordScopeKind = focusedIndex === 0 ? "namespace" : "namespace-path";
+ navigate(
+ `/agentcore/memory/record/list/${encodeURIComponent(memoryId)}/${kind}/${encodeURIComponent(scope)}`,
+ );
}
});
@@ -112,20 +127,22 @@ function MemoryRecordScopeScreen({ memoryId }: MemoryRecordScopeScreenProps) {
name="scope type"
helpText="Choose how the service should match record namespaces."
options={scopeOptions}
- selectedIndex={selectedIndex}
- />
- {
- setScope(value);
- setSubmitted(false);
- }}
- onSubmit={submit}
+ focusedIndex={focusedIndex}
+ selectedIndex={editing ? focusedIndex : undefined}
/>
+ {editing && (
+ {
+ setScope(value);
+ setSubmitted(false);
+ }}
+ />
+ )}
{submitted && scope.trim() === "" ? (
A namespace value is required.
) : null}
diff --git a/src/handlers/memory/record/record.screen.test.tsx b/src/handlers/memory/record/record.screen.test.tsx
index f9b778479..3a50a8afa 100644
--- a/src/handlers/memory/record/record.screen.test.tsx
+++ b/src/handlers/memory/record/record.screen.test.tsx
@@ -71,6 +71,21 @@ describe("Memory record list flow", () => {
expect(core.memory.calls.some((call) => call.method === "listMemoryRecords")).toBe(false);
});
+ test("reveals the scope input only after enter and hides it again on escape", async () => {
+ const screen = renderScreen("/agentcore/memory/record/list/memory-1");
+ const fieldHelp = "Enter the namespace value used to scope this request.";
+
+ await waitForText(screen.lastFrame, "scope type");
+ expect(screen.lastFrame()).not.toContain(fieldHelp);
+
+ await screen.press("return");
+ await waitForText(screen.lastFrame, fieldHelp);
+
+ await screen.press("escape");
+ await waitFor(() => !(screen.lastFrame() ?? "").includes(fieldHelp));
+ expect(screen.core.memory.calls).toEqual([]);
+ });
+
test("unwinds the record table through its scope and Memory pickers", async () => {
const memoryId = "memory/blue one";
const core = new TestCoreClient();
@@ -85,6 +100,7 @@ describe("Memory record list flow", () => {
await waitForText(screen.lastFrame, memoryId);
await screen.press("return");
await waitForText(screen.lastFrame, "scope type");
+ await screen.press("return"); // focus the namespace input
await screen.write("/customers/acme");
await screen.press("return");
await waitForText(screen.lastFrame, "Customer prefers email notifications.");
@@ -105,6 +121,7 @@ describe("Memory record list flow", () => {
await waitForText(screen.lastFrame, "scope type");
await screen.press("down");
await screen.press("up");
+ await screen.press("return"); // focus the namespace input
await screen.write("/customers/acme");
await screen.press("return");
await waitFor(() => core.memory.calls.some((call) => call.method === "listMemoryRecords"));
@@ -128,6 +145,7 @@ describe("Memory record list flow", () => {
});
await waitForText(screen.lastFrame, "scope type");
+ await screen.press("return"); // focus the namespace input
await screen.write("/customers/acme");
await screen.press("return");
await waitForText(screen.lastFrame, "Customer prefers email notifications.");
@@ -181,6 +199,7 @@ describe("Memory record list flow", () => {
await waitForText(screen.lastFrame, "scope type");
await screen.press("down");
+ await screen.press("return"); // focus the namespace path input
await screen.write("/customers/acme/*");
await screen.press("return");
await waitFor(() => core.memory.calls.some((call) => call.method === "listMemoryRecords"));
@@ -317,7 +336,8 @@ describe("Memory record list flow", () => {
const screen = renderScreen("/agentcore/memory/record/list/memory-1");
await waitForText(screen.lastFrame, "scope type");
- await screen.press("return");
+ await screen.press("return"); // focus the namespace input
+ await screen.press("return"); // submit the empty value
await waitForText(screen.lastFrame, "A namespace value is required.");
expect(screen.core.memory.calls).toEqual([]);
});
diff --git a/src/handlers/project/buildDeploy.screen.test.tsx b/src/handlers/project/buildDeploy.screen.test.tsx
index 50fcf9121..c283f0b2d 100644
--- a/src/handlers/project/buildDeploy.screen.test.tsx
+++ b/src/handlers/project/buildDeploy.screen.test.tsx
@@ -62,9 +62,6 @@ function fakeBackend(options: FakeBackendOptions = {}) {
async resolveDeployedResources() {
return [];
},
- async resolveProjectResources() {
- return [];
- },
};
return { backend, deploys };
}
diff --git a/src/handlers/project/create/screen.tsx b/src/handlers/project/create/screen.tsx
index f3ff40bd7..440367986 100644
--- a/src/handlers/project/create/screen.tsx
+++ b/src/handlers/project/create/screen.tsx
@@ -393,7 +393,7 @@ function WizardStep({ stepKey, values, patch, onNext, onBack, onSubmit }: Wizard
name="what should the project be built around?"
helpText="a project deploys either a managed harness or your own agent code"
options={PROJECT_KIND_OPTIONS}
- selectedIndex={PROJECT_KIND_OPTIONS.findIndex((option) => option.kind === values.kind)}
+ focusedIndex={PROJECT_KIND_OPTIONS.findIndex((option) => option.kind === values.kind)}
onSelect={(index) => patch({ kind: PROJECT_KIND_OPTIONS[index]!.kind })}
onNext={onNext}
onBack={onBack}
@@ -414,9 +414,7 @@ function WizardStep({ stepKey, values, patch, onNext, onBack, onSubmit }: Wizard
name="choose a template"
helpText="the agent code scaffolded into the project"
options={TEMPLATE_OPTIONS}
- selectedIndex={TEMPLATE_OPTIONS.findIndex(
- (option) => option.template === values.template,
- )}
+ focusedIndex={TEMPLATE_OPTIONS.findIndex((option) => option.template === values.template)}
onSelect={(index) => patch({ template: TEMPLATE_OPTIONS[index]!.template })}
onNext={onNext}
onBack={onBack}
@@ -428,7 +426,7 @@ function WizardStep({ stepKey, values, patch, onNext, onBack, onSubmit }: Wizard
name="choose a memory configuration"
helpText="how should the Strands agent remember conversations?"
options={MEMORY_OPTIONS}
- selectedIndex={MEMORY_OPTIONS.findIndex((option) => option.memory === values.memory)}
+ focusedIndex={MEMORY_OPTIONS.findIndex((option) => option.memory === values.memory)}
onSelect={(index) => patch({ memory: MEMORY_OPTIONS[index]!.memory })}
onNext={onNext}
onBack={onBack}
@@ -495,7 +493,7 @@ function RadioStep({
name,
helpText,
options,
- selectedIndex,
+ focusedIndex,
onSelect,
onNext,
onBack,
@@ -503,7 +501,7 @@ function RadioStep({
name: string;
helpText: string;
options: FormRadioOption[];
- selectedIndex: number;
+ focusedIndex: number;
onSelect: (index: number) => void;
onNext: () => void;
onBack: () => void;
@@ -514,11 +512,11 @@ function RadioStep({
return;
}
if (key.upArrow) {
- onSelect(Math.max(0, selectedIndex - 1));
+ onSelect(Math.max(0, focusedIndex - 1));
return;
}
if (key.downArrow) {
- onSelect(Math.min(options.length - 1, selectedIndex + 1));
+ onSelect(Math.min(options.length - 1, focusedIndex + 1));
return;
}
if (key.return) onNext();
@@ -530,7 +528,7 @@ function RadioStep({
name={name}
helpText={helpText}
options={options}
- selectedIndex={selectedIndex}
+ focusedIndex={focusedIndex}
/>
);
@@ -706,7 +704,7 @@ function ModelStep({
name="choose a model"
helpText="the provider and model that will power the harness"
options={options}
- selectedIndex={providerIndex}
+ focusedIndex={providerIndex}
/>
{fields.map((field, fieldIndex) => (