diff --git a/vibn-frontend/app/api/chat/route.ts b/vibn-frontend/app/api/chat/route.ts index 4398b63b..49fcd545 100644 --- a/vibn-frontend/app/api/chat/route.ts +++ b/vibn-frontend/app/api/chat/route.ts @@ -872,55 +872,70 @@ export async function POST(request: Request) { // view — without it, every fs.write / shell.exec mutation // stays trapped in the dev container's volume. // - // Run AFTER the assistant message is persisted because the - // user already saw the reply; a slow push shouldn't block - // the chat. If there's nothing to commit, the helper short- - // circuits with reason='clean' in <1s. + // Run BEFORE the final done event so we can surface the commit + // result in the UI (Fix 10). if ( activeProject?.id && activeProject?.slug && typeof activeProject?.giteaCloneUrl === "string" ) { - (async () => { - try { - // Best-effort clone in case the pre-loop kick-off was - // racing with container provisioning and never landed. - await ensureProjectRepoCloned({ - projectId: activeProject.id, - projectSlug: activeProject.slug, - giteaCloneUrl: activeProject.giteaCloneUrl, - }).catch(() => null); - // Commit message: prefer the assistant's own first - // sentence (one line, ≤200 chars). Falls back to a - // generic checkpoint when the assistant only made - // tool calls without prose. - const firstSentence = (assistantText || "") - .split(/(?<=[.!?])\s+/)[0] - ?.trim() - ?.slice(0, 180); - const message = firstSentence || "AI checkpoint"; - const result = await commitAndPushIfDirty({ - projectId: activeProject.id, - projectSlug: activeProject.slug, - message, - }); - if (result.committed) { - console.log( - `[chat] auto-commit project=${activeProject.slug} sha=${result.sha} pushed=${result.pushed}`, - ); - } else if ( - result.reason && - result.reason !== "clean" && - result.reason !== "no_repo" - ) { - console.warn( - `[chat] auto-commit failed project=${activeProject.slug} reason=${result.reason}`, - ); - } - } catch (err) { - console.warn("[chat] auto-commit fire-and-forget failed", err); + try { + // Best-effort clone in case the pre-loop kick-off was + // racing with container provisioning and never landed. + await ensureProjectRepoCloned({ + projectId: activeProject.id, + projectSlug: activeProject.slug, + giteaCloneUrl: activeProject.giteaCloneUrl, + }).catch(() => null); + // Commit message: prefer the assistant's own first + // sentence (one line, ≤200 chars). Falls back to a + // generic checkpoint when the assistant only made + // tool calls without prose. + const firstSentence = (assistantText || "") + .split(/(?<=[.!?])\s+/)[0] + ?.trim() + ?.slice(0, 180); + const commitMessage = firstSentence || "AI checkpoint"; + + const commitPromise = commitAndPushIfDirty({ + projectId: activeProject.id, + projectSlug: activeProject.slug, + message: commitMessage, + }); + const timeoutPromise = new Promise<{ + committed: false; + reason: string; + }>((resolve) => + setTimeout( + () => resolve({ committed: false, reason: "timeout" }), + 8000, + ), + ); + + const result = (await Promise.race([ + commitPromise, + timeoutPromise, + ])) as any; + + if (result.committed) { + emit({ type: "commit", sha: result.sha, pushed: result.pushed }); + console.log( + `[chat] auto-commit project=${activeProject.slug} sha=${result.sha} pushed=${result.pushed}`, + ); + } else if ( + result.reason && + result.reason !== "clean" && + result.reason !== "no_repo" + ) { + emit({ type: "commit_failed", reason: result.reason }); + console.warn( + `[chat] auto-commit failed project=${activeProject.slug} reason=${result.reason}`, + ); } - })(); + } catch (err) { + emit({ type: "commit_failed", reason: String(err) }); + console.warn("[chat] auto-commit failed", err); + } } // Fire-and-forget: ask Gemini for a 1-2 sentence "what got done"