CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 | /**
* Generate an AI commit message for the currently-staged diff and drop
* it into the Source Control input box.
*
* Strategy:
* 1. Read the staged diff via `git diff --cached` in the workspace.
* 2. POST it to `${host}/api/v2/ai/commit-message` with the user's PAT.
* 3. Format `{ subject, body }` into a single string and assign it to
* the active git SCM repository's `inputBox.value`.
*
* We talk to the API directly rather than shelling out to the `gluecron`
* CLI — the CLI may not be installed in the user's $PATH, and the API
* surface is the same.
*/
import * as vscode from "vscode";
import { execFile } from "node:child_process";
import { promisify } from "node:util";
import { getToken, signInFlow } from "../auth";
import { getHost, pickWorkspaceFolder } from "../repo";
const execFileP = promisify(execFile);
/**
* VS Code's built-in git extension exposes a typed API; we declare just
* the bits we touch so the extension compiles without bundling
* `@types/vscode.git`.
*/
interface GitInputBox {
value: string;
}
interface GitRepository {
rootUri: vscode.Uri;
inputBox: GitInputBox;
}
interface GitApi {
repositories: GitRepository[];
getRepository?(uri: vscode.Uri): GitRepository | null;
}
interface GitExtensionExports {
getAPI(version: 1): GitApi;
}
async function getStagedDiff(cwd: string): Promise<string> {
const { stdout } = await execFileP("git", ["diff", "--cached"], {
cwd,
maxBuffer: 16 * 1024 * 1024, // 16 MiB — generous; API caps server-side too.
});
return stdout;
}
function pickRepository(cwd: string): GitRepository | null {
const git = vscode.extensions.getExtension<GitExtensionExports>("vscode.git");
if (!git) return null;
const api = git.isActive ? git.exports.getAPI(1) : null;
if (!api) return null;
if (api.getRepository) {
const found = api.getRepository(vscode.Uri.file(cwd));
if (found) return found;
}
return api.repositories[0] || null;
}
export async function runAiCommitMessage(
context: vscode.ExtensionContext
): Promise<void> {
const folder = pickWorkspaceFolder();
if (!folder) {
vscode.window.showWarningMessage("Gluecron: no workspace open.");
return;
}
let token = await getToken(context);
if (!token) {
const ok = await signInFlow(context, getHost());
if (!ok) return;
token = await getToken(context);
}
if (!token) return;
let diff: string;
try {
diff = await getStagedDiff(folder.uri.fsPath);
} catch (err) {
vscode.window.showErrorMessage(
`Gluecron: failed to read staged diff (${(err as Error).message}).`
);
return;
}
if (!diff.trim()) {
vscode.window.showInformationMessage(
"Gluecron: nothing staged. Stage changes first (git add)."
);
return;
}
const message = await vscode.window.withProgress(
{
location: vscode.ProgressLocation.SourceControl,
title: "Gluecron: drafting commit message...",
cancellable: false,
},
async () => {
const url = `${getHost().replace(/\/+$/, "")}/api/v2/ai/commit-message`;
const res = await fetch(url, {
method: "POST",
headers: {
"content-type": "application/json",
accept: "application/json",
authorization: `Bearer ${token}`,
},
body: JSON.stringify({ diff, style: "conventional" }),
});
const text = await res.text();
if (!res.ok) {
throw new Error(`server returned ${res.status}: ${text.slice(0, 200)}`);
}
let json: { subject?: string; body?: string };
try {
json = JSON.parse(text);
} catch {
throw new Error("server returned non-JSON");
}
const subject = (json.subject || "").trim();
const body = (json.body || "").trim();
if (!subject) throw new Error("server returned an empty subject");
return body ? `${subject}\n\n${body}` : subject;
}
).then(
(v) => v,
(err: Error) => {
vscode.window.showErrorMessage(`Gluecron: ${err.message}`);
return null;
}
);
if (!message) return;
const repo = pickRepository(folder.uri.fsPath);
if (repo) {
repo.inputBox.value = message;
vscode.window.showInformationMessage(
"Gluecron: commit message dropped into Source Control."
);
} else {
// Git extension isn't ready — fall back to copying the message.
await vscode.env.clipboard.writeText(message);
vscode.window.showInformationMessage(
"Gluecron: copied commit message to clipboard (git SCM not ready)."
);
}
}
|