diff --git a/setup/auto.ts b/setup/auto.ts index 369f05a08..4becf6ec6 100644 --- a/setup/auto.ts +++ b/setup/auto.ts @@ -246,10 +246,33 @@ async function main(): Promise { ); } if (!skip.has('first-chat')) { + p.log.message( + dimWrap( + "Your assistant runs in an isolated sandbox. I'm going to send it a quick test message (ping) and wait for a reply (pong) to confirm it's responding. First startup typically takes 30–60 seconds while the sandbox warms up.", + 4, + ), + ); const ping = await confirmAssistantResponds(); if (ping === 'ok') { phEmit('first_chat_ready'); - await runFirstChat(); + const next = ensureAnswer( + await p.select({ + message: 'What next?', + options: [ + { + value: 'continue', + label: 'Continue with setup', + hint: 'recommended', + }, + { + value: 'chat', + label: 'Pause here and chat with your agent from the terminal', + }, + ], + }), + ) as 'continue' | 'chat'; + setupLog.userInput('first_chat_choice', next); + if (next === 'chat') await runFirstChat(); } else { phEmit('first_chat_failed', { reason: ping }); renderPingFailureNote(ping); diff --git a/setup/lib/claude-assist.ts b/setup/lib/claude-assist.ts index 551d938bb..c2b03677a 100644 --- a/setup/lib/claude-assist.ts +++ b/setup/lib/claude-assist.ts @@ -115,7 +115,7 @@ export async function offerClaudeAssist( const run = ensureAnswer( await p.confirm({ message: 'Run this command? (you can edit it before executing)', - initialValue: false, + initialValue: true, }), ); if (!run) return false; @@ -279,18 +279,24 @@ async function queryClaudeUnderSpinner( // No hard timeout — debugging can take a long time, and the cost of // cutting Claude off mid-investigation is worse than letting the // spinner run. The user can Ctrl-C if they want to abort. - const child = spawn( - 'claude', - [ - '-p', - '--output-format', - 'stream-json', - '--verbose', - '--permission-mode', - 'bypassPermissions', - ], - { cwd: projectRoot, stdio: ['pipe', 'pipe', 'pipe'] }, - ); + // + // Resume the same session on repeat invocations so Claude carries + // context across failures in one setup run. + const claudeArgs = [ + '-p', + '--output-format', + 'stream-json', + '--verbose', + '--permission-mode', + 'bypassPermissions', + ]; + if (claudeSessionId) { + claudeArgs.push('--resume', claudeSessionId); + } + const child = spawn('claude', claudeArgs, { + cwd: projectRoot, + stdio: ['pipe', 'pipe', 'pipe'], + }); child.stdout.on('data', (c: Buffer) => { lineBuf += c.toString('utf-8'); @@ -301,6 +307,16 @@ async function queryClaudeUnderSpinner( if (!line.trim()) continue; try { const event = JSON.parse(line) as StreamEvent; + // Capture the session id on the very first claude invocation of + // this process so later calls can --resume it. + if ( + !claudeSessionId && + event.type === 'system' && + event.subtype === 'init' && + typeof event.session_id === 'string' + ) { + claudeSessionId = event.session_id; + } handleStreamEvent(event, { setAction: (a) => { actions.push(a); @@ -335,10 +351,14 @@ async function queryClaudeUnderSpinner( } // Minimal shape of the stream-json events we care about. Claude emits -// many more, but we only read tool_use blocks (for breadcrumbs) and text -// blocks (to reassemble the final REASON/COMMAND answer). +// many more, but we only read tool_use blocks (for breadcrumbs), text +// blocks (to reassemble the final REASON/COMMAND answer), and the +// session_id on the init event so follow-up invocations can resume the +// same conversation. interface StreamEvent { type: string; + subtype?: string; + session_id?: string; message?: { content?: Array< | { type: 'text'; text: string } @@ -347,6 +367,11 @@ interface StreamEvent { }; } +// The session id from the first claude-assist invocation in this process. +// Subsequent invocations pass `--resume ` so Claude sees prior failures +// as conversation history instead of treating each failure in isolation. +let claudeSessionId: string | null = null; + function handleStreamEvent( event: StreamEvent, cb: { setAction: (a: string) => void; appendText: (t: string) => void },