12 KiB
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— choosesGENERATING_PLACEHOLDER_TEXTvsFALLBACK_PLACEHOLDER_TEXTbased on the feature flag only.app/src/code_review/git_dialog/commit.rs:209-222— kicks offgenerate_commit_messageon dialog open when there are changes.app/src/code_review/git_dialog/commit.rs:260-320—generate_commit_messagehelper; unconditionally callscode_review_ai.generate_code_review_content.app/src/code_review/git_dialog/commit.rs:385-396—CommitAndCreatePrchain callscreate_pr_with_ai_content.app/src/code_review/git_dialog/pr.rs:101-126— standalone Create PR confirm callscreate_pr_with_ai_content.app/src/code_review/git_dialog/pr.rs:134-184—create_pr_with_ai_contentissues twogenerate_code_review_contentrequests (title + body) and falls back togh pr create --fillon 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_internalsetting, shape we are copying for the newgit_operations_autogen_enabled_internal.app/src/settings/ai.rs:1536-1538—is_shared_block_title_generation_enabledgetter, shape we are copying for the newis_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_enabledvia 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_teamaccessor.crates/graphql/src/api/workspace.rs—CustomerTypeenum definition (reached viabilling_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_placeholderbecomesGENERATING_PLACEHOLDER_TEXTonly whenai_autogen_enabled; otherwiseFALLBACK_PLACEHOLDER_TEXT.- The
generate_commit_messagecall inside the spawn's resolution is only issued whenai_autogen_enabled && has_changes. When the gate is closed (or there are no changes), the spawn resolution skips autogen and the editor stays onFALLBACK_PLACEHOLDER_TEXT. No changes togenerate_commit_messageitself — 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_contentwith thecode_review_aihandle. - Fallback path:
create_pr(repo_path, None, None)(gh pr create --fill). Thecode_review_aihandle 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 returnResult<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::FeatureFlagcrate::settings::AISettingscrate::workspaces::user_workspaces::UserWorkspacesCustomerType(match the path used byshare_block_modal.rs:32)warp_core::channel::ChannelState(match the path used byshare_block_modal.rs)commit.rsandpr.rseach importshould_send_git_ops_ai_requestfromsuper.commit::new_statecalls it to set the initial placeholder;commit::start_confirmandpr::start_confirmeach 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, 4–9, 12–14:
- 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_enabledoff → open Commit dialog → editor shows"Type a commit message"immediately, no"Generating…", no AI request in the network log. (Invariants 4–6, 10.) - Still off → confirm a typed message → commit succeeds.
Commit and pushchain with a typed message → commit + push succeed. (Invariants 7, 11.) - Still off →
Commit and create PRwith 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_requestevaluates the same predicate, so open-time autogen (evaluated innew_state) and confirm-time PR creation (evaluated instart_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()returnsNone: if a signed-in user has no current team,is_some_andyieldsfalsefor both the enterprise branch and the Warp-plan branch. The enterprise-guard's outer negation means no-team users pass the gate — same behavior asshould_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_internalsetting 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
falsebranch — 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_requestor a sibling helper rather than inlining the enterprise-plus-Warp-plan-plus-dogfood clause again.