fix(tui): surface post-agent work so the wizard doesn't look hung#443
fix(tui): surface post-agent work so the wizard doesn't look hung#443
Conversation
After the Claude agent finished, the TUI sat on Screen.Run with all
agent task checkmarks ✓ while a static "Creating charts and dashboard
in Amplitude…" footer ran for up to 90 seconds with no other progress
signal — users read it as a hung wizard.
- Add a FinalizingPanel below the agent task list rendering the
post-agent step queue (commit events, create dashboard) with
per-step elapsed time and 20s/40s coaching anchored to the active
step's startedAt — not run elapsed.
- Drive the footer status from the active step's activeForm so the
visible task and the spinner footer can never disagree (single
source of truth).
- commitPlannedEventsStep and createDashboardStep each own their own
setPostAgentStep lifecycle calls (in_progress → completed | skipped
with reason). Skipped rows surface the reason inline.
- commitPlannedEventsStep: single retry of fetchAmplitudeUser before
giving up on appId resolution (covers the stale-OAuth-token race).
Failure now emits analytics.wizardCapture('planned events skipped')
+ ui.log.warn — no more silent drops.
- DASHBOARD_TIMEOUT_MS 90s → 60s; TODO comment for the deferred
direct-MCP-path follow-up.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
🧙 Wizard CIRun the Wizard CI and test your changes against wizard-workbench example apps by replying with a GitHub comment using one of the following commands: Test all apps:
Test all apps in a directory:
Test an individual app:
Show more apps
Results will be posted here when complete. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 2 potential issues.
Autofix Details
Bugbot Autofix prepared fixes for both issues found in the latest run.
- ✅ Fixed: Outer catch leaves create-dashboard step stuck at in_progress
- Added
getUI().setPostAgentStep(POST_AGENT_STEP_CREATE_DASHBOARD, { status: 'skipped', reason: 'unexpected error' })in the agent-runner catch block so the step always reaches a terminal state.
- Added
- ✅ Fixed: FinalizingPanel interval fires unnecessarily when steps empty
- Made the setInterval conditional on
hasActive(derived from steps having an in-progress entry) so the interval only runs when needed and the component avoids unnecessary re-renders.
- Made the setInterval conditional on
Or push these changes by commenting:
@cursor push 6b3c8fa00e
Preview (6b3c8fa00e)
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -1188,6 +1188,10 @@
err instanceof Error ? err.message : String(err)
}`,
);
+ getUI().setPostAgentStep(POST_AGENT_STEP_CREATE_DASHBOARD, {
+ status: 'skipped',
+ reason: 'unexpected error',
+ });
}
// MCP installation is handled by McpScreen — no prompt here
diff --git a/src/ui/tui/components/FinalizingPanel.tsx b/src/ui/tui/components/FinalizingPanel.tsx
--- a/src/ui/tui/components/FinalizingPanel.tsx
+++ b/src/ui/tui/components/FinalizingPanel.tsx
@@ -41,13 +41,18 @@
}
export const FinalizingPanel = ({ steps }: FinalizingPanelProps) => {
- // Tick once per second so the active-step elapsed time and coaching
- // tier transitions render — the panel is otherwise stateless.
+ const hasActive = steps.some(
+ (s) => s.status === PostAgentStepStatus.InProgress,
+ );
+
+ // Tick periodically so the active-step elapsed time and coaching
+ // tier transitions render — only while a step is in progress.
const [, setTick] = useState(0);
useEffect(() => {
+ if (!hasActive) return;
const id = setInterval(() => setTick((t) => t + 1), SPINNER_INTERVAL);
return () => clearInterval(id);
- }, []);
+ }, [hasActive]);
if (steps.length === 0) return null;You can send follow-ups to the cloud agent here.
|
Could not push Autofix changes. The PR branch may have changed since the Autofix ran, or the Autofix commit may no longer exist. |
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
There are 3 total unresolved issues (including 2 from previous reviews).
Autofix Details
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Refactored import outside try/catch breaks "never throws" contract
- Moved the
await import('./api.js')from outside the retry loop's try/catch to inside it, restoring the defensive error handling that prevents unhandled exceptions from propagating.
- Moved the
Or push these changes by commenting:
@cursor push 151cf6cfc7
Preview (151cf6cfc7)
diff --git a/src/lib/agent-runner.ts b/src/lib/agent-runner.ts
--- a/src/lib/agent-runner.ts
+++ b/src/lib/agent-runner.ts
@@ -330,9 +330,8 @@
label: string,
): Promise<string> {
try {
- const { getStoredToken, getStoredUser, storeToken } = await import(
- '../utils/ampli-settings.js'
- );
+ const { getStoredToken, getStoredUser, storeToken } =
+ await import('../utils/ampli-settings.js');
const { refreshAccessToken } = await import('../utils/oauth.js');
const { EXPIRY_BUFFER_MS } = await import('../utils/token-refresh.js');
const user = getStoredUser();
@@ -458,9 +457,8 @@
// process.exit. The success-path re-fires below catch the two failure
// modes that bypass wizardAbort (non-throwing return false, raw
// throw).
- const { registerCleanup, registerPriorityCleanup } = await import(
- '../utils/wizard-abort.js'
- );
+ const { registerCleanup, registerPriorityCleanup } =
+ await import('../utils/wizard-abort.js');
registerPriorityCleanup(() =>
restoreSetupReportIfMissing(session.installDir),
);
@@ -721,9 +719,8 @@
// skills are constants; the integration skill is resolved per framework
// (with a sensible default fallback). When a skill is pre-staged we drop
// the corresponding load_skill_menu / install_skill steps from the prompt.
- const { preStageSkills, bundledSkillExists } = await import(
- './wizard-tools.js'
- );
+ const { preStageSkills, bundledSkillExists } =
+ await import('./wizard-tools.js');
const integrationSkillId = config.metadata.getIntegrationSkillId
? config.metadata.getIntegrationSkillId(frameworkContext)
: (() => {
@@ -1092,9 +1089,8 @@
// Upload environment variables to hosting providers (auto-accept)
let uploadedEnvVars: string[] = [];
if (config.environment.uploadToHosting) {
- const { uploadEnvironmentVariablesStep } = await import(
- '../steps/index.js'
- );
+ const { uploadEnvironmentVariablesStep } =
+ await import('../steps/index.js');
uploadedEnvVars = await uploadEnvironmentVariablesStep(envVars, {
integration: config.metadata.integration,
session,
@@ -1174,9 +1170,8 @@
// Amplitude MCP response can't hang the whole run. Gracefully degrades:
// agent success is not affected by dashboard-step failure.
try {
- const { createDashboardStep } = await import(
- '../steps/create-dashboard.js'
- );
+ const { createDashboardStep } =
+ await import('../steps/create-dashboard.js');
await createDashboardStep({
session,
accessToken,
@@ -1251,9 +1246,8 @@
accessToken: string,
cloudRegion: string,
): Promise<void> {
- const { fetchHasAnyEventsMcp, fetchAmplitudeUser } = await import(
- '../lib/api.js'
- );
+ const { fetchHasAnyEventsMcp, fetchAmplitudeUser } =
+ await import('../lib/api.js');
const { logToFile } = await import('../utils/debug.js');
const POLL_INTERVAL_MS = 30_000;
@@ -1697,9 +1691,9 @@
accessToken: string,
cloudRegion: string,
): Promise<string | null> {
- const { fetchAmplitudeUser } = await import('./api.js');
for (let attempt = 1; attempt <= 2; attempt++) {
try {
+ const { fetchAmplitudeUser } = await import('./api.js');
const userInfo = await fetchAmplitudeUser(
accessToken,
cloudRegion as 'us' | 'eu',You can send follow-ups to the cloud agent here.
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes and found 1 potential issue.
Bugbot Autofix prepared a fix for the issue found in the latest run.
- ✅ Fixed: Existing dashboard path never marks step completed
- Added the missing
ui.setPostAgentStep(STEP_ID, { status: 'completed' })call before the early return in the existing-dashboard path, so the step correctly transitions from in_progress to completed.
- Added the missing
Or push these changes by commenting:
@cursor push ff9d4a6206
Preview (ff9d4a6206)
diff --git a/src/steps/create-dashboard.ts b/src/steps/create-dashboard.ts
--- a/src/steps/create-dashboard.ts
+++ b/src/steps/create-dashboard.ts
@@ -126,6 +126,7 @@
'duration ms': 0,
source: 'agent',
});
+ ui.setPostAgentStep(STEP_ID, { status: 'completed' });
return;
}You can send follow-ups to the cloud agent here.
Reviewed by Cursor Bugbot for commit 407795e. Configure here.
When an existing dashboard file is found (the agent already created it),
the function returned after setting the step to 'in_progress' without ever
transitioning to 'completed'. This left the FinalizingPanel showing the
create-dashboard row permanently stuck at in_progress with a running timer.
Add the missing ui.setPostAgentStep(STEP_ID, { status: 'completed' }) call
before the early return, matching the pattern used by all other exit paths.
Applied via @cursor push command


Summary
After the Claude agent finished, the TUI sat on
Screen.Runwith all agent task checkmarks ✓ while a static "Creating charts and dashboard in Amplitude…" footer ran for up to 90 seconds with no other progress signal — users read it as a hung wizard. Screenshots: tasks visibly done + static footer = "looks broken or done."This PR makes the post-agent gap visibly accounted for and quietly fixes a related silent-failure bug.
Visibility (the user-facing fix)
FinalizingPanelbelow the agent task list. Renders the post-agent step queue (commit events, create dashboard) with their own status icons (◻pending →▶in_progress →◼completed |○skipped + reason). Per-step elapsed time renders next to the active row. Coaching messages at 20s / 40s anchored to the active step'sstartedAt— not run elapsed (would have fired immediately after a 5-min agent run otherwise).activeForm— single source of truth, the visible task and the spinner footer can no longer disagree.postAgentSteps: PostAgentStep[]) rather than two named fields, so a future post-agent step drops in cleanly.commitPlannedEventsStep/createDashboardStep→setPostAgentStep). agent-runner just seeds + invokes.Reliability
DASHBOARD_TIMEOUT_MSlowered 90s → 60s with a TODO comment explaining the deferreddirectMCP path follow-up (real chart-tool schema discovery is needed first; the agent fallback stays as the only path for now).commitPlannedEventsStepno longer silently drops planned events on appId-resolution failure: single retry offetchAmplitudeUser(covers the common stale-OAuth-token race), thenanalytics.wizardCapture('planned events skipped', { reason })+ui.log.warn(...)+ the synthetic task ticksskippedwith a visible reason.progress: post_agent_seededandprogress: post_agent_stepNDJSON events so orchestrators see the same state.Architecture notes
tasksarray — keepssyncTodos's "agent-authoritative" invariant intact.Setupthrough post-agent work —Verifyimplies "your turn" and would mislead.PostAgentStepStatusinsession-constants.tsper the existing ESM/CJS workaround; thesession-constants-synctest enforces it.Test plan
pnpm test— 2548/2548 unit tests, 14 skipped (existing). 12 new assertions for store setters, FinalizingPanel rendering states, and step-id constant sync.pnpm test:bdd— 97/97 scenarios.pnpm build— clean.pnpm lint— clean.pnpm tryagainst a Next.js project — confirm: agent finishes → FinalizingPanel appears with "Save event plan" + "Create dashboard" rows ticking pending → in_progress → ✓ before the wizard transitions to Mcp screen; footer message matches the active step'sactiveForm.fetchAmplitudeUservia a network shim) — confirm synthetic task ticks toskippedwith reason, analytics breadcrumb fires, wizard advances.Out of scope (follow-ups)
directMCP path for chart/dashboard creation (create_chart× N →create_dashboard). Needs MCP write-tool schema discovery first —src/lib/planned-events.ts:100shows the catch (create_eventsreturns the literal "MCP error" sentinel today).Screen.Runinto separateRun/Verifyscreens — explicitly rejected (low value, high blast radius vs. the visibility fix).🤖 Generated with Claude Code
Note
Medium Risk
Touches the main
agent-runnerpost-run path and adds new session/UI state that affects multiple UIs, so regressions could impact completion flow or status reporting. Changes are additive with test coverage and mostly affect progress visibility and graceful-degradation paths.Overview
Adds a framework-controlled post-agent step queue (
postAgentSteps) that is seeded byagent-runnerafter the Claude agent completes and rendered in a new TUIFinalizingPanel, so users see progress (and per-step elapsed time/skip reasons) during the post-agent “silent gap”. The Run screen footer status now follows the active post-agent step’sactiveForm, and Agent/CI UIs emit/log correspondingseedPostAgentSteps/setPostAgentStepupdates (including NDJSONprogressevents).Makes post-agent work more robust and observable:
commitPlannedEventsStepnow marks step lifecycle, retriesfetchAmplitudeUseronce when resolvingappId, and emits analytics + a visible warning when planned-event commit is skipped;createDashboardStepsimilarly owns its lifecycle, shortens its timeout from 90s to 60s, and reports distinct skip reasons. Adds tests to lock down step-id constant sync,PostAgentStepStatussync between canonical and TUI constants, store behavior for queue patching (startedAt, no-op on unknown ids), and FinalizingPanel rendering states.Reviewed by Cursor Bugbot for commit fe440c9. Bugbot is set up for automated code reviews on this repo. Configure here.