DEV Community

nasir shahbaz
nasir shahbaz

Posted on

I Built a Native Windows App That Writes Your Git Commit Messages With AI Here's How

AI Commit Generator: A Native Windows Desktop App That Writes Your Git Commits With AI

You know that moment you've spent two hours debugging, the fix is finally in, and now you have to write a commit message. Ugh.

I built AI Commit Generator to solve exactly that. It's a native Windows desktop app that:

  1. Grabs your git diff (staged or unstaged)
  2. Sends it to an AI model (via OpenRouter or Google AI Studio)
  3. Returns a clean, conventional commit message
  4. Lets you copy it, edit it, regenerate it, or just commit and push right there

All in a 550×600 pixel window that lives in your system tray.

Here's a breakdown of the architecture and the interesting technical decisions along the way.


The Tech Stack: C++ + WebView2

The app is written in C++17 using Win32 APIs and Microsoft Edge WebView2 for the UI. Yes, really C++ and web technologies coexisting in the same binary.

The core idea:

  • C++ handles the heavy lifting: spawning git processes, managing the system tray, reading/writing config, HTTP requests to GitHub for update checks.
  • WebView2 handles the UI: the entire interface is HTML/CSS/JS rendered inside an embedded Edge WebView2 control.

This isn't a framework — it's raw Win32 CreateWindowExW, a message loop, and ICoreWebView2Controller. No Electron, no Tauri, no extra runtime dependencies beyond the Edge WebView2 runtime that ships with Windows 10/11.

Why WebView2 and not a full web framework?

I wanted a small, native-feeling Windows app. Electron bundles Chromium (~150MB+). WebView2 uses the Edge runtime already installed on the user's machine. The result is a tiny installer (~2MB compressed).


The Bridge Pattern: C++ ↔ JavaScript IPC

The most interesting architectural piece is the Bridge a C++ class that handles bidirectional communication between the WebView2 frontend and the native backend.

Here's how it works:

JS → C++ (frontend calls native code)

The JavaScript frontend calls window.chrome.webview.postMessage() with a JSON-RPC-style message:

{
  "id": "1",
  "method": "getGitDiff",
  "params": { "repoPath": "C:\\projects\\my-app", "staged": false }
}
Enter fullscreen mode Exit fullscreen mode

The C++ Bridge class receives this via ICoreWebView2WebMessageReceivedEventHandler, parses the method name, dispatches to the appropriate handler, and sends a response back:

{
  "id": "1",
  "result": "\"diff --git a/file.js b/file.js\\n...\"",
  "error": null
}
Enter fullscreen mode Exit fullscreen mode

Available RPC Methods

The bridge exposes these native operations to the frontend:

Method What it does
getConfig Read settings from %APPDATA%\AiCommitGen\settings.json
setConfig Write a config key-value pair
selectDirectory Open a native Win32 folder picker dialog
getGitDiff Run git diff or git diff --cached
getGitLog Run git log --oneline -10
commitAndPush Run git add -A, git commit -m "...", then git push with retry
checkForUpdates Hit the GitHub Releases API for the latest version
downloadUpdate Download the installer exe and launch it

This RPC pattern means the JavaScript never directly executes shell commands — everything goes through the bridge, which validates inputs and handles errors cleanly.


The AI Integration: Two Providers, One Prompt

The app supports two AI providers:

OpenRouter

Uses the standard OpenAI-compatible chat completions API. Supports models like GPT-4o, Claude 3.5 Haiku, Gemini Flash, and Llama 3.3.

const res = await fetch('https://openrouter.ai/api/v1/chat/completions', {
  method: 'POST',
  headers: {
    'Authorization': 'Bearer ' + apiKey,
    'Content-Type': 'application/json',
    'HTTP-Referer': 'https://github.com/ai-commit-generator',
  },
  body: JSON.stringify({
    model: model,
    messages: [
      { role: 'system', content: systemPrompt },
      { role: 'user', content: 'Recent commits:\n...\n\nGit diff:\n...' },
    ],
    temperature: 0.3,
    max_tokens: 300,
  }),
});
Enter fullscreen mode Exit fullscreen mode

Google AI Studio (Gemini)

Uses the Gemini REST API directly, with special handling for thinking models:

const config = { temperature: 0.3, maxOutputTokens: 1024 };
if (model.indexOf('gemini-2.5-flash') === 0) {
  config.thinkingConfig = { thinkingBudget: 0 };
} else if (model.indexOf('gemini-3') === 0) {
  config.thinkingConfig = { thinkingLevel: 'MINIMAL' };
}
Enter fullscreen mode Exit fullscreen mode

I learned the hard way that Gemini 2.5 Flash with thinking enabled will eat your entire token budget on chain-of-thought before producing the actual commit message. Disabling thinking (thinkingBudget: 0) for commit messages is a must.

The System Prompt

The prompt that actually generates the commit message follows conventional commits format:

You are an expert at writing clear, concise git commit messages.
Given a git diff and recent commit history, generate a fitting commit message.

Rules:

  • Use conventional commits format: type(scope): description
  • Types: feat, fix, chore, docs, style, refactor, perf, test, ci, build, revert
  • Keep the subject line under 72 characters
  • Focus on WHAT and WHY, not HOW
  • Match the style of recent commits shown in the log

The key insight: sending the last 10 commits as context helps the AI match the existing commit style of the project.


Smart Git Operations

The commitAndPush handler does more than just run commands — it has real-world resilience built in:

// Push with retry for transient server errors
const int maxRetries = 3;
for (int i = 0; i < maxRetries; i++) {
    auto pushRes = RunCmd(L"git push", repoPath);
    if (pushRes.exitCode == 0) break;

    // Retry on 500/502/503 errors
    if (pushRes.output.find(L"500") != std::wstring::npos ||
        pushRes.output.find(L"502") != std::wstring::npos ||
        pushRes.output.find(L"503") != std::wstring::npos) {
        Sleep(1000 * (1 << i));  // Exponential backoff
        continue;
    }
    break;
}
Enter fullscreen mode Exit fullscreen mode

It also detects "nothing to commit" gracefully, handles "Everything up-to-date", and distinguishes between commit failures and push failures so you know exactly what went wrong.


The Window Plumbing

Setting up a Win32 window with WebView2 is surprisingly involved. Here's the rough lifecycle:

int WINAPI WinMain(HINSTANCE hInstance, ...) {
    // 1. Register window class with dark background
    wc.hbrBackground = CreateSolidBrush(RGB(13, 17, 23));
    RegisterClassExW(&wc);

    // 2. Create fixed-size window (no resize)
    g_hWnd = CreateWindowExW(0, L"AiCommitGen", ...,
        WS_OVERLAPPEDWINDOW & ~WS_MAXIMIZEBOX & ~WS_THICKFRAME, ...);

    // 3. Initialize system tray icon
    InitTrayIcon();

    // 4. Create WebView2 with custom user data directory
    CreateCoreWebView2EnvironmentWithOptions(
        nullptr, userDataDir.c_str(), envOptions.Get(), ...);

    // 5. Use virtual host mapping to serve local files
    webview3->SetVirtualHostNameToFolderMapping(
        L"app.commitgen", (exeDir + L"\\web").c_str(),
        COREWEBVIEW2_HOST_RESOURCE_ACCESS_KIND_DENY_CORS);
    g_webview->Navigate(L"https://app.commitgen/index.html");

    // 6. Message loop
    MSG msg;
    while (GetMessageW(&msg, nullptr, 0, 0)) {
        TranslateMessage(&msg);
        DispatchMessageW(&msg);
    }
}
Enter fullscreen mode Exit fullscreen mode

One subtle detail: virtual host mapping (SetVirtualHostNameToFolderMapping) is used instead of navigating to file:// directly. This avoids the file:// origin restrictions in WebView2 and gives the frontend a proper https:// origin for CORS and security features.


System Tray: The Unsung UX

The close button doesn't actually close the app — it hides to the system tray. This is intentional: commit messages are most useful when the app is always one double-click away.

case WM_CLOSE:
    if (!g_isQuitting) {
        g_webviewController->put_IsVisible(FALSE);
        ShowWindow(hWnd, SW_HIDE);
        return 0;  // Don't destroy — just hide
    }
    break;
Enter fullscreen mode Exit fullscreen mode

The tray icon supports:

  • Double-click to restore
  • Right-click menu with Open and Exit options
  • "Start hidden in tray" option for power users who want it always available but never visible

Auto-Update System

The app checks GitHub Releases on startup. The update flow:

  1. C++ sends a WinHTTP request to github.com/NSTechBytes/ai-commit-generator/releases/latest
  2. Parses the redirect to extract the tag name
  3. Compares versions in JavaScript using a semver comparison
  4. If a newer version exists, downloads the installer exe and launches it

The version is embedded in the Windows executable's VS_FIXEDFILEINFO resource and read at runtime:

static std::wstring GetApplicationVersion() {
    // ... reads VS_FIXEDFILEINFO from the running .exe
    return L"1.1.0";
}
Enter fullscreen mode Exit fullscreen mode

Project Structure

AiCommitGen.sln              # Visual Studio solution
AiCommitGen/
  main.cpp                   # Win32 entry point + WebView2 init
  bridge.h / bridge.cpp      # C++ ↔ JS RPC bridge
  web/
    index.html               # UI (single-page, all CSS inline)
    index.js                 # Frontend logic (~400 lines)
    assets/                  # Icons, images
installer/
  setup.nsi                  # NSIS installer script
  setup.ps1                  # Release build + installer pipeline
Build.ps1                    # Debug/Release build script
Enter fullscreen mode Exit fullscreen mode

Getting Started

Prerequisites:

  • Windows 10/11
  • Visual Studio 2022 (Desktop development with C++)
  • WebView2 Runtime (comes with Windows)
  • NSIS (for building the installer)

Build:

# Debug build
./Build.ps1

# Release + Installer
./setup.ps1
Enter fullscreen mode Exit fullscreen mode

Configuration: API keys and settings are stored in %APPDATA%\AiCommitGen\settings.json.


What I Learned

  1. WebView2 is surprisingly capable. It's basically a modern IE/Edge control that renders Chromium content. The virtual host mapping feature alone makes it viable for local-file apps.

  2. JSON parsing in C++ without libraries is tedious. I wrote a custom JsonGetString / ParseFlatStringObject implementation. It works for flat config files, but I wouldn't recommend it for nested JSON.

  3. Git commands through CreateProcessW need careful pipe management. Redirecting stdout/stderr through Win32 pipes, then reading from the read end until EOF, is a pattern that looks simple but has subtle gotchas around handle inheritance and buffer sizes.

  4. Conventional commits + recent commit history = better AI output. Sending the last 10 commits as context makes the AI match the project's actual style, not just a generic format.


Try It Out

The app is open source on GitHub: NSTechBytes/ai-commit-generator

Install the latest release, paste your OpenRouter or Google AI Studio API key, select a repo, and hit Generate. It's the fastest way I've found to go from "I have no idea what to call this commit" to a clean push.


Built with C++17, Win32, WebView2, and a healthy impatience for writing commit messages.

Top comments (0)