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.
| 63fe719 | 1 | /** |
| 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 | ||
| 16 | import * as vscode from "vscode"; | |
| 17 | import { execFile } from "node:child_process"; | |
| 18 | import { promisify } from "node:util"; | |
| 19 | import { getToken, signInFlow } from "../auth"; | |
| 20 | import { getHost, pickWorkspaceFolder } from "../repo"; | |
| 21 | ||
| 22 | const 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 | */ | |
| 29 | interface GitInputBox { | |
| 30 | value: string; | |
| 31 | } | |
| 32 | interface GitRepository { | |
| 33 | rootUri: vscode.Uri; | |
| 34 | inputBox: GitInputBox; | |
| 35 | } | |
| 36 | interface GitApi { | |
| 37 | repositories: GitRepository[]; | |
| 38 | getRepository?(uri: vscode.Uri): GitRepository | null; | |
| 39 | } | |
| 40 | interface GitExtensionExports { | |
| 41 | getAPI(version: 1): GitApi; | |
| 42 | } | |
| 43 | ||
| 44 | async 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 | ||
| 52 | function 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 | ||
| 64 | export 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 | } |