first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,71 @@
|
||||
# Auth Secret Deletion From Selector — Product Spec
|
||||
|
||||
## Summary
|
||||
|
||||
Users can delete Warp-managed harness auth secrets directly from the auth secret selector chip menu. The delete affordance appears as a right-aligned `X` button on each existing secret row, opens a confirmation modal, removes the secret from the server after confirmation, updates the menu state, and confirms success or failure with a toast.
|
||||
|
||||
## Figma
|
||||
|
||||
Figma: none provided. The prompt includes screenshots of the auth selector delete affordance and the requested destructive confirmation modal layout.
|
||||
|
||||
## Behavior
|
||||
|
||||
1. The feature applies to the auth secret selector chip menu shown for non-Oz cloud harnesses after auth-secret FTUX has been completed.
|
||||
|
||||
2. When the auth secret selector chip menu opens, each existing Warp-managed secret row shows:
|
||||
- The secret name on the left.
|
||||
- A right-aligned `X` button on the same row.
|
||||
- Hover, selected, spacing, icon, and text styling consistent with existing Warp menus.
|
||||
|
||||
3. The `X` button appears only on existing managed-secret rows. It does not appear on:
|
||||
- The header row.
|
||||
- The "Inherit key from environment" row.
|
||||
- Loading or error placeholder rows.
|
||||
- The "New" row.
|
||||
- Secret-type rows in the "New" sidecar submenu.
|
||||
|
||||
4. Selecting the secret row outside the `X` button preserves current behavior: the row becomes the selected auth secret for the active harness, the per-harness selection is persisted, and the menu closes.
|
||||
|
||||
5. Selecting the `X` button opens a confirmation modal for that specific secret. It must not select the secret first, must not open the "New" sidecar, and must not accidentally dispatch the row's normal select action.
|
||||
|
||||
6. The confirmation modal shows:
|
||||
- Title: "Delete secret".
|
||||
- Description: "Are you sure you want to delete {SECRET_NAME}? This action cannot be undone. Any agents or environments referencing this secret will no longer have access to it."
|
||||
- Buttons: "Cancel" and "Delete".
|
||||
- Layout and destructive styling consistent with existing Warp confirmation dialogs and the provided mock.
|
||||
|
||||
7. Choosing "Cancel", dismissing the modal, or otherwise closing it leaves the secret untouched and does not start a delete request. Choosing "Delete" starts deletion for the secret named in the modal.
|
||||
|
||||
8. Deletion is server-backed. A secret is considered deleted only after the server delete request succeeds. The UI must not present deletion as successful before the server confirms it.
|
||||
|
||||
9. While deletion is pending, the user should not be able to fire duplicate delete requests for the same harness-owned secret from the same open menu. Pending delete state must stay scoped to the deletion target rather than every same-named secret. Acceptable treatments include disabling the `X` button for that row, closing the menu, or otherwise preventing a second click.
|
||||
|
||||
10. On successful deletion:
|
||||
- The deleted secret disappears from the selector menu the next time the menu is shown, and ideally immediately if the menu remains open.
|
||||
- A success toast is shown.
|
||||
- If the deleted secret was currently selected for the active harness, the selected auth secret is cleared and the chip falls back to the no-secret/inherit label.
|
||||
- Any persisted last-selected secret for the deleted harness is cleared if it points to the deleted secret, even if the user changes active harnesses before the server response arrives.
|
||||
|
||||
11. On failed deletion:
|
||||
- The secret remains available in the selector menu.
|
||||
- The current selected secret does not change.
|
||||
- A failure toast is shown.
|
||||
- The user can retry deletion after the failure.
|
||||
|
||||
12. If the deleted secret is selected by another visible selector instance, that selector should refresh when it observes updated harness secret state. It must not keep showing a deleted secret as a valid selectable item after the secret list is refreshed.
|
||||
|
||||
13. If the secret list is stale and the server reports that the secret no longer exists, the delete attempt is treated as a failure unless the server explicitly reports the delete as successful. The failure toast should use the server/user-facing error text when available.
|
||||
|
||||
14. If the user is offline or unauthenticated when pressing "Delete" in the confirmation modal, the delete fails and shows a failure toast. The menu must not remove the secret locally unless a later server response confirms deletion.
|
||||
|
||||
15. The success and failure toasts are ephemeral, consistent with nearby app toasts, and do not require a custom action button.
|
||||
|
||||
16. Keyboard behavior must remain usable:
|
||||
- Existing keyboard navigation for selecting a secret row remains unchanged.
|
||||
- Adding the `X` button must not make the menu trap focus or prevent Escape/close behavior.
|
||||
- If the `X` button is reachable by keyboard, it must have an accessible label such as "Delete API key".
|
||||
|
||||
17. The menu layout remains stable for long secret names. Long names should truncate or shrink according to existing menu behavior rather than overlapping the right-aligned `X` button.
|
||||
|
||||
18. The feature does not add editing, renaming, creating, or bulk-deleting secrets. It only deletes one existing secret at a time from the auth selector menu.
|
||||
|
||||
@@ -0,0 +1,175 @@
|
||||
# Auth Secret Deletion From Selector — Tech Spec
|
||||
|
||||
## Context
|
||||
|
||||
This spec implements the behavior in `PRODUCT.md` for the ambient cloud-mode auth selector chip.
|
||||
|
||||
Relevant current code:
|
||||
|
||||
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:61` defines `AuthSecretSelectorAction`; it currently supports toggling the menu, selecting a secret, clearing/inheriting, opening the "New" sidecar, and selecting a new-secret type.
|
||||
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:147` subscribes the selector to `HarnessAvailabilityModel` auth-secret load/create/fetch-failure events and refreshes the menu/button.
|
||||
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:389` builds the menu. Loaded secrets are rendered as `MenuItem::Item` rows with `SelectSecret(name)` actions; the "New" row is separate and opens the sidecar.
|
||||
- `app/src/terminal/view/ambient_agent/auth_secret_selector.rs:498` handles selector actions and persists selected/cleared secrets in `CloudAgentSettings.last_selected_auth_secret`.
|
||||
- `app/src/ai/harness_availability.rs:55` stores per-harness auth secret fetch state as `AuthSecretFetchState::Loaded(Vec<AuthSecretEntry>)`.
|
||||
- `app/src/ai/harness_availability.rs:184` fetches harness auth secrets from `ManagedSecretsClient::list_harness_auth_secrets`.
|
||||
- `app/src/ai/harness_availability.rs:250` already centralizes secret creation through `ManagedSecretManager`, updates the local cache, and emits create/failure events.
|
||||
- `crates/managed_secrets/src/manager.rs:87` exposes `ManagedSecretManager::delete_secret(owner, name)`.
|
||||
- `app/src/server/server_api/managed_secrets.rs:132` wires `delete_managed_secret` to the server GraphQL mutation.
|
||||
- `app/src/menu.rs:394` defines reusable `MenuItemFields`. It already has right-side label/icon rendering, but right-side icons are decorative and do not dispatch a separate action from the row.
|
||||
- Existing destructive confirmation dialogs such as `app/src/settings_view/delete_environment_confirmation_dialog.rs` and `app/src/settings_view/mcp_servers/destructive_mcp_confirmation_dialog.rs` establish the preferred `Dialog` + `DangerPrimaryTheme` pattern for the new modal.
|
||||
|
||||
The current `AuthSecretEntry` contains only `name`, but server `ManagedSecret` results include `owner`. Deleting team-owned secrets correctly requires preserving enough owner information to call `delete_secret` with `SecretOwner::Team { team_uid }` instead of always assuming `CurrentUser`.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
1. Extend auth-secret cache entries with owner metadata.
|
||||
|
||||
Update `AuthSecretEntry` in `app/src/ai/harness_availability.rs` to include `owner: SecretOwner`. Convert `warp_graphql::object::Space` from fetched/created `ManagedSecret` values into `SecretOwner`:
|
||||
|
||||
- `SpaceType::User` -> `SecretOwner::CurrentUser`
|
||||
- `SpaceType::Team` -> `SecretOwner::Team { team_uid: space.uid.into_inner() }`
|
||||
|
||||
This keeps deletion ownership close to the existing fetch/create cache and avoids guessing owner at click time.
|
||||
|
||||
2. Add deletion to `HarnessAvailabilityModel`.
|
||||
|
||||
Add a method similar to `create_auth_secret`:
|
||||
|
||||
```rust
|
||||
pub fn delete_auth_secret(
|
||||
&mut self,
|
||||
harness: Harness,
|
||||
name: String,
|
||||
owner: SecretOwner,
|
||||
ctx: &mut ModelContext<Self>,
|
||||
)
|
||||
```
|
||||
|
||||
The method should call `ManagedSecretManager::delete_secret(owner, name.clone())`. On success, remove the matching owner/name entry from `AuthSecretFetchState::Loaded` for that harness and emit a new event. On failure, leave the cache unchanged, report the error, and emit a failure event with the harness/name/owner/error.
|
||||
|
||||
Add events:
|
||||
|
||||
- `AuthSecretDeleted { harness, name, owner }`
|
||||
- `AuthSecretDeletionFailed { harness, name, owner, error }`
|
||||
|
||||
Existing subscribers that only care about menu freshness should refresh on `AuthSecretDeleted`; subscribers that do not care should explicitly ignore both new events so match exhaustiveness remains clear.
|
||||
|
||||
3. Add a selector action for deleting a secret.
|
||||
|
||||
Extend `AuthSecretSelectorAction` with enough data to delete the row, for example:
|
||||
|
||||
```rust
|
||||
DeleteSecret {
|
||||
name: String,
|
||||
owner: SecretOwner,
|
||||
}
|
||||
```
|
||||
|
||||
Track pending deletes in `AuthSecretSelector` with a small set keyed by `(harness, name, owner)` or by a stable local row id if one is introduced. This satisfies PRODUCT behavior 9 and avoids duplicate delete requests from the same menu.
|
||||
|
||||
4. Render the right-aligned `X` affordance using the menu system's existing styling.
|
||||
|
||||
Preferred implementation: extend `MenuItemFields` with an optional right-side action, keeping the visual rendering path for `with_right_side_icon(Icon::X)` consistent with other menus while allowing the right icon to dispatch a different action than the row.
|
||||
|
||||
The right-side action should:
|
||||
|
||||
- Stop mouse event propagation so clicking `X` does not also dispatch `SelectSecret`.
|
||||
- Close the menu or disable the row while the delete request is pending.
|
||||
- Preserve the existing row action when the user clicks anywhere outside the `X`.
|
||||
|
||||
If changing `MenuItemFields` is too broad during implementation, an acceptable alternative is a custom-label row in `auth_secret_selector.rs` that composes the existing text/icon primitives and explicitly separates row-click from `X` click. Prefer the reusable menu extension only if it stays small and does not alter existing right-side icon behavior.
|
||||
|
||||
5. Add a dedicated confirmation dialog before deleting.
|
||||
|
||||
Add a small destructive dialog view under `app/src/terminal/view/ambient_agent/` that mirrors existing confirmation dialogs:
|
||||
|
||||
- Title: `Delete secret`
|
||||
- Body: `Are you sure you want to delete {SECRET_NAME}? This action cannot be undone. Any agents or environments referencing this secret will no longer have access to it.`
|
||||
- Buttons: `Cancel` and destructive `Delete`
|
||||
- Dismiss-to-cancel behavior and centered overlay placement
|
||||
|
||||
The dialog should retain a captured deletion payload containing harness, secret name, and owner. This avoids deleting the wrong target if selector state changes while the modal is open.
|
||||
|
||||
6. Wire deletion action handling in `AuthSecretSelector`.
|
||||
|
||||
In the `DeleteSecret` branch:
|
||||
|
||||
- Read the active harness from `AmbientAgentViewModel`.
|
||||
- Close the selector menu/sidecar.
|
||||
- Show the confirmation dialog with the captured deletion payload.
|
||||
|
||||
On confirmation:
|
||||
|
||||
- Mark the row pending.
|
||||
- Call `HarnessAvailabilityModel::delete_auth_secret(harness, name, owner, ctx)`.
|
||||
- Refresh the menu into a disabled pending state.
|
||||
|
||||
On cancel or dismiss:
|
||||
|
||||
- Hide the dialog.
|
||||
- Leave pending-delete state untouched and do not call the server-backed delete API.
|
||||
|
||||
In the selector's `HarnessAvailabilityModel` subscription:
|
||||
|
||||
- On `AuthSecretDeleted`, clear pending state for the matching `(harness, name, owner)` target and show a success toast when this selector initiated that exact deletion.
|
||||
- Always remove `CloudAgentSettings.last_selected_auth_secret[harness.config_name()]` if it equals the deleted name, even when the user has switched to another active harness before the server responds.
|
||||
- Only when the deleted harness is still active, clear `AmbientAgentViewModel::selected_harness_auth_secret_name()` if it matches the deleted name and refresh the visible menu/button state.
|
||||
- On `AuthSecretDeletionFailed`, clear pending state for the matching `(harness, name, owner)` target, refresh the visible menu only when that harness is active, and show a failure toast containing the user-facing error when available.
|
||||
|
||||
Use `DismissibleToast::success(...)` and `DismissibleToast::error(...)` via `ToastStack::add_ephemeral_toast`, matching nearby toast patterns.
|
||||
|
||||
7. Keep adjacent auth-secret surfaces consistent.
|
||||
|
||||
`AuthSecretFtuxDropdown`, orchestration pickers, and model selector subscribers must compile with the new deletion events. They can ignore failure events and refresh on success where they render auth secret lists. This ensures a secret deleted from the chip no longer appears in other already-mounted selectors after the model emits.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
No new helper-only unit tests are retained in this implementation pass. The following behaviors remain suitable for future targeted coverage if higher-value seams are introduced:
|
||||
|
||||
1. Cache update behavior for PRODUCT behaviors 8, 10, and 11:
|
||||
- Successful deletion removes only the matching auth secret from the loaded list and emits `AuthSecretDeleted`.
|
||||
- Failed deletion leaves the list unchanged and emits `AuthSecretDeletionFailed`.
|
||||
- Team-owned entries call delete with `SecretOwner::Team`, user-owned entries call delete with `SecretOwner::CurrentUser`.
|
||||
|
||||
2. Selector and confirmation routing for PRODUCT behaviors 4, 5, 7, 9, and 10:
|
||||
- Clicking/selecting the row still selects the secret.
|
||||
- Dispatching the delete affordance opens the confirmation dialog without deleting immediately.
|
||||
- Cancelling or dismissing the confirmation dialog does not start deletion.
|
||||
- Confirming deletion transitions into the existing pending-delete flow.
|
||||
- Duplicate confirmed delete dispatches for a pending secret are ignored.
|
||||
- Deleting the currently selected secret clears the selected model value and persisted setting.
|
||||
|
||||
3. Menu rendering coverage if the reusable `MenuItemFields` right-side action API is introduced:
|
||||
- Existing right-side icon rendering remains decorative when no right-side action is set.
|
||||
- A right-side action dispatches independently from the row action.
|
||||
|
||||
4. Manual validation against the screenshot state, if revisited later:
|
||||
- Open the auth selector chip menu with multiple secrets.
|
||||
- Confirm only existing secret rows have a right-aligned `X`.
|
||||
- Confirm the "New" row and sidecar rows do not show an `X`.
|
||||
- Click `X` and verify the destructive confirmation modal appears with the specified copy.
|
||||
- Cancel/dismiss the modal and verify the row remains untouched.
|
||||
- Confirm deletion for an unselected secret and verify it disappears plus a success toast appears.
|
||||
- Confirm deletion for the selected secret and verify the chip label falls back to no-secret/inherit state.
|
||||
- Force a delete failure/offline state and verify the failure toast appears and the row remains.
|
||||
|
||||
5. Run focused checks:
|
||||
|
||||
```sh
|
||||
cargo check -p warp
|
||||
cargo fmt -- --check
|
||||
```
|
||||
|
||||
Skip test execution for this implementation pass per request.
|
||||
|
||||
## Parallelization
|
||||
|
||||
Parallel agents are not recommended. The work is small and tightly coupled across one UI view, one shared model, and a small optional menu extension; splitting would increase merge risk around action/event names without materially reducing wall-clock time.
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **Accidental row selection when clicking `X`:** stop event propagation on the `X` hit target and test the separate action path.
|
||||
- **Deleting the wrong owner scope:** store `SecretOwner` on each fetched `AuthSecretEntry`; do not infer owner from the active workspace at click time.
|
||||
- **Stale selected secret after deletion:** clear both `AmbientAgentViewModel` selection and `CloudAgentSettings.last_selected_auth_secret` when the deleted name matches the active harness selection.
|
||||
- **Over-broad menu API changes:** keep any `MenuItemFields` addition opt-in so existing menus with right-side icons remain visually and behaviorally unchanged.
|
||||
|
||||
Reference in New Issue
Block a user