feat(source-control): AI commit message generation#1487
Open
leynier wants to merge 13 commits intostablyai:mainfrom
Open
feat(source-control): AI commit message generation#1487leynier wants to merge 13 commits intostablyai:mainfrom
leynier wants to merge 13 commits intostablyai:mainfrom
Conversation
Adds a "Generate with AI" button to the Source Control panel that drafts a commit message from the staged diff using a non-interactive agent CLI. Ships Claude Code and Codex; the spec abstraction is built so additional agents can be added later by appending entries. The agent + model + per-model thinking effort + custom prompt suffix are configured under Settings → AI Commit Messages. The button only appears once the user opts in, only enables when the textarea is empty, and routes through the SSH relay (new agent.execNonInteractive RPC) when committing on a remote worktree so the agent runs on the same host as the rest of the SSH session. Defaults pick the smallest model and lowest effort: Claude Haiku 4.5 with effort=low; if the user switches to Codex the default lands on gpt-5.4-mini with reasoning-effort=low.
- Codex models now render in version-descending order (5.5 → 5.2) to match the official `codex` model picker. - Swap thinking-effort support between Haiku 4.5 (removed — non-reasoning model) and gpt-5.3-codex-spark (added — accepts model_reasoning_effort). - Generator extracts the actionable error from agent CLIs that print a multi-line preamble before failing (Codex Spark with a non-Pro account now surfaces "The 'gpt-5.3-codex-spark' model is not supported …" instead of dumping the full runtime banner). - AI commit settings nest inside the existing Git section instead of living as a standalone Settings entry. - Respect the existing Orca Attribution toggle: when on, the generator appends the same Co-authored-by trailer the terminal git/gh shim uses, via a single shared constant in src/shared/orca-attribution.ts. - Add a space before the ellipsis in the "Generating …" button label. Tests: extractAgentErrorMessage covers the Codex Spark JSON case, raw-payload fallback, multiple ERROR lines, plain `Error:` prefix, and no-error input. Spec tests assert the new model order, Haiku without effort, and Spark with effort.
Replaces the dedicated full-width "Generate with AI" button below the textarea with a small Sparkles icon button positioned absolutely in the top-right of the textarea itself. Switches to a circular RefreshCw spinner during generation and reserves right-padding on the textarea so typed text does not slide under the icon. Disabled state, tooltips, and the underlying handleGenerate flow are unchanged.
Adds a stop affordance on the in-textarea AI icon: while generating, the
spinning RefreshCw stays in place by default, but on hover/focus it swaps
to a filled Square ("stop") with a destructive tint. Clicking it cancels
the run.
The cancel path:
- Renderer calls window.api.git.cancelGenerateCommitMessage({worktreePath,
connectionId}) — fire-and-forget; the in-flight generateCommitMessage
promise resolves with `{success: false, canceled: true}` once the kill
propagates, which clears the spinner and (because canceled is treated as
intentional) leaves the inline error empty.
- Local: cancelGenerateCommitMessageLocal looks up the in-flight child
process by worktreePath and SIGKILLs it.
- SSH: SshGitProvider sends `agent.cancelExec({cwd})` to the relay; the
relay's AgentExecHandler tracks running children by cwd and SIGKILLs
the matching one, returning the captured stdout/stderr with
`canceled: true` so the SSH provider can map it to the same
`{canceled: true}` response shape.
The renderer's handleGenerate detects the `canceled` flag and skips the
inline error, so a user-initiated cancel feels like a no-op rather than a
failure.
On Windows, npm-installed CLIs like `claude` and `codex` are `.cmd` shims
that Node spawns through an implicit `cmd.exe /d /s /c` wrapper. Calling
`child.kill('SIGKILL')` only terminates the cmd.exe wrapper while the real
node.exe doing the inference keeps running until completion — which is why
hitting Stop appeared to do nothing on Windows.
Adds a `killProcessTree(child)` helper that uses `taskkill /T /F` on
Windows (walks the tree from the spawned PID) and falls back to
`child.kill('SIGKILL')` on POSIX. Used in three places per side:
user cancel, generation timeout, and stdout/stderr buffer overflow. The
helper lives in both the local generator and the relay handler — kept
duplicated rather than shared because the relay is a separate runtime
that ships independently.
Also reword the Agent dropdown description in Settings → Git to make
clear that the agent CLI must be installed on whichever machine hosts
the worktree (laptop for local worktrees, SSH host for remote ones).
"Which agent CLI Orca runs in the background…" parsed ambiguously — the
"CLI Orca" could be read as a noun phrase ("Orca, which is a CLI"). Rewrite
so the subject is clear: "Which agent drafts your commit messages. Orca
invokes its CLI in the background…".
Adds a "Custom" entry to the AI commit messages agent dropdown. Selecting
it shows a single command-line field where the user types the binary plus
arguments to spawn for generation.
The placeholder is `{prompt}`. Substitution is argv-based, never
shell-evaluated:
- Without `{prompt}` → prompt is piped via stdin (matches `claude -p`).
- With `{prompt}` (bare or quoted) → substituted into that argv slot. Quotes
in the template are tokenizer-only grouping, so `{prompt}` and "{prompt}"
produce identical argv. No double-quoting risk.
Tokenizer is POSIX-shell-style for grouping only: single + double quotes,
backslash escapes inside double quotes. We do NOT expand `$VAR`, backticks,
or globs — keeps cross-platform behavior predictable.
Wiring:
- `planCustomCommand(template, prompt)` in shared/commit-message-prompt.ts
produces `{ binary, args, stdinPayload }`.
- `runAgent` in commit-message-generator.ts is binary-agnostic; the local
generator branches on `isCustomAgentId(params.agentId)` to either tokenize
or look up a preset spec.
- SshGitProvider mirrors the branch — the relay's existing
`agent.execNonInteractive` already accepts arbitrary `binary + args`,
so no relay changes were needed.
- Settings pane hides the model and thinking dropdowns for "custom" and
shows the command input with helper text explaining the placeholder.
- Generate button stays disabled until the user types a non-empty command.
Tests cover the tokenizer (whitespace, quotes, escapes, unclosed errors),
planCustomCommand (stdin-vs-argv routing, embedded substitution, equivalence
of `{prompt}` and "{prompt}"), and the new `isCustomAgentId` helper.
Replaces em-dashes with regular hyphens in: - the base prompt sent to the agent - the Agent and Custom command setting descriptions Code comments keep em-dashes since they are not user-facing.
The local generator and the SSH provider duplicated the entire spec/model/thinking validation chain plus the build-args logic — about 60 lines each, only differing in variable names. Extract into a single `planCommitMessageGeneration()` in src/shared/commit-message-plan.ts that returns a unified `CommitMessagePlan` (binary, args, stdinPayload, label). This collapses runAgent's parameter sprawl too: it now takes the plan object plus the runtime concerns (cwd, attributionEnabled) instead of six positional args. Net: ~150 lines removed; the SSH provider's generateCommitMessage shrinks to just orchestration (diff fetch, plan, relay request, response mapping) with no remaining duplication of the validation logic. Also tighten one comment in cleanGeneratedCommitMessage that narrated WHAT the code did instead of WHY.
- Add `cursor-pointer` to the Agent / Model / Thinking effort SelectItems in the AI Commit Messages settings pane. shadcn/ui's base SelectItem ships with `cursor-default`; without an override the items stayed on the arrow cursor instead of the expected hand pointer. - Drop the trailing `\n` from `applyOrcaAttribution`. With it, the trailer ended with a newline that the commit textarea rendered as an empty line below the attribution. The blank line separating the body from the trailer block (required by `git interpret-trailers`) is preserved.
`applyOrcaAttribution` already returned the trailer without a final newline, but the no-attribution branch passed the message straight through. If the agent returned a final stray whitespace character that slipped past `cleanGeneratedCommitMessage`, the textarea rendered an empty line at the bottom. Trim trailing whitespace on both paths and skip the trailer if the already-trimmed message includes it.
…-ai-commit-messages
Contributor
|
Good idea! I'll check out the PR tmr |
…-ai-commit-messages # Conflicts: # src/main/ipc/filesystem.ts # src/renderer/src/components/right-sidebar/CommitArea.test.tsx # src/renderer/src/components/right-sidebar/SourceControl.tsx
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Closes #1484.
Summary
Adds an opt-in "Generate with AI" affordance inside the commit area of the Source Control panel. The feature ships Claude Code and Codex as preset agents and a Custom option for any other CLI the user wants to use (Ollama, OpenCode, gateway proxy, etc.).
UX
Settings (nested under Git)
Customentry that swaps the model/thinking dropdowns for a single command-line field.{prompt}as the placeholder. Argv-based, never shell-evaluated;{prompt}and"{prompt}"produce identical argv.gpt-5.4-miniwithlowif the user switches to Codex.Architecture
src/shared/commit-message-agent-spec.ts- preset registry (Claude + Codex with the 3 + 6 models from each CLI's official picker).src/shared/commit-message-prompt.ts- base prompt, custom-suffix builder, diff truncation (200 KB), agent-error JSON extractor (parsesERROR: {...}lines so Codex/Claude failures surface a legible message instead of dumping the runtime preamble), and the POSIX-style tokenizer for custom commands (groups via single/double quotes; does not expand$VAR, backticks, or globs).src/shared/commit-message-plan.ts- single planner shared by the local generator and the SSH provider. Validates agent/model/thinking, builds argv via the spec'sbuildArgs, and returns a unified plan ({ binary, args, stdinPayload, label }).src/main/git/commit-message-generator.ts- local spawn with timeout (60 s), tree-kill on cancel/timeout (taskkill /T /Fon Windows because npm-installed CLIs are.cmdshims), per-worktree cancel-token map.src/relay/agent-exec-handler.ts- new JSON-RPC methodagent.execNonInteractivefor non-PTY exec on the remote, plusagent.cancelExeckeyed bycwd. Mirrors the local cancel/tree-kill semantics on the remote host.src/main/providers/ssh-git-provider.ts- orchestrates remote diff fetch + relay request via the shared planner.src/main/ipc/filesystem.ts- IPC handlersgit:generateCommitMessageandgit:cancelGenerateCommitMessage(both branch onconnectionId→ SSH provider vs local). The IPC readsenableGitHubAttributionfrom the persisted store and applies the existing Orca trailer to the generated message when the toggle is on (paritying the terminal git shim's behavior).src/shared/orca-attribution.ts- single source of truth forCo-authored-by: Orca <help@stably.ai>, shared between the terminal shim and the AI commit generator.src/renderer/src/components/right-sidebar/SourceControl.tsx- Generate / Cancel UI insideCommitAreaplus per-worktree state; mirrors the existing commit-in-flight pattern.src/renderer/src/components/settings/CommitMessageAiPane.tsx- rendered nested inside the Git settings section.Notes
updateSettingsIPC, which debounces disk writes viapersistence.ts:300ms- no extra debounce needed in the textareas.$VAR, command substitution, and globs are not expanded - keeps cross-platform behavior predictable and avoids a shell-evaluation surface we don't need.commit-message-generator.ts(local) andagent-exec-handler.ts(relay) by design: the relay is a separate runtime that ships independently to remote hosts and can't share main-process modules.Test plan
pnpm typecheck,pnpm lint,pnpm testclean for the new files (55 unit tests covering spec, prompt builder, error extractor, tokenizer, planner, and the CommitArea component).--model claude-sonnet-4-6and the effort dropdown appear; rerun.claude -p(no{prompt}→ stdin) andcodex exec {prompt}(argv). Both should produce the same draft as the equivalent preset. Verify{prompt}and"{prompt}"behave identically.Use Conventional Commits.and verify the output starts withfeat:/fix:/ etc.Co-authored-by: Orca <help@stably.ai>(no trailing blank line).execFileargv avoids shell quoting issues. Tree-kill on Windows verified to terminate the.cmd-wrapped child.Screenshots