Skip to content

Implement auth secret deletion from selector menu#11241

Open
abhishekp106 wants to merge 4 commits into
masterfrom
oz/cloud-mode-auth-secret-deletion-spec
Open

Implement auth secret deletion from selector menu#11241
abhishekp106 wants to merge 4 commits into
masterfrom
oz/cloud-mode-auth-secret-deletion-spec

Conversation

@abhishekp106
Copy link
Copy Markdown
Contributor

@abhishekp106 abhishekp106 commented May 18, 2026

Summary

Adds the ability to delete secrets from the auth selector menu. I copied the modal design from the web app.

Testing

https://www.loom.com/share/40647dedc738436e9106ce763c7c98c6

This PR was created by Oz (running Claude Code).

Co-Authored-By: Oz <oz-agent@warp.dev>
@cla-bot cla-bot Bot added the cla-signed label May 18, 2026
Adds a right-aligned X button on each Warp-managed secret row in the auth
selector chip menu (non-Oz cloud harnesses). Clicking it deletes the
secret via ManagedSecretManager, surfaces a success or failure toast,
and clears the selection (and persisted last-selected) when the deleted
secret was the active one. Duplicate clicks while a delete is in flight
are dropped.

- Extends AuthSecretEntry with SecretOwner so per-secret deletes use the
  correct ownership scope (User vs Team) rather than guessing at click
  time.
- Adds HarnessAvailabilityModel::delete_auth_secret plus
  AuthSecretDeleted / AuthSecretDeletionFailed events, and updates all
  match-exhaustive subscribers (FTUX, model selector, orchestration
  pickers, run-agents card) to refresh on Deleted.
- Adds an opt-in right-side action hit target to MenuItemFields so the
  X dispatches a separate action and stops the row's select click from
  firing alongside it. Existing right-side icons remain decorative.
- Derives PartialEq/Eq/Hash on SecretOwner so it can ride along on the
  selector's PartialEq action enum.

Co-Authored-By: Oz <oz-agent@warp.dev>
@abhishekp106 abhishekp106 changed the title Add auth secret deletion specs Implement auth secret deletion from selector menu May 18, 2026
Copy link
Copy Markdown
Contributor Author

This stack of pull requests is managed by Graphite. Learn more about stacking.

@abhishekp106 abhishekp106 requested a review from liliwilson May 20, 2026 20:48
@abhishekp106 abhishekp106 marked this pull request as ready for review May 20, 2026 21:00
@oz-for-oss
Copy link
Copy Markdown
Contributor

oz-for-oss Bot commented May 20, 2026

@abhishekp106

I'm starting a first review of this pull request.

You can view the conversation on Warp.

I completed the review and no human review was requested for this pull request.

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Copy link
Copy Markdown
Contributor

@oz-for-oss oz-for-oss Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Overview

This PR adds owner metadata for harness auth secrets, a selector-menu delete affordance, a destructive confirmation dialog, server-backed deletion handling, and refresh/toast behavior for related auth-secret surfaces.

Concerns

  • Clicking the right-side delete icon can still dispatch the parent row selection because the menu row does not defer child-handled events, and the disabled icon state does not consume clicks.
  • Successful deletion updates the cached auth-secret list by name only even though the server delete is scoped by owner, so same-named user/team secrets can be hidden incorrectly.
  • Delete completion only clears selected/persisted state when the deleted harness is still the active harness, leaving stale settings if the user switches harnesses while deletion is in flight.

Verdict

Found: 0 critical, 3 important, 0 suggestions

Request changes

Comment /oz-review on this pull request to retrigger a review (up to 3 times on the same pull request).

Powered by Oz

Comment thread app/src/menu.rs
});
// Swallow mouse-down too so the row's click handler
// doesn't latch onto the press that targets the icon.
hoverable = hoverable.on_mouse_down(|_, _, _| {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This does not stop the parent row Hoverable from also processing the click: parent hoverables only skip child-handled events when with_defer_events_to_children() is set, and the disabled icon path installs no handler at all. Clicking the X can still select/close the row instead of only opening delete confirmation; make the row defer to the icon hit target and consume disabled-icon clicks too.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems legit - if I click the "x" and then click cancel, the key I "x"ed is now selected

if let Some(AuthSecretFetchState::Loaded(entries)) =
me.auth_secrets.get_mut(&harness)
{
entries.retain(|entry| entry.name != name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] Deletion is scoped by (owner, name), but this removes every cached secret with the same name for the harness. If a user and team secret share a name, deleting one hides both locally until a refetch; retain by both name and owner and include owner in the deletion event/state.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤨 it seems after a quick look in warp-server that this statement about uniqueness is, in fact, correct

let active_harness = self.ambient_agent_model.as_ref(ctx).selected_harness();
let removed_pending = self.pending_deletes.remove(&name);

if harness == active_harness {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ [IMPORTANT] This gates selected/persisted cleanup on whichever harness is active when the async delete completes. If the user confirms deletion and switches harnesses before the server responds, last_selected_auth_secret for the deleted harness remains and can restore a non-existent secret later; clear persisted state for the event harness regardless of current visibility, and only limit UI refreshes to the active harness.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems legit

Copy link
Copy Markdown
Contributor

@liliwilson liliwilson left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woohoo, this is awesome! I can finally make test keys with horrible names and get rid of them at lightning speed


Add a small destructive dialog view under `app/src/terminal/view/ambient_agent/` that mirrors existing confirmation dialogs:

- Title: `Delete secret`
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: "Delete credentials" maybe better, if we're trying not to use the secret language here?

if let Some(AuthSecretFetchState::Loaded(entries)) =
me.auth_secrets.get_mut(&harness)
{
entries.retain(|entry| entry.name != name);
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🤨 it seems after a quick look in warp-server that this statement about uniqueness is, in fact, correct

Comment thread app/src/pane_group/mod.rs
if self.pane_with_open_auth_secret_delete_confirmation_dialog == Some(pane_id) {
self.pane_with_open_auth_secret_delete_confirmation_dialog = None;
}
if self.pane_with_open_auth_secret_delete_confirmation_dialog == Some(pane_id) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Accidental extra copy paste?

Comment thread app/src/pane_group/mod.rs
.terminal_view_from_pane_id(pane_id, app)
.and_then(|tv| {
tv.as_ref(app)
.auth_secret_delete_confirmation_dialog_element(app)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

OOC why pass the element back directly here instead of the handle? (Noticing the surrounding fns use handle)

let active_harness = self.ambient_agent_model.as_ref(ctx).selected_harness();
let removed_pending = self.pending_deletes.remove(&name);

if harness == active_harness {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This seems legit

} = pending_deletion;

// Drop duplicate dispatches while a delete for this same
// secret is still in flight (PRODUCT behavior 9).
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

nit: can we remove the PRODUCT reference from this comment

Comment on lines +38 to +39
visible: bool,
pending_deletion: Option<PendingAuthSecretDeletion>,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do we need these to be two separate fields, or can they be consolidated into the pending_deletion Option type

Comment thread app/src/menu.rs
Comment on lines 421 to +435
right_side_icon: Option<(icons::Icon, Option<Fill>)>,
/// Optional action dispatched when the right-side icon is clicked. When
/// set, the right-side icon becomes its own hit target: clicking it
/// dispatches this action without firing the row's own `on_select_action`,
/// and prevents the row click from propagating.
right_side_icon_action: Option<A>,
/// Optional accessibility label for the right-side icon hit target.
right_side_icon_a11y_label: Option<String>,
/// When true, the right-side icon is rendered as disabled (no hover,
/// no click action). Used to lock the affordance while a pending
/// request is in flight.
right_side_icon_disabled: bool,
/// Optional mouse state for the right-side icon so its hover state is
/// tracked independently from the row.
right_side_icon_mouse_state: MouseStateHandle,
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What do you think about bundling these 6 fields into a RightSideIconConfig?

struct RightSideIconConfig<A> {
    icon: icons::Icon,
    override_color: Option<Fill>,
    action: Option<A>,
    a11y_label: Option<String>,
    disabled: bool,
    mouse_state: MouseStateHandle,
}

Comment thread app/src/menu.rs
});
// Swallow mouse-down too so the row's click handler
// doesn't latch onto the press that targets the icon.
hoverable = hoverable.on_mouse_down(|_, _, _| {});
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also seems legit - if I click the "x" and then click cancel, the key I "x"ed is now selected

.with_override_hover_background_color(hover_background)
.with_icon(Icon::Plus)
.with_right_side_label("›", Properties::default())
.with_right_side_icon(Icon::ChevronRight)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image

Not sure if this is where this changed but the ">" seems a bit close now

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants