Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
40 changes: 40 additions & 0 deletions docs/frontend-ui-audit-2026-08-04/CodexSetup.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
# Frontend UI Audit — CodexSetup

**File:** `src/scaffold/WizardSystem/variants/KeyVault/components/setup/CodexSetup.tsx` (269 LOC)
**Date:** 2026-08-04
**Auditor:** Codex session

## D1 — Raw HTML vs Design System

| Line | Element | Verdict | Reason | Suggested change |
| ---- | ----------- | ------- | ---------------------------------------------------------------------------------------------- | ---------------- |
| — | No findings | — | The changed interaction uses the existing `Button` and `InlineAlert` design-system components. | — |

## D2 — Arbitrary Tailwind Value vs Token

| Line | Value | Verdict | Reason | Suggested change |
| ---- | ----------- | ------- | -------------------------------------------------------------------------------------- | ---------------- |
| — | No findings | — | The feedback change introduces no arbitrary CSS variables, colors, or Tailwind values. | — |

## D3 — Hardcoded Sizes / Colors

| Line | Value | Verdict | Reason | Suggested change |
| ---- | ----------- | ------- | ------------------------------------------------------------------- | ---------------- |
| — | No findings | — | The feedback change introduces no hardcoded size or color literals. | — |

## D4 — Accessibility

| Line | Element | Verdict | Reason | Suggested change |
| ---- | ------------------------- | ---------------- | ----------------------------------------------------------------------------------------------------------------------------- | ---------------- |
| 178 | Detect `Button` | keep with reason | The design-system button has a visible localized label, native keyboard semantics, and is disabled while detection is active. | — |
| 230 | Detection feedback region | keep with reason | Progress and success use a polite status region; failures use an assertive alert region and retain a dismiss control. | — |

## D5 — Visual Patterns Observed

- No new repeated visual pattern. The implementation reuses the existing inline-alert feedback pattern.

## Summary

- 0 fixes recommended
- 2 kept with documented reason
- 0 abstract candidates (>= 3 occurrences)
1 change: 1 addition & 0 deletions src-tauri/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions src-tauri/crates/key-vault/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ reqwest = { workspace = true }
regex = "1.9.5"
log = "0.4"
tracing = "0.1.37"
libc = "0.2"
uuid = { version = "1", features = ["v4"] }
dirs = "6.0"
sha2 = "0.10"
Expand Down
10 changes: 5 additions & 5 deletions src-tauri/crates/key-vault/src/key_store/service/keys.rs
Original file line number Diff line number Diff line change
Expand Up @@ -86,11 +86,7 @@ impl KeyService {

/// Save or update a key
pub fn save_key(&self, key: ModelKey) -> Result<ModelKey, String> {
self.update_store(|store| {
let entry = key.clone();
store.set(key);
entry
})
self.update_store(|store| store.set(key))
}

/// Record behaviorally-observed reasoning capability for `model` on key
Expand Down Expand Up @@ -236,6 +232,10 @@ impl KeyService {
if let Some(enabled) = enabled_models {
entry.enabled_models = enabled;
}
// A successful model refresh is authoritative. Removed models
// must not survive in enabled_models when callers omit that
// optional field, and caller-provided lists are normalized too.
entry.normalize_enabled_models();
if let Some(quota) = quota_info {
entry.quota_info = Some(quota);
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,11 @@ fn deserialize_key_store(contents: &str) -> Result<LoadedKeyStore, serde_json::E
}

match serde_json::from_value::<ModelKey>(raw.clone()) {
Ok(key) => {
Ok(mut key) => {
// Older catalog refreshes could leave removed model ids in
// enabled_models. Normalize on hydration so every reader sees
// a consistent account even before the next persisted write.
key.normalize_enabled_models();
store.keys.insert(storage_id, key);
}
Err(error) => invalid_credentials.push(InvalidStoredCredential {
Expand Down
5 changes: 4 additions & 1 deletion src-tauri/crates/key-vault/src/key_store/store.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,10 +65,13 @@ impl KeyStore {
}

/// Save or update a key
pub fn set(&mut self, mut key: ModelKey) {
pub fn set(&mut self, mut key: ModelKey) -> ModelKey {
key.normalize_enabled_models();
key.updated_at = Utc::now();
let saved = key.clone();
self.keys.insert(key.id.clone(), key);
self.updated_at = Utc::now();
saved
}

/// Delete key by agent type and optional ID
Expand Down
82 changes: 82 additions & 0 deletions src-tauri/crates/key-vault/src/key_store/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,86 @@ fn test_key_crud() {
assert_eq!(empty.len(), 0);
}

#[test]
fn test_save_key_normalizes_enabled_models_to_available_catalog() {
let temp_dir = tempdir().unwrap();
let service = KeyService::new(Some(temp_dir.path().to_path_buf()));

let mut key = ModelKey::new(ModelType::Codex);
key.available_models = vec!["gpt-5.5".to_string(), "gpt-5.4".to_string()];
key.enabled_models = vec![
"gpt-5.6-sol".to_string(),
"gpt-5.5".to_string(),
"gpt-5.5".to_string(),
];

let saved = service.save_key(key).unwrap();

assert_eq!(saved.enabled_models, vec!["gpt-5.5".to_string()]);
assert_eq!(
service.get_key_by_id(&saved.id).unwrap().enabled_models,
vec!["gpt-5.5".to_string()]
);
let persisted: serde_json::Value = serde_json::from_str(
&std::fs::read_to_string(temp_dir.path().join("credentials.json")).unwrap(),
)
.unwrap();
assert_eq!(
persisted["credentials"][&saved.id]["enabled_models"],
serde_json::json!(["gpt-5.5"]),
"serialized credentials must not retain unavailable enabled models"
);
}

#[test]
fn test_load_normalizes_legacy_stale_enabled_models() {
let temp_dir = tempdir().unwrap();
let service = KeyService::new(Some(temp_dir.path().to_path_buf()));

let mut key = ModelKey::new(ModelType::Codex);
let key_id = key.id.clone();
key.available_models = vec!["gpt-5.5".to_string()];
key.enabled_models = vec!["gpt-5.6-sol".to_string(), "gpt-5.5".to_string()];

let mut raw_store = KeyStore::default();
raw_store.keys.insert(key_id.clone(), key);
std::fs::write(
temp_dir.path().join("credentials.json"),
serde_json::to_string_pretty(&raw_store).unwrap(),
)
.unwrap();

let loaded = service.get_key_by_id(&key_id).unwrap();
assert_eq!(loaded.enabled_models, vec!["gpt-5.5".to_string()]);
}

#[test]
fn test_model_refresh_removes_enabled_models_missing_from_new_catalog() {
let temp_dir = tempdir().unwrap();
let service = KeyService::new(Some(temp_dir.path().to_path_buf()));

let mut key = ModelKey::new(ModelType::Codex);
key.available_models = vec!["gpt-5.6-sol".to_string(), "gpt-5.5".to_string()];
key.enabled_models = key.available_models.clone();
let saved = service.save_key(key).unwrap();

service
.update_key_health(
&saved.id,
HealthStatus::Valid,
None,
Some(vec!["gpt-5.5".to_string()]),
None,
None,
None,
)
.unwrap();

let refreshed = service.get_key_by_id(&saved.id).unwrap();
assert_eq!(refreshed.available_models, vec!["gpt-5.5".to_string()]);
assert_eq!(refreshed.enabled_models, vec!["gpt-5.5".to_string()]);
}

#[test]
fn retired_gemini_cli_credentials_do_not_corrupt_the_vault() {
let temp_dir = tempdir().unwrap();
Expand Down Expand Up @@ -1181,6 +1261,7 @@ fn test_cross_type_env_zenmux_as_claude_code_uses_anthropic_endpoint() {

let mut zenmux_key = ModelKey::new(ModelType::ZenmuxApi);
zenmux_key.api_key = Some("sk-zenmux-test123".to_string());
zenmux_key.available_models = vec!["claude-sonnet-4-20250514".to_string()];
zenmux_key.enabled_models = vec!["claude-sonnet-4-20250514".to_string()];
let key_id = zenmux_key.id.clone();
service.save_key(zenmux_key).unwrap();
Expand Down Expand Up @@ -1235,6 +1316,7 @@ fn test_cross_type_env_atlascloud_as_claude_code_uses_anthropic_endpoint() {

let mut atlas_key = ModelKey::new(ModelType::AtlascloudApi);
atlas_key.api_key = Some("atlas-test-key".to_string());
atlas_key.available_models = vec!["zai-org/glm-5.1".to_string()];
atlas_key.enabled_models = vec!["zai-org/glm-5.1".to_string()];
// The stored /v1 URL is OpenAI-protocol; the Anthropic export must
// ignore it and use the bare host instead.
Expand Down
16 changes: 15 additions & 1 deletion src-tauri/crates/key-vault/src/key_store/types.rs
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use chrono::{DateTime, NaiveDateTime, Utc};
use serde::{Deserialize, Deserializer, Serialize, Serializer};
// Deserializer is used by the flexible_datetime modules below
use std::collections::HashMap;
use std::collections::{HashMap, HashSet};
use uuid::Uuid;

// Custom serde for flexible datetime parsing (naive timestamps without timezone)
Expand Down Expand Up @@ -544,6 +544,20 @@ impl ModelKey {
}
}

/// Restore the persisted model-selection invariant after catalog changes:
/// every enabled model must still exist in the provider's available-model
/// catalog. Preserve user ordering while also removing duplicate rows.
///
/// This is deliberately owned by `ModelKey`, rather than individual UI or
/// runtime consumers, so old credentials and every write path converge on
/// the same representation.
pub fn normalize_enabled_models(&mut self) {
let available: HashSet<&str> = self.available_models.iter().map(String::as_str).collect();
let mut seen = HashSet::new();
self.enabled_models
.retain(|model| available.contains(model.as_str()) && seen.insert(model.clone()));
}

/// Mask sensitive data for display
pub fn mask_api_key(&self) -> Option<String> {
self.api_key.as_ref().map(|key| {
Expand Down
Loading
Loading