first pass of merging in warp (doesn't build)
This commit is contained in:
@@ -0,0 +1,117 @@
|
||||
# Auth Secret Creation Flow — Product Spec
|
||||
|
||||
## Summary
|
||||
|
||||
When a user selects a non-Oz harness (e.g. Claude Code) in cloud mode, they must provide an authentication secret (API key) before the agent can run. This feature adds a first-time setup (FTUX) flow that guides users through selecting or creating an auth secret, and a compact selector chip for returning users who have already configured one.
|
||||
|
||||
## Problem
|
||||
|
||||
Today the Warp desktop client has no way to associate an auth secret with a third-party harness in cloud mode. The oz web app supports this, but the native client silently launches runs without credentials, causing failures. Users need a clear, guided way to provide auth credentials the first time they use a non-Oz harness, and an efficient way to change credentials afterward.
|
||||
|
||||
## Goals
|
||||
|
||||
- Guide first-time users through selecting or creating an auth secret when they pick a non-Oz harness.
|
||||
- Let returning users quickly see and change their selected auth secret via a compact chip.
|
||||
- Support all three Claude Code auth secret types: Anthropic API Key, Bedrock API Key, and Bedrock Access Key.
|
||||
- Persist FTUX completion per-harness so users are not re-prompted unnecessarily.
|
||||
- Include the selected auth secret in the agent spawn config so the server can inject it.
|
||||
|
||||
## Non-goals
|
||||
|
||||
- Editing or deleting existing secrets (users go to Warp Drive for that).
|
||||
- Auth secret flows for harnesses other than Claude Code (will extend the same pattern later).
|
||||
- A dedicated secrets management UI beyond the existing Warp Drive secrets pane.
|
||||
- Changes to the warp-server — all necessary GraphQL queries and mutations already exist.
|
||||
|
||||
## Figma
|
||||
|
||||
- FTUX flow: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=8466-53234&m=dev
|
||||
- Dropdown open: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7239-48787&m=dev
|
||||
- Creation flow (raw value input): https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7239-48345&m=dev
|
||||
- Toast after creation: https://www.figma.com/design/CsBdBW4YoLgSAbr5eSkwV6/House-of-Agents?node-id=7239-49211&m=dev
|
||||
|
||||
**Not in Figma** (filled in during design):
|
||||
- The "New" dropdown item with plus icon and submenu for each secret type.
|
||||
- The auth secret selector chip in the top row (non-FTUX mode).
|
||||
- Multiple input fields for secret types requiring more than one env var (e.g. Bedrock Access Key needs 4 fields).
|
||||
|
||||
## User Experience
|
||||
|
||||
### Trigger: selecting a non-Oz harness
|
||||
|
||||
1. The user opens cloud mode composing and selects a non-Oz harness (e.g. "Claude Code") from the harness selector.
|
||||
2. The system checks a per-harness setting to determine whether this harness has had its auth secret FTUX completed.
|
||||
|
||||
### FTUX flow (first time)
|
||||
|
||||
3. If FTUX has not been completed for the chosen harness, the input editor area is replaced by the FTUX view. The top row of selector chips (host, harness, MCP config) remains visible above.
|
||||
4. The FTUX view displays:
|
||||
- Description text explaining what is needed (e.g. "Please enter your Claude API key to use it as a remote agent.").
|
||||
- A dropdown input labeled with the primary env var name (e.g. `ANTHROPIC_API_KEY`). The dropdown has a search icon on the left and a key icon on the right.
|
||||
- A bottom row with: "Already logged in? Click here to skip and continue" on the left; Cancel and Continue buttons on the right.
|
||||
5. Clicking the dropdown opens a menu (720px wide) populated with:
|
||||
- Existing auth secrets for the selected harness (fetched from the server).
|
||||
- A "New" item at the bottom with a `+` icon. Hovering or selecting it opens a submenu listing each auth secret type (e.g. "New Anthropic API Key", "New Bedrock API Key", "New Bedrock Access Key").
|
||||
6. **Selecting an existing secret**: updates the dropdown selection. The user hits Continue to proceed.
|
||||
7. **Selecting a "New" secret type**: enters the creation sub-flow.
|
||||
|
||||
### Creation sub-flow
|
||||
|
||||
8. When a "New" secret type is selected, input fields appear below the dropdown — one per required env var for that type:
|
||||
- **Anthropic API Key**: 1 field (`ANTHROPIC_API_KEY`).
|
||||
- **Bedrock API Key**: 2 fields (`AWS_BEARER_TOKEN_BEDROCK`, `AWS_REGION`).
|
||||
- **Bedrock Access Key**: 4 fields (`AWS_ACCESS_KEY_ID`, `AWS_SECRET_ACCESS_KEY`, `AWS_SESSION_TOKEN` (optional), `AWS_REGION`).
|
||||
9. Each field has a label above it showing the env var name in gray text (10px, `#6f6e6d`).
|
||||
10. If the user switches the "New" type selection in the dropdown, the input fields re-render to match the newly selected type.
|
||||
11. If the user types a raw value into the dropdown search and no existing secrets match, helper text appears: "No secrets found. Save to use this value directly or click the key to add a secret."
|
||||
12. Hitting Continue with filled fields creates the secret on the server, shows a success toast ("API key saved. Manage secrets"), and transitions to the normal composing input with the newly created secret selected.
|
||||
|
||||
### Skip and Cancel
|
||||
|
||||
13. "Click here to skip and continue" skips auth secret selection and proceeds to the normal composing input with no auth secret set.
|
||||
14. Cancel dismisses the FTUX and returns to Oz harness selection.
|
||||
15. After any successful selection or creation, the per-harness FTUX setting is marked as completed.
|
||||
|
||||
### Returning user flow (FTUX already completed)
|
||||
|
||||
16. If the FTUX setting is already completed for the harness, the normal composing input is shown with an **auth secret selector chip** in the top row, next to the harness selector.
|
||||
17. The chip shows: key icon + selected secret name + chevron-down. It uses the same `NakedHeaderButtonTheme` as the host and harness selectors.
|
||||
18. Clicking the chip opens a dropdown with the same items as the FTUX dropdown (existing secrets + "New" submenu), but at a narrower width.
|
||||
19. Selecting "New {type}" from the chip reopens the FTUX creation flow, but **without** the "Click here to skip" link (since the user has already completed FTUX before).
|
||||
|
||||
### Toast notification
|
||||
|
||||
20. After successfully creating a new secret, an ephemeral toast appears: "API key saved." with a "Manage secrets" button that opens the Warp Drive secrets pane.
|
||||
|
||||
### Spawn integration
|
||||
|
||||
21. When the user submits their prompt, the selected auth secret name is included in the `AgentConfigSnapshot.harness_auth_secrets` field sent to the server.
|
||||
|
||||
## Success Criteria
|
||||
|
||||
- Selecting Claude Code in cloud mode when FTUX has not been completed shows the FTUX view with description, dropdown, and Cancel/Continue buttons. The top row (host/harness selectors) remains visible.
|
||||
- The FTUX dropdown is populated with existing auth secrets fetched from `harnessAuthSecrets` for the selected harness.
|
||||
- The "New" item in the dropdown opens a submenu with all three Claude Code secret types.
|
||||
- Selecting "New Anthropic API Key" shows 1 input field; "New Bedrock API Key" shows 2 fields; "New Bedrock Access Key" shows 4 fields (one optional).
|
||||
- Filling in the fields and hitting Continue creates the secret via `createManagedSecret`, shows the success toast, and returns to the normal composing input with the secret selected.
|
||||
- After completing FTUX, re-selecting Claude Code shows the normal input with the auth secret chip in the top row pre-populated with the previously selected secret.
|
||||
- The auth secret chip dropdown lets the user change their selection or create a new secret.
|
||||
- Creating a new secret from the chip opens the FTUX creation flow without the "Click here to skip" link.
|
||||
- The selected auth secret name is included in `harness_auth_secrets.claude_auth_secret_name` when the agent is spawned.
|
||||
- Cancelling the FTUX returns to Oz harness selection. Skipping proceeds with no auth secret.
|
||||
- The FTUX setting persists across app restarts.
|
||||
|
||||
## Validation
|
||||
|
||||
- **Manual**: select Claude Code, verify FTUX appears, select an existing secret, hit Continue, verify normal input returns. Repeat with creating a new secret.
|
||||
- **Manual**: after completing FTUX, reopen cloud mode with Claude Code, verify the chip appears with the previously selected secret.
|
||||
- **Manual**: from the chip, select "New Anthropic API Key", fill in the field, hit Continue, verify toast and that the chip updates.
|
||||
- **Manual**: verify Cancel returns to Oz, Skip proceeds with no secret.
|
||||
- **Compile check**: `cargo check` passes with the V2 feature flag on and off.
|
||||
- **Unit tests**: FTUX setting read/write helpers round-trip correctly. Auth secret type metadata returns the correct fields for each type.
|
||||
|
||||
## Resolved Decisions
|
||||
|
||||
- **Skip behavior**: "Skip" marks the per-harness FTUX as completed, just like a successful selection or creation. The user is not re-prompted on subsequent harness reselections.
|
||||
- **Empty / failure state**: The dropdown always shows the "New" item with the sidecar of secret types. When the fetch fails (network error or `Failed` state), the existing-secrets section shows a single disabled "Unable to load secrets" row but the "New" item remains so the user can still create. When secret creation fails, an error toast is shown via `ToastStack`.
|
||||
- **Initial selection**: The FTUX always starts with no secret selected. The user must explicitly pick an existing secret or create a new one (or use Skip / Cancel).
|
||||
@@ -0,0 +1,458 @@
|
||||
# Auth Secret Creation Flow — Tech Spec
|
||||
|
||||
## Problem
|
||||
|
||||
The cloud mode input V2 composing UI lets users select a non-Oz harness (e.g. Claude Code), but has no way to associate an auth secret with that harness. The server already supports `harnessAuthSecrets` (query) and `createManagedSecret` (mutation), and `AgentConfigSnapshot` already has a `harness_auth_secrets` field — but the client never populates it. We need to add the UI for selecting/creating auth secrets and wire the selection into the spawn config.
|
||||
|
||||
## Relevant Code
|
||||
|
||||
### Client (warp-internal)
|
||||
|
||||
- `app/src/ai/harness_availability.rs` — `HarnessAvailabilityModel` singleton. Fetches and caches harness availability from the server. **Will be extended** with per-harness auth secret state.
|
||||
- `app/src/terminal/view/ambient_agent/harness_selector.rs` — `HarnessSelector` view (`ActionButton` + `Menu<A>`). The auth secret selector chip mirrors this pattern.
|
||||
- `app/src/terminal/view/ambient_agent/model.rs` (165-221) — `AmbientAgentViewModel`. Tracks `harness`, `worker_host`, `harness_model_id`. **Will add** `harness_auth_secret_name`.
|
||||
- `app/src/terminal/view/ambient_agent/model.rs` (841-861) — `build_default_spawn_config()`. Builds `AgentConfigSnapshot`. **Will add** `harness_auth_secrets` population.
|
||||
- `app/src/ai/ambient_agents/task.rs` (27-65) — `AgentConfigSnapshot` struct. Already has `harness_auth_secrets: Option<HarnessAuthSecretsConfig>` with `claude_auth_secret_name`.
|
||||
- `app/src/terminal/input/agent.rs` (507-525) — `render_cloud_mode_v2_content()`. Builds `top_row + input_container`. **Branch point** for FTUX vs normal rendering.
|
||||
- `app/src/terminal/input/agent.rs` (539-556) — `render_cloud_mode_v2_top_row()`. **Will add** `AuthSecretSelector` chip conditionally.
|
||||
- `app/src/server/server_api/managed_secrets.rs` — `ManagedSecretsClient` trait. Has `create_managed_secret` and `list_secrets`. **Will add** `list_harness_auth_secrets`.
|
||||
|
||||
### Server (warp-server) — read-only, no changes needed
|
||||
|
||||
- `graphql/v2/queries/managed_secrets.graphqls` — `harnessAuthSecrets` query, `ListHarnessAuthSecretsInput`, `HarnessAuthSecretsResult`.
|
||||
- `graphql/v2/mutations/create_managed_secret.graphqls` — `createManagedSecret` mutation.
|
||||
- `model/types/enums/managed_secret_type.go` (46-52) — `authSecretTypesByHarness` map. Claude Code maps to 3 types.
|
||||
- `logic/managed_secrets/secret_type.go` — Secret value structs and their JSON field shapes.
|
||||
|
||||
### GraphQL client crate
|
||||
|
||||
- `crates/warp_graphql` — cynic-based GraphQL query modules. **Will add** `list_harness_auth_secrets` query module.
|
||||
|
||||
## Current State
|
||||
|
||||
- `HarnessAvailabilityModel` fetches harness metadata on auth/network/workspace changes and caches it. It has no awareness of auth secrets.
|
||||
- `AmbientAgentViewModel` tracks harness selection and builds the spawn config, but never sets `harness_auth_secrets`.
|
||||
- The cloud mode V2 composing UI renders a top row (host + harness selectors) and an input container below it. There is no auth secret UI.
|
||||
- The oz web app (`client/packages/agents/src/components/HarnessAuthSecretSelector`) already has a React component and `useHarnessAuthSecrets` hook that query the same server endpoint.
|
||||
|
||||
## Proposed Changes
|
||||
|
||||
### 1. GraphQL query: `harnessAuthSecrets`
|
||||
|
||||
**New file**: `crates/warp_graphql/src/queries/list_harness_auth_secrets.rs`
|
||||
|
||||
Add a cynic query module for the server's `harnessAuthSecrets` query. The input takes an `AgentHarness` enum; the output reuses the existing `ManagedSecret` fragment.
|
||||
|
||||
```rust
|
||||
#[derive(cynic::QueryVariables)]
|
||||
pub struct ListHarnessAuthSecretsVariables {
|
||||
pub request_context: RequestContext,
|
||||
pub input: ListHarnessAuthSecretsInput,
|
||||
}
|
||||
|
||||
#[derive(cynic::InputObject)]
|
||||
pub struct ListHarnessAuthSecretsInput {
|
||||
pub harness: AgentHarness,
|
||||
}
|
||||
|
||||
#[derive(cynic::QueryFragment)]
|
||||
#[cynic(graphql_type = "RootQuery", variables = "ListHarnessAuthSecretsVariables")]
|
||||
pub struct ListHarnessAuthSecrets {
|
||||
#[arguments(requestContext: $request_context, input: $input)]
|
||||
pub harness_auth_secrets: HarnessAuthSecretsResult,
|
||||
}
|
||||
```
|
||||
|
||||
Extend `ManagedSecretsClient` in `app/src/server/server_api/managed_secrets.rs`:
|
||||
|
||||
```rust
|
||||
async fn list_harness_auth_secrets(
|
||||
&self,
|
||||
harness: AgentHarness,
|
||||
) -> Result<Vec<ManagedSecret>>;
|
||||
```
|
||||
|
||||
### 2. Extend `HarnessAvailabilityModel` with auth secrets
|
||||
|
||||
**File**: `app/src/ai/harness_availability.rs`
|
||||
|
||||
Add a lazily-fetched per-harness auth secret map to the existing singleton, keeping all harness-related data in one place.
|
||||
|
||||
```rust
|
||||
pub struct HarnessAvailabilityModel {
|
||||
harnesses: Vec<HarnessAvailability>,
|
||||
/// Lazily fetched when a non-Oz harness is selected.
|
||||
auth_secrets: HashMap<Harness, AuthSecretFetchState>,
|
||||
}
|
||||
|
||||
pub enum AuthSecretFetchState {
|
||||
NotFetched,
|
||||
Loading,
|
||||
Loaded(Vec<AuthSecretEntry>),
|
||||
/// Surfaced as "Unable to load secrets" in the dropdown; subsequent
|
||||
/// `ensure_auth_secrets_fetched` retries.
|
||||
Failed(String),
|
||||
}
|
||||
|
||||
pub struct AuthSecretEntry {
|
||||
pub name: String,
|
||||
}
|
||||
```
|
||||
|
||||
New events added to `HarnessAvailabilityEvent`:
|
||||
|
||||
```rust
|
||||
pub enum HarnessAvailabilityEvent {
|
||||
Changed,
|
||||
AuthSecretsLoaded { harness: Harness },
|
||||
AuthSecretCreated { harness: Harness, name: String },
|
||||
AuthSecretCreationFailed { error: String },
|
||||
}
|
||||
```
|
||||
|
||||
Key new methods:
|
||||
|
||||
- `auth_secrets_for(&self, harness) -> &AuthSecretFetchState` — returns current state for the harness.
|
||||
- `ensure_auth_secrets_fetched(&mut self, harness, ctx)` — if `NotFetched` or `Failed`, fires async `list_harness_auth_secrets` and transitions to `Loading`. On success, stores `Loaded(entries)` and emits `AuthSecretsLoaded`.
|
||||
- `create_auth_secret(&mut self, harness, name, value, ctx)` where `value: ManagedSecretValue` — calls `ManagedSecretManager::create_secret(SecretOwner::CurrentUser, name, value, None)`. On success, appends a new `AuthSecretEntry` to the cached `Loaded` list (or transitions `NotFetched`/`Failed` directly to `Loaded(vec![entry])`) and emits `AuthSecretCreated { harness, name }`. On error, emits `AuthSecretCreationFailed { error }` and leaves the cache untouched.
|
||||
- `invalidate_auth_secrets(&mut self, harness)` — resets to `NotFetched`. Called when the user signs out (subscription on `AuthManager`'s `AuthComplete` already exists; we extend it to clear the per-harness cache).
|
||||
|
||||
### 3. Auth secret type metadata
|
||||
|
||||
**New file**: `app/src/ai/auth_secret_types.rs`
|
||||
|
||||
Client-side mirror of the server's `authSecretTypesByHarness`. Maps each harness to its secret types and their input field schemas.
|
||||
|
||||
```rust
|
||||
pub struct AuthSecretTypeField {
|
||||
pub label: &'static str,
|
||||
pub json_key: &'static str,
|
||||
pub optional: bool,
|
||||
}
|
||||
|
||||
pub struct AuthSecretTypeInfo {
|
||||
pub display_name: &'static str,
|
||||
pub secret_type: &'static str,
|
||||
pub fields: &'static [AuthSecretTypeField],
|
||||
}
|
||||
|
||||
pub fn auth_secret_types_for_harness(harness: Harness) -> &'static [AuthSecretTypeInfo] {
|
||||
match harness {
|
||||
Harness::Claude => &[
|
||||
AuthSecretTypeInfo {
|
||||
display_name: "Anthropic API Key",
|
||||
secret_type: "anthropic_api_key",
|
||||
fields: &[
|
||||
AuthSecretTypeField { label: "ANTHROPIC_API_KEY", json_key: "api_key", optional: false },
|
||||
],
|
||||
},
|
||||
AuthSecretTypeInfo {
|
||||
display_name: "Bedrock API Key",
|
||||
secret_type: "anthropic_bedrock_api_key",
|
||||
fields: &[
|
||||
AuthSecretTypeField { label: "AWS_BEARER_TOKEN_BEDROCK", json_key: "aws_bearer_token_bedrock", optional: false },
|
||||
AuthSecretTypeField { label: "AWS_REGION", json_key: "aws_region", optional: false },
|
||||
],
|
||||
},
|
||||
AuthSecretTypeInfo {
|
||||
display_name: "Bedrock Access Key",
|
||||
secret_type: "anthropic_bedrock_access_key",
|
||||
fields: &[
|
||||
AuthSecretTypeField { label: "AWS_ACCESS_KEY_ID", json_key: "aws_access_key_id", optional: false },
|
||||
AuthSecretTypeField { label: "AWS_SECRET_ACCESS_KEY", json_key: "aws_secret_access_key", optional: false },
|
||||
AuthSecretTypeField { label: "AWS_SESSION_TOKEN", json_key: "aws_session_token", optional: true },
|
||||
AuthSecretTypeField { label: "AWS_REGION", json_key: "aws_region", optional: false },
|
||||
],
|
||||
},
|
||||
],
|
||||
_ => &[],
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 4. Per-harness FTUX setting
|
||||
|
||||
**File**: `app/src/ai/settings.rs` (or equivalent)
|
||||
|
||||
```rust
|
||||
pub static HARNESS_AUTH_FTUX_COMPLETED: Setting<HashMap<String, bool>> = Setting::new(
|
||||
"harness_auth_ftux_completed",
|
||||
HashMap::new(),
|
||||
);
|
||||
|
||||
pub fn is_harness_auth_ftux_completed(harness: Harness) -> bool { ... }
|
||||
pub fn mark_harness_auth_ftux_completed(harness: Harness) { ... }
|
||||
```
|
||||
|
||||
### 5. `AmbientAgentViewModel` changes
|
||||
|
||||
**File**: `app/src/terminal/view/ambient_agent/model.rs`
|
||||
|
||||
Add field:
|
||||
|
||||
```rust
|
||||
harness_auth_secret_name: Option<String>,
|
||||
```
|
||||
|
||||
Update `set_harness()` to clear `harness_auth_secret_name` when harness changes.
|
||||
|
||||
Update `build_default_spawn_config()` to populate `harness_auth_secrets`:
|
||||
|
||||
```rust
|
||||
let harness_auth_secrets = self.harness_auth_secret_name.as_ref().map(|name| {
|
||||
HarnessAuthSecretsConfig {
|
||||
claude_auth_secret_name: Some(name.clone()),
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
Add getter/setter and new event `AuthSecretSelected`.
|
||||
|
||||
### 6. Auth Secret Selector — single shared dropdown with sidecar submenu
|
||||
|
||||
**File**: `app/src/terminal/view/ambient_agent/auth_secret_selector.rs` (already exists, will be extended).
|
||||
|
||||
The selector is the single source of truth for the dropdown UX. It is rendered both as the top-row chip (returning users) and as the FTUX dropdown (FTUX flow), so the menu items are defined in exactly one place.
|
||||
|
||||
#### Dropdown structure
|
||||
|
||||
The `Menu<AuthSecretSelectorAction>` shows:
|
||||
|
||||
1. A non-clickable header (`MenuItem::Header`) labeled "Auth secret".
|
||||
2. The list of existing secrets for the harness, each as a `MenuItem::Item` with `SelectSecret(name)` as its on-select action. When the fetch state is `Loaded` and empty, this section is omitted (per the design decision that the empty-state dropdown shows only the "New" parent).
|
||||
3. A `MenuItem::Item` labeled "New" with a `+` icon and a chevron-right indicator on the right side, with `OpenNewTypeSidecar` as its on-select action. Hovering this item opens the sidecar.
|
||||
4. While `Loading`, replace the secrets list with a single disabled "Loading…" item; while `Failed`/`NotFetched`, show a disabled "Unable to load secrets" item but **still show the "New" item** so the user can always create a secret even when listing fails.
|
||||
|
||||
#### Sidecar pattern (for "New" expansion)
|
||||
|
||||
Following the precedent set by `app/src/terminal/profile_model_selector.rs` (`ModelSpecSidecar`) and the new-session menu in `workspace/view.rs` (`update_new_session_sidecar` / `configure_worktree_new_session_sidecar`), the "New" item is *not* a `MenuItem::Submenu` (which is deprecated and unused). Instead:
|
||||
|
||||
- The selector owns a second `Menu<AuthSecretSelectorAction>` view (`new_type_sidecar`).
|
||||
- Hovering the "New" item triggers `OpenNewTypeSidecar`, which sets `is_new_type_sidecar_open = true` on the selector and calls `set_safe_zone_target` + `set_submenu_being_shown_for_item_index` on the main menu (mirroring the worktree-config sidecar in `view.rs:8800-8814`).
|
||||
- The render method adds the sidecar as a positioned overlay child next to the main menu, using `OffsetPositioning::offset_from_save_position_element` with the main menu's save-position id (same approach as `profile_model_selector.rs:1972-1984`). When it would overflow the right window edge, it renders to the left.
|
||||
- The sidecar menu items are built from `auth_secret_types_for_harness(harness)`: one `MenuItem::Item` per `AuthSecretTypeInfo`, with `SelectNewType(type_index)` as the on-select action and an icon (`Icon::Key`) on the left.
|
||||
- Selecting a type fires `SelectNewType(type_index)`, which:
|
||||
- Closes the sidecar and the main menu.
|
||||
- Emits `AuthSecretSelectorEvent::NewTypeSelected { harness, type_index }` so the FTUX view can swap into creation mode.
|
||||
|
||||
#### Updated action and event enums
|
||||
|
||||
```rust
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum AuthSecretSelectorAction {
|
||||
ToggleMenu,
|
||||
SelectSecret(String),
|
||||
/// Hovering the "New" parent item opens the sidecar.
|
||||
OpenNewTypeSidecar,
|
||||
/// User picked one of the new-secret types from the sidecar.
|
||||
SelectNewType(usize),
|
||||
}
|
||||
|
||||
pub enum AuthSecretSelectorEvent {
|
||||
MenuVisibilityChanged { open: bool },
|
||||
/// Emitted when the user picks "New {type}" from the sidecar.
|
||||
NewTypeSelected {
|
||||
harness: Harness,
|
||||
type_index: usize,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
#### Selector struct
|
||||
|
||||
```rust
|
||||
pub struct AuthSecretSelector {
|
||||
button: ViewHandle<ActionButton>,
|
||||
menu: ViewHandle<Menu<AuthSecretSelectorAction>>,
|
||||
new_type_sidecar: ViewHandle<Menu<AuthSecretSelectorAction>>,
|
||||
is_menu_open: bool,
|
||||
is_new_type_sidecar_open: bool,
|
||||
menu_positioning_provider: Arc<dyn MenuPositioningProvider>,
|
||||
ambient_agent_model: ModelHandle<AmbientAgentViewModel>,
|
||||
}
|
||||
```
|
||||
|
||||
Subscribes to `HarnessAvailabilityModel` for `AuthSecretsLoaded` / `AuthSecretCreated` (refresh both menus) and to `AmbientAgentViewModel` for `HarnessSelected` (rebuild menus when the harness changes).
|
||||
|
||||
### 7. Auth Secret FTUX View
|
||||
|
||||
**New file**: `app/src/terminal/view/ambient_agent/auth_secret_ftux_view.rs`
|
||||
|
||||
A `TypedActionView` that owns the FTUX content. The FTUX view *embeds* the existing `AuthSecretSelector` rather than building its own dropdown — this is the single-source-of-truth requirement.
|
||||
|
||||
```rust
|
||||
pub struct AuthSecretFtuxView {
|
||||
ambient_agent_model: ModelHandle<AmbientAgentViewModel>,
|
||||
/// Shared dropdown view; same one rendered as the top-row chip when FTUX is
|
||||
/// already completed. The FTUX view subscribes to its events.
|
||||
auth_secret_selector: ViewHandle<AuthSecretSelector>,
|
||||
/// Single-line editors, one per field of the currently-selected new type.
|
||||
/// Empty when no "New {type}" has been selected.
|
||||
field_editors: Vec<ViewHandle<EditorView>>,
|
||||
creation_state: Option<SecretCreationState>,
|
||||
/// Whether to show the "Click here to skip" link. Always true for the
|
||||
/// initial FTUX entry; false when the FTUX is re-entered from the chip's
|
||||
/// "New {type}" path (since the user has already completed FTUX once).
|
||||
show_skip_link: bool,
|
||||
}
|
||||
|
||||
pub struct SecretCreationState {
|
||||
pub harness: Harness,
|
||||
pub secret_type_index: usize,
|
||||
/// Per-field name for the secret being created. Server requires this.
|
||||
pub secret_name: String,
|
||||
pub is_saving: bool,
|
||||
}
|
||||
```
|
||||
|
||||
#### Field input model
|
||||
|
||||
For each `AuthSecretTypeField` of the selected type, the FTUX view constructs a single-line `EditorView` (using `EditorView::single_line` with `SingleLineEditorOptions`, mirroring `model_selector.rs`'s search input). The editor's buffer text is the field value; the FTUX view reads them on Continue without needing per-keystroke `UpdateField` actions.
|
||||
|
||||
The view also constructs a name-of-secret editor (the user must name the saved secret); this is a required field that the server uses as the unique key.
|
||||
|
||||
#### Actions
|
||||
|
||||
```rust
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum AuthSecretFtuxAction {
|
||||
Cancel,
|
||||
Continue,
|
||||
Skip,
|
||||
}
|
||||
```
|
||||
|
||||
No per-field `UpdateField` action is needed — the editors hold their own state and `Continue` reads them.
|
||||
|
||||
#### Behavior
|
||||
|
||||
- On construction: calls `HarnessAvailabilityModel::ensure_auth_secrets_fetched(harness, ctx)`.
|
||||
- Subscribes to the embedded `auth_secret_selector` for `NewTypeSelected { harness, type_index }`. On that event, populates `creation_state` and rebuilds `field_editors` to match `auth_secret_types_for_harness(harness)[type_index].fields`.
|
||||
- Subscribes to `HarnessAvailabilityModel` for `AuthSecretCreated` (transition out of saving state, set the secret as selected on `AmbientAgentViewModel`, mark FTUX completed) and `AuthSecretCreationFailed` (transition out of saving, surface error toast via `Input`'s existing toast subscription).
|
||||
- Continue button:
|
||||
- If a secret is already selected on the view model (from clicking an existing item in the dropdown), mark FTUX completed and `ctx.notify()`.
|
||||
- If `creation_state` is `Some`, validate that all non-optional fields are filled, build the matching `ManagedSecretValue` (e.g. `anthropic_api_key`, `anthropic_bedrock_api_key`, `anthropic_bedrock_access_key`), set `is_saving = true`, and call `HarnessAvailabilityModel::create_auth_secret(harness, name, value, ctx)`. Disable the button while `is_saving`.
|
||||
- Cancel button: switches harness back to Oz via `AmbientAgentViewModel::set_harness(Harness::Oz, ctx)`. (Same as the existing `CancelAuthSecretFtux` action.)
|
||||
- Skip button: marks FTUX completed via `CloudAgentSettings::mark_harness_auth_ftux_completed(harness, ctx)`. (Same as the existing `SkipAuthSecretFtux` action.)
|
||||
|
||||
#### Wiring in `Input`
|
||||
|
||||
The FTUX view is constructed in `Input::new` alongside the existing `AuthSecretSelector` and stored on `AmbientAgentViewState`. The `agent.rs` rendering of `render_auth_secret_ftux_content` is replaced by `ChildView::new(&self.auth_secret_ftux_view)`.
|
||||
|
||||
The inline `SkipAuthSecretFtux` / `CancelAuthSecretFtux` `InputAction` variants and their handlers in `input.rs` are removed in favor of the FTUX view dispatching `AuthSecretFtuxAction` directly to itself.
|
||||
|
||||
### 8. Rendering tree integration
|
||||
|
||||
**File**: `app/src/terminal/input/agent.rs`
|
||||
|
||||
The branch point is `render_cloud_mode_v2_content` (line 507-525). Currently:
|
||||
|
||||
```
|
||||
render_cloud_mode_v2_content()
|
||||
└── Align (centered)
|
||||
└── ConstrainedBox (max-width: 720px)
|
||||
└── Flex::column (gap: 10px)
|
||||
├── render_cloud_mode_v2_top_row() ← ALWAYS rendered
|
||||
└── render_cloud_mode_v2_input_container() ← normal input
|
||||
```
|
||||
|
||||
With FTUX active (non-Oz harness + FTUX not completed):
|
||||
|
||||
```
|
||||
render_cloud_mode_v2_content()
|
||||
└── Align (centered)
|
||||
└── ConstrainedBox (max-width: 720px)
|
||||
└── Flex::column (gap: 10px)
|
||||
├── render_cloud_mode_v2_top_row() ← SAME
|
||||
└── ChildView::new(&auth_secret_ftux_view) ← replaces input container
|
||||
```
|
||||
|
||||
With FTUX completed (returning user):
|
||||
|
||||
```
|
||||
render_cloud_mode_v2_content()
|
||||
└── Align (centered)
|
||||
└── ConstrainedBox (max-width: 720px)
|
||||
└── Flex::column (gap: 10px)
|
||||
├── render_cloud_mode_v2_top_row()
|
||||
│ └── HostSelector | HarnessSelector | AuthSecretSelector | ...
|
||||
└── render_cloud_mode_v2_input_container() ← normal input
|
||||
```
|
||||
|
||||
The top row always renders. The second child swaps based on `should_show_auth_secret_ftux()`.
|
||||
|
||||
### 9. Toast notification
|
||||
|
||||
Subscribe to `HarnessAvailabilityEvent::AuthSecretCreated` in `Input` and show an ephemeral `DismissibleToast` via `ToastStack`: "API key saved." with a "Manage secrets" action button.
|
||||
|
||||
## End-to-End Flow
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant User
|
||||
participant HarnessSelector
|
||||
participant AmbientAgentVM as AmbientAgentViewModel
|
||||
participant HarnessAvail as HarnessAvailabilityModel
|
||||
participant FtuxView as AuthSecretFtuxView
|
||||
participant Server as warp-server
|
||||
|
||||
User->>HarnessSelector: Select "Claude Code"
|
||||
HarnessSelector->>AmbientAgentVM: set_harness(Claude)
|
||||
Note over AmbientAgentVM: Emits HarnessSelected
|
||||
Note over FtuxView: render_cloud_mode_v2_content checks FTUX setting
|
||||
alt FTUX not completed
|
||||
FtuxView->>HarnessAvail: ensure_auth_secrets_fetched(Claude)
|
||||
HarnessAvail->>Server: harnessAuthSecrets(CLAUDE_CODE)
|
||||
Server-->>HarnessAvail: [secret1, secret2, ...]
|
||||
HarnessAvail-->>FtuxView: AuthSecretsLoaded
|
||||
FtuxView->>FtuxView: Populate dropdown
|
||||
alt User selects existing secret
|
||||
User->>FtuxView: Select "secret1"
|
||||
User->>FtuxView: Click Continue
|
||||
FtuxView->>AmbientAgentVM: set_harness_auth_secret_name("secret1")
|
||||
FtuxView->>FtuxView: mark_harness_auth_ftux_completed(Claude)
|
||||
Note over FtuxView: Re-render → normal input with chip
|
||||
else User creates new secret
|
||||
User->>FtuxView: Select "New Anthropic API Key"
|
||||
FtuxView->>FtuxView: Show input field(s)
|
||||
User->>FtuxView: Fill ANTHROPIC_API_KEY, click Continue
|
||||
FtuxView->>HarnessAvail: create_auth_secret(...)
|
||||
HarnessAvail->>Server: createManagedSecret(...)
|
||||
Server-->>HarnessAvail: OK
|
||||
HarnessAvail-->>FtuxView: AuthSecretCreated
|
||||
Note over FtuxView: Toast + transition to normal input
|
||||
end
|
||||
else FTUX completed
|
||||
Note over FtuxView: Render normal input + AuthSecretSelector chip
|
||||
end
|
||||
User->>User: Types prompt, submits
|
||||
AmbientAgentVM->>Server: spawn_agent (config includes harness_auth_secrets)
|
||||
```
|
||||
|
||||
## Risks and Mitigations
|
||||
|
||||
- **Shared singleton mutation**: `HarnessAvailabilityModel` is a singleton; adding mutable auth secret state must not interfere with the existing harness availability data. Mitigated by using a separate `HashMap` field that is never touched by the existing `refresh()` path.
|
||||
- **Lazy fetch timing**: The FTUX view calls `ensure_auth_secrets_fetched` on construction. If the fetch is slow, the dropdown shows a loading state. The user cannot hit Continue until secrets are loaded or they enter a raw value.
|
||||
- **Secret encryption**: Creating a secret requires client-side encryption using the user's upload key. The existing `ManagedSecretsClient::create_managed_secret` already handles this — we reuse that path.
|
||||
- **Feature flag coupling**: This feature requires `CloudModeInputV2` and `CloudMode` and `AgentHarness` flags to all be enabled. If any is off, none of the new code runs.
|
||||
- **Stale cache**: After creating a secret, we append to the cached list optimistically. If the server rejects the creation, we show an error toast and don't modify the cache.
|
||||
|
||||
## Testing and Validation
|
||||
|
||||
- **Unit tests**: `auth_secret_types_for_harness` returns correct field schemas for Claude and empty for Oz/Unknown. FTUX setting helpers round-trip correctly.
|
||||
- **Compile check**: `cargo check` with and without the `cloud_mode_input_v2` feature flag.
|
||||
- **Manual verification**:
|
||||
1. Select Claude Code → FTUX appears, top row persists, dropdown populates from server.
|
||||
2. Select existing secret → Continue → normal input with chip showing selected secret.
|
||||
3. Select "New Anthropic API Key" → 1 field appears → fill → Continue → toast + chip updates.
|
||||
4. Select "New Bedrock Access Key" → 4 fields appear (1 optional) → fill required → Continue.
|
||||
5. Cancel → returns to Oz. Skip → proceeds with no secret.
|
||||
6. Restart app → re-select Claude Code → FTUX skipped, chip shows previous secret.
|
||||
7. Submit prompt → verify `harness_auth_secrets.claude_auth_secret_name` is set in the spawn request.
|
||||
|
||||
## Follow-ups
|
||||
|
||||
- Extend `auth_secret_types_for_harness` when new harnesses (e.g. Gemini, Codex) are added.
|
||||
- Support raw-value passthrough (type the API key directly without creating a named secret).
|
||||
- Add the "Manage secrets" toast button action to open the Warp Drive secrets pane.
|
||||
- Consider pre-selecting the most recently used secret when returning to the FTUX or chip.
|
||||
Reference in New Issue
Block a user