From 28bf8fb47d38140bc1e5ee09d2152b356ec7e5fe Mon Sep 17 00:00:00 2001 From: WompaJango Date: Sat, 13 Jun 2026 06:37:06 -0700 Subject: [PATCH] feat(dashboard): clone profiles from any source --- .../src/app/chat/sidebar/profile-switcher.tsx | 1 + .../app/profiles/create-profile-dialog.tsx | 45 ++++++++------ apps/desktop/src/app/profiles/index.tsx | 51 +++++++++------- apps/desktop/src/i18n/en.ts | 3 + apps/desktop/src/i18n/ja.ts | 3 + apps/desktop/src/i18n/types.ts | 3 + apps/desktop/src/i18n/zh-hant.ts | 3 + apps/desktop/src/i18n/zh.ts | 3 + apps/desktop/src/types/hermes.ts | 2 +- hermes_cli/web_server.py | 18 +++--- tests/hermes_cli/test_web_server.py | 30 ++++++++-- web/src/i18n/af.ts | 4 +- web/src/i18n/de.ts | 4 +- web/src/i18n/en.ts | 3 +- web/src/i18n/es.ts | 3 +- web/src/i18n/fr.ts | 3 +- web/src/i18n/ga.ts | 4 +- web/src/i18n/hu.ts | 4 +- web/src/i18n/it.ts | 4 +- web/src/i18n/ja.ts | 4 +- web/src/i18n/ko.ts | 4 +- web/src/i18n/pt.ts | 3 +- web/src/i18n/ru.ts | 4 +- web/src/i18n/tr.ts | 4 +- web/src/i18n/types.ts | 3 +- web/src/i18n/uk.ts | 3 +- web/src/i18n/zh-hant.ts | 4 +- web/src/i18n/zh.ts | 4 +- web/src/lib/api.ts | 3 +- web/src/pages/ProfileBuilderPage.tsx | 2 +- web/src/pages/ProfilesPage.tsx | 58 ++++++++++--------- 31 files changed, 182 insertions(+), 105 deletions(-) diff --git a/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx b/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx index d9d62a66440..85b9dfaade8 100644 --- a/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx +++ b/apps/desktop/src/app/chat/sidebar/profile-switcher.tsx @@ -284,6 +284,7 @@ export function ProfileRail() { selectProfile(name) }} open={createOpen} + profiles={profiles} /> void onCreated?: (name: string) => Promise | void open: boolean + profiles?: ProfileInfo[] }) { const { t } = useI18n() const p = t.profiles const [name, setName] = useState('') - const [cloneFromDefault, setCloneFromDefault] = useState(true) + const [cloneFrom, setCloneFrom] = useState('default') const [soul, setSoul] = useState('') const [status, setStatus] = useState<'done' | 'idle' | 'saving'>('idle') const [error, setError] = useState(null) @@ -43,7 +46,7 @@ export function CreateProfileDialog({ } setName('') - setCloneFromDefault(true) + setCloneFrom('default') setSoul('') setError(null) setStatus('idle') @@ -66,7 +69,7 @@ export function CreateProfileDialog({ setError(null) try { - await createProfile({ name: trimmed, clone_from_default: cloneFromDefault }) + await createProfile({ name: trimmed, clone_from: cloneFrom }) if (soul.trim()) { await updateProfileSoul(trimmed, soul) @@ -107,17 +110,25 @@ export function CreateProfileDialog({

- +
+ + +

{p.cloneFromDesc}

+
diff --git a/apps/desktop/src/app/profiles/index.tsx b/apps/desktop/src/app/profiles/index.tsx index 8aab185f542..32249c47906 100644 --- a/apps/desktop/src/app/profiles/index.tsx +++ b/apps/desktop/src/app/profiles/index.tsx @@ -12,6 +12,7 @@ import { DialogTitle } from '@/components/ui/dialog' import { Input } from '@/components/ui/input' +import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' import { Textarea } from '@/components/ui/textarea' import { createProfile, @@ -82,14 +83,14 @@ export function ProfilesView({ onClose }: ProfilesViewProps) { }, [profiles, selectedName]) const handleCreate = useCallback( - async (name: string, cloneFromDefault: boolean) => { + async (name: string, cloneFrom: null | string) => { const trimmed = name.trim() if (!isValidProfileName(trimmed)) { throw new Error(p.nameHint) } - await createProfile({ name: trimmed, clone_from_default: cloneFromDefault }) + await createProfile({ name: trimmed, clone_from: cloneFrom }) notify({ kind: 'success', title: p.created, message: trimmed }) setSelectedName(trimmed) await refresh() @@ -180,8 +181,9 @@ export function ProfilesView({ onClose }: ProfilesViewProps) { setCreateOpen(false)} - onCreate={async (name, cloneFromDefault) => handleCreate(name, cloneFromDefault)} + onCreate={async (name, cloneFrom) => handleCreate(name, cloneFrom)} open={createOpen} + profiles={profiles ?? []} /> !open && !deleting && setPendingDelete(null)} open={pendingDelete !== null}> @@ -453,16 +455,18 @@ function SoulEditor({ profileName }: { profileName: string }) { function CreateProfileDialog({ onClose, onCreate, - open + open, + profiles }: { onClose: () => void - onCreate: (name: string, cloneFromDefault: boolean) => Promise + onCreate: (name: string, cloneFrom: null | string) => Promise open: boolean + profiles: ProfileInfo[] }) { const { t } = useI18n() const p = t.profiles const [name, setName] = useState('') - const [cloneFromDefault, setCloneFromDefault] = useState(true) + const [cloneFrom, setCloneFrom] = useState('default') const [saving, setSaving] = useState(false) const [error, setError] = useState(null) @@ -472,7 +476,7 @@ function CreateProfileDialog({ } setName('') - setCloneFromDefault(true) + setCloneFrom('default') setError(null) setSaving(false) }, [open]) @@ -493,7 +497,7 @@ function CreateProfileDialog({ setError(null) try { - await onCreate(trimmed, cloneFromDefault) + await onCreate(trimmed, cloneFrom) onClose() } catch (err) { setError(err instanceof Error ? err.message : p.failedCreate) @@ -528,18 +532,25 @@ function CreateProfileDialog({

- +
+ + +

{p.cloneFromDesc}

+
{error && (
diff --git a/apps/desktop/src/i18n/en.ts b/apps/desktop/src/i18n/en.ts index 2ab95b3e61f..dc8dfc72777 100644 --- a/apps/desktop/src/i18n/en.ts +++ b/apps/desktop/src/i18n/en.ts @@ -903,6 +903,9 @@ export const en: Translations = { deleting: 'Deleting...', createDesc: 'Profiles are independent Hermes environments: separate config, skills, and SOUL.md.', nameLabel: 'Name', + cloneFrom: 'Clone from', + cloneFromNone: 'None (blank)', + cloneFromDesc: 'Copies config, skills, and SOUL.md from the selected source profile.', cloneFromDefault: 'Clone from default', cloneFromDefaultDesc: 'Copy config, skills, and SOUL.md from your default profile.', invalidName: hint => `Invalid name. ${hint}`, diff --git a/apps/desktop/src/i18n/ja.ts b/apps/desktop/src/i18n/ja.ts index a44019045fe..cae9539bccc 100644 --- a/apps/desktop/src/i18n/ja.ts +++ b/apps/desktop/src/i18n/ja.ts @@ -1041,6 +1041,9 @@ export const ja = defineLocale({ deleting: '削除中...', createDesc: 'プロファイルは独立した Hermes 環境です:設定、スキル、SOUL.md が別々になります。', nameLabel: '名前', + cloneFrom: '複製元', + cloneFromNone: 'なし(空)', + cloneFromDesc: '選択したプロファイルから設定、スキル、SOUL.md をコピーします。', cloneFromDefault: 'デフォルトプロファイルから設定を複製', cloneFromDefaultDesc: 'デフォルトプロファイルから設定、スキル、SOUL.md をコピーします。', invalidName: hint => `無効なプロファイル名。${hint}`, diff --git a/apps/desktop/src/i18n/types.ts b/apps/desktop/src/i18n/types.ts index 1f65dc57287..44e03e87a97 100644 --- a/apps/desktop/src/i18n/types.ts +++ b/apps/desktop/src/i18n/types.ts @@ -695,6 +695,9 @@ export interface Translations { deleting: string createDesc: string nameLabel: string + cloneFrom: string + cloneFromNone: string + cloneFromDesc: string cloneFromDefault: string cloneFromDefaultDesc: string invalidName: (hint: string) => string diff --git a/apps/desktop/src/i18n/zh-hant.ts b/apps/desktop/src/i18n/zh-hant.ts index c7bdf3ba6da..e28091a9d93 100644 --- a/apps/desktop/src/i18n/zh-hant.ts +++ b/apps/desktop/src/i18n/zh-hant.ts @@ -999,6 +999,9 @@ export const zhHant = defineLocale({ deleting: '刪除中…', createDesc: '設定檔是獨立的 Hermes 環境:各自擁有獨立的設定、技能和 SOUL.md。', nameLabel: '名稱', + cloneFrom: '複製來源', + cloneFromNone: '無(空白)', + cloneFromDesc: '從選取的來源設定檔複製設定、技能和 SOUL.md。', cloneFromDefault: '從預設設定檔複製設定', cloneFromDefaultDesc: '從您的預設設定檔複製設定、技能和 SOUL.md。', invalidName: hint => `設定檔名稱無效。${hint}`, diff --git a/apps/desktop/src/i18n/zh.ts b/apps/desktop/src/i18n/zh.ts index a047c0d44cd..0c073ba1e77 100644 --- a/apps/desktop/src/i18n/zh.ts +++ b/apps/desktop/src/i18n/zh.ts @@ -1092,6 +1092,9 @@ export const zh: Translations = { deleting: '删除中…', createDesc: '配置档案是相互独立的 Hermes 环境:各自拥有独立的配置、技能和 SOUL.md。', nameLabel: '名称', + cloneFrom: '克隆来源', + cloneFromNone: '无(空白)', + cloneFromDesc: '从选中的来源配置档案复制配置、技能和 SOUL.md。', cloneFromDefault: '从默认档案克隆', cloneFromDefaultDesc: '从你的默认配置档案复制配置、技能和 SOUL.md。', invalidName: hint => `名称无效。${hint}`, diff --git a/apps/desktop/src/types/hermes.ts b/apps/desktop/src/types/hermes.ts index f90e31c53bb..86dc41862db 100644 --- a/apps/desktop/src/types/hermes.ts +++ b/apps/desktop/src/types/hermes.ts @@ -470,7 +470,7 @@ export interface CronJobUpdates { export interface ProfileCreatePayload { clone_all?: boolean - clone_from?: string + clone_from?: null | string clone_from_default?: boolean name: string no_skills?: boolean diff --git a/hermes_cli/web_server.py b/hermes_cli/web_server.py index 527aae07cf2..78a327a4767 100644 --- a/hermes_cli/web_server.py +++ b/hermes_cli/web_server.py @@ -8518,15 +8518,13 @@ async def scan_skill_hub(identifier: str = ""): class ProfileCreate(BaseModel): name: str + clone_from: Optional[str] = None + # Backward compatibility for older dashboard/desktop clients. New clients + # send clone_from="default" (or another profile name) explicitly. clone_from_default: bool = False clone_all: bool = False no_skills: bool = False description: Optional[str] = None - # Explicit source profile to clone from (e.g. duplicating an existing - # profile). When set, it takes precedence over ``clone_from_default``, - # which always sources from "default". ``clone_all`` still selects a full - # state copytree vs. a config/skills/SOUL copy. - clone_from: Optional[str] = None provider: Optional[str] = None model: Optional[str] = None # Profile-builder additions — all optional, all applied best-effort AFTER @@ -8798,10 +8796,16 @@ async def create_profile_endpoint(body: ProfileCreate): clone = True clone_from = explicit_source clone_config = not body.clone_all + elif body.clone_all: + # Preserve the dashboard's historical clone-all behavior: a full-copy + # request with no explicit dropdown source copies from default. + clone = True + clone_from = "default" + clone_config = False else: - clone = body.clone_from_default or body.clone_all + clone = body.clone_from_default clone_from = "default" if clone else None - clone_config = body.clone_from_default and not body.clone_all + clone_config = clone try: path = profiles_mod.create_profile( name=body.name, diff --git a/tests/hermes_cli/test_web_server.py b/tests/hermes_cli/test_web_server.py index 73d5a1a667f..01a76b92816 100644 --- a/tests/hermes_cli/test_web_server.py +++ b/tests/hermes_cli/test_web_server.py @@ -2633,7 +2633,7 @@ class TestNewEndpoints: resp = self.client.post( "/api/profiles", - json={"name": "writer", "clone_from_default": False}, + json={"name": "writer", "clone_from": None}, ) assert resp.status_code == 200 @@ -2641,7 +2641,7 @@ class TestNewEndpoints: assert wrapper_path.exists() assert wrapper_path.read_text() == '#!/bin/sh\nexec hermes -p writer "$@"\n' - def test_profiles_create_with_clone_from_default_copies_default_skills(self, monkeypatch): + def test_profiles_create_with_clone_from_copies_source_skills(self, monkeypatch): from hermes_constants import get_hermes_home import hermes_cli.profiles as profiles_mod @@ -2652,7 +2652,7 @@ class TestNewEndpoints: resp = self.client.post( "/api/profiles", - json={"name": "cloned", "clone_from_default": True}, + json={"name": "cloned", "clone_from": "default"}, ) assert resp.status_code == 200 @@ -2685,6 +2685,28 @@ class TestNewEndpoints: ) assert cloned_skill.exists() + def test_profiles_create_clone_all_from_named_source(self, monkeypatch): + from hermes_constants import get_hermes_home + import hermes_cli.profiles as profiles_mod + + monkeypatch.setattr(profiles_mod, "create_wrapper_script", lambda name: None) + + assert self.client.post("/api/profiles", json={"name": "full-src"}).status_code == 200 + source_dir = get_hermes_home() / "profiles" / "full-src" + (source_dir / "config.yaml").write_text("model:\n provider: source-only\n", encoding="utf-8") + (source_dir / "workspace" / "artifact.txt").parent.mkdir(parents=True, exist_ok=True) + (source_dir / "workspace" / "artifact.txt").write_text("copied", encoding="utf-8") + + resp = self.client.post( + "/api/profiles", + json={"name": "full-copy", "clone_from": "full-src", "clone_all": True}, + ) + + assert resp.status_code == 200 + target_dir = get_hermes_home() / "profiles" / "full-copy" + assert (target_dir / "config.yaml").read_text(encoding="utf-8") == "model:\n provider: source-only\n" + assert (target_dir / "workspace" / "artifact.txt").read_text(encoding="utf-8") == "copied" + def test_profiles_create_without_clone_seeds_bundled_skills(self, monkeypatch): from hermes_constants import get_hermes_home import hermes_cli.profiles as profiles_mod @@ -2701,7 +2723,7 @@ class TestNewEndpoints: resp = self.client.post( "/api/profiles", - json={"name": "fresh", "clone_from_default": False}, + json={"name": "fresh", "clone_from": None}, ) assert resp.status_code == 200 diff --git a/web/src/i18n/af.ts b/web/src/i18n/af.ts index 5d5dc004222..2a8af6f0843 100644 --- a/web/src/i18n/af.ts +++ b/web/src/i18n/af.ts @@ -286,8 +286,8 @@ export const af: Translations = { nameRequired: "Naam word vereis", nameRule: "Slegs kleinletters, syfers, _ en -; moet met 'n letter of syfer begin; tot 64 karakters.", - invalidName: "Ongeldige profielnaam", - cloneFromDefault: "Kloon konfigurasie vanaf verstekprofiel", + invalidName: "Ongeldige profielnaam", cloneFrom: "Kloon konfigurasie vanaf profiel", + cloneFromNone: "Geen (leeg)", allProfiles: "Profiele", noProfiles: "Geen profiele gevind nie.", defaultBadge: "verstek", diff --git a/web/src/i18n/de.ts b/web/src/i18n/de.ts index e2eb1429c09..11b4a095cb6 100644 --- a/web/src/i18n/de.ts +++ b/web/src/i18n/de.ts @@ -286,8 +286,8 @@ export const de: Translations = { nameRequired: "Name ist erforderlich", nameRule: "Nur Kleinbuchstaben, Ziffern, _ und -; muss mit einem Buchstaben oder einer Ziffer beginnen; maximal 64 Zeichen.", - invalidName: "Ungültiger Profilname", - cloneFromDefault: "Konfiguration vom Standardprofil klonen", + invalidName: "Ungültiger Profilname", cloneFrom: "Konfiguration klonen von", + cloneFromNone: "Keine (leer)", allProfiles: "Profile", noProfiles: "Keine Profile gefunden.", defaultBadge: "Standard", diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 853eeb4a9c1..10fd8df4300 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -297,7 +297,8 @@ export const en: Translations = { nameRule: "Lowercase letters, digits, _ and - only; must start with a letter or digit; up to 64 characters.", invalidName: "Invalid profile name", - cloneFromDefault: "Clone config from default profile", + cloneFrom: "Clone config from", + cloneFromNone: "None (blank)", allProfiles: "Profiles", noProfiles: "No profiles found.", defaultBadge: "default", diff --git a/web/src/i18n/es.ts b/web/src/i18n/es.ts index 421837007ca..598e0a3ad24 100644 --- a/web/src/i18n/es.ts +++ b/web/src/i18n/es.ts @@ -287,7 +287,8 @@ export const es: Translations = { nameRule: "Solo letras minúsculas, dígitos, _ y -; debe comenzar con una letra o dígito; hasta 64 caracteres.", invalidName: "Nombre de perfil no válido", - cloneFromDefault: "Clonar configuración del perfil predeterminado", + cloneFrom: "Clonar desde el perfil", + cloneFromNone: "Ninguno (vacío)", allProfiles: "Perfiles", noProfiles: "No se encontraron perfiles.", defaultBadge: "predeterminado", diff --git a/web/src/i18n/fr.ts b/web/src/i18n/fr.ts index 4887dc9c07e..659700a5864 100644 --- a/web/src/i18n/fr.ts +++ b/web/src/i18n/fr.ts @@ -287,7 +287,8 @@ export const fr: Translations = { nameRule: "Lettres minuscules, chiffres, _ et - uniquement ; doit commencer par une lettre ou un chiffre ; jusqu'à 64 caractères.", invalidName: "Nom de profil invalide", - cloneFromDefault: "Cloner la configuration du profil par défaut", + cloneFrom: "Cloner depuis le profil", + cloneFromNone: "Aucun (vide)", allProfiles: "Profils", noProfiles: "Aucun profil trouvé.", defaultBadge: "défaut", diff --git a/web/src/i18n/ga.ts b/web/src/i18n/ga.ts index 6f71635b3cb..214d69373a1 100644 --- a/web/src/i18n/ga.ts +++ b/web/src/i18n/ga.ts @@ -294,8 +294,8 @@ export const ga: Translations = { nameRequired: "Tá ainm riachtanach", nameRule: "Litreacha cás íochtair, digití, _ agus - amháin; caithfidh tús a chur le litir nó digit; suas le 64 carachtar.", - invalidName: "Ainm próifíle neamhbhailí", - cloneFromDefault: "Clónáil cumraíocht ón bpróifíl réamhshocraithe", + invalidName: "Ainm próifíle neamhbhailí", cloneFrom: "Clónáil cumraíocht ón bpróifíl", + cloneFromNone: "Dada (folamh)", allProfiles: "Próifílí", noProfiles: "Níor aimsíodh próifílí.", defaultBadge: "réamhshocraithe", diff --git a/web/src/i18n/hu.ts b/web/src/i18n/hu.ts index a413820744e..cf9d121a06a 100644 --- a/web/src/i18n/hu.ts +++ b/web/src/i18n/hu.ts @@ -286,8 +286,8 @@ export const hu: Translations = { nameRequired: "A név kötelező", nameRule: "Csak kisbetűk, számjegyek, _ és - karakterek; betűvel vagy számjeggyel kell kezdődnie; legfeljebb 64 karakter.", - invalidName: "Érvénytelen profilnév", - cloneFromDefault: "Konfiguráció klónozása az alapértelmezett profilból", + invalidName: "Érvénytelen profilnév", cloneFrom: "Konfiguráció klónozása ebből a profilból", + cloneFromNone: "Nincs (üres)", allProfiles: "Profilok", noProfiles: "Nem található profil.", defaultBadge: "alapértelmezett", diff --git a/web/src/i18n/it.ts b/web/src/i18n/it.ts index 61ca8b7bb8e..777f913075d 100644 --- a/web/src/i18n/it.ts +++ b/web/src/i18n/it.ts @@ -286,8 +286,8 @@ export const it: Translations = { nameRequired: "Il nome è obbligatorio", nameRule: "Solo lettere minuscole, cifre, _ e -; deve iniziare con una lettera o cifra; fino a 64 caratteri.", - invalidName: "Nome del profilo non valido", - cloneFromDefault: "Clona la configurazione dal profilo predefinito", + invalidName: "Nome del profilo non valido", cloneFrom: "Clona configurazione dal profilo", + cloneFromNone: "Nessuno (vuoto)", allProfiles: "Profili", noProfiles: "Nessun profilo trovato.", defaultBadge: "predefinito", diff --git a/web/src/i18n/ja.ts b/web/src/i18n/ja.ts index e3db6b9a257..eb0f237a86c 100644 --- a/web/src/i18n/ja.ts +++ b/web/src/i18n/ja.ts @@ -285,8 +285,8 @@ export const ja: Translations = { nameRequired: "名前は必須です", nameRule: "小文字、数字、_ および - のみ使用可能。最初は文字または数字で始める必要があります。最大 64 文字。", - invalidName: "無効なプロファイル名", - cloneFromDefault: "デフォルトプロファイルから設定を複製", + invalidName: "無効なプロファイル名", cloneFrom: "プロファイルから複製", + cloneFromNone: "なし(空)", allProfiles: "プロファイル", noProfiles: "プロファイルが見つかりません。", defaultBadge: "デフォルト", diff --git a/web/src/i18n/ko.ts b/web/src/i18n/ko.ts index b624938f8df..44f689aa5f2 100644 --- a/web/src/i18n/ko.ts +++ b/web/src/i18n/ko.ts @@ -285,8 +285,8 @@ export const ko: Translations = { nameRequired: "이름은 필수입니다", nameRule: "소문자, 숫자, _ 및 - 만 사용 가능합니다. 문자나 숫자로 시작해야 하며 최대 64자입니다.", - invalidName: "잘못된 프로필 이름입니다", - cloneFromDefault: "기본 프로필에서 설정 복제", + invalidName: "잘못된 프로필 이름입니다", cloneFrom: "프로필에서 복제", + cloneFromNone: "없음 (빈 상태)", allProfiles: "프로필", noProfiles: "프로필을 찾을 수 없습니다.", defaultBadge: "기본", diff --git a/web/src/i18n/pt.ts b/web/src/i18n/pt.ts index 109027bc775..7ad8f15b9ca 100644 --- a/web/src/i18n/pt.ts +++ b/web/src/i18n/pt.ts @@ -287,7 +287,8 @@ export const pt: Translations = { nameRule: "Apenas letras minúsculas, dígitos, _ e -; deve começar com letra ou dígito; até 64 caracteres.", invalidName: "Nome de perfil inválido", - cloneFromDefault: "Clonar configuração do perfil predefinido", + cloneFrom: "Clonar a partir do perfil", + cloneFromNone: "Nenhum (vazio)", allProfiles: "Perfis", noProfiles: "Não foram encontrados perfis.", defaultBadge: "predefinido", diff --git a/web/src/i18n/ru.ts b/web/src/i18n/ru.ts index 51eaf774c54..8f7fcab6126 100644 --- a/web/src/i18n/ru.ts +++ b/web/src/i18n/ru.ts @@ -286,8 +286,8 @@ export const ru: Translations = { nameRequired: "Имя обязательно", nameRule: "Только строчные буквы, цифры, _ и -; должно начинаться с буквы или цифры; до 64 символов.", - invalidName: "Недопустимое имя профиля", - cloneFromDefault: "Клонировать конфигурацию из профиля по умолчанию", + invalidName: "Недопустимое имя профиля", cloneFrom: "Клонировать конфигурацию из профиля", + cloneFromNone: "Нет (пусто)", allProfiles: "Профили", noProfiles: "Профили не найдены.", defaultBadge: "по умолчанию", diff --git a/web/src/i18n/tr.ts b/web/src/i18n/tr.ts index 85910c88e4a..c597e3d6852 100644 --- a/web/src/i18n/tr.ts +++ b/web/src/i18n/tr.ts @@ -286,8 +286,8 @@ export const tr: Translations = { nameRequired: "Ad gereklidir", nameRule: "Yalnızca küçük harfler, rakamlar, _ ve - kullanılabilir; harf veya rakamla başlamalı; en fazla 64 karakter.", - invalidName: "Geçersiz profil adı", - cloneFromDefault: "Varsayılan profilden yapılandırmayı klonla", + invalidName: "Geçersiz profil adı", cloneFrom: "Profilden yapılandırmayı klonla", + cloneFromNone: "Hiçbiri (boş)", allProfiles: "Profiller", noProfiles: "Profil bulunamadı.", defaultBadge: "varsayılan", diff --git a/web/src/i18n/types.ts b/web/src/i18n/types.ts index aecb863544e..68a5c569377 100644 --- a/web/src/i18n/types.ts +++ b/web/src/i18n/types.ts @@ -354,7 +354,8 @@ export interface Translations { nameRequired: string; nameRule: string; invalidName: string; - cloneFromDefault: string; + cloneFrom: string; + cloneFromNone: string; allProfiles: string; noProfiles: string; defaultBadge: string; diff --git a/web/src/i18n/uk.ts b/web/src/i18n/uk.ts index ce1a4babfec..1382c1b2bf1 100644 --- a/web/src/i18n/uk.ts +++ b/web/src/i18n/uk.ts @@ -287,7 +287,8 @@ export const uk: Translations = { nameRule: "Лише малі літери, цифри, _ та -; має починатися з літери або цифри; до 64 символів.", invalidName: "Недопустима назва профілю", - cloneFromDefault: "Клонувати конфігурацію з профілю за замовчуванням", + cloneFrom: "Клонувати з профілю", + cloneFromNone: "Жоден (порожній)", allProfiles: "Профілі", noProfiles: "Профілів не знайдено.", defaultBadge: "за замовчуванням", diff --git a/web/src/i18n/zh-hant.ts b/web/src/i18n/zh-hant.ts index e2c4ff7252f..09f611bb558 100644 --- a/web/src/i18n/zh-hant.ts +++ b/web/src/i18n/zh-hant.ts @@ -285,8 +285,8 @@ export const zhHant: Translations = { nameRequired: "名稱為必填", nameRule: "僅允許小寫字母、數字、底線及連字號;首字必須為字母或數字;最多 64 個字元。", - invalidName: "設定檔名稱無效", - cloneFromDefault: "從預設設定檔複製設定", + invalidName: "設定檔名稱無效", cloneFrom: "從設定檔複製", + cloneFromNone: "無(空白)", allProfiles: "設定檔", noProfiles: "找不到設定檔。", defaultBadge: "預設", diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index d60dea816e5..2bac16c3dec 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -282,8 +282,8 @@ export const zh: Translations = { nameRequired: "名称必填", nameRule: "仅允许小写字母、数字、下划线和短横线;首字符必须是字母或数字;最多 64 个字符。", - invalidName: "多Agent配置名称非法", - cloneFromDefault: "从默认多Agent配置克隆配置", + invalidName: "多Agent配置名称非法", cloneFrom: "从配置文件克隆", + cloneFromNone: "无(空白)", allProfiles: "多Agent配置列表", noProfiles: "暂无多Agent配置。", defaultBadge: "默认", diff --git a/web/src/lib/api.ts b/web/src/lib/api.ts index b4390b80729..fab64b64c84 100644 --- a/web/src/lib/api.ts +++ b/web/src/lib/api.ts @@ -552,7 +552,8 @@ export const api = { }), createProfile: (body: { name: string; - clone_from_default: boolean; + clone_from?: string | null; + clone_from_default?: boolean; clone_all?: boolean; no_skills?: boolean; description?: string; diff --git a/web/src/pages/ProfileBuilderPage.tsx b/web/src/pages/ProfileBuilderPage.tsx index 4747878be8e..6aedb8dc147 100644 --- a/web/src/pages/ProfileBuilderPage.tsx +++ b/web/src/pages/ProfileBuilderPage.tsx @@ -220,7 +220,7 @@ export default function ProfileBuilderPage() { try { const res = await api.createProfile({ name: n, - clone_from_default: false, + clone_from: null, description: description.trim() || undefined, provider: pickedModel?.provider, model: pickedModel?.model, diff --git a/web/src/pages/ProfilesPage.tsx b/web/src/pages/ProfilesPage.tsx index 29220ea9639..fdf89fa4a41 100644 --- a/web/src/pages/ProfilesPage.tsx +++ b/web/src/pages/ProfilesPage.tsx @@ -35,11 +35,11 @@ import { Badge } from "@nous-research/ui/ui/components/badge"; import { Button } from "@nous-research/ui/ui/components/button"; import { Input } from "@nous-research/ui/ui/components/input"; import { Label } from "@nous-research/ui/ui/components/label"; -import { Checkbox } from "@nous-research/ui/ui/components/checkbox"; import { Select, SelectOption, } from "@nous-research/ui/ui/components/select"; +import { Checkbox } from "@nous-research/ui/ui/components/checkbox"; import { useI18n } from "@/i18n"; import { usePageHeader } from "@/contexts/usePageHeader"; import { cn, themedBody } from "@/lib/utils"; @@ -312,7 +312,7 @@ export default function ProfilesPage() { // Create modal const [createModalOpen, setCreateModalOpen] = useState(false); const [newName, setNewName] = useState(""); - const [cloneFromDefault, setCloneFromDefault] = useState(true); + const [cloneFrom, setCloneFrom] = useState("default"); const [cloneAll, setCloneAll] = useState(false); const [noSkills, setNoSkills] = useState(false); const [newDescription, setNewDescription] = useState(""); @@ -429,7 +429,7 @@ export default function ProfilesPage() { } setCreating(true); try { - const cloning = cloneAll || cloneFromDefault; + const cloning = cloneFrom !== null; const picked = modelChoice ? modelChoices?.find( (c) => `${c.provider}\u0000${c.model}` === modelChoice, @@ -437,8 +437,8 @@ export default function ProfilesPage() { : undefined; const res = await api.createProfile({ name, - clone_from_default: cloneAll ? false : cloneFromDefault, - clone_all: cloneAll, + clone_from: cloneFrom, + clone_all: cloning && cloneAll, no_skills: cloning ? false : noSkills, description: newDescription.trim() || undefined, provider: picked?.provider, @@ -455,7 +455,7 @@ export default function ProfilesPage() { setNewDescription(""); setNoSkills(false); setCloneAll(false); - setCloneFromDefault(true); + setCloneFrom("default"); setModelChoice(""); setCreateModalOpen(false); load(); @@ -772,7 +772,7 @@ export default function ProfilesPage() { }; }, [setEnd, t.common.create, loading, navigate]); - const cloning = cloneAll || cloneFromDefault; + const cloning = cloneFrom !== null; if (loading) { return ( @@ -862,6 +862,26 @@ export default function ProfilesPage() {

+
+ + +
+