Files
galaxy/specs/APP-4118/TECH.md
T

115 lines
12 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# APP-4118: Toggle AI auto-gen + Enterprise gate — Tech Spec
Product spec: `specs/APP-4118/PRODUCT.md`
Parent: APP-3918 (git operations button)
## Context
APP-3918 landed the git-operations split button and wired three dialogs (Commit, Push, Create PR) plus two chains (`CommitAndPush`, `CommitAndCreatePr`). Every AI-backed path in those flows is gated only on `FeatureFlag::GitOperationsInCodeReview`; none of them respect `AISettings::is_any_ai_enabled` or enterprise status. This spec mirrors the `share_block_modal.rs::should_send_title_gen_request` pattern: a dedicated per-feature AI setting, plus a shared `should_send_git_ops_ai_request` helper that folds the feature flag, the per-feature AI toggle, and an enterprise check (with Warp-plan exception and dogfood override) together and routes every AI call site in the git-operations flow through it.
### Relevant code
- `app/src/code_review/git_dialog/commit.rs:116-120` — chooses `GENERATING_PLACEHOLDER_TEXT` vs `FALLBACK_PLACEHOLDER_TEXT` based on the feature flag only.
- `app/src/code_review/git_dialog/commit.rs:209-222` — kicks off `generate_commit_message` on dialog open when there are changes.
- `app/src/code_review/git_dialog/commit.rs:260-320``generate_commit_message` helper; unconditionally calls `code_review_ai.generate_code_review_content`.
- `app/src/code_review/git_dialog/commit.rs:385-396``CommitAndCreatePr` chain calls `create_pr_with_ai_content`.
- `app/src/code_review/git_dialog/pr.rs:101-126` — standalone Create PR confirm calls `create_pr_with_ai_content`.
- `app/src/code_review/git_dialog/pr.rs:134-184``create_pr_with_ai_content` issues two `generate_code_review_content` requests (title + body) and falls back to `gh pr create --fill` on failure / empty content.
- `app/src/settings/ai.rs:1468-1477``AISettings::is_any_ai_enabled` (already folds in auth + remote-session org policy).
- `app/src/settings/ai.rs:815-823``shared_block_title_generation_enabled_internal` setting, shape we are copying for the new `git_operations_autogen_enabled_internal`.
- `app/src/settings/ai.rs:1536-1538``is_shared_block_title_generation_enabled` getter, shape we are copying for the new `is_git_operations_autogen_enabled`.
- `app/src/terminal/share_block_modal.rs:1161-1174``should_send_title_gen_request`, the AI-title-gen gate we are mirroring (`is_active_ai_enabled` via per-feature getter, enterprise check with Warp-plan exception, dogfood override).
- `app/src/workspaces/workspace.rs:562``BillingMetadata::is_warp_plan()` accessor used by the Warp-plan exception.
- `app/src/workspaces/user_workspaces.rs``UserWorkspaces`, `current_team` accessor.
- `crates/graphql/src/api/workspace.rs``CustomerType` enum definition (reached via `billing_metadata`).
## Current state
The commit dialog opens, sets the `"Generating…"` placeholder, fires a diff-build plus a `generate_code_review_content` call, and swaps the placeholder + writes the generated text when the future resolves. Failure and empty-content cases already exist and are well-tested: both fall back to `FALLBACK_PLACEHOLDER_TEXT` with an empty editor. The PR flow already handles AI failure via `gh pr create --fill`. In other words: the "skip AI entirely" code paths already exist — we just need to take them deterministically when the gate is closed.
## Proposed changes
### 1. New per-feature AI setting
Add `git_operations_autogen_enabled_internal` to the `define_settings_group!(AISettings, ...)` block in `app/src/settings/ai.rs`, matching the shape of `shared_block_title_generation_enabled_internal` (`ai.rs:815-823`):
```rust path=null start=null
git_operations_autogen_enabled_internal: GitOperationsAutogenEnabled {
type: bool,
default: true,
supported_platforms: SupportedPlatforms::ALL,
sync_to_cloud: SyncToCloud::Globally(RespectUserSyncSetting::Yes),
private: false,
toml_path: "agents.oz.active_ai.git_operations_autogen_enabled",
description: "Controls whether AI auto-generates commit messages and PR title/body in the code review dialogs.",
}
```
And the getter on `AISettings`, matching `is_shared_block_title_generation_enabled` (`ai.rs:1547-1549`):
```rust path=null start=null
pub fn is_git_operations_autogen_enabled(&self, app: &AppContext) -> bool {
self.is_active_ai_enabled(app) && *self.git_operations_autogen_enabled_internal
}
```
`is_active_ai_enabled` composes `is_any_ai_enabled` (global AI toggle + auth + remote-session org policy) with the active-AI internal toggle and `AppExecutionMode::allows_active_ai()`, so the new getter transitively inherits all of those checks.
Expose it in the AI settings page (`app/src/settings_view/ai_page.rs`) alongside the existing per-feature toggles.
### 2. New helper: `should_send_git_ops_ai_request`
Location: `app/src/code_review/git_dialog/mod.rs` (new private helper, co-located with the other shared git-dialog utilities like `show_toast` and `user_facing_git_error`). Structure copied from `share_block_modal.rs::should_send_title_gen_request` (`share_block_modal.rs:1161-1174`):
```rust path=null start=null
fn should_send_git_ops_ai_request(app: &AppContext) -> bool {
FeatureFlag::GitOperationsInCodeReview.is_enabled()
&& AISettings::as_ref(app).is_git_operations_autogen_enabled(app)
&& (!UserWorkspaces::as_ref(app)
.current_team()
.is_some_and(|team| team.billing_metadata.customer_type == CustomerType::Enterprise)
// Allow the Warp Stable team to use this.
|| UserWorkspaces::as_ref(app)
.current_team()
.is_some_and(|team| team.billing_metadata.is_warp_plan())
// Override the enterprise check for dogfood builds, as our dogfood team
// is an enterprise team.
|| ChannelState::channel().is_dogfood())
}
```
Shared helper (vs inlining like `share_block_modal.rs`) because we call it from both `commit::new_state` and `pr::new_state`, and keeping the enterprise / Warp-plan / dogfood details out of `commit.rs` and `pr.rs` avoids pulling billing + channel imports into those files.
### 2a. Gate evaluation
`should_send_git_ops_ai_request` is called directly at each decision point rather than snapshotted onto dialog state. In `commit::new_state` it is called once to set the initial placeholder and decide whether to kick off open-time autogen. In `commit::start_confirm` and `pr::start_confirm` it is called again at confirm time to decide whether to materialize a `code_review_ai` handle. This is consistent with how the sibling `should_send_title_gen_request` is used.
### 3. Gate `commit.rs` open-time autogen
In `commit::new_state`, read `ai_autogen_enabled = should_send_git_ops_ai_request(ctx)` once and use it for both:
- `initial_placeholder` becomes `GENERATING_PLACEHOLDER_TEXT` only when `ai_autogen_enabled`; otherwise `FALLBACK_PLACEHOLDER_TEXT`.
- The `generate_commit_message` call inside the spawn's resolution is only issued when `ai_autogen_enabled && has_changes`. When the gate is closed (or there are no changes), the spawn resolution skips autogen and the editor stays on `FALLBACK_PLACEHOLDER_TEXT`.
No changes to `generate_commit_message` itself — it is unreachable when the gate is closed.
### 4. Gate `pr.rs::start_confirm`
`start_confirm` calls `should_send_git_ops_ai_request(ctx)` directly and branches:
- AI path: `create_pr_with_ai_content` with the `code_review_ai` handle.
- Fallback path: `create_pr(repo_path, None, None)` (`gh pr create --fill`).
The `code_review_ai` handle is only materialized on the AI branch so we do not hold an unused handle in the AI-off path. Existing success/error handling is unchanged — both branches return `Result<PrInfo>`.
### 5. Gate the `CommitAndCreatePr` chain
`commit::start_confirm` calls `should_send_git_ops_ai_request(ctx)` and uses the result to decide whether to materialize `code_review_ai`. The `Option<Arc<dyn AIClient>>` is captured into the `ctx.spawn` async body; the `CommitAndCreatePr` arm matches on it:
```rust path=null start=null
CommitIntent::CommitAndCreatePr => {
run_push(&repo_path, &branch_name).await?;
let pr = match code_review_ai {
Some(ai) => create_pr_with_ai_content(&repo_path, &branch_name, ai.as_ref()).await?,
None => create_pr(&repo_path, None, None).await?,
};
CommitOutcome::PrCreated(pr)
}
```
### 6. Imports
The helper in `git_dialog/mod.rs` needs:
- `warp_core::features::FeatureFlag`
- `crate::settings::AISettings`
- `crate::workspaces::user_workspaces::UserWorkspaces`
- `CustomerType` (match the path used by `share_block_modal.rs:32`)
- `warp_core::channel::ChannelState` (match the path used by `share_block_modal.rs`)
`commit.rs` and `pr.rs` each import `should_send_git_ops_ai_request` from `super`. `commit::new_state` calls it to set the initial placeholder; `commit::start_confirm` and `pr::start_confirm` each call it again at confirm time.
## Testing and validation
No unit tests are added for this change. Validation is manual; maps to Behavior invariants 2, 49, 1214:
- With AI enabled + non-enterprise: commit / push / PR flows unchanged from APP-3918 (no regressions). Covered by re-running APP-3918's manual validation list.
- Toggle `is_any_ai_enabled` off → open Commit dialog → editor shows `"Type a commit message"` immediately, no `"Generating…"`, no AI request in the network log. (Invariants 46, 10.)
- Still off → confirm a typed message → commit succeeds. `Commit and push` chain with a typed message → commit + push succeed. (Invariants 7, 11.)
- Still off → `Commit and create PR` with a typed message → PR gets created with the latest commit's subject/body (via `--fill`). (Invariant 8.)
- Still off → open Create PR dialog → confirm → PR gets created via `--fill`. (Invariant 9.)
- Enterprise user (AI on): same behaviors as AI-off. (Invariant 1, second clause.)
- Toggle AI off *while* the dialog is open mid-`"Generating…"`: the dialog keeps its in-flight state; close and reopen → now behaves as AI-off. (Invariants 12, 14.)
### Verification aid
The existing log line `"Failed to autogenerate commit message"` at `commit.rs:310` is a useful canary: it must not fire in AI-off/enterprise runs of the manual validation (because no request is attempted). Invariant 10 depends on this.
## Risks and mitigations
- **Drift between commit-message autogen and PR-creation gate decisions**: each call to `should_send_git_ops_ai_request` evaluates the same predicate, so open-time autogen (evaluated in `new_state`) and confirm-time PR creation (evaluated in `start_confirm`) both reflect the current AI state at the time they run. Invariant 14a (gate evaluated at dialog-open) holds for the initial placeholder/autogen decision; confirm re-evaluates but in practice the user cannot change AI state between open and confirm in normal usage.
- **Enterprise detection via `current_team()` returns `None`**: if a signed-in user has no current team, `is_some_and` yields `false` for both the enterprise branch and the Warp-plan branch. The enterprise-guard's outer negation means no-team users pass the gate — same behavior as `should_send_title_gen_request`.
- **Dogfood override on production builds**: `ChannelState::channel().is_dogfood()` is false on Preview/Stable, so the override only fires where it is intended.
- **Per-feature setting drift**: new `git_operations_autogen_enabled_internal` setting needs to be exposed in the AI settings page alongside the other per-feature toggles — easy to forget. Covered under §1.
## Follow-ups
- Consider adding the same enterprise check to `code_review_view.rs:7104`'s `"Add diff set as context"` gate if product wants a uniform enterprise AI posture across code review (Invariant 15 explicitly scopes this out for now).
- If telemetry ever needs to distinguish "AI skipped because disabled" from "AI attempted and failed", add an event in the gate's `false` branch — not required by this spec.
- If other code-review AI call sites (e.g. future review-comment summarization) land, reuse `should_send_git_ops_ai_request` or a sibling helper rather than inlining the enterprise-plus-Warp-plan-plus-dogfood clause again.