From 6a93913dcf2539ebec6246cf4975aac8a2357d37 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 3 Sep 2026 15:08:41 -0400 Subject: [PATCH 1/3] chore: add select state to radio buttons and only show inputs on enter --- src/components/FormRadioGroup.tsx | 30 ++++++-- src/components/HarnessWizard.tsx | 36 +++++----- src/handlers/memory/record/list/screen.tsx | 81 +++++++++++++--------- src/handlers/project/create/screen.tsx | 20 +++--- 4 files changed, 100 insertions(+), 67 deletions(-) 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" && ( { - 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/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) => ( Date: Thu, 3 Sep 2026 15:33:49 -0400 Subject: [PATCH 2/3] fix: remove duplicate import failing typecheck --- src/handlers/project/buildDeploy.screen.test.tsx | 3 --- 1 file changed, 3 deletions(-) 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 }; } From 5b1725ab99e71df3b402166efce8026f89e0cea2 Mon Sep 17 00:00:00 2001 From: Nicolas Borges Date: Thu, 3 Sep 2026 15:34:23 -0400 Subject: [PATCH 3/3] chore: update tests to reflect new ENTER to select behavior --- .../harness/create/create.screen.test.tsx | 21 ++++++++++++++++++ .../harness/update/update.screen.test.tsx | 6 +++-- .../memory/record/record.screen.test.tsx | 22 ++++++++++++++++++- 3 files changed, 46 insertions(+), 3 deletions(-) diff --git a/src/handlers/harness/create/create.screen.test.tsx b/src/handlers/harness/create/create.screen.test.tsx index d5281853c..a21f51249 100644 --- a/src/handlers/harness/create/create.screen.test.tsx +++ b/src/handlers/harness/create/create.screen.test.tsx @@ -111,6 +111,27 @@ describe("harness create wizard", () => { 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/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([]); });