feat(project): add TUI wizards for four project add resources - #2166
feat(project): add TUI wizards for four project add resources#2166notgitika wants to merge 3 commits into
project add resources#2166Conversation
Selecting `add` from the project menu reported "no interactive screen yet".
It now opens the resource menu, and four of the fifteen resources have a
wizard: memory, gateway, policy-engine and config-bundle. The other eleven
keep the not-implemented screen and are unchanged on the command line.
The three wizards that already existed (project create, HarnessWizard,
EndpointWizard) had each hand-rolled the same shell: a step list, a phase
machine, esc-goes-back, and a per-step useInput that spends 30 lines asking
for one string. That shell is now one component. A screen declares its
questions as <Step> children and branches with a plain conditional, so a
step that does not apply is absent from the flow and from the stepper:
{isCustomJwt && (
<Step name="authorizer-configuration" question="...">
<TextAreaField ... />
</Step>
)}
Position is keyed by step name rather than index, because branches have
different lengths and a step appearing must not move the user.
Screens submit through projectManager.addResource, as project create submits
through projectManager.create - not through the handler, whose result goes to
a stderr captured at wiring time that Ink's alternate screen would swallow.
To keep the two entry points from drifting, the screens reuse the handlers'
own helpers rather than copies of them: toDefaultStrategy,
EventExpiryDurationSchema, ComponentsSchema, gatewayResourceName and
policyEngineResourceName, three of which are newly exported for it.
Validation messages come from the flags' own schemas, so the wizard rejects
what the flag rejects and says the same thing about it.
Required-ness is a field prop, not a schema change: flag schemas stay
`.optional()` so Commander cannot reject a bare command before the TUI
middleware runs (69aa0c1), and the handler's own throw stays authoritative.
Screens resolve the project themselves via ProjectGate. withProject wraps
`handle` only, so middleware never runs for a screen the user navigated to
and ProjectKey is absent unless the launching command happened to set it.
The gate reports the same not-found guidance the CLI prints.
The Form* components now omit their label and help rows when passed empty
strings, so a field whose <Step> already asks the question renders just the
control instead of restating itself three times over. Existing callers pass
non-empty strings and are unaffected.
Two defects the shell's own tests found: the first frame rendered a footer
with no action key, because fields publish hints from an effect; and two
enter presses in one Ink drain submitted twice.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
|
Claude Security Review: no high-confidence findings. (run) |
Codecov Report❌ Patch coverage is Additional details and impacted files@@ Coverage Diff @@
## refactor #2166 +/- ##
============================================
+ Coverage 97.25% 97.28% +0.02%
============================================
Files 508 519 +11
Lines 33902 35073 +1171
============================================
+ Hits 32972 34121 +1149
- Misses 930 952 +22 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
There was a problem hiding this comment.
AgentCore Harness Review
Verdict: Looks good
Nice, cohesive PR. The wizard shell is small, well-tested, and the four new screens all funnel through the same ProjectGate → core.projectManager.addResource(...) path that the flag-driven handlers use, so telemetry, error handling and schema validation are inherited rather than duplicated. A few things I explicitly looked at and liked:
- Schemas are exported from the handler modules and reused by the wizards (
ComponentsSchema,MemoryNameSchema,EventExpiryDurationSchema,toDefaultStrategy,GatewayAuthorizerConfigSchema). No copies to drift. Wizardpositions by step key, so conditional branches ({isCustomJwt && <Step name="authorizer-configuration" />},{hasGateways && isAttaching && <Step name="mode" />}) don't shift the user, andstepElementsis derived viaReact.Children.toArray(...).filter(isStepElement)sofalse/nullbranch children are dropped cleanly.submittingref makes buffered double-enter idempotent;ExitOnErrorcorrectly routes failures throughuseApp().exit(error)sorenderTuiAt'swaitUntilExit()rejects and the normal CLI error path prints the message — as opposed to a React stack.- Tests are real end-to-end:
createGatewayProjectTestHarnessscaffolds a real project on a temp dir, and assertions read the actualagentcore.jsonback. Nofs/ProjectManagermocks, no fake stdin — good. - The
configBundletest verifies the exportedCOMPONENTS_EXAMPLEitself parses and satisfies the schema, which is exactly the guard you want on an on-screen example.
Nothing that needs to change before merging.
… flags and wizard Each handler now exports `XxxInput` and `toAddXxxInput(...)`, the single place defaults, cross-field rules and resource assembly live. `handle` reduces to flags → input; the screen's `onSubmit` reduces to form values → input. Neither path states what a resource is, so they cannot drift. This fixes one drift already present: the flag path validated --authorizer-configuration with the strict schema, the wizard with the non-strict one, so the SDK's `customJWTAuthorizer` casing was rejected by flags but accepted by the wizard and then failed at submit. Both now share GatewayAuthorizerConfigurationInputSchema; a screen test pins it. Also in the shell: duplicate <Step name> throws at render, and the one-field-per-step invariant is stated on Step with the compound-field route for anything that needs more.
|
Claude Security Review: no high-confidence findings. (run) |
CI runs oxlint 1.80, whose react(refs) rule rejects `latest.current = hints` in useKeyHints. The effect now runs on every render and consults the ref only inside the effect, publishing when the hints' content differs from what the footer already shows — the same behaviour, with the ref where React wants it.
|
Claude Security Review: no high-confidence findings. (run) |
AlexanderRichey
left a comment
There was a problem hiding this comment.
A lot of this looks good, but I'm concerned about potential duplication with existing components/patterns. Can you take a pass over this to make sure we're not duplicating code/components/logic we already have?
| advance(); | ||
| }); | ||
|
|
||
| return ( |
There was a problem hiding this comment.
Are we moving stuff around? I think we already have components for all of this. Let's reuse and not duplicate.
|
temporarily closing this |
What
Selecting
addfrom the project menu now opens a resource menu, and four of the fifteen resources get a wizard: memory, gateway, policy-engine, config-bundle. The other eleven keep the not-implemented screen and are unchanged on the command line.memorygatewaypolicy-engineconfig-bundleDesign
Two pieces, each with one job.
1. A shared
<Wizard>shell (components/wizard/, 277 lines). It owns the step list, position, key handling, and theform → running → success | errorphases. A screen supplies only the questions, as<Step>children, and branches with a plain conditional:React.Children.toArraydrops thefalsea closed branch produces, so an inapplicable step is absent from the flow and the stepper alike. Position is keyed by step name, not index, so a step appearing or vanishing never moves the user. Duplicate names throw at render. Each field owns its ownuseInput, so the shell has no focus concept — one field per step is the invariant; a step that needs two related inputs gets a compound field, not shell-level focus management.2. One input builder per resource, exported from the handler:
toAddGatewayInput(project, input),toAddMemoryInput(input),toAddConfigBundleInput(input),toAddPolicyEngineInput(project, input). Each takes a typedXxxInput(already-parsed JSON, already-split tags) and is the only place defaults, cross-field rules, and resource assembly live. The flag handler'shandlereduces to flags →XxxInput; the screen'sonSubmitreduces to form values →XxxInput. Neither knows what a Gateway is.This is what stops the two paths drifting. It already caught one: the flag path validated
--authorizer-configurationwithGatewayAuthorizerConfigSchema.strict(), the first cut of the wizard used the non-strict schema, so the SDK'scustomJWTAuthorizercasing was rejected by flags but accepted by the wizard — and then failed at submit as a missingcustomJwtAuthorizer. Both now shareGatewayAuthorizerConfigurationInputSchema; a screen test pins it.The wizard convention that makes the builders' cross-field errors unreachable from the TUI: pass only the answers to steps the user saw. A JWT config typed before switching the authorizer to
NONEis dropped, not sent.Decisions worth a reviewer's attention
projectManager.addResource, nothandler.handle. Handlers write to aconfig.io.stderrcaptured at wiring time, which Ink's alternate screen swallows..optional(). Required-ness is a field prop; a non-optional schema lets Commander reject a bare command before the TUI middleware can open a screen (69aa0c19).ProjectGateresolvesProjectKeyfor screens the user navigated to (middleware never ran) and reports the CLI's own not-found guidance.Form*components omit label/help rows when passed empty strings, so a field under a<Step>renders just the control. Existing callers pass non-empty strings and are unaffected.Testing
bun test: 2645 pass, 0 fail.tsc --noEmit,oxlint,prettier --checkclean.38 new tests. Handler flag tests are unchanged and now exercise the shared builders; screen tests exercise the same builders from the form side.
add.screen.test.tsxreads its case list off the compiled Commander tree, so a resource routed later without a screen fails the suite.Not in scope
agentcore project add memorystill runs the flag path. Opening the wizard needs a per-command launcher, not the router-widewithTuiOnEmptyFlagsAndArgs.project create,HarnessWizard,EndpointWizardonto the shell.create's forks are expressible as{cond && <Step/>}; that's the natural next PR.addresources. Each follows the same recipe: exportXxxInput+toAddXxxInputfrom the handler, reducehandleto flag conversion, write a screen of<Step>s.