diff --git a/package.json b/package.json index 2289be8..2d7300e 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "kcode", - "version": "2.10.64", + "version": "2.10.65", "description": "AI-powered coding assistant for the terminal - by Astrolexis", "author": "Astrolexis", "module": "src/index.ts", diff --git a/src/core/conversation.ts b/src/core/conversation.ts index 2866139..bd893a5 100644 --- a/src/core/conversation.ts +++ b/src/core/conversation.ts @@ -1551,6 +1551,39 @@ export class ConversationManager { for (const msg of postTurnResult.injectMessages) this.state.messages.push(msg); if (postTurnResult.action === "break") { + // Phase 22: the model has finished its final response and the + // agent loop is about to exit. This is the correct firing + // point for auto-launch — the previous placement at the + // bottom of the while iteration was dead code because + // handlePostTurn breaks out of the loop BEFORE reaching it. + // The hook itself is guarded by runtime-intent + write-in-turn + // checks so it's safe to call on every normal break. + if (stopReason === "end_turn") { + try { + const { maybeAutoLaunchDevServer } = await import( + "./auto-launch-dev-server.js" + ); + const { getUserTexts } = await import("./session-tracker.js"); + const launchResult = await maybeAutoLaunchDevServer( + this.config.workingDirectory, + this.state.messages, + getUserTexts(), + ); + if (launchResult) { + this.state.messages.push({ + role: "assistant", + content: launchResult.notice, + }); + yield { type: "text_delta", text: launchResult.notice }; + log.info( + "auto-launch", + `phase 22 fired: ${launchResult.url ?? "no url"}`, + ); + } + } catch (err) { + log.debug("auto-launch", `hook failed (non-fatal): ${err}`); + } + } this.abortController = null; break; } @@ -1897,37 +1930,12 @@ export class ConversationManager { guardState.consecutiveDenials = 0; } - // Phase 22: if the model just finished its final response AND the - // user's original prompt had runtime intent AND a Write just - // completed, proactively launch the dev server and tell the user - // how to stop it. Fires only on "end_turn" — never during - // tool_use continuation, so we don't launch mid-reasoning. - if (stopReason === "end_turn") { - try { - const { maybeAutoLaunchDevServer } = await import( - "./auto-launch-dev-server.js" - ); - const { getUserTexts } = await import("./session-tracker.js"); - const launchResult = await maybeAutoLaunchDevServer( - this.config.workingDirectory, - this.state.messages, - getUserTexts(), - ); - if (launchResult) { - this.state.messages.push({ - role: "assistant", - content: launchResult.notice, - }); - // Must be text_delta — StreamEvent has no "text" variant. - // The UI renders text_delta events via print-mode and - // stream-handler. The bug from the initial phase 22 ship - // was yielding type: "text", which silently dropped. - yield { type: "text_delta", text: launchResult.notice }; - } - } catch (err) { - log.debug("auto-launch", `hook failed (non-fatal): ${err}`); - } - } + // Phase 22 moved: the correct firing point is inside the + // handlePostTurn break branch earlier in the loop. Placing it + // here was dead code — handlePostTurn's `action: "break"` path + // exits the loop BEFORE reaching this line for every normal + // end_turn, so the hook never ran in production. See Bug #8 + // in the v2.10.64 audit. yield { type: "turn_end", stopReason }; // Loop continues for next agent turn