feat: finalise multi-profil Codex/Claude avec catalogue de modèles

- Backend : clone_profile_from_seed généralisé (non OpenCode)
- Backend : catalogue static Claude/Codex (3 modèles chacun, 1 recommandé)
- Backend : commandes Tauri list_claude_models/list_codex_models
- Frontend : ProfilesSettings refonte en onglets Codex/Claude + create/duplicate/edit/delete
- Frontend : ModelSelect searchable partagé + fallback saisie manuelle
- Frontend : assignation agent nom · modèle
- Tests QA : 4 profils modèles distincts (2 Claude, 2 Codex) assignés à agents
This commit is contained in:
2026-07-26 14:56:26 +02:00
parent c807a70fea
commit ea7ea71230
21 changed files with 1298 additions and 109 deletions

View File

@ -4,15 +4,17 @@
use app_tauri_lib::dto::{
parse_delete_profile, parse_profile_id, CloneOpenCodeProfileFromSeedRequestDto,
ConfigureProfilesRequestDto, DetectProfilesRequestDto, DetectProfilesResponseDto,
FirstRunStateDto, ProfileListDto, SaveProfileRequestDto,
CloneProfileFromSeedRequestDto, ConfigureProfilesRequestDto, DetectProfilesRequestDto,
DetectProfilesResponseDto, FirstRunStateDto, ProfileListDto, ProfileModelCatalogDto,
SaveProfileRequestDto,
};
use application::{
CloneOpenCodeProfileFromSeedInput, ConfigureProfilesInput, DetectProfilesInput,
DetectProfilesOutput, FirstRunStateOutput, ProfileAvailability, SaveProfileInput,
CloneOpenCodeProfileFromSeedInput, CloneProfileFromSeedInput, ConfigureProfilesInput,
DetectProfilesInput, DetectProfilesOutput, FirstRunStateOutput, ProfileAvailability,
SaveProfileInput,
};
use domain::ids::{LocalModelServerId, ProfileId};
use domain::profile::{AgentProfile, ContextInjection, OpenCodeConfig};
use domain::profile::{AgentProfile, ContextInjection, OpenCodeConfig, StructuredAdapter};
use serde_json::json;
use uuid::Uuid;
@ -121,6 +123,41 @@ fn clone_opencode_profile_from_seed_request_deserialises_camelcase_config() {
assert_eq!(opencode.local_model_server_id, Some(server_id));
}
#[test]
fn clone_profile_from_seed_request_deserialises_camelcase_overrides() {
let seed_id = Uuid::from_u128(42);
let raw = json!({
"seedProfileId": seed_id.to_string(),
"name": "Codex GPT-5",
"model": "gpt-5-codex"
});
let dto: CloneProfileFromSeedRequestDto = serde_json::from_value(raw).unwrap();
let input: CloneProfileFromSeedInput = dto.into();
assert_eq!(input.seed_profile_id, ProfileId::from_uuid(seed_id));
assert_eq!(input.name.as_deref(), Some("Codex GPT-5"));
assert_eq!(input.model.as_deref(), Some("gpt-5-codex"));
}
#[test]
fn profile_model_catalogue_dto_serialises_searchable_camelcase_entries() {
let dto = ProfileModelCatalogDto(vec![app_tauri_lib::dto::ProfileModelCatalogEntryDto {
adapter: StructuredAdapter::Codex,
model_id: "gpt-5-codex".to_owned(),
display_name: "GPT-5 Codex".to_owned(),
aliases: vec!["codex".to_owned()],
recommended: true,
}]);
let value = serde_json::to_value(&dto).unwrap();
let arr = value.as_array().expect("transparent array");
assert_eq!(arr[0]["adapter"], "codex");
assert_eq!(arr[0]["modelId"], "gpt-5-codex");
assert_eq!(arr[0]["displayName"], "GPT-5 Codex");
assert_eq!(arr[0]["aliases"], json!(["codex"]));
assert_eq!(arr[0]["recommended"], true);
}
#[test]
fn opencode_config_dto_omits_local_model_server_id_when_none() {
let config = OpenCodeConfig::new(