fix(ai): sync auto-commit with streamed result to surface commit SHA to UI (Fix 10)

This commit is contained in:
2026-05-16 12:26:54 -07:00
parent 89c9b01669
commit ca8a915fe2

View File

@@ -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"