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

12 KiB
Raw Blame History

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-320generate_commit_message helper; unconditionally calls code_review_ai.generate_code_review_content.
  • app/src/code_review/git_dialog/commit.rs:385-396CommitAndCreatePr 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-184create_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-1477AISettings::is_any_ai_enabled (already folds in auth + remote-session org policy).
  • app/src/settings/ai.rs:815-823shared_block_title_generation_enabled_internal setting, shape we are copying for the new git_operations_autogen_enabled_internal.
  • app/src/settings/ai.rs:1536-1538is_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-1174should_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:562BillingMetadata::is_warp_plan() accessor used by the Warp-plan exception.
  • app/src/workspaces/user_workspaces.rsUserWorkspaces, current_team accessor.
  • crates/graphql/src/api/workspace.rsCustomerType 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):

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):

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):

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:

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.