From f6c24c42e8f0a05da58f684dae7e051e9e664df0 Mon Sep 17 00:00:00 2001 From: Daniel Schwarz Date: Tue, 14 Jul 2026 16:14:39 +0200 Subject: [PATCH] feat(ai): support commit suggestions from unstaged changes Fall back to the working-tree diff when no changes are staged, and pin focused models for Codex and Claude suggestions. Update prompts and documentation to reflect the new behavior. --- README.md | 8 +++++--- ROADMAP.md | 12 +++++++----- TASKS.md | 3 ++- crates/strand-tauri/src/ai/claude.rs | 4 ++++ crates/strand-tauri/src/ai/codex.rs | 4 ++++ crates/strand-tauri/src/ai/mod.rs | 2 +- crates/strand-tauri/src/ai/prompt.rs | 6 +++--- crates/strand-tauri/src/commands.rs | 10 +++++++++- ui/src/views/LocalChanges.tsx | 16 +++++++++------- website/docs/everyday-git.md | 2 +- website/docs/settings.md | 11 ++++++----- 11 files changed, 51 insertions(+), 27 deletions(-) diff --git a/README.md b/README.md index 97b08ff..b508d4f 100644 --- a/README.md +++ b/README.md @@ -125,9 +125,11 @@ keyboard alone, and the mouse stays first-class. `.code-workspace`), native macOS menubar, open in your editor or terminal, settings (⌘,) for appearance / diff / git / integrations / AI, in-app updates. -- **AI commit messages** — suggest subject + body from staged changes via - your ChatGPT subscription (Codex CLI) or Claude Code CLI; Settings → AI - for sign-in, provider choice, and CLI health checks. +- **AI commit messages** — suggest subject + body from staged changes (or all + unstaged changes when nothing is staged) via + your ChatGPT subscription (Codex CLI, `gpt-5.6-luna`) or Claude Code CLI + (`claude-sonnet-5`); Settings → AI for sign-in, provider choice, and CLI + health checks. - **Fast by design** — reads go through [gix](https://github.com/GitoxideLabs/gitoxide), writes through git2 and your system `git`. Performance targets live in [`PRD.md`](./PRD.md) §8 and are measured in diff --git a/ROADMAP.md b/ROADMAP.md index 6ce04d6..7af4309 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -1661,15 +1661,17 @@ is preflighted before Strand reports that the browser or CLI flow has started. `docs/strand-cli.md` + task breakdown in TASKS.md. - Plugin / extension surface - AI features (writing suggestions, conflict hints) — PRD Q3 - - ☑ Commit message suggestions from staged diffs (Codex / Claude Code CLIs) + - ☑ Commit message suggestions from staged diffs, or all unstaged changes + when nothing is staged (Codex / Claude Code CLIs) - ☑ Pull-request title/description suggestions from committed branch diffs - ◐ Built-in PR review surface — moved into 1.0 with the GitHub/Azure read-only foundation; GitLab/Bitbucket adapters and review/merge parity continue here. -**AI commit messages (2026-07-01):** Subscription-first suggestions from staged -diffs — Codex CLI (`codex login` / `codex exec`) for ChatGPT Plus, Claude Code -CLI (`claude -p`) for Anthropic. Rust `ai/` module + four IPC commands; Settings -→ AI for sign-in; CommitBar sparkle button; palette + ⌘⇧M. Verified: +**AI commit messages (2026-07-01):** Subscription-first suggestions prefer the +staged diff, and fall back to all unstaged changes when no staged diff exists — +Codex CLI (`codex login` / `codex exec`) for ChatGPT Plus, Claude Code CLI +(`claude -p`) for Anthropic. Rust `ai/` module + four IPC commands; Settings → +AI for sign-in; CommitBar sparkle button; palette + ⌘⇧M. Verified: `cargo test -p strand-tauri`, `pnpm --filter ./ui exec tsc --noEmit`. --- diff --git a/TASKS.md b/TASKS.md index faf1f8c..5f5fe4f 100644 --- a/TASKS.md +++ b/TASKS.md @@ -1760,7 +1760,8 @@ extraction above as prerequisite. **Do not start before 1.0 ships** - ☑ Rust `ai/` module + IPC (`ai_provider_*`, `repo_suggest_commit_message`) - ☑ Settings → AI (ChatGPT / Claude Code sign-in, custom CLI paths) -- ☑ CommitBar Suggest + palette / ⌘⇧M shortcut +- ☑ CommitBar Suggest + palette / ⌘⇧M shortcut (prefers the staged diff; falls + back to all unstaged changes when no staged diff exists) - ☑ Pull-request title/description suggestions from committed merge-base diffs (`repo_suggest_pull_request`, Create PR **Fill with Codex/Claude Code**) - ☑ Windows CLI spawning hardened (DAN-11: `ai/bin.rs` resolves `.exe`/`.cmd`/ diff --git a/crates/strand-tauri/src/ai/claude.rs b/crates/strand-tauri/src/ai/claude.rs index 42a432e..4c2647b 100644 --- a/crates/strand-tauri/src/ai/claude.rs +++ b/crates/strand-tauri/src/ai/claude.rs @@ -4,6 +4,8 @@ use super::bin::{resolve_claude, run_capture, spawn_detached, STATUS_TIMEOUT, SU use super::AiProviderStatus; const CLAUDE_INSTALL: &str = "https://code.claude.com/docs/en/setup"; +/// A fast, focused model is sufficient for short commit and PR copy. +const SUGGEST_MODEL: &str = "claude-sonnet-5"; pub fn status(cli_override: Option<&str>) -> AiProviderStatus { let Some(bin) = resolve_claude(cli_override) else { @@ -88,6 +90,8 @@ pub fn suggest( &bin, &[ "-p", + "--model", + SUGGEST_MODEL, "--output-format", "json", "--tools", diff --git a/crates/strand-tauri/src/ai/codex.rs b/crates/strand-tauri/src/ai/codex.rs index a5432f2..a6e272d 100644 --- a/crates/strand-tauri/src/ai/codex.rs +++ b/crates/strand-tauri/src/ai/codex.rs @@ -4,6 +4,8 @@ use super::bin::{resolve_codex, run_capture, spawn_detached, STATUS_TIMEOUT, SUG use super::AiProviderStatus; const CODEX_INSTALL: &str = "https://developers.openai.com/codex"; +/// A fast, focused model is sufficient for short commit and PR copy. +const SUGGEST_MODEL: &str = "gpt-5.6-luna"; pub fn status(cli_override: Option<&str>) -> AiProviderStatus { let Some(bin) = resolve_codex(cli_override) else { @@ -61,6 +63,8 @@ pub fn suggest( &bin, &[ "exec", + "--model", + SUGGEST_MODEL, "--cd", repo_path.to_str().ok_or("invalid repo path")?, "--sandbox", diff --git a/crates/strand-tauri/src/ai/mod.rs b/crates/strand-tauri/src/ai/mod.rs index 67936be..b4ea3f5 100644 --- a/crates/strand-tauri/src/ai/mod.rs +++ b/crates/strand-tauri/src/ai/mod.rs @@ -117,7 +117,7 @@ pub fn suggest_commit_message( cli_override: Option<&str>, ) -> Result { if diffs.is_empty() { - return Err("Nothing staged — stage changes before generating a message.".into()); + return Err("Nothing changed — make a change before generating a message.".into()); } let text = format!( "{}\n\nRemember: reply with JSON only: {{\"subject\":\"...\",\"body\":\"...\"}}", diff --git a/crates/strand-tauri/src/ai/prompt.rs b/crates/strand-tauri/src/ai/prompt.rs index 7e808b1..29a3e5d 100644 --- a/crates/strand-tauri/src/ai/prompt.rs +++ b/crates/strand-tauri/src/ai/prompt.rs @@ -3,7 +3,7 @@ use strand_core::diff::{DiffStatus, FileDiff}; const MAX_FILES: usize = 8; const MAX_TOTAL_CHARS: usize = 12_000; -const INSTRUCTION: &str = "Write a git commit message for the staged changes below.\n\ +const INSTRUCTION: &str = "Write a git commit message for the changes below.\n\ Use conventional commit style when appropriate.\n\ Reply with JSON only, no markdown fences: {\"subject\":\"...\",\"body\":\"...\"}\n\ Keep subject at most 72 characters. Body may be empty string if not needed."; @@ -14,10 +14,10 @@ Reply with JSON only, no markdown fences: {\"title\":\"...\",\"description\":\". Keep the title concise. Write a useful Markdown description that explains what changed and why.\n\ Mention testing only when the changes provide clear evidence; do not invent results or implementation details."; -/// Build the user prompt sent to Codex / Claude from staged file diffs. +/// Build the user prompt sent to Codex / Claude from the selected file diffs. pub fn build_prompt(diffs: &[FileDiff]) -> String { let mut out = String::from(INSTRUCTION); - out.push_str("\n\n## Staged changes\n"); + out.push_str("\n\n## Changes\n"); append_diffs(&mut out, diffs); out } diff --git a/crates/strand-tauri/src/commands.rs b/crates/strand-tauri/src/commands.rs index 56045ca..c9090fa 100644 --- a/crates/strand-tauri/src/commands.rs +++ b/crates/strand-tauri/src/commands.rs @@ -1300,7 +1300,15 @@ pub async fn repo_suggest_commit_message( ) -> CmdResult { run_blocking("ai suggest", move || { let repo = Repo::discover(&path)?; - let diffs = repo.diff_staged()?; + // A commit must use staged changes, but a suggestion is useful before + // the user has staged anything. Prefer the exact staged set whenever + // it exists; otherwise describe the whole working-tree delta. + let staged_diffs = repo.diff_staged()?; + let diffs = if staged_diffs.is_empty() { + repo.diff_unstaged()? + } else { + staged_diffs + }; let override_path = ai_cli_override(provider, openai_cli, anthropic_cli); ai::suggest_commit_message( provider, diff --git a/ui/src/views/LocalChanges.tsx b/ui/src/views/LocalChanges.tsx index b936a8d..aa72586 100644 --- a/ui/src/views/LocalChanges.tsx +++ b/ui/src/views/LocalChanges.tsx @@ -398,7 +398,7 @@ export function LocalChanges() { - 0} /> + 0} hasChanges={staged.length > 0 || unstaged.length > 0} /> {resolverFile && ( setResolverFile(null)} /> @@ -1271,7 +1271,7 @@ function BlockActions({ // ─── Commit bar ───────────────────────────────────────────────────────────── -function CommitBar({ canCommit }: { canCommit: boolean }) { +function CommitBar({ canCommit, hasChanges }: { canCommit: boolean; hasChanges: boolean }) { const activePath = useRepo((s) => s.activePath); const commit = useRepo((s) => s.commit); const suggestCommitSignal = useRepo((s) => s.suggestCommitSignal); @@ -1311,7 +1311,7 @@ function CommitBar({ canCommit }: { canCommit: boolean }) { } const suggest = useCallback(async () => { - if (!activePath || !canCommit) return; + if (!activePath || !hasChanges) return; setSuggesting(true); setCommitError(null); try { @@ -1339,7 +1339,7 @@ function CommitBar({ canCommit }: { canCommit: boolean }) { } finally { setSuggesting(false); } - }, [activePath, aiProvider, anthropicCli, canCommit, openaiCli]); + }, [activePath, aiProvider, anthropicCli, hasChanges, openaiCli]); useEffect(() => { if (!suggestCommitSignal) return; @@ -1369,11 +1369,13 @@ function CommitBar({ canCommit }: { canCommit: boolean }) { const disabled = submitting || !subject.trim() || (!canCommit && !amend); // Missing CLI does not disable the button: clicking surfaces the backend's // install/sign-in hint inline, instead of a dead control (DAN-11). - const suggestDisabled = suggesting || submitting || !canCommit; + const suggestDisabled = suggesting || submitting || !hasChanges; const suggestTitle = suggesting ? 'Generating commit message…' - : !canCommit - ? 'Stage changes to suggest a commit message' + : !hasChanges + ? 'Make changes to suggest a commit message' + : !canCommit + ? 'Suggest commit message from all unstaged changes' : !aiInstalled ? 'Install the CLI (Settings → AI) to enable suggestions' : 'Suggest commit message from staged changes'; diff --git a/website/docs/everyday-git.md b/website/docs/everyday-git.md index e3a70f7..e6d3039 100644 --- a/website/docs/everyday-git.md +++ b/website/docs/everyday-git.md @@ -32,7 +32,7 @@ The commit form takes a subject and an optional description body. `Mod+Enter` in ### AI commit message suggestions -The sparkle button next to the subject field (or `Mod+Shift+M`, or the palette action "Suggest commit message") generates a commit message from your **staged** changes. Suggestions use your own subscription CLIs — the Codex CLI (ChatGPT) or the Claude Code CLI — with no Strand-side API key. Pick the provider and sign in under Settings → AI; see [Settings](settings.md) for setup. +The sparkle button next to the subject field (or `Mod+Shift+M`, or the palette action "Suggest commit message") generates a commit message from your **staged** changes. If nothing is staged, it uses all unstaged changes instead, so it is available as soon as there is work to describe. Suggestions use your own subscription CLIs — the Codex CLI (ChatGPT) or the Claude Code CLI — with no Strand-side API key. Pick the provider and sign in under Settings → AI; see [Settings](settings.md) for setup. ## Fetch, pull, push diff --git a/website/docs/settings.md b/website/docs/settings.md index e2946eb..4aeca48 100644 --- a/website/docs/settings.md +++ b/website/docs/settings.md @@ -53,12 +53,13 @@ Configure the external editor and terminal that Strand's "Open in editor" (`Mod+ ## AI -Strand can suggest a commit message from staged changes or draft pull-request -text from committed branch changes. It has no API key of its own — suggestions -run through a CLI you already have, on your own subscription. Auth and billing -stay entirely in the vendor's CLI; Strand only orchestrates it. +Strand can suggest a commit message from staged changes, or all unstaged changes +when nothing is staged, and draft pull-request text from committed branch +changes. It has no API key of its own — suggestions run through a CLI you +already have, on your own subscription. Auth and billing stay entirely in the +vendor's CLI; Strand only orchestrates it. -- **AI writing provider** — "OpenAI (ChatGPT subscription)" (default), which uses your ChatGPT subscription via the Codex CLI, or "Anthropic (Claude Code CLI)", which uses the Claude Code CLI (`claude`). +- **AI writing provider** — "OpenAI (ChatGPT subscription)" (default), which uses your ChatGPT subscription via the Codex CLI (`gpt-5.6-luna`), or "Anthropic (Claude Code CLI)", which uses the Claude Code CLI (`claude-sonnet-5`). These fixed, focused models keep short commit-message and PR-draft requests responsive rather than inheriting your coding CLI's default model. - **Codex CLI** — an optional custom path (leave empty to use `codex` on PATH), a status line, and **Sign in with ChatGPT** / **Sign out** buttons. - **Claude Code CLI** — an optional custom path (leave empty to use `claude` on PATH), a status line, and **Sign in to Claude Code** / **Sign out** buttons. - **Check CLI status** — checks both CLIs and reports whether each is missing,