feat(setup): Claude-assisted error recovery with resume-at-step retry
When a setup step fails — whether hard via fail() or soft via the
"What's left" / "Skipping the first chat" notes — offer to ask Claude
to diagnose. On consent, spawn `claude -p --output-format stream-json`
with a scrolling 3-line action window ("Reading x", "Running y") so
the 1–4 minute investigations feel active rather than hung. No hard
timeout: debugging can take time, Ctrl-C is the escape hatch.
The prompt is minimal: one-paragraph framing, failed step name + msg +
hint, and a list of file references (not contents). Claude's Read/Grep
tools fetch what they need. A per-step map in claude-assist.ts gives
the most relevant files per step; the rest is README + auto.ts +
logs/setup.log + the per-step raw log.
Claude responds with REASON + COMMAND lines. We show the reason in a
clack note, prefill the command via setup/run-suggested.sh (bash 4+
readline, 3.x fallback to Enter-to-run), and eval on the user's
confirm.
When the user runs a fix, fail() now offers to retry the failing step
rather than aborting. setup/logs.ts tracks successfully-completed step
names in-memory; fail() threads those as NANOCLAW_SKIP on a spawnSync
retry, so the child picks up exactly where the parent left off — no
rebuilding containers or reinstalling OneCLI.
Other polish in this change:
- fitToWidth + dimWrap in lib/theme.ts to prevent long spinner labels
from soft-wrapping (each terminal row stacks a stale copy otherwise).
- Shorter container step label ("Preparing your assistant's sandbox…")
so it fits on narrow terminals.
- Wordmark anchored in the clack intro line on every run.
- All 25 existing fail() call sites updated to await fail(...) since
fail is now async.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -77,6 +77,28 @@ function visibleLength(s: string): number {
|
||||
return s.replace(ANSI_RE, '').length;
|
||||
}
|
||||
|
||||
/**
|
||||
* Truncate a label so the final line — base + reserved suffix — fits in
|
||||
* the terminal width. Use on spinner labels that get an elapsed counter
|
||||
* appended: if the total exceeds terminal width, clack's cursor-up
|
||||
* redraw math breaks and each tick stacks a copy of the line instead
|
||||
* of replacing it.
|
||||
*
|
||||
* `suffix` is the reserved space for what we'll append after `fit()`
|
||||
* returns (e.g. ` (999s)` or a tool-use breadcrumb). We don't include
|
||||
* it in the output — caller appends it.
|
||||
*/
|
||||
export function fitToWidth(base: string, suffix: string): string {
|
||||
const cols = process.stdout.columns ?? 80;
|
||||
// Overhead we reserve before sizing the label:
|
||||
// spinner icon (1) + 2 padding spaces = 3
|
||||
// clack's animated ellipsis after the label = up to 3 (". " -> "...")
|
||||
// 1-char safety margin so wide-char glyphs don't tip over the edge
|
||||
// Total reserved budget = 7 cols plus the caller's suffix.
|
||||
const budget = Math.max(20, cols - 7 - visibleLength(suffix));
|
||||
return base.length > budget ? base.slice(0, budget - 1) + '…' : base;
|
||||
}
|
||||
|
||||
function wrapLine(line: string, width: number): string {
|
||||
if (visibleLength(line) <= width) return line;
|
||||
const words = line.split(' ');
|
||||
|
||||
Reference in New Issue
Block a user