Skip to content

AI Commit Message Generation

dazeb edited this page Sep 17, 2026 · 2 revisions

AI Commit Message Generation

AI commit message generation is implemented in an Electron-free core module, src/core/commit-message.ts. The renderer asks for a message from the source-control panel; the core builds a conventional-commit prompt around the staged diff, detects an available local agent CLI, runs it read-only, and parses a single subject line from its output.

A key implementation note: in the captured source, generation is not routed through the chat model-provider adapters. It is a BYO local agent CLI flow: prefer claude, then fall back to codex, otherwise fail with an installation hint. The same core is reusable by the Server Edition because it has no Electron dependency.

Responsibilities and files

File Responsibility
src/core/commit-message.ts Prompt construction, diff bounding, CLI detection, CLI execution, output parsing, orchestration, result shaping.
src/core/commit-message.test.ts TDD coverage for prompt content, truncation, parser cleanup, and CLI preference order.
src/shared/ipc.ts Defines the shared channel name git:commit-message.
src/renderer/src/components/SourceControlPanel.tsx UI trigger and state: calls window.termsprawl.git.commitMessage(target), sets the commit input, surfaces busy/error/status.
src/preload/index.ts Declares the narrow window.termsprawl context-bridge surface; the renderer does not touch ipcRenderer directly.

Call chain

sequenceDiagram
  participant SC as SourceControlPanel
  participant API as window.termsprawl.git
  participant Main as main git:commit-message handler
  participant Core as core/commit-message
  participant Git as stagedDiff
  participant CLI as claude/codex

  SC->>SC: setAiBusy(true); clear error/status
  SC->>API: commitMessage(target)
  API->>Main: IPC.gitCommitMessage = git:commit-message

  alt local target
    Main->>Core: generateCommitMessage(repoRoot)
    Core->>Git: stagedDiff(repoRoot)
    Git-->>Core: staged diff
  else remote target
    Main->>Main: fetch staged diff over ssh first (documented in core)
    Main->>Core: generateCommitMessageFromDiff(diff, cwd)
  end

  Core->>Core: buildCommitPrompt(diff), cap at 6000 chars
  Core->>Core: detectAgentCli(binOnPath)
  Core->>CLI: spawn(tool, argv, cwd)
  CLI-->>Core: stdout / stderr / exit status
  Core->>Core: parseCommitMessage(stdout)
  Core-->>Main: CommitMessageResult
  Main-->>API: result
  API-->>SC: result
  SC->>SC: setAiBusy(false); setMsg(message) or setError(error)
Loading

Key nodes:

  • The renderer owns the review flow. SourceControlPanel.generateCommitMsg sets aiBusy, clears previous feedback, calls the git API, and on success writes into the commit-message input rather than committing immediately.
  • target is derived from the project kind: a local folder sends { cwd }, while a remote project sends { remote }.
  • The IPC contract is centralized as IPC.gitCommitMessage = 'git:commit-message'.
  • The core exposes two entry points. generateCommitMessage(repoRoot) reads the local staged diff via stagedDiff; generateCommitMessageFromDiff(diff, cwd) is the seam for callers that already have the diff, including the documented remote path where the main handler fetches the diff over SSH first.
  • buildCommitPrompt, detectAgentCli, runAgentCli, and parseCommitMessage form the actual generation pipeline.

Prompt construction and context boundary

buildCommitPrompt(diff) is deliberately narrow. The only repository context included is the staged diff. It does not include branch names, status output, recent commits, project settings, or chat context.

The prompt asks for:

  • A conventional commit message.
  • type(scope): subject format.
  • Allowed types: feat/fix/chore/docs/refactor/test/style/perf/build/ci.
  • Only the message as the response: one-line subject, optional short body, no explanations and no code fences.
  • The diff wrapped in explicit <diff> / </diff> tags.

The diff is bounded by MAX_DIFF_CHARS = 6000. If the diff is longer, it is sliced to the first 6000 characters and suffixed with …(truncated). This protects the argv-passed prompt from unbounded growth and keeps the CLI invocation predictable.

One boundary to note: the prompt allows an optional body, but parseCommitMessage returns only cleaned[0], so only the first usable line becomes the result message. In practice, the UI receives the subject line.

Agent CLI detection and execution

detectAgentCli(probe) implements a fixed preference order:

  1. claude
  2. codex
  3. 'none'

The production probe is binOnPath, which runs spawnSync('which', [bin], { stdio: 'ignore' }) and checks only the exit status. No shell string is used.

runAgentCli(tool, prompt, cwd) builds arguments per tool:

  • claude: ['-p', prompt]
  • codex: ['exec', prompt]

The prompt is passed as a single argv argument. This is the clean-room boundary documented at the top of the module: argv-array spawns only, never shell interpolation.

Execution details:

  • spawn(tool, args, { cwd, stdio: ['pipe', 'pipe', 'pipe'] })
  • stdout and stderr are accumulated as strings.
  • child.stdin.end() is called immediately because the CLI should not wait for stdin.
  • Spawn errors resolve as { error: '<tool> failed to spawn: ...' }.
  • Non-zero exits resolve as { error: '<tool> exited <code>: <stderr first 500 chars>' }.
  • A 120_000 ms timeout resolves as { error: '<tool> timed out' }.
  • A settled flag ensures the promise resolves only once.

Parsing and result contract

parseCommitMessage(output) cleans possibly chatty CLI output:

  • Splits on newlines and trims each line.
  • Ignores blank lines.
  • Ignores markdown fence lines beginning with ```.
  • Ignores conversational prefixes such as here is, here's, commit message, suggested, the commit, and based on.
  • Strips surrounding straight and curly quotes via stripWrap.
  • Returns the first remaining cleaned line, or ''.

The result type is shaped by literals in this module:

  • Failure: { ok: false, error, tool? }
  • Success: { ok: true, message, tool }

Failure cases include:

  • Empty staged diff: nothing staged to commit
  • No CLI available: no agent CLI found; install claude or codex
  • Spawn failure, non-zero exit, or timeout
  • CLI output with no usable message: agent returned no commit message

Key state

The core has no persistent state. State is per invocation:

  • settled prevents double resolution.
  • timer enforces the timeout.
  • output and stderr buffer process output.
  • The detected tool is included in both success and most failure results.

The renderer state is in SourceControlPanel:

  • msg: the commit-message input, filled by generation but not auto-committed.
  • aiBusy: separate busy flag for AI generation.
  • busy: general git operation busy flag.
  • error / status: feedback surfaces; success sets a status like message from ${res.tool}.

The user-facing flow intentionally stops at filling the input. Committing remains a separate window.termsprawl.git.commit(target, text) action.

Boundary conditions

  • Generation requires a non-empty staged diff.
  • The prompt contains only the staged diff, not broader project context.
  • Diff context is truncated at 6000 characters.
  • CLI availability is PATH-based; no settings or provider configuration is consulted in this core.
  • claude always wins over codex when both are present.
  • The process timeout is 120 seconds.
  • The parser returns only one cleaned subject line and may drop an optional body.
  • The spawn path never uses a shell, and the which probe uses status only.

Extension points

  • Add another agent CLI by extending the detection order in detectAgentCli, adding its argv shape in runAgentCli, and updating the shared CommitAgentCli type.
  • Adjust prompt wording, allowed conventional types, or diff wrapping in buildCommitPrompt.
  • Tune MAX_DIFF_CHARS and AGENT_TIMEOUT_MS.
  • Change output cleanup rules in parseCommitMessage / stripWrap.
  • Reuse generateCommitMessageFromDiff as the stable seam for remote projects or any caller that already has a diff.
  • If generation should later go through a configured model provider instead of a local CLI, runAgentCli and generateCommitMessageFromDiff are the natural replacement points while preserving the CommitMessageResult contract used by the renderer.

Testing

src/core/commit-message.test.ts covers the pure and injectable parts:

  • Prompt includes the staged diff and asks for conventional format.
  • Very large diffs are truncated below the test threshold.
  • Parser extracts a subject from fenced output, strips quotes, and returns empty when nothing usable exists.
  • detectAgentCli prefers claude, falls back to codex, and returns none.

The tests inject the probe function, so CLI detection is covered without requiring either executable to be installed.

Sources: src/core/commit-message.ts, src/core/commit-message.test.ts, src/shared/ipc.ts, src/renderer/src/components/SourceControlPanel.tsx, src/preload/index.ts

termsprawl

App Shell & Platform Foundations

Canvas, Nodes & Renderer State

Terminals & Session Continuity

Persistence, Projects & Files

Agent Runtime & Tooling

Chat Nodes & Model Providers

Git & Source Control

Embedded Browser Nodes

Server Edition

Relay & Remote Access

Integrations & Secondary Surfaces

Settings, Updates & Maintenance

Clone this wiki locally