Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+85
View File
@@ -0,0 +1,85 @@
# APP-3923: AI-autogenerated commit messages, PR titles, and PR descriptions
## Summary
Pre-populate the code review git dialogs with AI-generated copy so users don't have to write commit messages, PR titles, or PR descriptions by hand. When the commit dialog opens, a draft commit message is generated from the current diff and dropped into the editor. When a PR is created (either standalone "Create PR" or the "Commit and create PR" chain), the PR title and body are generated from the branch's diff and commit history just before `gh pr create` runs.
## Problem
APP-3922 shipped the "Create PR" dialog and the `CommitAndCreatePr` chain, but both relied on the user to write a commit message and both passed `--fill` to `gh pr create`, which just copies the most recent commit subject and body into the PR. That produces mediocre PR titles/descriptions and still requires the user to write a good commit message manually. Writing these is low-value boilerplate users typically want to skip.
## Goals
- Generate a draft commit message automatically when the commit dialog opens so the user only has to review and optionally edit it.
- Generate a PR title and body automatically at PR creation time (both flows: standalone and "Commit and create PR").
- Fail safely: if any generation call fails, the user is told what happened and can recover (type a message manually; retry the PR).
- Keep the user in full control: the generated commit message is always editable; the user can also clear it and type their own.
- Make the commit confirm button's enabled state directly reflect whether a committable message exists — no silent second generation at confirm time.
## Non-goals
- Adding a new feature flag for this capability. `FeatureFlag::GitOperationsInCodeReview` already gates the entire git dialog surface, so autogen is only reachable inside that gate.
- Adding an `AISettings` opt-out or an enterprise / customer-type guard for sending diffs to AI. Both are noted as explicit follow-ups (see `app/src/ai/generate_code_review_content/mod.rs` TODO) and tracked separately.
- A preview/edit UI for the generated PR title and body — they are sent directly to `gh pr create`.
- Regenerate button for commit messages / PR fields.
- Draft PR support (inherited from APP-3922).
- Editing reviewers, labels, or milestones in the dialog (inherited from APP-3922).
## Figma
none provided — this feature is copy-only and reuses the existing dialog layouts from APP-3920 / APP-3922.
## User experience
### Commit message autogeneration (commit dialog)
When the commit dialog opens:
- Placeholder reads `Generating commit message…`.
- Editor buffer is empty.
- Confirm button is disabled (no message, file changes may also still be loading).
- An AI generation request fires immediately in the background using the current diff as input (staged + unstaged, with untracked files included as synthetic diff hunks when `include_unstaged` is true).
On generation success:
- If the editor is still empty (user hasn't typed anything), the generated message is inserted into the editor.
- If the user has already typed a non-empty message, the generated draft is silently discarded — user input is never clobbered.
- Placeholder changes to `Type a commit message` (visible only if the user later clears the buffer).
- Confirm button becomes enabled once file changes have also loaded.
On generation failure (network, server, or empty response):
- Placeholder changes to `Type a commit message`.
- No toast — autogen is best-effort background work, the user can't retry it, and the empty editor plus placeholder already communicate what happened.
- Editor buffer stays empty.
- Confirm button stays disabled until the user types a non-empty message.
### PR title and body autogeneration
Both flows generate PR title and body at confirm time, right before `gh pr create` runs:
**Standalone "Create PR" dialog:**
1. User clicks `Create PR` → dialog goes into loading state (`Creating…`).
2. Compute diff vs main branch and collect commit subjects on the current branch.
3. Generate PR title via AI.
4. Generate PR body via AI.
5. Run `gh pr create --title <generated> --body <generated>`.
6. On success: standard "PR successfully created." toast with `Open PR` link (unchanged from APP-3922).
7. On AI title/body failure: fall back to `gh pr create --fill` so the PR still gets created (with the latest commit's subject/body). On any other step's failure (diff fetch, `gh pr create` itself, etc.): dialog closes, friendly error toast (unchanged from APP-3922 — error mapping is per-call-site log + `user_facing_git_error`).
**"Commit and create PR" chain (commit dialog with `CommitAndCreatePr` intent):**
1. Commit runs.
2. Push runs.
3. Same PR-title / PR-body / `gh pr create` sequence as above.
4. Same success toast.
### Diff input and truncation
- Max diff length sent to AI: 16,000 characters. Beyond that, the diff is truncated on a UTF-8 char boundary with a trailing `... (diff truncated)` marker.
- For commit message generation with `include_unstaged = true`, untracked files are synthesised into diff hunks so the LLM has context for new-file-only commits. Per-untracked-file cap: 4,000 bytes. Binary files are detected from the first 1,024 bytes and skipped.
- For PR generation, the diff is `{base}..origin/{current}` when the remote ref exists, falling back to `{base}..HEAD` otherwise. Commit subjects on the branch are also sent alongside the diff.
### Editor interactions and state rules
1. Confirm is enabled iff there is at least one file change **and** the commit message editor holds a non-empty (trimmed) string.
2. While generation is in flight, the editor is empty, so confirm is implicitly disabled by rule (1); no separate "is autogenerating" flag is exposed.
3. The generated draft never overwrites user input. If the user has typed anything by the time generation resolves, the draft is discarded.
4. Clearing a successful draft after the fact leaves the placeholder `Type a commit message` visible and disables confirm until the user types.
5. There is no confirm-time fallback regeneration for commit messages: once the open-time generation has resolved (success or failure), the user is responsible for the message.
6. A generation failure during PR creation falls back to `gh pr create --fill` so the PR is still created (using the latest commit's subject/body). Only a failure in the PR-creation command itself aborts the flow; in that case, for the `CommitAndCreatePr` chain, the commit and push already succeeded but the PR was not created, and the user can retry via the standalone "Create PR" button.
## Success criteria
1. Opening the commit dialog on a branch with changes kicks off a background AI request within the same frame; the placeholder reads `Generating commit message…` until the request resolves.
2. On generation success with an untouched editor, the generated message appears in the editor within a short time (bounded by AI latency) and the confirm button becomes enabled once file changes are loaded.
3. On generation success with a user-typed message, the generated message is discarded and the user's text is preserved.
4. On generation failure, the placeholder changes to `Type a commit message` (no toast), and confirm stays disabled until the user types.
5. The confirm button is disabled whenever the editor's trimmed content is empty, regardless of whether an auto-generation is still in flight.
6. Clearing a previously-populated AI draft flips the confirm button back to disabled and shows the `Type a commit message` placeholder.
7. Confirming a PR (standalone or via `CommitAndCreatePr`) creates the PR with an AI-generated title and body; the user never types either.
8. A network outage that causes PR title or body generation to fail falls back to `gh pr create --fill`; the PR is still created, using the latest commit's subject/body as title/body.
9. No user-facing AI-related UI appears outside the commit dialog and the two PR-creation confirm paths.
## Validation
- Open the commit dialog on a branch with a non-trivial diff and verify the `Generating commit message…` placeholder, followed by a populated editor with a reasonable draft.
- Type into the editor before the AI responds; verify the typed text is preserved and the draft is discarded.
- Disable networking, open the commit dialog, verify the fallback placeholder (no toast) and that confirm stays disabled until the user types.
- Populate the editor via AI, clear it, verify confirm disables and the `Type a commit message` placeholder re-appears.
- On a pushed branch with no existing PR, click `Create PR` and verify the created PR has a generated title and body (not `--fill`-derived).
- On a branch with pending changes, select `Commit and create PR` in the commit dialog, let it run, and verify commit + push + PR creation, with the PR body and title generated.
- Simulate a failure in PR body generation (e.g. mid-flight network drop) for the `CommitAndCreatePr` flow; verify the PR still gets created via `gh pr create --fill` and the usual success toast appears.
## Open questions
- Should we add a regenerate button for the commit message draft? Currently the user can clear the field and type their own, but they cannot re-trigger AI generation without re-opening the dialog.
- Should PR title and body be editable before `gh pr create` fires? Current design sends them blind.
- AI settings opt-out and enterprise customer-type guard are explicit follow-ups; see `app/src/ai/generate_code_review_content/mod.rs`.
+184
View File
@@ -0,0 +1,184 @@
# APP-3923: AI-autogenerated commit messages and PR metadata — Tech Spec
Product spec: `specs/APP-3923/PRODUCT.md`
Parent stack: APP-3918 (header button) → APP-3920 (commit/push dialog) → APP-3922 (create-PR dialog) → **APP-3923** (this branch).
## Problem
APP-3922 landed the `GitDialog::CreatePr` mode and the `CommitIntent::CommitAndCreatePr` chain, both of which called `gh pr create --fill`. `--fill` just copies the latest commit subject/body into the PR, and commit messages themselves had to be typed manually. We want AI-generated copy for all three: commit message (at dialog open time), PR title and PR body (at confirm time).
The work has three logical layers that needed new plumbing:
1. A server endpoint for AI generation of review-adjacent content.
2. A client-side block-service method and request/response types.
3. Git helpers that produce the LLM input (diff + branch commit messages).
Plus editor-state changes in `commit.rs` so that an autogenerated draft is discoverable, overridable, and failure-visible.
## Relevant code
- `app/src/ai/generate_code_review_content/api.rs` — new `GenerateCodeReviewContentRequest` / `Response` + `OutputType` enum
- `app/src/ai/generate_code_review_content/mod.rs` — module root (plus the follow-up TODO)
- `app/src/ai/mod.rs:44` — registers the new module
- `app/src/server/server_api/block.rs:54,175-196` — new `BlockClient::generate_code_review_content` trait method and `ServerApi` impl, following the pattern of `generate_shared_block_title`
- `app/src/util/git.rs:445-527``MAX_DIFF_CHARS_FOR_AI`, `MAX_UNTRACKED_FILE_BYTES`, `BINARY_CHECK_BYTES`, `MAX_PR_TITLE_BYTES`, `truncate_on_char_boundary`, `get_diff_for_commit_message`
- `app/src/util/git.rs:677-721``get_diff_for_pr`, `get_branch_commit_messages`
- `app/src/util/git.rs:723-798``create_pr(repo_path, Option<&str>, Option<&str>)` with `--fill` fallback when title/body are `None`; `sanitize_pr_title` helper
- `app/src/code_review/git_dialog/commit.rs:66-70` — placeholder constants
- `app/src/code_review/git_dialog/commit.rs:239-305``generate_commit_message` (open-time)
- `app/src/code_review/git_dialog/commit.rs:313-319``is_ready_to_confirm`
- `app/src/code_review/git_dialog/commit.rs:362-473``start_confirm` (PR title/body gen for `CommitAndCreatePr`)
- `app/src/code_review/git_dialog/pr.rs:101-174``start_confirm` + `create_pr_with_ai_content` (shared helper used by both standalone PR and `CommitAndCreatePr`; parallelizes title/body with `futures::try_join!`, falls back to `--fill` on AI failure)
- `app/src/code_review/git_dialog/mod.rs:484-496``refresh_confirm_enabled` call site updated for the new `is_ready_to_confirm` signature
- Server side: `warp-server/router/handlers/generate_code_review_content.go` (already deployed)
## Current state
Before this branch:
- `commit.rs` required a typed commit message; placeholder was `"Leave blank to autogenerate a commit message"` but there was no autogeneration wired — the confirm was disabled on empty.
- `pr.rs::start_confirm` called `create_pr(&repo_path)``gh pr create --fill`.
- `commit.rs::start_confirm` `CommitAndCreatePr` branch likewise called `create_pr(&repo_path)`.
- `BlockClient` only had `generate_shared_block_title` as an AI-adjacent method.
- `util/git.rs` had no diff-for-AI helpers.
The dialog parent (`GitDialog`) owns chrome (title, buttons, loading state) and each mode owns its own state, body renderer, and confirm async; events collapse to `Completed | Cancelled`. That contract is preserved by this branch — all new work lives inside the existing per-mode submodules.
## Proposed changes
### 1. `generate_code_review_content` module (`app/src/ai/generate_code_review_content/`)
Mirrors the shape of `generate_block_title/`: a `mod.rs` that declares `pub(crate) mod api;` and an `api.rs` with request/response types.
```rust path=null start=null
pub enum OutputType {
CommitMessage,
PrTitle,
PrDescription,
}
pub struct GenerateCodeReviewContentRequest {
pub output_type: OutputType,
pub diff: String,
#[serde(skip_serializing_if = "String::is_empty", default)]
pub branch_name: String,
#[serde(skip_serializing_if = "Vec::is_empty", default)]
pub commit_messages: Vec<String>,
}
pub struct GenerateCodeReviewContentResponse {
pub content: String,
}
```
A single endpoint + request type is enough because all three output types share the same inputs (diff, optional branch name, optional commit subjects). The server dispatches on `output_type`.
### 2. `BlockClient::generate_code_review_content`
Added alongside `generate_shared_block_title` in `app/src/server/server_api/block.rs`. The `ServerApi` implementation POSTs to `{server_root_url}/ai/generate_code_review_content` with bearer auth, JSON body, and JSON response decoding — same skeleton as `generate_shared_block_title`. Reusing `BlockClient` keeps this off the GraphQL path (which would require a new mutation and cynic codegen) and matches where block-title gen already lives.
### 3. Diff helpers in `app/src/util/git.rs`
Four module-scope consts (`MAX_DIFF_CHARS_FOR_AI = 16_000`, `MAX_UNTRACKED_FILE_BYTES = 4_000`, `BINARY_CHECK_BYTES = 1_024`, `MAX_PR_TITLE_BYTES = 200`) plus a `truncate_on_char_boundary` helper and three git-diff helpers, all `#[cfg(feature = "local_fs")]` with wasm stubs to match existing conventions in the file. All byte-length truncation uses `truncate_on_char_boundary` to avoid UTF-8 panics on diffs/source files containing non-ASCII text.
- `get_diff_for_commit_message(repo_path, include_unstaged) -> Result<String>`
- `git diff HEAD` when `include_unstaged`, else `git diff --cached`.
- When `include_unstaged`, iterates `git ls-files --others --exclude-standard -z` (NUL-separated to survive paths with spaces/non-ASCII), skips binaries via `warp_util::file_type::is_buffer_binary(&bytes[..BINARY_CHECK_BYTES])`, and appends synthetic unified-diff hunks for each new file (capped at `MAX_UNTRACKED_FILE_BYTES`) so the LLM sees new-file-only commits.
- Final output truncated at `MAX_DIFF_CHARS_FOR_AI` with `\n... (diff truncated)` marker.
- `get_diff_for_pr(repo_path) -> Result<String>`
- Diffs `{base}..origin/{current}` when `git rev-parse --verify origin/{current}` succeeds, else `{base}..HEAD`.
- Same truncation rule as above.
- `get_branch_commit_messages(repo_path) -> Result<Vec<String>>`
- `git log {base}..HEAD --format=%s`, one subject per vec element.
### 4. `create_pr` signature change
`create_pr(repo_path, title: Option<&str>, body: Option<&str>) -> Result<PrInfo>` replaces `create_pr(repo_path)`. When both fields are `Some`, invokes `gh pr create --title <t> --body <b>` (title passes through `sanitize_pr_title` to first-line and cap at `MAX_PR_TITLE_BYTES` — GitHub silently collapses newlines in titles otherwise). When either is `None`, falls back to `gh pr create --fill`, used as a last-resort source when AI title/body generation fails so the PR is still created. Both wasm stub and both call sites (`pr.rs` and `commit.rs`) updated.
### 5. Commit-dialog open-time autogen (`commit.rs`)
Two placeholder constants in `commit.rs`:
- `GENERATING_PLACEHOLDER_TEXT = "Generating commit message…"` (shown while gen is in flight; was `"Leave blank to autogenerate a commit message"`).
- `FALLBACK_PLACEHOLDER_TEXT = "Type a commit message"` (shown after gen resolves, success or failure).
A new private `generate_commit_message(repo_path, branch_name, include_unstaged, ctx)` fires from `new_state` at dialog construction. It:
1. Awaits `get_diff_for_commit_message` and `block_client.generate_code_review_content(CommitMessage, ...)`.
2. On success: if the editor is still empty (`!buffer_text.trim().is_empty()` is false), `editor.system_reset_buffer_text(generated.trim(), ctx)`; otherwise discards. Placeholder swaps to `FALLBACK_PLACEHOLDER_TEXT`. `refresh_confirm_enabled`.
3. On failure: placeholder swaps to `FALLBACK_PLACEHOLDER_TEXT`, `refresh_confirm_enabled`. No toast — the empty editor plus placeholder already communicate that no draft arrived, and the failure isn't retryable. `log::warn!` for the underlying error.
No `is_autogenerating` field on `CommitState`. Confirm enablement is purely `!file_changes.is_empty() && commit_message(state, app).is_some()`; while gen is in flight the buffer is empty, so this is false naturally.
### 6. Confirm-time PR gen: shared `create_pr_with_ai_content` helper
Both flows (standalone `pr::start_confirm` and the `CommitAndCreatePr` branch of `commit::start_confirm`) delegate to `pr::create_pr_with_ai_content(repo_path, branch_name, block_client)`:
1. `get_diff_for_pr(repo_path)`.
2. `get_branch_commit_messages(repo_path)` (wrapped in `.unwrap_or_default()` — commit subjects are advisory).
3. Parallel `block_client.generate_code_review_content` calls (`PrTitle` + `PrDescription`) via `futures::try_join!`, both sharing the same `diff`, `branch_name`, and `commit_messages`.
4. On AI success: `create_pr(&repo_path, Some(&pr_title), Some(&pr_body))`.
5. On AI failure (either call): `log::warn!` and fall back to `create_pr(&repo_path, None, None)` so the PR still gets created via `gh pr create --fill`.
Non-AI errors (diff fetch, `gh pr create` itself) bubble via `?` into the existing `Err` handler, which logs and calls `show_toast(user_facing_git_error(&err.to_string()), ctx)`. AI errors no longer reach that path since they're converted to the `--fill` fallback.
### 7. `is_ready_to_confirm` simplification
Before: `(state, app)` → required non-empty file changes AND non-empty commit message.
Interim (mid-branch): dropped `app`, added `is_autogenerating` flag, made message optional.
Final: `(state, app)` → required non-empty file changes AND non-empty commit message again. The `is_autogenerating` flag is gone; the empty-buffer state during gen is what gates confirm.
`start_confirm` is correspondingly simplified: `let Some(message) = commit_message(state, ctx) else { return; };` as a defensive guard (handles keyboard-shortcut dispatch that bypasses the button's disabled state), then straight into `run_commit`. The previously-added confirm-time AI fallback branch is deleted.
## End-to-end flows
### Commit message autogeneration
```mermaid
sequenceDiagram
participant User
participant CommitDialog as commit.rs
participant Git as util/git.rs
participant AI as BlockClient
User->>CommitDialog: Open dialog
CommitDialog->>CommitDialog: placeholder = "Generating…"; confirm disabled
CommitDialog->>Git: get_diff_for_commit_message
Git-->>CommitDialog: diff (≤ 16k chars, + synthesised untracked files)
CommitDialog->>AI: generate_code_review_content(CommitMessage)
alt success
AI-->>CommitDialog: draft
CommitDialog->>CommitDialog: if editor empty, insert draft<br/>placeholder = "Type a commit message"
CommitDialog->>User: editor populated → confirm enabled
else failure
AI-->>CommitDialog: error
CommitDialog->>CommitDialog: placeholder = "Type a commit message"<br/>log::warn + refresh_confirm_enabled
CommitDialog->>User: blank editor → confirm still disabled
end
```
### PR title/body autogeneration (both flows)
```mermaid
sequenceDiagram
participant User
participant Dialog as pr.rs / commit.rs
participant Git as util/git.rs
participant AI as BlockClient
participant GH as gh CLI
User->>Dialog: Click Create PR (or Commit and create PR)
Dialog->>Dialog: set_loading("Creating…" or intent loading label)
note over Dialog,Git: CommitAndCreatePr also runs run_commit + run_push first
Dialog->>Git: get_diff_for_pr
Git-->>Dialog: diff
Dialog->>Git: get_branch_commit_messages
Git-->>Dialog: commit subjects
Dialog->>AI: generate_code_review_content(PrTitle)
AI-->>Dialog: title
Dialog->>AI: generate_code_review_content(PrDescription)
AI-->>Dialog: body
Dialog->>GH: gh pr create --title --body
GH-->>Dialog: PrInfo | error
Dialog->>User: success toast with Open PR link | friendly error toast
```
### State machine for the commit message editor
```mermaid
stateDiagram-v2
[*] --> Generating
Generating --> Populated: gen success && editor empty
Generating --> Failed: gen error or empty response
Generating --> UserTyped: user types during gen
Populated --> UserTyped: user edits
Populated --> Empty: user clears
Failed --> UserTyped: user types
Empty --> UserTyped: user types
UserTyped --> Empty: user clears
Populated --> [*]: confirm
UserTyped --> [*]: confirm
Failed --> [*]: cancel
Empty --> [*]: cancel
note right of Failed
placeholder = "Type a commit message"
no toast (silent)
confirm disabled
end note
note left of Empty
placeholder = "Type a commit message"
confirm disabled
end note
```
## Risks and mitigations
**AI latency blocks the user.** Commit message gen runs at open time in the background, so the user can start typing immediately; gen results are discarded if the user has typed anything. PR title/body gen runs at confirm time, which does extend the `Creating…` loading phase by two sequential AI calls. Mitigation is bounded by server SLA; a failure simply surfaces the existing friendly error toast.
**AI errors don't surface to the user.** AI-generation errors are caught inside `create_pr_with_ai_content` and transparently fall back to `gh pr create --fill`, so the user sees a successfully-created PR with latest-commit-derived copy rather than an error toast. Only non-AI errors (diff fetch, `gh pr create` itself) still flow through `user_facing_git_error`, which doesn't know about them specifically and maps them to the generic git fallback. Improving that mapping is an explicit follow-up.
**Sequential PR gen calls double the latency.** Addressed in-branch: `create_pr_with_ai_content` now runs `PrTitle` + `PrDescription` concurrently via `futures::try_join!`. The diff payload is cloned across both requests but latency is bounded by the slower of the two.
**`CommitAndCreatePr` chain can leave the branch pushed but PR not created.** If PR creation fails after `run_commit` + `run_push` (non-AI failure — AI failures now fall back to `--fill`), the commit and push are real but no PR exists. The header button transitions to the `CreatePr` state on the next diff-metadata refresh, so the user can retry via the standalone dialog. Documented in the product spec; no code-level mitigation in-branch.
**Duplicate PR title/body gen code.** The ~25-line PR title + PR body generation block appears verbatim in both `commit.rs::start_confirm` (`CommitAndCreatePr` branch) and `pr.rs::start_confirm`. An extracted helper would sit naturally in `git_dialog/mod.rs`. Deferred to follow-up.
**Privacy / opt-out.** `GitOperationsInCodeReview` gates the UI, but does not address AI-specific privacy concerns (sending diffs to an LLM). See the TODO at the top of `app/src/ai/generate_code_review_content/mod.rs` — follow-up work needs to add an `AISettings` toggle (mirroring `is_shared_block_title_generation_enabled`) and a customer-type guard that excludes Enterprise unless on Warp plan or dogfood, matching the pattern in `terminal/share_block_modal.rs::should_send_title_gen_request`.
## Testing and validation
No automated tests added — the parent branches (APP-3920, APP-3922) also ship without tests and the `git_dialog` module has no test harness yet. Manual validation covers each path in the product spec's **Validation** section.
When we add a test harness, the highest-value targets are:
- `is_ready_to_confirm` transitions through the editor states (empty → typed → cleared).
- `generate_commit_message`'s "user typed before response landed → discard" path.
- `get_diff_for_commit_message` truncation and untracked-file synthesis.
## Follow-ups
- Add `AISettings::is_commit_message_generation_enabled` (or similar) and wire it through `generate_commit_message` and `create_pr_with_ai_content`. Mirror `share_block_modal.rs::should_send_title_gen_request`.
- Add customer-type guard for Enterprise users (allow Warp plan + dogfood, deny otherwise).
- Route AI errors to dedicated toast copy instead of the git-error mapper. Options explored: typed-error marker + `downcast_ref` (clean but heavier) vs. fall-through-on-unknown-error (simpler but changes behavior for unknown git errors). Pick one and implement.
- Extract the `origin/{current}`-or-HEAD resolution helper — duplicated between `get_branch_diff_entries` (APP-3922) and `get_diff_for_pr` (this branch).
- Consider a regenerate button for the commit message draft, and a preview/edit UI for PR title/body before `gh pr create` fires.
- Update PR #23945 title and description to cover all three generated fields (currently title only mentions commit messages).