Pre-launch — Gluecron is in final validation. Public signups and git hosting for non-owner users open after launch review.
CodeIssuesDiscussionsWikiPull RequestsProjectsCommitsActionsReleasesContributorsPulse● GatesSecuritySettingsDeploymentsPipelineInsightsAgents✨ Explain✨ Ask AI✨ Workspace✨ Spec✨ Tests▓ Debt Map✨ NL Search🏛 Archaeology
Blame · Line-by-line history

ai-commit.ts

Each line is annotated with the commit that last touched it. Click any SHA to jump to that commit and see the surrounding change.

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