first pass of merging in warp (doesn't build)

This commit is contained in:
Ryan Ward
2026-07-01 16:08:58 -05:00
parent 2f64909469
commit 4770ac06b5
3662 changed files with 414574 additions and 89772 deletions
+109
View File
@@ -0,0 +1,109 @@
# REMOTE-1318: Tech spec
See `PRODUCT.md` for user-facing behavior.
## Context
The command denylist controls which commands the agent must ask permission to execute. Two sources can contribute entries: the organization (workspace) and the user's execution profile.
**Current data flow:**
- `AiAutonomySettings.execute_commands_denylist: Option<Vec<AgentModeCommandExecutionPredicate>>` — org override, from `workspace.rs (663-670)`.
- `AIExecutionProfile.command_denylist: Vec<AgentModeCommandExecutionPredicate>` — per-profile user list, from `execution_profiles/mod.rs (233)`.
- `get_execute_commands_denylist_for_profile()` in `permissions.rs (395-413)` — resolves the effective list. Currently `unwrap_or_else`: org **replaces** user.
- `is_command_denylist_editable()` in `ai.rs (1730-1736)` — returns `false` when org override exists, disabling both the editor and all row remove buttons.
**Current UI rendering:**
- `render_list_section()` in `ui_helpers.rs (640-691)` — generic list renderer for the profile editor. Passes a single `is_editable` bool; when false, wraps the **entire** section in a workspace tooltip via `wrap_disabled_with_workspace_override_tooltip`.
- `render_command_denylist()` in `ai_page.rs (4483-4513)` — legacy settings page denylist. Same pattern: passes `!is_command_denylist_editable(app)` as a single `disabled` flag.
- `render_input_list()` in `settings_page.rs (1013-1056)` — renders the editor + item rows. Takes a single `disabled: bool` for all rows.
- `InputListItem` in `settings_page.rs (1003-1007)``{ item, mouse_state_handle, on_remove_action }`. No per-item disabled state.
- Mouse state handles are stored as `Vec<MouseStateHandle>` per list (one per row, for close buttons). Recreated when the list changes.
**Other callers of `render_input_list`** (must stay backward-compatible): `ai_page.rs` (allowlist, directory allowlist, MCP lists), `ui_helpers.rs` (all list types via `render_list_section`), `update_environment_form.rs` (environment setup commands).
## Proposed changes
### 1. Merge denylist in `permissions.rs`
Change `get_execute_commands_denylist_for_profile()`:
```
When org override is Some(org_list):
merged = org_list
for each item in user_profile.command_denylist:
if merged does not contain item:
merged.push(item)
return merged
When org override is None:
return user_profile.command_denylist (unchanged)
```
Add `pub fn get_org_execute_commands_denylist(ctx: &AppContext) -> Vec<AgentModeCommandExecutionPredicate>` — returns just the org entries (or empty vec). Used by rendering code to determine which rows are org-owned.
### 2. Per-item disabled state in `InputListItem` (`settings_page.rs`)
Add one field:
- `is_disabled: bool` — whether the remove button is disabled and text uses disabled color.
Remove the `disabled: bool` parameter from `render_input_list`. Each row uses `item.is_disabled` instead.
Keep `wrap_disabled_with_workspace_override_tooltip` in `ui_helpers.rs` — it is not needed by `render_input_list`. Tooltip wrapping is handled at the call site (see sections 4 and 5 below).
Update all 6 callers of `render_input_list`:
- Non-denylist callers: set `is_disabled` uniformly based on the existing global disabled logic.
- Denylist callers: set per-item `is_disabled` based on org membership.
### 3. Always enable denylist editor
`is_command_denylist_editable()` in `ai.rs`: remove the `has_override_for_execute_commands_denylist()` check. Return `self.is_any_ai_enabled(app)` only.
In `editor/mod.rs`, `update_all_editor_interaction_states()` (line 1269): change the denylist editor line to enable whenever AI is on (drop the `&& !ai_autonomy_settings.has_override_for_execute_commands_denylist()` condition).
In `ai_page.rs`, two handlers update the denylist editor state:
- `TeamsChanged` handler (line 474): same change.
- `IsAnyAIEnabled` handler (line 891): same change.
### 4. Profile editor denylist rendering (`ui_helpers.rs` + `editor/mod.rs`)
Add `command_denylist_tooltip_mouse_state_handles: Vec<MouseStateHandle>` to `ExecutionProfileEditorView`. Create alongside existing `command_denylist_mouse_state_handles` in `update_mouse_state_handles()`, one per merged-list item (used only for org rows).
Update `render_command_denylist_section()` in `ui_helpers.rs`:
- Get org denylist via `BlocklistAIPermissions::get_org_execute_commands_denylist(app)`
- Build rows directly using `render_alternating_color_list_item` (not via `render_list_section`) so each disabled org row can be individually wrapped with `wrap_disabled_with_workspace_override_tooltip`, which stays in `ui_helpers.rs`
- Always show the editor (regardless of org override)
`render_list_section()` is unchanged — non-denylist lists still use its existing `is_editable` + whole-list tooltip pattern.
### 5. Legacy settings page denylist rendering (`ai_page.rs`)
Add `command_denylist_tooltip_mouse_state_handles: Vec<MouseStateHandle>` to `AISettingsPageView`.
Update `render_command_denylist()`:
- Get org denylist
- Build `InputListItem`s with per-item `is_disabled` based on org membership
- Wrap each disabled row in `wrap_disabled_with_workspace_override_tooltip` (imported from `crate::ai::execution_profiles::editor::ui_helpers`)
- Always pass the editor
Update mouse state handle creation in `new()` and `AISettingsChangedEvent::AgentModeCommandExecutionDenylist` handler to size both handle Vecs to the merged list.
### 6. Profile data event handling
In `ExecutionProfileEditorView`, subscribe to `UserWorkspacesEvent::TeamsChanged` to refresh mouse state handles and denylist rendering when the org override changes at runtime.
## Testing and validation
**Unit tests** — extend `permissions_test.rs`:
- Test that `get_execute_commands_denylist_for_profile` returns the union when org override is set (PRODUCT.md invariant 1).
- Test deduplication: user entry matching org entry appears once (invariant 2).
- Test merged list is used for execution checks (invariant 3).
- Test cross-profile: org denylist applies to all profiles (invariant 4).
- Test no-override path is unchanged (invariant 5).
- Test empty org override (`Some([])`) still allows user entries (invariant 15).
- Test `get_org_execute_commands_denylist` returns the right entries.
**Manual verification:**
- With an org denylist override active: verify the editor is enabled, org rows show disabled × and tooltip, user rows are removable.
- Without an org override: verify unchanged behavior.
- Add a user entry that duplicates an org entry: verify it doesn't appear twice.
- Remove a user entry: verify it disappears and remaining rows render correctly.
**Presubmit:** `cargo fmt` + `cargo clippy` must pass.