Skip to content

feat(ai-ide): add AI-generated commit message button#17669

Open
sgraband wants to merge 3 commits into
masterfrom
sg/commit-agent
Open

feat(ai-ide): add AI-generated commit message button#17669
sgraband wants to merge 3 commits into
masterfrom
sg/commit-agent

Conversation

@sgraband

Copy link
Copy Markdown
Contributor

What it does

Adds an AI-powered "generate commit message" button to the SCM commit widget.
The work spans two packages.

@theia/ai-terminal gets a new PredefinedShellTool base class. It is an
abstract ToolProvider for tools that run a fixed, hardcoded shell command.
Subclasses declare a typed parameter schema and a buildCommand(args) method
that assembles the exact command. The LLM only supplies typed arguments, so the
command itself is fully controlled by the subclass. Unlike shellExecute,
instances do not consult ShellCommandPermissionService and do not need to
appear on the user's allow/deny lists. The tool itself participates in the
regular ToolConfirmationMode flow.

@theia/ai-ide uses that to add the commit-message generator:

  • GetGitChangesTool (getGitChanges) returns the repository's git diff.
    With stagedOnly = true it runs git diff --cached. With stagedOnly = false
    it combines git diff HEAD with the contents of untracked, non-ignored files
    (rendered as addition diffs via git diff --no-index) so brand-new files
    reach the model. It is scoped to the selected SCM repository's root.
  • CommitMessageAgent is an AbstractStreamParsingChatAgent with its own
    prompt template. It is intentionally not registered as a ChatAgent: it does
    not appear in the @-mention list and is not invokable from the chat panel.
  • CommitMessageRunner drives the agent silently against a private
    MutableChatModel (no chat-panel session), writes the resulting message into
    the SCM commit input, and handles cancellation, the overwrite-confirm prompt,
    and a one-time tool-allow gate that asks the user to flip getGitChanges to
    ALWAYS_ALLOW when it is currently on CONFIRM or DISABLED. The dialog
    copy differs between the two cases.
  • AiAwareScmCommitWidget rebinds the SCM commit widget to overlay two sparkle
    buttons (staged / all). It is gated on AIActivationService.isActive, so
    users with AI globally disabled keep the stock SCM input with no overlay and
    no extra padding. While one scope is generating, the other scope's button is
    disabled with a tooltip explaining the situation.
  • CommitMessageCommandContribution exposes two commands that toggle
    run / cancel; the buttons fire them.

How to test

Workspace root is a git repo, AI features enabled, working model behind the default/universal alias, SCM view open.

  • Toggle AI features off: SCM input matches the stock Theia widget (no extra padding, no buttons).
  • Turn AI on with no repo changes: no buttons appear.
  • Stage one file, leave another modified but unstaged: both sparkle buttons (staged / all) appear.
  • Disable the Commit Message agent in AI Configuration → Agents: buttons stay visible but disabled, with a tooltip pointing to the AI configuration view.
  • Type @ in the chat view: CommitMessage does not show up in the mention list.
  • With getGitChanges on the default Confirm mode, click a sparkle then pick Cancel: nothing runs, the tool stays on Confirm.
  • Click again and pick Allow: the run starts, the message is written into the input, and the tool is now Always allow; the next click runs with no dialog.
  • Set getGitChanges to Disabled and click a sparkle: the dialog text changes to mention the tool is currently disabled, picking Allow flips it back to Always allow and runs.
  • Create a single untracked file with meaningful content: only the "all" sparkle appears, and the generated message reflects the new file's content.
  • Add a .gitignore entry for some artefact: its content does not influence the generated message.
  • Empty input, click a sparkle: the message is written directly and focus moves into the textarea.
  • Non-empty input, click a sparkle: a Replace / Cancel prompt appears; Cancel keeps the original text, Replace overwrites it.
  • During a running generation, click the spinning sparkle: the run aborts with the input unchanged.
  • Click a sparkle while existing text is in the input, then click the spinner before responding to the Replace prompt: the agent is not invoked, regardless of whether the prompt is then dismissed via Replace or Cancel.
  • With both sparkles visible, start one: the other becomes disabled with a tooltip saying another generation is in progress; cancelling the running one re-enables it.
  • Multi-root workspace with two git repos: switching the selected repo updates which buttons are visible, and the generated message reflects that repo's diff.
  • Run "Generate Commit Message from Staged Changes" from the command palette with no repository selected: a warning appears, no agent is invoked.

Breaking changes

  • This PR introduces breaking changes and requires careful review. If yes, the breaking changes section in the changelog has been updated.

Attribution

Review checklist

Reminder for reviewers

@sgraband sgraband requested a review from sdirix June 17, 2026 07:30
@github-project-automation github-project-automation Bot moved this to Waiting on reviewers in PR Backlog Jun 17, 2026
@ndoschek ndoschek mentioned this pull request Jun 22, 2026
29 tasks

@sdirix sdirix left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The design of the commit-message-agent is really weird to me:

  • It should not be a chat agent because we don't use it in chat
  • Because it is a chat agent you need to add all kind of weird workarounds to prevent it from showing up in chat, throwing away the chat model etc.
  • I need to manually confirm the tool it will call:
    • I already clicked on "Generate message from all changes" so it's unexpected to me that I need to again confirm the tool.
    • Because notifications are shown by default in small text on the bottom right, it's easily overlooked
    • The notification only allows No and Yes (forever)

For a single tool I would not even give the agent the control over it but just run it and inject the result manually into the prompt. This is how we do it for other non-chat agents.

I can understand giving the agent a bit more tools in case that just the diffs are not enough and it also wants to check past commits, maybe more of the workspace, naming conventions, agent.mds etc. But I think all of these are then implicitly always allowed. Why would I even run the agent if I don't want it to have that information?

Comment on lines +79 to +81
return 'git diff HEAD --no-color; '
+ 'git ls-files --others --exclude-standard -z '
+ '| xargs -0 -r -I{} sh -c \'git diff --no-index --no-color -- /dev/null "$1" || true\' _ {}';

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The "all changes" command is POSIX-only: ; as separator, git ls-files ... -z | xargs -0, sh -c, and /dev/null don't work in cmd.exe. ShellExecutionServer spawns with shell: true (master), which is cmd.exe on Windows, so this branch breaks there while the staged path is fine. Either build a Windows-aware command or document that "all changes" generation is POSIX-only.

return choice === replace;
}

protected async invokeAndAwaitCompletion(request: MutableChatRequestModel): Promise<void> {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

AbstractStreamParsingChatAgent.invoke() already resolves only after the response is finished (it awaits onResponseComplete at the end and catches its own errors), so the parallel completion-listener here looks redundant. task-context-service drives an agent the same way with a plain await agent.invoke(...) then reads the response (master). A single await invoke() plus the existing state checks should be enough.

}

async run(scope: CommitMessageScope): Promise<void> {
if (this.running.has(scope)) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

run only guards the same scope, so running GENERATE_FROM_STAGED and GENERATE_FROM_ALL from the command palette can start both at once and race on repository.input.value. The widget prevents this by disabling the other button, but the palette path bypasses that. Guarding any in-flight run here would enforce the invariant the buttons rely on.

@github-project-automation github-project-automation Bot moved this from Waiting on reviewers to Waiting on author in PR Backlog Jun 24, 2026
sgraband added 3 commits June 30, 2026 13:12
… with hardcoded commands

Introduces a generic abstract `PredefinedShellTool` base class for tools that
run a fixed, hardcoded shell command. Subclasses declare a typed parameter
schema and a `buildCommand(args)` method that assembles the exact shell
command from the typed arguments — the LLM only supplies the typed arguments,
so the command itself is fully controlled by the subclass.

Unlike the general-purpose `shellExecute` tool, instances of
`PredefinedShellTool` do not consult `ShellCommandPermissionService` and do
not need to appear on the user shell allow/deny lists: the safety boundary
is the subclass-controlled `buildCommand`, which must not be coercible into
running arbitrary commands. The tool participates in the regular
`ToolConfirmationMode` flow; callers driving an agent without a UI (where
the per-call confirmation modal has nowhere to render) are responsible for
arranging the appropriate confirmation via `ToolConfirmationManager`.

Re-exported from the ai-terminal browser entry point for consumption by
other packages.
Adds an AI-powered commit message generator to the SCM commit widget:

- `GetGitChangesTool`, a `PredefinedShellTool` exposing the repository git
  diff (staged or all changes vs HEAD, plus untracked files as addition
  diffs for the all-changes case) to the agent.
- `CommitMessageAgent` and `CommitMessageRunner` that drive a silent agent
  invocation against a private `MutableChatModel` to produce a commit
  message from the current changes, without surfacing a chat session in
  the panel.
- `CommitMessageCommandContribution` and command wiring.
- `AiAwareScmCommitWidget` rebinding the SCM commit widget to render the
  generate button, gated on `AIActivationService`, plus the prompt template
  and styles.
- A one-time tool-allow gate in the runner that asks the user to flip
  `getGitChanges` to `ALWAYS_ALLOW` when it is currently on `CONFIRM` or
  `DISABLED`, with differentiated dialog copy for each case.
Signed-off-by: Simon Graband <sgraband@eclipsesource.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Waiting on author

Development

Successfully merging this pull request may close these issues.

2 participants