Skip to content

feat(project): build and deploy TUI screens render the CLI's own progress steps - #2172

Merged
notgitika merged 6 commits into
aws:refactorfrom
notgitika:feat/project-build-deploy-tui
Sep 3, 2026
Merged

feat(project): build and deploy TUI screens render the CLI's own progress steps#2172
notgitika merged 6 commits into
aws:refactorfrom
notgitika:feat/project-build-deploy-tui

Conversation

@notgitika

@notgitika notgitika commented Sep 2, 2026

Copy link
Copy Markdown
Contributor

What

project build and project deploy open from the TUI menu instead of reporting "not implemented", and show exactly the progress the command shows — same steps, same ✓/✕/spinner glyphs, same output tail under the running step.

 agentcore → project → deploy → deploy the project to AWS
──────────────────────────────────────────────────────────────
 ╭────────────────────────────────────────────────────────────╮
 │ project  orders                                            │
 │ target   default                                           │
 ╰────────────────────────────────────────────────────────────╯
 ✓ Verifying AWS account 111122223333
 ⠋ Deploying AgentCore-orders-default
   │ CREATE_IN_PROGRESS | AWS::IAM::Role
──────────────────────────────────────────────────────────────
[ctl+c] quit

build starts as soon as the screen opens (nothing to confirm — it only writes inside the project). Done, it suggests agentcore project deploy; enter returns to the project menu.

deploy behaves as the command does: it deploys at once, and asks only in the one case the command asks — when agentcore.json declares nothing deployable, so deploying would remove the stack. Then the confirmation is the CLI's teardown question, and "y" is the pre-answered --yes. With several targets in aws-targets.json it first asks which (the TUI's stand-in for --target); with one or none it uses that or default, provisioning it on first deploy exactly as the command does.

How

Everything on screen is a CLI piece wrapped in the TUI frame:

the command uses the screen uses
projectManager.build(project) / .deploy(project, …) the same generator, verbatim: the screen's action is yield* core.projectManager.build(project)
TaskList, rendered inline by runWithProgress the same TaskList, rendered by ConfirmAction while the generator streams
the generator drain + event→task fold inside runWithProgress extracted to driveProgress (over pure applyProgressEvent / settleProgress) in src/tui/progress.tsx; both renderers call it and supply only how to draw the tasks
ConfirmAction (the confirm → run → success | error body behind harness delete) the whole screen. It learns to accept an AsyncGenerator<ProgressEvent, …> as its action and shows the TaskList in place of the bare spinner. message/title/rows become optional (no question → run when ready; no header → no box). Existing callers unchanged
Confirm (y/N) the teardown confirmation
the readline teardown question teardownQuestion(), exported from the handler and used by both; declaresNothingDeployable() (now exported) decides whether it is asked
Built project '…' / Deployed project '…' to target '…' builtMessage() / deployedMessage(), exported and used by both. The deploy outcome is read from result.tornDown, as the command does — not from the preflight heuristic, which synthesis can disagree with
DataTable (the invoke picker) the target picker
KeyValueTable ConfirmAction's header and result rows (replacing its fixed-width renderer)
withProject's not-found message projectNotFoundMessage(), exported; useProject / ProjectGate show it for a screen the user navigated to, esc back to the menu

Stack outputs are not listed on success, matching the command, which prints them only with --json. Bare agentcore project deploy on the command line is unchanged.

Also

  • ProjectManager.listTargets(project) — the declared targets, [] with no file; resolveTarget delegates to it.
  • ProjectInvokePickerScreen drops its inline project-resolution effect and message copy for useProject.
  • ConfirmAction doesn't advertise esc while the action runs — nothing listens for it mid-flight, by design — and gains doneLabel / nextSteps / onCancel.
  • flatFrame / waitForFlatText test helpers.

Testing

bun test: 2754 pass, 0 fail. tsc --noEmit, oxlint, prettier --check clean.

9 screen tests stub the backend exactly as the handler tests do, so both paths are asserted against identical events: build happy (enter → menu) / failure (✕ step with tail kept, esc → menu) / outside a project (esc → menu); deploy one target / several targets (picker) / fresh project with no aws-targets.json (default provisioned, step streamed) / empty-project teardown confirm / result-vs-heuristic disagreement / failure. 6 unit tests for the extracted fold and driver.

Driven by hand against a real project as well.

@github-actions github-actions Bot added the size/l PR size: L label Sep 2, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added agentcore-harness-reviewing AgentCore Harness review in progress claude-security-reviewing Claude Code /security-review in progress labels Sep 2, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 2, 2026
@codecov-commenter

codecov-commenter commented Sep 2, 2026

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 97.11%. Comparing base (e44a533) to head (ef7fb21).
⚠️ Report is 1 commits behind head on refactor.

Additional details and impacted files
@@             Coverage Diff              @@
##           refactor    #2172      +/-   ##
============================================
+ Coverage     97.07%   97.11%   +0.03%     
============================================
  Files           535      538       +3     
  Lines         36872    37167     +295     
============================================
+ Hits          35794    36094     +300     
+ Misses         1078     1073       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@agentcore-devx-automation agentcore-devx-automation Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AgentCore Harness Review

Verdict: Looks good

Nice refactor. Extracting applyProgressEvent/settleProgress as pure functions and driving both the CLI's inline TaskList and the new TUI screens off the same reducer is exactly the right shape — the TUI genuinely becomes a frame around the CLI's own progress rather than a parallel implementation. The ConfirmAction extension via a discriminated Promise | AsyncGenerator union is backward-compatible and the Symbol.asyncIterator check is a safe discriminator.

A few observations, none blocking:

  • src/handlers/project/deploy/screen.tsx: the target is hard-coded to DEFAULT_TARGET_NAME with no way to choose another from the TUI. Likely intentional for a first pass, but worth a follow-up once the picker exists.
  • src/components/ConfirmAction.tsx:116-119: the running phase advertises esc back and ctl+c quit in the key hints, but no useInput handler is installed while running, so esc is a no-op there. That's arguably the correct behavior (you don't want to abort a deploy mid-flight), but the hint is misleading — consider dropping esc from the hints during running.
  • Tests use real temp directories, scaffold via the real project create handler, and stub only at the ProjectBackend boundary. Good example of the pattern to prefer.
  • No new telemetry, but the sibling build/deploy command handlers don't emit any either, so this doesn't introduce a gap that wasn't already there.

@agentcore-devx-automation agentcore-devx-automation Bot removed the agentcore-harness-reviewing AgentCore Harness review in progress label Sep 2, 2026
@github-actions github-actions Bot added size/l PR size: L and removed size/l PR size: L labels Sep 2, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 2, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 2, 2026
@notgitika
notgitika marked this pull request as ready for review September 2, 2026 22:00
@github-actions github-actions Bot added size/xl PR size: XL and removed size/l PR size: L size/xl PR size: XL labels Sep 3, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Sep 3, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
gitikavj added 2 commits September 3, 2026 06:30
…gress steps

`project build` and `project deploy` open from the TUI menu instead of
reporting "not implemented". Each screen runs the very generator the
command runs — projectManager.build / projectManager.deploy — and renders
its step events through the same TaskList runWithProgress renders inline
on the command line, so the two paths show identical steps, glyphs and
output tails. Nothing about progress is re-implemented: the event→task
fold is extracted from runWithProgress into applyProgressEvent /
settleProgress and both renderers call it.

The screens are ConfirmAction instances — the existing confirm→run→
success|error body behind `harness delete` — which learns to accept a
progress generator as its action and shows the TaskList while it streams.
Its summary/success rows now align on the longest label rather than a
fixed 8 columns.

The command's other behaviours carry over from the same source:
builtMessage / deployedMessage / teardownQuestion are exported from the
handlers and shown on the screens; declaresNothingDeployable decides
whether the confirmation is the teardown question, and confirming it is
the pre-answered decision the backend consults, as with --yes.

useProject / ProjectGate resolve the enclosing project for a screen the
user navigated to, using withProject's now-exported not-found message;
ProjectInvokePickerScreen drops its inline copy of both.
…rogress driving

Review follow-ups on the build/deploy screens:

- The deploy success title came from the preflight declaresNothingDeployable
  heuristic, which the backend's post-synth count can disagree with, so the
  screen could say "Project removed" over "Deployed project…". The action
  now returns its own title from result.tornDown via deployedMessage, the
  same line the command prints; ConfirmAction's action may return
  { title, rows } for outcomes only known after running.
- ProjectGate's resolution error offered no way off but ctl+c. It takes an
  onBack and handles esc, advertised in the footer.
- ConfirmAction's rows are rendered by KeyValueTable instead of a second
  longest-label renderer.
- The generator drain lives once, in driveProgress; runWithProgress and
  ConfirmAction each pass only how to draw the tasks.
- The running phase no longer advertises esc, since nothing listens for it
  mid-action.
gitikavj added 3 commits September 3, 2026 06:30
…tion unless tearing down

Shaped by trying the screens on a real project:

- build starts as soon as the screen opens — it changes nothing outside the
  project — and shows no header. Done, it suggests `agentcore project
  deploy` and enter returns to the project menu instead of exiting.
- deploy confirms only when the spec declares nothing deployable, the one
  case the command asks its readline question; otherwise it deploys at
  once. With several targets in aws-targets.json it first asks which
  (a DataTable of target/account/region, the TUI's stand-in for --target);
  with one or none it uses that or `default`. ProjectManager gains
  listTargets for this; resolveTarget delegates to it.
- deploy no longer errors on a fresh project: resolveTarget returning
  undefined (no aws-targets.json yet) was fed straight to useQuery, which
  treats undefined data as an error. The manager's "Created default
  deployment target" step now streams through as it does on the CLI.
- the header is just project/target; stack outputs are not listed (the
  command prints them only with --json); the success hint reads "go back".

ConfirmAction: message, title and rows are optional (no question → run
when ready; no header → no box), doneLabel names where enter leads,
nextSteps lists follow-up commands, and an error without a confirmation
to return to leaves instead of re-running.
…eProject, recoverable target loading

Review follow-ups on aws#2172:

- ConfirmAction takes `trigger: {kind:"confirm"; message} | {kind:"immediate"}`
  instead of inferring immediacy from a missing message. The initial phase
  follows the trigger, so an immediate action never paints a y/n footer.
- useProject is a useQuery (seed as initialData, never refetched); ProjectGate
  and the invoke screen keep their shape. LoadingFrame is the shared
  spinner-or-error with esc back and r retry, used by ProjectGate and by the
  deploy screen's target loading, which previously offered only ctl+c.
- The create wizard drives its progress through driveProgress and renders
  TaskList, dropping its own event list.
@notgitika
notgitika force-pushed the feat/project-build-deploy-tui branch from 4ccf0a4 to 8dd11f0 Compare September 3, 2026 06:39
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Sep 3, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@notgitika

notgitika commented Sep 3, 2026

Copy link
Copy Markdown
Contributor Author

Demo of the new project build and project deploy TUI screens (2:21, narrated): the TUI renders the same progress steps, glyphs and messages the plain CLI prints

  • project build on the plain CLI, then the same build in the TUI
  • project deploy with the target picker: several targets → it asks, one target → it just deploys

Demo:
https://github.com/user-attachments/assets/65cf4ead-fd01-42c6-a13c-da30cc3aafc1

@tejaskash tejaskash left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Verified locally: tsc --noEmit clean, touched tests pass (202/202). No blockers, a few low-severity notes inline.

const targets = useQuery({
queryKey: ["project-targets", project.rootPath],
queryFn: () => core.projectManager.listTargets(project),
});

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

With the default gcTime this list is served from cache on a second visit, so DeployConfirm mounts and immediate fires run() before the refetch lands. If aws-targets.json gained a second target in between, targetName flips to undefined, the picker renders, and DeployConfirm unmounts while the deploy keeps running detached.

Since this is a local file the CLI reads fresh every time, gcTime: 0 here (or gating on !targets.isFetching) would avoid it.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Agreed. I fixed it and added a regression test


const declared = targets.data;
const targetName =
chosen ?? (declared.length <= 1 ? (declared[0]?.name ?? DEFAULT_TARGET_NAME) : undefined);

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

One declared target that is not named default gets deployed to here, whereas the CLI with no --target would provision and deploy default. The PR description says this is intended, flagging only so it stays a conscious choice.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes, this was intended

},
// A seeded project is authoritative — it is what the launching command ran
// against — so it is never refetched from the cwd.
...(seed && { initialData: seed, staleTime: Infinity }),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same cache shape as the targets query in the deploy screen: without a seed, a revisit reads project.spec from cache and declaresNothingDeployable and the action closure act on it before the refetch. Milder here since the backend re-reads from disk, but gcTime: 0 would make both behave the same way.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed!

Comment thread src/components/ConfirmAction.tsx Outdated
const [phase, setPhase] = useState<Phase>({ kind: "confirm" });
const cancel = onCancel ?? (() => navigate(-1));
const [phase, setPhase] = useState<Phase>({
kind: trigger.kind === "confirm" ? "confirm" : "idle",

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Phase is fixed from trigger.kind once at mount. A caller that mounts with isPending: true and later switches the trigger from immediate to confirm would stay idle and run the action with no question asked. No current caller does this, but it is easy to trip on in a component meant to guard destructive actions. Either document that trigger.kind must be stable or resolve the phase when isPending flips false.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fixed by keeping the action in a waiting phase until loading completes, then snapshotting the latest trigger and confirmation message.

Comment thread src/components/ConfirmAction.tsx Outdated
}

function toItems(rows: SummaryRow[]): Record<string, string> {
return Object.fromEntries(rows.map((row) => [row.label, row.value]));

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Converting to a Record drops what SummaryRow[] promises: duplicate labels collapse to the last value and integer-like labels get reordered by object key ordering. Nothing hits this today. If KeyValueTable is the renderer, taking Record<string, string> at the prop boundary would make the type match what is shown.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fixed

// A read-only lookup, so callers (e.g. the deploy handler's up-front teardown
// confirmation) can name the target's account and region without triggering
// the default-target provisioning deploy performs.
public async listTargets(project: Project): Promise<AwsDeploymentTarget[]> {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deploy further up still reads aws-targets.json inline. It also needs the exists check for its error branches, so not a one-line swap, but the read itself could go through listTargets now that it owns that path.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done!

Comment thread src/components/Root.tsx
@@ -116,7 +118,7 @@ import type { Context } from "../router";
// PROJECT_COMMANDS are the `agentcore project` subcommands that are listed in
// the menu but have no screen of their own yet (`create` has the wizard). Each

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Stale now: invoke, build, and deploy have screens too.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

this one is gone in the next PR

@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Sep 3, 2026
@agentcore-devx-automation agentcore-devx-automation Bot added the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@agentcore-devx-automation

Copy link
Copy Markdown
Contributor

Claude Security Review: no high-confidence findings. (run)

@agentcore-devx-automation agentcore-devx-automation Bot removed the claude-security-reviewing Claude Code /security-review in progress label Sep 3, 2026
@github-actions github-actions Bot added size/xl PR size: XL and removed size/xl PR size: XL labels Sep 3, 2026
@notgitika
notgitika merged commit 2e30e1f into aws:refactor Sep 3, 2026
23 of 32 checks passed
@notgitika
notgitika deleted the feat/project-build-deploy-tui branch September 3, 2026 17:43
This was referenced Sep 3, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

size/xl PR size: XL

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants