Skip to content

fix(desktop): make OpenAI key re-enterable after first save in card mint dialog - #4140

Draft
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/card-mint-key-update
Draft

fix(desktop): make OpenAI key re-enterable after first save in card mint dialog#4140
wpfleger96 wants to merge 3 commits into
mainfrom
duncan/card-mint-key-update

Conversation

@wpfleger96

@wpfleger96 wpfleger96 commented Aug 1, 2026

Copy link
Copy Markdown
Member

Fixes a write-once dead-end in the card mint dialog where a user with an expired OpenAI key had no way to replace it.

Source-aware key status (Rust + TypeScript). card_mint_key_status returns a layer discriminant ("none" | "global" | "persona" | "agent" | "process") instead of a boolean. A pure resolve_key_layer() helper in card.rs owns the classification logic; card_mint_key_status delegates to it, so the production path is under direct test with no duplicate logic.

Mint form always reachable. The key panel replaces the mint form only for none (first-time setup) or when the user explicitly opens the edit panel (editingKey). Keys from agent/persona/process layers show an inline provenance row on the mint form with a "Why?" affordance; clicking it shows the read-only redirect in a panel with a Cancel button that returns to the mint form — never a terminal state.

Precise auth-error matching. The 401 handling in cardMintStore.ts matches startsWith("Card mint failed (HTTP 401 ") plus the specific Incorrect API key text, so avatar-fetch 401 errors pass through unchanged.

Tri-state key status row. "Using your saved OpenAI key · Update" renders only when keyLayer === "global" (confirmed writable key). Query pending or errored hides the row without asserting key existence.

Real tests. Panel visibility derivations live in cardMintKeyUtils.ts, which AgentCardMintDialog.tsx imports directly. Tests cover all layers including the mint-reachability invariant (Mint reachable for every resolved layer; only none gates setup).

  • card.rs — new resolve_key_layer() pure helper; card_mint_key_status delegates to it; 999 lines (under the 1000-line ratchet)
  • card/tests.rs — precedence test calls resolve_key_layer() directly (no test-local closure); adds process-layer and blank-value cases
  • tauriPersonas.tsCardMintKeyLayer type; updated cardMintKeyStatus signature
  • cardMintKeyUtils.tsshowKeyPanel, showReadOnlyRow, showCancelButton, keyPanelTitle, and helpers; component imports all of them
  • AgentCardMintDialog.tsx — inline provenance rows for all key sources; key panel only for setup/edit; no unused variables
  • cardMintStore.ts — precise 401 prefix matching
  • e2eBridge.tscard_mint_key_status stub returns "global" (not boolean)
  • Tests: 3959 JS passing, 2089 Rust passing, tsc --noEmit clean

…int dialog

Once any key resolved, AgentCardMintDialog hid the key-setup panel forever
(needsKey is false) — a write-once shadow field with no update path. Users
with an expired key (e.g. HTTP 401) had no way to replace it.

Add an editingKey boolean state. When false and needsKey is false the dialog
shows the mint form with a new 'Update API key' link button near the cost
note. Clicking it sets editingKey = true, which re-enters the existing key
panel reusing the same save mutation, testids, and Rust seam
(card_mint_save_openai_key). On successful save, editingKey resets to false
and the user is back at the mint form. A Cancel button is shown only in
update mode (editingKey && !needsKey) so first-time setup is unchanged.

Also improve the 401 / 'Incorrect API key' error branch in cardMintStore:
instead of surfacing the raw OpenAI wire message, emit actionable copy that
names the problem (invalid/expired key) and points at the new affordance.

Tests: 3919 passing, tsc --noEmit clean.
Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
@wpfleger96 wpfleger96 self-assigned this Aug 1, 2026
npub1ng3jzsaqxdhrfq22dg85j3lpr0zsh3jp7g2h9jyxl59wraayapnsu6kvfg and others added 2 commits August 1, 2026 13:03
Finding 1 — source-aware key update (Rust):
card_mint_key_status now returns a layer discriminant ("none" | "global" |
"persona" | "agent" | "process") instead of a bool, mirroring the exact
resolution priority of mint_agent_card. The dialog uses the layer to decide
whether to offer a writable input (none/global) or a read-only redirect
(agent/persona/process) pointing the user to the correct settings surface.
The write seam (card_mint_save_openai_key) is only offered when writing global
will actually become the resolved key. New Rust precedence test
key_status_layer_matches_mint_resolution_priority proves the status layer
matches what mint_agent_card would resolve.

Finding 2 — precise 401 matching (cardMintStore.ts):
Replace bare message.includes("401") with startsWith("Card mint failed (HTTP
401 ") to match only the OpenAI-call envelope. Avatar fetch 401 errors now
pass through unchanged. New passthrough test confirms avatar 401 is not
rewritten.

Finding 3 — tri-state key status row (AgentCardMintDialog.tsx):
"Using your saved OpenAI key" now renders only when keyLayer === "global"
(confirmed, writable key). While query is pending or errors, keyLayer is
undefined and the status row is hidden — no false claims about key existence.
Fail-open mint behavior preserved.

Finding 4 — real tests (cardMintKeyUtils.ts + test):
Extract showKeyPanel, isWritableLayer, isReadOnlyLayer, showCancelButton,
showKeyStatusRow, keyPanelTitle into cardMintKeyUtils.ts. The component imports
and uses these functions directly. AgentCardMintDialog.test.mjs now imports the
production module — removing any function will break the tests. Old predicate-
copy approach deleted.

3938 JS tests passing, 16 Rust card tests passing, tsc --noEmit clean.

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
- showKeyPanel now gates only on none||editingKey; read-only layers
  (agent/persona/process) show an inline provenance row on the mint form
  with a Why? affordance that opens the redirect panel with Cancel —
  Mint card is reachable for every resolved layer
- Extract resolve_key_layer() pure helper in card.rs; card_mint_key_status
  delegates to it instead of duplicating the classification logic; the
  Rust precedence test now calls the production function directly (no
  test-local closure), also covering process and blank-value cases
- card.rs: 999 lines (under the 1000-line ratchet)
- e2eBridge.ts card_mint_key_status stub returns "global" (not boolean true)
- Component now imports showCancelButton and keyPanelTitle from
  cardMintKeyUtils.ts and uses them; new showReadOnlyRow helper added
- Tests: 3959 JS passing, 2089 Rust passing, tsc --noEmit clean

Co-authored-by: Will Pfleger <pfleger.will@gmail.com>
Signed-off-by: Will Pfleger <pfleger.will@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant