feat(cli): offer AI build debug in onboarding TUI#2328
Conversation
The Ink-based `build init` wizard runs `requestBuildInternal` with a custom
logger so build output flows through React state. On failure today the build
code still tries to drive the AI flow with `@clack/prompts` (`confirm`,
`select`, `spinner`) — but Ink owns the terminal, so those clack writes/reads
collide with React redraws and the AI prompt either corrupts the TUI or hangs.
Net effect: users in the onboarding wizard never get a usable AI debug offer.
This change adds a `caller-handled` mode so the wizard can drive the AI UX
with Ink-native components.
Layering (matches docs/superpowers/specs in wiki):
- `cli/src/ai/analyze.ts`
- `runCapgoAiAnalysis({ apiHost, apikey, jobId, appId })` reads the
`/tmp/capgo-builds/<jobId>.log` file, returns `too_big` over 10 MB,
`error { message: 'log_unavailable' }` if missing, otherwise delegates
to `postAnalyzeRequest`.
- `releaseCapturedLogs(jobId)` thin wrapper around
`cleanupCapturedJobFiles({ keepAiPromptFile: false })`.
- `cli/src/ai/telemetry.ts`
- Extend `AiAnalysisTriggeredBy` with `'onboarding'` so PostHog can
segment onboarding-driven AI usage from CLI menu / CI flag.
- `cli/src/schemas/build.ts`
- `BuildRequestOptions.aiAnalysisMode?: 'auto-prompt' | 'caller-handled' | 'skip'`
(default `'auto-prompt'` — direct CLI invocation matrix unchanged).
- `BuildRequestResult.aiAnalysis?: { jobId, capturedLogPath, ready }`.
- `cli/src/build/request.ts`
- Branch the failure-AI block on `aiAnalysisMode`. `'caller-handled'`
keeps the captured log alive (via `keepPromptFile=true`) and surfaces
it on the result so callers can run `runCapgoAiAnalysis`. `'skip'`
no-ops. `'auto-prompt'` keeps the existing clack flow.
- Capture stays enabled in caller-handled mode regardless of TTY so the
captured log is always available to the caller; explicit `'skip'`
suppresses capture entirely.
- `cli/src/build/onboarding/{types,android/types}.ts`
- New `OnboardingStep`/`AndroidOnboardingStep`: `'ai-analysis-prompt'`,
`'ai-analysis-running'`, `'ai-analysis-result'`. `STEP_PROGRESS` +
`getPhaseLabel` entries.
- `cli/src/build/onboarding/{ui,android/ui}/app.tsx`
- Pass `aiAnalysisMode: 'caller-handled'` to `requestBuildInternal`. On
failure with `aiAnalysis.ready === true`, transition to
`'ai-analysis-prompt'` (Ink `<Select>` with Debug-with-AI / Skip).
Pick "Debug" -> `'ai-analysis-running'` (`<SpinnerLine>`) which calls
`runCapgoAiAnalysis` and transitions to `'ai-analysis-result'`. Render
the analysis via existing `renderMarkdown` (ANSI passes through Ink's
`<Text>`). On exit, call `releaseCapturedLogs(jobId)`.
- PostHog: `trackAiAnalysisChoice({ ..., triggeredBy: 'onboarding' })`
fires when the user picks Debug or Skip; `trackAiAnalysisResult` fires
after `postAnalyzeRequest` resolves. Existing
`trackBuilderOnboardingStep` continues to fire on each new step.
CI/CD safety: no new detection. The two existing layers handle it —
`shouldCaptureLogs()` gates on TTY, and Ink itself requires a TTY to render.
In CI, capture is off, `result.aiAnalysis` is undefined, and the wizard goes
straight to `build-complete`.
Build infra: TypeScript 6.0.3 turns the deprecated `baseUrl` into a hard
error. Adding `"ignoreDeprecations": "6.0"` per TS recommendation unblocks
`bun run build` on main. Unrelated to the feature but bundled here so the
binary is buildable on the branch.
Test coverage:
- `cli/test/test-ai-onboarding-mode.mjs` (9 tests, all passing):
- `decideAnalyzeBehavior` matrix unchanged (regression guard)
- `runCapgoAiAnalysis` happy path, too-big, missing log
- `releaseCapturedLogs` deletes the log + is best-effort
- Existing `test-ai-analyze-flow`, `test-ai-log-capture`,
`test-ai-render-markdown`, `test-onboarding-progress`, and
`test-onboarding-recovery` all still pass.
|
Important Review skippedDraft detected. Please check the settings in the CodeRabbit UI or the ⚙️ Run configurationConfiguration used: Organization UI Review profile: ASSERTIVE Plan: Pro Run ID: You can disable this status message by setting the Use the checkbox below for a quick retry:
Comment |
|
Review the following changes in direct dependencies. Learn more about Socket for GitHub.
|
Merging this PR will not alter performance
Comparing Footnotes
|
125378e to
79fae03
Compare
After the AI analysis renders, the user often needs to apply the suggested
fix in their editor and re-run the build. Previously the only option from
'ai-analysis-result' was "Continue" -> exit, which forced the user to
re-launch the wizard (and re-run credential discovery) just to re-attempt
the build.
This change lets the user retry directly from inside the wizard:
- 'ai-analysis-result' now shows two options when retries remain:
🔄 I fixed it, retry build (N retries left)
⏭ Continue (skip retry)
- On retry: release the captured log for the current jobId, reset AI state
(aiJobId / aiAnalysisText / aiResultMessage), increment aiRetryCount,
and transition back to 'requesting-build'. The existing useEffect for
that step re-runs `requestBuildInternal` and (on failure) routes back
through the AI flow with the new jobId.
- After MAX_AI_RETRIES (= 2) the retry option is removed and a single
"Continue" is shown. A dim note explains how many retries were used.
- Total wizard-internal attempts capped at 3: initial + 2 retries.
PostHog: extend `AiAnalysisChoice` with 'retry' (additive). The Choice
event fires per-retry-attempt, carrying the current jobId — so the
funnel can measure "AI suggestion was actionable enough to merit a
retry" and "retry led to a successful subsequent build" as separate
metrics by joining on app/job.
Mirrored to both iOS (cli/src/build/onboarding/ui/app.tsx) and Android
(cli/src/build/onboarding/android/ui/app.tsx) wizards.
Privacy: as before, only closed-enum choice values cross the telemetry
boundary; the AI diagnosis text is never observed.
If the on-failure AI diagnosis is taller than the user's terminal viewport, rendering it inline causes the earlier lines to scroll off the top before the retry/skip picker appears — which we explicitly do not want in the onboarding wizard. Earlier lines scrolled into the terminal history aren't the "active" screen, so the user can't see both the diagnosis and the choices at the same time and the wizard ends up in a confusing state. This change adds a conservative fit estimator and routes overflowing analyses through a new fullscreen scrollable viewer modeled on the existing `FullscreenDiffViewer` from main. ### Fit estimator — `cli/src/build/onboarding/ai-fit.ts` `isAiAnalysisTooTall(text, terminalRows, terminalCols)` estimates rendered rows (per logical line × wrap factor, ignoring ANSI escapes for length) and compares against `terminalRows - AI_RESULT_CHROME_ROWS` (default 20). The 20-row chrome reserve is deliberately generous: prefer a false-positive scroll (one extra keystroke) over a false-negative inline render (lines disappear off the top). ### Scrollable viewer — `cli/src/build/onboarding/ui/components.tsx` `FullscreenAiViewer` mirrors the shape of `FullscreenDiffViewer` on main but takes pre-rendered ANSI text lines instead of structured diff lines. Keybindings: ↑/↓ or j/k (line), PgUp/PgDn or u/d/Space (page), g/G (top/ bottom), Esc/Enter (dismiss). Shows "Showing N-M of TOTAL lines" footer and a status-aware exit hint. ### New step — `'ai-analysis-result-scroll'` (iOS + Android) Added to both `OnboardingStep` and `AndroidOnboardingStep`, plus `STEP_PROGRESS` and `getPhaseLabel`. The outer wizard Header / progress bar / log panel are hidden during this step so the viewer gets the full screen (same pattern main uses for `view-workflow-diff`). ### Wizard wiring (both iOS and Android `ui/app.tsx`) - New `aiViewedFull` state. The step useEffect for `ai-analysis-result` checks fit and transitions to `ai-analysis-result-scroll` when needed. - The scroll step renders `<FullscreenAiViewer />`; on exit it sets `aiViewedFull=true` and returns to `ai-analysis-result`. - The inline `ai-analysis-result` render now branches on `aiViewedFull`: if true, replaces the body dump with a compact "📖 Analysis already shown above (scroll your terminal back to re-read it)" marker so the retry/skip picker is always visible. - Retry handler resets `aiViewedFull` so the next attempt re-evaluates fit against the new analysis text. ### Test coverage `cli/test/test-ai-fit.mjs` (12 tests, all green): - ANSI strip - Empty / short / overflowing inputs - Borderline conservative case - Single very long wrapping line Existing `test-ai-onboarding-mode`, `test-ai-analyze-flow`, `test-ai-log-capture`, `test-ai-render-markdown` continue to pass — no regression in the underlying flow.
Two bugs caused the FullscreenAiViewer to push content into the terminal's
scrollback history, forcing the user to scroll the terminal emulator (not
the viewer) to see the analysis:
1. Dimensions were captured at mount time. When the terminal was resized
the viewer kept its original `viewportRows` and chrome reserve, so the
actual rendered output could be taller (or shorter) than the live screen.
2. Both `viewportRows` and `maxScrollOffset` treated each logical line as
one terminal row. Long lines wrap, so a "10-row" slice could actually
render as 30+ rows on a narrow terminal — overflowing the viewport.
This change addresses both:
- `FullscreenAiViewer` now subscribes to `stdout.on('resize', ...)` and
stores `{ rows, cols }` in state. Every resize triggers a re-render that
recomputes the viewport, max scroll offset, divider width, and visible
slice against the live dimensions.
- Two new helpers in `ai-fit.ts`:
`pickVisibleLines(lines, scrollOffset, viewportRows, terminalCols)`
— wrap-aware slice. Stops adding logical lines once their cumulative
wrapped row count would exceed the viewport. Always includes at
least one line so the viewer body is never empty.
`computeMaxScrollOffset(lines, viewportRows, terminalCols)`
— wrap-aware scroll bound. Walks backwards from the last line,
packing as many tail lines as fit (counting wrap), and returns the
offset of the first fully-visible tail line.
- Chrome reserve bumped from 8 to 10 rows to absorb chrome lines that
themselves wrap on narrow terminals (subtitle, position line, exit hint
are all sentence-length and can wrap on <60-col terminals).
- Dividers now scale to `min(60, cols - 1)` so they never wrap and silently
eat a viewport row.
Test coverage in `test-ai-fit.mjs` (22 tests, all green):
- ANSI strip, simple row counting, wrap accounting (preserved)
- `pickVisibleLines`: empty input, scroll past end, simple slice, wrap-
triggered early stop, one-line floor for hostile single lines, offset.
- `computeMaxScrollOffset`: empty input, viewport-larger-than-content,
simple packing, wrap-aware tail packing.
…hrome
Ink renders in normal-terminal mode, so each step transition leaves the
previous frame in the user's scrollback. When the AI prompt step's Select
fires and the wizard transitions to `ai-analysis-running` (or beyond), the
new frame previously rendered its own Header — producing the visible
artifact of two stacked "🚀 Capgo Cloud Build · Onboarding" boxes:
╔══════════════════════════╗ ← frozen above by the previous frame
║ Capgo Cloud Build ║
║ · Onboarding ║
╚══════════════════════════╝
AI debug · 92%
...
> Debug with AI ✔
Skip
╔══════════════════════════╗ ← duplicate, rendered by the new frame
║ Capgo Cloud Build ║
║ · Onboarding ║
╚══════════════════════════╝
AI debug · 95%
...
Analyzing build log with Capgo AI (Kimi K2.5)...
This was already partly addressed for `ai-analysis-result` and
`ai-analysis-result-scroll`. Extending the same suppression to
`ai-analysis-prompt` and `ai-analysis-running` keeps the entire AI
sub-flow Header-free, so the most recent Header in scrollback is the
one tied to the user's current AI step instead of a stale earlier one.
The frozen "Debug with AI ✔ / Skip" line is left visible — that's
normal Ink behavior in non-alternative-screen mode (the previous
frame's content stays in scrollback) and would require a fullscreen
mode switch to address, out of scope here.
Mirrored to both iOS (cli/src/build/onboarding/ui/app.tsx) and Android
(cli/src/build/onboarding/android/ui/app.tsx) wizards.
… steps) The previous fix went too far and hid the Header on every AI sub-flow step including the entry one (`ai-analysis-prompt`). That left the user seeing a chrome-less "Build failed." prompt with no wizard anchoring, which felt jarring — the screenshot showed an isolated `AI debug` phase label + progress bar over a bare error message and Select. Refining: keep the Header on `ai-analysis-prompt` (the entry into the AI sub-flow — first thing the user sees, needs wizard anchoring) and only suppress it on the steps that render AFTER the prompt's Select fires (`ai-analysis-running`, `ai-analysis-result`, `ai-analysis-result-scroll`). Those are the ones at risk of stacking a fresh Header below the frozen previous frame in the user's scrollback. Net result: - First entry to AI flow: Header + AI debug + progress + "Build failed" + Select — looks like a normal wizard step, no missing chrome. - User picks: Select freezes that frame into scrollback. - New frame (running): no Header — frozen Header from prompt remains the most recent one visible, no duplicate. - Same for result / result-scroll. Mirrored to both iOS and Android wizards.
…plicates The previous "hide Header on post-Select AI steps" fix removed the duplicate banner artifact, but at the cost of leaving the AI-running step Header-less — the wizard then looked decapitated as soon as the user picked "Debug with AI". The right tool for this is Ink's `<Static>`. Static items are written to the terminal exactly once, above the dynamic render area, and never re-rendered. Putting the Header inside `<Static>` gives us: - A single persistent "🚀 Capgo Cloud Build · Onboarding" banner that the user sees on every step (including the whole AI sub-flow). - Zero risk of a duplicate banner ever appearing — Ink does not re-emit static content on subsequent renders, so step transitions, terminal resizes, and the @inkjs/ui Select committing its selected line into scrollback all leave the banner untouched. The previous `showHeader` conditional and the `isPostPromptAiStep` helper are no longer needed; removed in both wizards. `showLog` and `showProgress` keep their existing per-step semantics — only the Header moved to Static. Module-level `STATIC_HEADER_ITEMS = ['header']` keeps the items array reference stable across renders so Static doesn't ever decide a new item appeared. Ink's Static items prop is typed as a mutable `string[]`, so the constant is declared accordingly (the array is never mutated in practice). Mirrored to both iOS (cli/src/build/onboarding/ui/app.tsx) and Android (cli/src/build/onboarding/android/ui/app.tsx) wizards.
This replaces the previous Ink-Static-Header approach (which made the banner permanent at the cost of losing it on `requesting-build` and the scrollable AI viewer) with terminal alt-screen mode for the whole wizard. In alt-screen mode the terminal uses a separate buffer where each Ink frame fully replaces the previous one. That removes the underlying cause of the duplicate-Header artifact (Ink committing each step's frame into scrollback) and unlocks the original `showHeader` conditional, so we can go back to: - Header VISIBLE on every interactive step including the entire AI sub-flow (`ai-analysis-prompt`, `ai-analysis-running`, `ai-analysis-result`) — fixes the "no banner on Analyzing build log" regression. - Header HIDDEN on `requesting-build` and `ai-analysis-result-scroll` — those steps get the full terminal height for build output and the scrollable AI viewer, as before the Static experiment. ### Mechanics `command.ts`: - `enterAltScreen()` writes `ESC[?1049h ESC[H` (enter alt buffer + cursor home) before `render()`. - `exitAltScreen()` writes `ESC[?1049l` after `waitUntilExit()` so the user's pre-wizard terminal content is restored on clean exit. - Process-level cleanup handlers (`exit` / `SIGINT` / `SIGTERM` / `uncaughtException`) also call `exitAltScreen()` so the user is never stranded in an alt buffer if something crashes. - After exit, prints a one-line summary `✔ Capgo onboarding complete for <appId> (<platform>).` so the user has a visible breadcrumb in the normal terminal flow that the wizard finished (the wizard's last frame is wiped by the buffer restore — same UX as vim/htop/less, which is the expected behavior for a TUI). ### iOS / Android wizards - Reverted commit 2d7ea70 (Ink Static for Header). - `showHeader = step !== 'requesting-build' && !isAiResultScroll` — the same conditional intent as before the duplicate-Header bug ever surfaced. - Removed the now-redundant `isPostPromptAiStep` helper. ### Trade-offs - The wizard's per-step output is no longer left in scrollback when the user exits — that's the expected TUI behavior and is documented by the completion summary line. - Power users who wanted to scroll back through wizard steps DURING the flow lose that ability (alt buffer doesn't keep scrollback). They can use the wizard's own back/retry affordances instead.
This reverts commit a661967.
The Static-Header approach kept the banner visible across every step (including the entire AI sub-flow), with no duplicate-banner artifact at step transitions and no need for alt-screen mode. The remaining gripe was the row cost on `requesting-build` and the scrollable AI viewer — the double-bordered box was 4-5 rows tall, eating real estate where vertical space matters. This change replaces the bordered box with a compact two-row banner: 🚀 Capgo Cloud Build · Onboarding ──────────────────────────────── A single bold line + a thin dim divider gives the same "I'm in the Capgo wizard" anchoring at one third of the row cost. Heavy steps now lose ~2 rows instead of ~4-5. No code changes outside `components.tsx` — `<Static>` and the call sites already render whatever the Header component returns.
Static was rejected by the user. Restoring conditional Header rendering: - `showHeader = step !== 'requesting-build' && !isAiResultScroll` - Header is shown on every interactive step including the full AI sub-flow (prompt → running → result), giving the user the "Capgo Cloud Build · Onboarding" anchoring they explicitly asked for on the AI running step. - Header is hidden on `requesting-build` and on the scrollable AI viewer so those steps get the full terminal height as before. The Header itself stays in its compact two-row form (bold line + thin divider) introduced earlier — when visible it only costs ~2 rows, and when hidden the user gets the full screen on heavy steps. Static is gone from both `ui/app.tsx` files: no more `<Static>` wrapper, no `STATIC_HEADER_ITEMS` constant, and the `Static` symbol is dropped from the `ink` import. Trade-off acknowledged in earlier reviews: if Ink's render diff hits a size-change edge case (e.g. terminal narrow enough that the prompt frame scrolls), the previous frame's content can persist in the user's scrollback. We're accepting that artifact rather than going back to Static or to alternative-screen mode, both of which the user has explicitly rejected.
The compact two-row variant was introduced solely to reduce the row cost when the Header was always visible (via Static). Now that Header is conditional again and hidden on heavy steps (requesting-build, scroll viewer), the original double-bordered banner is back — same visual identity as the rest of the wizard.
…mfortable binary Each step is now exactly two forms again: comfortable (boxed, spaced) shown when it fits, or dense (flush, fits the frame) when it doesn't. The in-between "spaced dense" tier — dense layout with blank-line gaps re-added based on the parent measuring leftover rows — was the source of the recurring "terminal too small (need 18)" on a 17-row terminal: enabling the gaps inflated the measured body, which fed back into the too-small / neededRows math. Removed: the `spaced` prop + gap logic in GoogleSignInStep, the parent's signInDenseSpaced state/effect, SIGN_IN_GAP_ROWS, and the now-dead extraSpacingFits helper + its tests. Wording is unchanged (shared constants). This restores a clean, building baseline. The durable fix for "never resize mid-onboarding" (decide density once against the tallest step + gate at startup, and make the frame-fit test measure the REAL frame incl. the progress bar) comes next on top of this. Suite 15/15, build + lint green.
…nal floor) find-min-onboarding-size.mjs renders every STATIC onboarding step (comfortable form, worst-case props) into the real frame through the VT engine and reports the minimal rows-per-width at which nothing clips — the floor to enforce at startup so onboarding never hits "resize" mid-flow. Writes a full per-step table to /tmp/onboarding-size-report.txt and prints a single-line floor per width. onboarding-fixtures.mjs holds the 35 static-step worst cases (longest ids / messages, most options) lifted from the existing frame-fit tests. DYNAMIC steps (AI prompt/result, build log, completed-steps log) are excluded — they scroll or cut, so they don't constrain the static floor. Verified: all 35 fixtures render with no errors (exit 0). Floor measures ~22 rows, width-independent — driven by a many-option Select step, not wrapping (needs the full report to confirm which; pending a clean read).
A prior commit added find-min-onboarding-size.mjs but its imported helpers were left untracked, so the harness couldn't run from a clean checkout. Commit the whole set together + the @xterm/headless devDependency (package.json + lock). The harness renders each onboarding step's REAL ANSI through @xterm/headless (the VT engine VS Code's terminal uses) and reads the actual grid — replacing the by-hand row counting that repeatedly miscounted. Pieces: • helpers/vt-grid.mjs — renderInkFrame + frameToGrid + analyzeFrame • helpers/onboarding-frame.mjs — builds the real frame (header+progress+step) • helpers/onboarding-fixtures.mjs — 40 static-step worst cases • helpers/size-search.mjs — minimal-(cols,rows) search • find-min-onboarding-size.mjs — runner: floor per width + per-step report KEY FINDING (real VT measurement, comfortable form, full frame): floor = 46 rows @ 80 cols, 53 @ 60 cols — driven by google-sign-in-learn-more (46), api-key-instructions (35), play-developer-id-actions (34). i.e. the comfortable forms are 25–46 rows; that is WHY the dense flag exists. Dropping dense to show comfortable as-is would force a ~46-row minimum. So the next step is to lean each tall form (strip Alert boxes + double-blank spacing, keep the words) and re-run the harness until the floor is a sane terminal size.
… rows Corrected understanding of the goal: dense becomes the ONLY form (drop the comfortable variant, keep the compact one that makes the most of the space), then enforce the min size that fits dense everywhere. Flip the fixtures to dense:true and re-measure through the real VT frame: floor = 21 rows @ 80 cols, 23 @ 70/60 — a sane, enforceable minimum (vs 46 for comfortable). Tallest dense steps: learn-more 21, api-key-instructions 18, play-developer-id-actions 18, google-sign-in 16. Next: make dense always-on, delete the measure-then-decide machinery (bodyRef/measureElement/shouldCollapseToDense + comfortable branches), and add a resize-reactive startup gate at the measured floor so onboarding never hits "resize" mid-flow.
…pping dense) Previous commit measured dense:true, but the goal is to DROP dense and ship the comfortable (fully-decorated) form. Measure that instead. Harness then reports the true floor for the form we'll actually render.
gcp-projects-select and ci-secrets-setup rendered 0 rows (wrong prop shapes:
gcp takes {options:[{label,value}], onChange}; ci-secrets takes
{advice:[{target:{provider,label,cli}, reason, message, commands}], onChoose}).
Fixed to the real signatures — both now render (ci-secrets-setup 24 rows,
gcp-projects-select 20), errors=0 across all 40 fixtures.
The true max floor is unchanged: 46 rows @ 80 cols (43@100, 45@90, 48@70,
53@60), driven by google-sign-in-learn-more. This is the size we'll enforce at
startup so onboarding (comfortable form, all decorations) never resizes mid-flow.
…ured by VT harness
The minimum is measured, not guessed: the VT size-search harness renders every
static onboarding step's full frame (boxed banner + cut-form completed-steps log
+ progress + comfortable step, worst-case content) through @xterm/headless (the
VT engine VS Code's terminal uses) and reports the tallest at 49 rows @ 80 cols
(google-sign-in-learn-more). Enforcing this at startup lets the steps drop their
adaptive dense fallback and always render the full form without ever resizing
mid-flow.
- src/build/onboarding/min-terminal-size.ts: MIN_COLS=80, MIN_ROWS=49,
terminalFitsOnboarding(). One source of truth for the (coming) startup gate.
- test/test-onboarding-min-size.mjs: renders every static step through the VT
grid at 80 cols and FAILS if any exceeds 49 rows, so the enforced number can
never silently drift from what the steps need. 40 passed, 0 failed.
- test/helpers/onboarding-frame.mjs: include the minimal cut-form completed-
steps log so the measured floor accounts for it (46 -> 49).
- package.json: wire test:onboarding-min-size.
Build green, frame-fit 15/15.
MinSizeGate wraps the wizard in the shell: below the measured floor (80x49) it renders a resize prompt instead of the wizard, naming whichever dimension is short; at/above the floor it renders the wizard unchanged. Resize-reactive (cols/rows from useTerminalSize), so the wizard mounts the moment the terminal reaches the floor — no restart. This is the guarantee the harness/floor work was building toward: past the gate every step fits, so the wizard never shows a too-small prompt mid-flow. That in turn makes the steps' adaptive dense fallback unreachable — its removal is the next (pure-simplification) step. test/test-min-size-gate.mjs (real VT grid): too-short -> prompt names rows + hides wizard; too-narrow -> names columns; exactly the floor and above -> renders the wizard; and the prompt itself fits a small terminal. 10 passed. Build green.
Per the goal — drop the dynamic banner, always show the boxed one. The startup size gate guarantees the rows for it, so degrading to the one-line header on short terminals is no longer needed (removing it cuts complexity). Header now ignores compact and always renders boxed; the prop is kept as accepted-but- ignored so call sites compile — the prop + the headerCompact computations are removed in the dense-cleanup pass. Build green, frame-fit 15/15.
The startup size gate guarantees a terminal large enough for the full forms, so the adaptive dense fallback is unreachable. Force dense = false: always comfortable, in-app tooSmall never fires (gate owns 'does it fit'), and the fragile measure→decide coupling that caused the resize flicker + false 'terminal too small' is gone. Step-component dense branches + measure machinery are now dead code, swept in a follow-up. Build green.
…xed) The always-boxed-banner change made the real Header 5 rows, breaking this test's 16/19/24-row composition assertions which assumed a 1-row compact header. The test's subject is the log-capping math (budgets use COMPACT_HEADER_ROWS=1), not the banner, so it now renders a fixed 1-row header stub. 12/12 green.
…omplete) 7a72426 changed only one of the two Header usages, leaving HeaderStub undefined and the import unused — the test was red (2/9). Define the 1-row HeaderStub (with 'Capgo Cloud Build' text so the gap tests' header findIndex works), use it in both frame() and headerPlusLog(), drop the unused Header import. 12/12 green; full frame-fit suite 15/15.
…hrase At 50 cols the 'continue automatically' sentence wraps, so no single grid line contained the phrase. Assert the real property instead: natural height <= rows (prompt not clipped) + the 'too small' warning and 'Resize this window' opener are visible. 9/9 green.
iOS mirror of the Android change. The startup size gate guarantees a terminal large enough for the full forms, so force dense = false: always comfortable, in-app tooSmall never fires (the gate owns 'does it fit'), and the fragile measure→decide coupling is gone. Step-component dense branches + measure machinery are now dead code, removed in a follow-up. Build green.
|
|
||
| test('UNCAPPED long log overflows — proving the cap is what prevents too-small', () => { | ||
| const rows = 19 | ||
| const uncapped = frameRows(frame(longLog, rows, 80, 6), 80) |
| import OnboardingApp from './app.js' | ||
| import { Header } from './components.js' | ||
| import { pickPlatformLayout } from './frame-fit.js' | ||
| import { MinSizeGate } from './min-size-gate.js' |
| import { CompletedStepsLog } from './completed-steps-log.js' | ||
| import { BOX_HEADER_ROWS, COMPACT_HEADER_ROWS, Divider, FullscreenAiViewer, FullscreenBuildOutput, Header, TerminalTooSmall, WIZARD_PADDING_ROWS } from './components.js' | ||
| import type { AiResultKind } from './components.js' | ||
| import { COMPACT_HEADER_TOTAL_ROWS, isFrameTooSmall, logBudgetRows, shouldCollapseToDense } from './frame-fit.js' |
…e runner Both VT-harness suites existed but only had standalone scripts; add test:min-size-gate and ensure both run in the aggregate `test` script so the minimum-size contract + the startup gate are checked in CI.
… the aggregate references Commit 4846909 added both to the aggregate `test` script but they had no standalone definitions, so `bun run test` failed with 'Script not found'. Add the two definitions after test:frame-fit. Both now resolve and pass (40/40 + 9/9).
…ays renders comfortable)
…nders comfortable)
…ys renders comfortable)
…nders comfortable)
…nders comfortable)
…s renders comfortable)
…t-onboarding-min-size)
…t-onboarding-min-size)
…oo-small; dense always false)
|



Summary
build initwizard renders build output through React state, but on failure the underlyingrequestBuildInternalcalls@clack/prompts(confirm/select/spinner) to offer AI analysis. Clack and Ink fight for the terminal, so the AI offer either corrupts the TUI or hangs — users in onboarding never get a usable AI debug option.aiAnalysisMode: 'auto-prompt' | 'caller-handled' | 'skip'toBuildRequestOptionsso callers like the onboarding wizard can suppress the clack flow and run the AI step themselves with Ink-native components. Default'auto-prompt'keeps the direct-CLI matrix (and CI/CD detection) untouched.cli/src/build/onboarding/ui/app.tsx) and Android (cli/src/build/onboarding/android/ui/app.tsx) wizards gain three new Ink steps —ai-analysis-prompt(Select: Debug with AI / Skip),ai-analysis-running(SpinnerLine),ai-analysis-result(renders the markdown analysis + AI safety warning, then exits).AiAnalysisTriggeredBywith'onboarding'(additive) so the existing dashboard can segment onboarding-driven AI usage. ReusestrackBuilderOnboardingStep,trackAiAnalysisChoice,trackAiAnalysisResultunchanged.baseUrlinto a hard error, breakingbun run buildon main. Adding"ignoreDeprecations": "6.0"per TS's own recommendation unblocks the build. Unrelated to the feature but bundled here so the binary is buildable on the branch.Design plan lives in the wiki at
projects/capgo/plans/2026-05-22-onboarding-ai-debug.md(intentionally not committed to the repo).CI/CD safety
No new detection layer. Two existing constraints handle CI by construction:
aiAnalysisMode'auto-prompt'(default)show_menu/ask_then_menu) — unchanged'auto-prompt'(default)decideAnalyzeBehavior→skiporauto_upload— unchanged'caller-handled'result.aiAnalysis.ready=true→ Ink prompts user'caller-handled'aiAnalysisundefined → wizard goes straight tobuild-completeshouldCaptureLogs()is already TTY-gated, and Ink itself requires a TTY to render anything.Test plan
cli/test/test-ai-onboarding-mode.mjs— 9 tests, all greendecideAnalyzeBehaviormatrix unchanged (regression guard for direct CLI)runCapgoAiAnalysishappy path posts to/build/ai_analyzewith correct shaperunCapgoAiAnalysisreturnstoo_bigover 10 MB without calling backendrunCapgoAiAnalysisreturnserrorwhen the captured log is missingreleaseCapturedLogsdeletes the log and is best-effort on missing filestest-ai-analyze-flow,test-ai-log-capture,test-ai-render-markdown,test-onboarding-progress,test-onboarding-recoveryall passbun run build(CLI workspace) produces adist/index.jsbinarybuild init→ verify Ink prompt renders cleanly, AI flow runs without TUI corruption, analysis displays readablybuild requeston a failing build → confirm clack-based menu still appears (no regression)npx @capgo/cli build request --platform ios … | cat→ no prompts, no Ink, no crashOut of scope (intentional)
BuildTable.vue(the dashboard doesn't have full logs; would require a server-side log fetch endpoint).build ai-debug <jobId>re-run subcommand.🤖 Generated with Claude Code