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

@ -42,6 +42,7 @@ import type {
ProjectWorkState,
ProjectSystemPermissions,
ProfileAvailability,
ProfileModelCatalogEntry,
ResolvedAgentSystemPermissions,
SystemPermissionSet,
Skill,
@ -50,6 +51,7 @@ import type {
TurnPage,
} from "@/domain";
import type {
CloneProfileFromSeedInput,
CloneOpenCodeProfileFromSeedInput,
ConversationGateway,
ConversationPageRequest,
@ -176,6 +178,17 @@ export class HttpProfileGateway implements ProfileGateway {
async deleteProfile(profileId: string): Promise<void> {
await this.http.invoke("delete_profile", { profileId });
}
cloneProfileFromSeed(input: CloneProfileFromSeedInput): Promise<AgentProfile> {
return this.http.invoke<AgentProfile>("clone_profile_from_seed", {
request: { seedProfileId: input.seedProfileId, name: input.name, model: input.model },
});
}
listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
return this.http.invoke<ProfileModelCatalogEntry[]>("list_claude_models");
}
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
return this.http.invoke<ProfileModelCatalogEntry[]>("list_codex_models");
}
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]> {
return this.http.invoke<AgentProfile[]>("configure_profiles", { request: { profiles } });
}

View File

@ -35,6 +35,7 @@ import type {
McpToolCatalogue,
McpToolPolicy,
OpenCodeProviderCatalogEntry,
ProfileModelCatalogEntry,
EffectivePermissions,
PairedDevice,
PairingCode,
@ -80,6 +81,7 @@ import type {
ConversationGateway,
ConversationPageRequest,
ConversationDetails,
CloneProfileFromSeedInput,
CloneOpenCodeProfileFromSeedInput,
CreateAgentInput,
CreateMemoryInput,
@ -1293,6 +1295,54 @@ const MOCK_OPENCODE_PROVIDERS: OpenCodeProviderCatalogEntry[] = [
},
];
const MOCK_CLAUDE_MODELS: ProfileModelCatalogEntry[] = [
{
adapter: "claude",
modelId: "claude-sonnet-5",
displayName: "Claude Sonnet 5",
aliases: ["sonnet"],
recommended: true,
},
{
adapter: "claude",
modelId: "claude-opus-4-8",
displayName: "Claude Opus 4.8",
aliases: ["opus"],
recommended: false,
},
{
adapter: "claude",
modelId: "claude-haiku-4-5-20251001",
displayName: "Claude Haiku 4.5",
aliases: ["haiku"],
recommended: false,
},
];
const MOCK_CODEX_MODELS: ProfileModelCatalogEntry[] = [
{
adapter: "codex",
modelId: "gpt-5-codex",
displayName: "GPT-5 Codex",
aliases: ["codex"],
recommended: true,
},
{
adapter: "codex",
modelId: "gpt-5",
displayName: "GPT-5",
aliases: ["general"],
recommended: false,
},
{
adapter: "codex",
modelId: "gpt-5-mini",
displayName: "GPT-5 mini",
aliases: ["mini", "fast"],
recommended: false,
},
];
/**
* In-memory profiles gateway. Tracks configured profiles and a first-run flag so
* the wizard can be driven and tested fully offline. By default it reports the
@ -1348,6 +1398,36 @@ export class MockProfileGateway implements ProfileGateway {
return structuredClone(profiles);
}
async cloneProfileFromSeed(
input: CloneProfileFromSeedInput,
): Promise<AgentProfile> {
const seed = [...this.profiles, ...MOCK_REFERENCE_PROFILES].find(
(p) => p.id === input.seedProfileId,
);
if (!seed) throw new Error(`unknown profile seed: ${input.seedProfileId}`);
this.cloneCounter += 1;
const cloned: AgentProfile = {
...structuredClone(seed),
id: `mock-profile-clone-${this.cloneCounter}`,
name: input.name ?? `${seed.name} copy`,
model:
input.model !== undefined && input.model.trim() !== ""
? input.model
: seed.model,
};
this.profiles.push(cloned);
this.configured = true;
return structuredClone(cloned);
}
async listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
return structuredClone(MOCK_CLAUDE_MODELS);
}
async listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
return structuredClone(MOCK_CODEX_MODELS);
}
async cloneOpenCodeProfileFromSeed(
input: CloneOpenCodeProfileFromSeedInput = {},
): Promise<AgentProfile> {

View File

@ -12,9 +12,11 @@ import type {
AgentProfile,
FirstRunState,
OpenCodeProviderCatalogEntry,
ProfileModelCatalogEntry,
ProfileAvailability,
} from "@/domain";
import type {
CloneProfileFromSeedInput,
CloneOpenCodeProfileFromSeedInput,
ProfileGateway,
SaveOpenCodeProviderProfileInput,
@ -47,6 +49,24 @@ export class TauriProfileGateway implements ProfileGateway {
await invoke("delete_profile", { profileId });
}
cloneProfileFromSeed(input: CloneProfileFromSeedInput): Promise<AgentProfile> {
return invoke<AgentProfile>("clone_profile_from_seed", {
request: {
seedProfileId: input.seedProfileId,
name: input.name,
model: input.model,
},
});
}
listClaudeModels(): Promise<ProfileModelCatalogEntry[]> {
return invoke<ProfileModelCatalogEntry[]>("list_claude_models");
}
listCodexModels(): Promise<ProfileModelCatalogEntry[]> {
return invoke<ProfileModelCatalogEntry[]>("list_codex_models");
}
configureProfiles(profiles: AgentProfile[]): Promise<AgentProfile[]> {
return invoke<AgentProfile[]>("configure_profiles", {
request: { profiles },