Clone profile from any source via dropdown
Cluster 23746 · hermes cli. Data as of
Nothing open on GitHub. Fix merged in #45630 on 13 Jun 2026. Check it covers your case.
The short answer
- Known?
- Yes. 3 related items grouped here, 1 issue already closed.
- Fix event
- A fix was merged to main in PR #45630. Check it covers your case. No release data is tracked.
- Tracker
- The maintainers' tracker marks this
resolved, deciding PR #45630. That is separate from the fix event. - Still open
- Nothing open on GitHub.
These issues and PRs relate to adding or requesting a feature that allows cloning a new profile from any existing profile, replacing a checkbox with a dropdown selector. The theme is consistent across a dashboard UI change and a corresponding profile backend feature.
Open issues0
Open PRs0
Fixes on record
-
#45630 feat(profile): clone profiles from any source
files:
apps/desktop/src/app/chat/sidebar/profile-switcher.tsx,apps/desktop/src/app/profiles/create-profile-dialog.tsx,apps/desktop/src/app/profiles/index.tsx,apps/desktop/src/i18n/en.ts,apps/desktop/src/i18n/ja.ts,apps/desktop/src/i18n/types.ts,apps/desktop/src/i18n/zh-hant.ts,apps/desktop/src/i18n/zh.ts,apps/desktop/src/types/hermes.ts,docs/design/profile-builder.md,hermes_cli/main.py,hermes_cli/profiles.py,hermes_cli/subcommands/profile.py,hermes_cli/web_server.py,scripts/release.py,tests/hermes_cli/test_web_server.py,web/src/i18n/af.ts,web/src/i18n/de.ts,web/src/i18n/en.ts,web/src/i18n/es.ts(+24 more)Show the diff
-
apps/desktop/src/app/chat/sidebar/profile-switcher.tsx@@ -284,6 +284,7 @@ export function ProfileRail() { selectProfile(name) }} open={createOpen} + profiles={profiles} /> <RenameProfileDialog -
apps/desktop/src/app/profiles/create-profile-dialog.tsx@@ -2,14 +2,15 @@ import { useEffect, useState } from 'react' import { ActionStatus } from '@/components/ui/action-status' import { Button } from '@/components/ui/button' -import { Checkbox } from '@/components/ui/checkbox' import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, 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, updateProfileSoul } from '@/hermes' import { useI18n } from '@/i18n' import { AlertTriangle } from '@/lib/icons' import { cn } from '@/lib/utils' +import type { ProfileInfo } from '@/types/hermes' const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/ @@ -23,16 +24,18 @@ export function isValidProfileName(name: string): boolean { export function CreateProfileDialog({ onClose, onCreated, - open + open, + profiles = [] }: { onClose: () => void onCreated?: (name: string) => Promise<void> | 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<null | string>('default') const [soul, setSoul] = useState('') const [status, setStatus] = useState<'done' | 'idle' | 'saving'>('idle') const [error, setError] = useState<null | string>(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> </div> - <label className="flex cursor-pointer select-none items-start gap-2.5 px-0.5 py-1"> - <Checkbox - checked={cloneFromDefault} - className="mt-0.5 shrink-0" - onCheckedChange={checked => setCloneFromDefault(checked === true)} - /> - <span className="grid gap-0.5 leading-snug"> - <span className="text-sm font-medium">{p.cloneFromDefault}</span> - <span className="text-xs text-muted-foreground">{p.cloneFromDefaultDesc}</span> - </span> - </label> + <div className="grid gap-1.5"> + <label className="text-xs font-medium" htmlFor="new-profile-clone-from"> + {p.cloneFrom} + </label> + <Select onValueChange={value => setCloneFrom(value === '__none__' ? null : value)} value={cloneFrom ?? '__none__'}> + <SelectTrigger className="h-9 rounded-md" id="new-profile-clone-from"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="__none__">{p.cloneFromNone}</SelectItem> + {profiles.map(profile => ( + <SelectItem key={profile.name} value={profile.name}> + {profile.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <p className="text-xs text-muted-foreground">{p.cloneFromDesc}</p> + </div> <div className="grid gap-1.5"> <label className="text-xs font-medium" htmlFor="new-profile-soul"> @@ -127,7 +138,7 @@ export function CreateProfileDialog({ className="min-h-28 font-mono text-xs leading-5" id="new-profile-soul" onChange={event => setSoul(event.target.value)} - placeholder={p.soulPlaceholder(cloneFromDefault ? p.soulPlaceholderCloned : p.soulPlaceholderEmpty)} + placeholder={p.soulPlaceholder(cloneFrom ? p.soulPlaceholderCloned : p.soulPlaceholderEmpty)} value={soul} /> </div> -
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) { <CreateProfileDialog onClose={() => setCreateOpen(false)} - onCreate={async (name, cloneFromDefault) => handleCreate(name, cloneFromDefault)} + onCreate={async (name, cloneFrom) => handleCreate(name, cloneFrom)} open={createOpen} + profiles={profiles ?? []} /> <Dialog onOpenChange={open => !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<void> + onCreate: (name: string, cloneFrom: null | 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<null | string>('default') const [saving, setSaving] = useState(false) const [error, setError] = useState<null | string>(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> </div> - <label className="flex cursor-pointer items-center gap-2 rounded-md border border-border/40 bg-background/50 px-3 py-2 text-sm"> - <input - checked={cloneFromDefault} - className="size-4 accent-primary" - onChange={event => setCloneFromDefault(event.target.checked)} - type="checkbox" - /> - <span> - <span className="font-medium">{p.cloneFromDefault}</span> - <span className="ml-2 text-xs text-muted-foreground">{p.cloneFromDefaultDesc}</span> - </span> - </label> + <div className="grid gap-1.5"> + <label className="text-xs font-medium" htmlFor="new-profile-clone-from"> + {p.cloneFrom} + </label> + <Select onValueChange={value => setCloneFrom(value === '__none__' ? null : value)} value={cloneFrom ?? '__none__'}> + <SelectTrigger className="h-9 rounded-md" id="new-profile-clone-from"> + <SelectValue /> + </SelectTrigger> + <SelectContent> + <SelectItem value="__none__">{p.cloneFromNone}</SelectItem> + {profiles.map(profile => ( + <SelectItem key={profile.name} value={profile.name}> + {profile.name} + </SelectItem> + ))} + </SelectContent> + </Select> + <p className="text-xs text-muted-foreground">{p.cloneFromDesc}</p> + </div> {error && ( <div className="flex items-start gap-2 rounded-md border border-destructive/30 bg-destructive/10 px-3 py-2 text-xs text-destructive"> -
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}`, -
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}`, -
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 -
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}`, -
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}`, -
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 -
docs/design/profile-builder.md@@ -85,6 +85,8 @@ Extend `ProfileCreate` and the create endpoint — no new endpoints, no rewrite: ```python class ProfileCreate(BaseModel): name: str + clone_from: Optional[str] = None + # Backward compatibility for older dashboard/desktop clients. clone_from_default: bool = False clone_all: bool = False no_skills: bool = False -
hermes_cli/main.py@@ -9860,19 +9860,20 @@ def cmd_profile(args): try: clone_from = getattr(args, "clone_from", None) + clone_config = clone or clone_from is not None profile_dir = create_profile( name=name, clone_from=clone_from, clone_all=clone_all, - clone_config=clone, + clone_config=clone_config, no_alias=no_alias, no_skills=no_skills, description=getattr(args, "description", None), ) print(f"\nProfile '{name}' created at {profile_dir}") - if clone or clone_all: + if clone_config or clone_all: source_label = ( getattr(args, "clone_from", None) or get_active_profile_name() ) @@ -9886,8 +9887,8 @@ def cmd_profile(args): f"Cloned config, .env, SOUL.md, and skills from {source_label}." ) - # Auto-clone Honcho config for the new profile (only with --clone/--clone-all) - if clone or clone_all: + # Auto-clone Honcho config for the new profile (only with clone operations) + if clone_config or clone_all: try: from plugins.memory.honcho.cli import clone_honcho_for_profile @@ -9896,10 +9897,10 @@ def cmd_profile(args): except Exception: pass # Honcho plugin not installed or not configured - # Seed bundled skills (skip if --clone-all already copied them, or - # if --no-skills was passed — in which case seed_profile_skills() - # honors the marker file and returns skipped_opt_out=True). - if not clone_all: + # Seed bundled skills for fresh profiles only. Clone operations + # already copied the source profile's skills, including any + # user-installed or intentionally removed skills. + if not (clone_config or clone_all): result = seed_profile_skills(profile_dir) if result and result.get("skipped_opt_out"): print( -
hermes_cli/profiles.py@@ -784,9 +784,9 @@ def create_profile( Path The newly created profile directory. """ - if no_skills and (clone_config or clone_all): + if no_skills and (clone_from is not None or clone_config or clone_all): raise ValueError( - "--no-skills is mutually exclusive with --clone / --clone-all " + "--no-skills is mutually exclusive with --clone / --clone-from / --clone-all " "(cloning explicitly copies skills from the source profile)." ) canon = normalize_profile_name(name) -
hermes_cli/subcommands/profile.py@@ -35,17 +35,17 @@ def build_profile_parser(subparsers, *, cmd_profile: Callable) -> None: profile_create.add_argument( "--clone", action="store_true", - help="Copy config.yaml, .env, SOUL.md from active profile", + help="Copy config.yaml, .env, SOUL.md, and skills from active profile", ) profile_create.add_argument( "--clone-all", action="store_true", - help="Full copy of active profile (all state)", + help="Full copy of active profile (all state, excluding per-profile history)", ) profile_create.add_argument( "--clone-from", metavar="SOURCE", - help="Source profile to clone from (default: active)", + help="Source profile to clone from; implies --clone unless --clone-all is set", ) profile_create.add_argument( "--no-alias", action="store_true", help="Skip wrapper script creation" -
hermes_cli/web_server.py@@ -8493,15 +8493,13 @@ def _run(): 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 @@ -8773,10 +8771,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, -
scripts/release.py@@ -494,6 +494,7 @@ "[redacted:email]": "nik1t7n", "[email protected]": "nik1t7n", "[redacted:email]": "suncokret12", + "[redacted:email]": "WompaJango", "[redacted:email]": "mioimotoai-lgtm", "[redacted:email]": "heyitsaamir", "[redacted:email]": "johnncenae", -
tests/hermes_cli/test_web_server.py@@ -2633,7 +2633,7 @@ def test_profiles_create_creates_wrapper_alias_when_safe(self, monkeypatch, tmp_ 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 @@ def test_profiles_create_creates_wrapper_alias_when_safe(self, monkeypatch, tmp_ 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 @@ def test_profiles_create_with_clone_from_default_copies_default_skills(self, mon 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 @@ def test_profiles_create_with_clone_from_duplicates_source(self, monkeypatch): ) 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 @@ def fake_seed(profile_dir, quiet=False): resp = self.client.post( "/api/profiles", - json={"name": "fresh", "clone_from_default": False}, + json={"name": "fresh", "clone_from": None}, ) assert resp.status_code == 200 -
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", -
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", -
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", -
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", -
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", -
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", -
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", -
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", -
web/src/i18n/ja.ts@@ -285,8 +285,8 @@ export const ja: Translations = { nameRequired: "名前は必須です", nameRule: "小文字、数字、_ および - のみ使用可能。最初は文字または数字で始める必要があります。最大 64 文字。", - invalidName: "無効なプロファイル名", - cloneFromDefault: "デフォルトプロファイルから設定を複製", + invalidName: "無効なプロファイル名", cloneFrom: "プロファイルから複製", + cloneFromNone: "なし(空)", allProfiles: "プロファイル", noProfiles: "プロファイルが見つかりません。", defaultBadge: "デフォルト", -
web/src/i18n/ko.ts@@ -285,8 +285,8 @@ export const ko: Translations = { nameRequired: "이름은 필수입니다", nameRule: "소문자, 숫자, _ 및 - 만 사용 가능합니다. 문자나 숫자로 시작해야 하며 최대 64자입니다.", - invalidName: "잘못된 프로필 이름입니다", - cloneFromDefault: "기본 프로필에서 설정 복제", + invalidName: "잘못된 프로필 이름입니다", cloneFrom: "프로필에서 복제", + cloneFromNone: "없음 (빈 상태)", allProfiles: "프로필", noProfiles: "프로필을 찾을 수 없습니다.", defaultBadge: "기본", -
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", -
web/src/i18n/ru.ts@@ -286,8 +286,8 @@ export const ru: Translations = { nameRequired: "Имя обязательно", nameRule: "Только строчные буквы, цифры, _ и -; должно начинаться с буквы или цифры; до 64 символов.", - invalidName: "Недопустимое имя профиля", - cloneFromDefault: "Клонировать конфигурацию из профиля по умолчанию", + invalidName: "Недопустимое имя профиля", cloneFrom: "Клонировать конфигурацию из профиля", + cloneFromNone: "Нет (пусто)", allProfiles: "Профили", noProfiles: "Профили не найдены.", defaultBadge: "по умолчанию", -
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", -
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; -
web/src/i18n/uk.ts@@ -287,7 +287,8 @@ export const uk: Translations = { nameRule: "Лише малі літери, цифри, _ та -; має починатися з літери або цифри; до 64 символів.", invalidName: "Недопустима назва профілю", - cloneFromDefault: "Клонувати конфігурацію з профілю за замовчуванням", + cloneFrom: "Клонувати з профілю", + cloneFromNone: "Жоден (порожній)", allProfiles: "Профілі", noProfiles: "Профілів не знайдено.", defaultBadge: "за замовчуванням", -
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: "預設", -
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: "默认", -
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; -
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, -
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<string | null>("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() { </p> </div> + <div className="grid gap-2"> + <Label htmlFor="clone-from">{t.profiles.cloneFrom}</Label> + <Select + id="clone-from" + value={cloneFrom ?? ""} + onValueChange={(v) => { + const next = v || null; + setCloneFrom(next); + if (next === null) setCloneAll(false); + }} + > + <SelectOption value="">{t.profiles.cloneFromNone}</SelectOption> + {profiles.map((profile) => ( + <SelectOption key={profile.name} value={profile.name}> + {profile.name} + </SelectOption> + ))} + </Select> + </div> + <div className="grid gap-2"> <Label htmlFor="profile-description"> {L.descriptionOptional} @@ -909,33 +929,19 @@ export default function ProfilesPage() { {L.advancedOptions} </legend> - <div className="flex items-center gap-2.5"> - <Checkbox - checked={cloneFromDefault} - id="clone-from-default" - disabled={cloneAll} - onCheckedChange={(checked) => - setCloneFromDefault(checked === true) - } - /> - - <Label - className="font-mondwest normal-case tracking-normal text-sm cursor-pointer" - htmlFor="clone-from-default" - > - {t.profiles.cloneFromDefault} - </Label> - </div> - <div className="flex items-center gap-2.5"> <Checkbox checked={cloneAll} + disabled={!cloning} id="clone-all" onCheckedChange={(checked) => setCloneAll(checked === true)} /> <Label - className="font-mondwest normal-case tracking-normal text-sm cursor-pointer" + className={cn( + "font-mondwest normal-case tracking-normal text-sm cursor-pointer", + !cloning && "opacity-50", + )} htmlFor="clone-all" > {L.cloneAll} -
website/docs/reference/cli-commands.md@@ -1396,7 +1396,7 @@ Manage profiles — multiple isolated Hermes instances, each with its own config |------------|-------------| | `list` | List all profiles. | | `use <name>` | Set a sticky default profile. | -| `create <name> [--clone] [--clone-all] [--clone-from <source>] [--no-alias]` | Create a new profile. `--clone` copies config, `.env`, and `SOUL.md` from the active profile. `--clone-all` copies all state. `--clone-from` specifies a source profile. | +| `create <name> [--clone] [--clone-all] [--clone-from <source>] [--no-alias]` | Create a new profile. `--clone` copies config, `.env`, `SOUL.md`, and skills from the active profile. `--clone-all` copies all state. `--clone-from` specifies a source profile and implies config clone unless paired with `--clone-all`. | | `delete <name> [-y]` | Delete a profile. | | `show <name>` | Show profile details (home directory, config, etc.). | | `alias <name> [--remove] [--name NAME]` | Manage wrapper scripts for quick profile access. | -
website/docs/reference/faq.md@@ -634,7 +634,7 @@ No. Each messaging platform (Telegram, Discord, etc.) requires exclusive access ### Do profiles share memory or sessions? -No. Each profile has its own memory store, session database, and skills directory. They are completely isolated. If you want to start a new profile with existing memories and sessions, use `hermes profile create newname --clone-all` to copy everything from the current profile. +No. Each profile has its own memory store, session database, and skills directory. They are completely isolated. If you want to start a new profile with existing memories and sessions, use `hermes profile create newname --clone-all` to copy everything from the current profile, or add `--clone-from <profile>` to copy from a specific source profile. ### What happens when I run `hermes update`? -
website/docs/reference/profile-commands.md@@ -80,12 +80,12 @@ Creates a new profile. | Argument / Option | Description | |-------------------|-------------| | `<name>` | Name for the new profile. Must be a valid directory name (alphanumeric, hyphens, underscores). | -| `--clone` | Copy `config.yaml`, `.env`, and `SOUL.md` from the current profile. | +| `--clone` | Copy `config.yaml`, `.env`, `SOUL.md`, and skills from the current profile. | | `--clone-all` | Copy everything (config, memories, skills, cron, plugins) from the current profile. Excludes per-profile history: sessions, `state.db`, backups, state-snapshots, checkpoints. | -| `--clone-from <profile>` | Clone from a specific profile instead of the current one. Used with `--clone` or `--clone-all`. | +| `--clone-from <profile>` | Clone config/skills/SOUL from a specific profile instead of the current one. Implies `--clone` unless paired with `--clone-all`. | | `--no-alias` | Skip wrapper script creation. | | `--description "<text>"` | One- or two-sentence description of what this profile is good at. Used by the kanban orchestrator to route tasks based on role instead of profile name alone. Skip and add later via `hermes profile describe`. Persisted in `<profile_dir>/profile.yaml`. | -| `--no-skills` | Create an **empty** profile with zero bundled skills enabled. Writes a `.no-bundled-skills` marker into the profile so future `hermes update` runs won't re-seed the bundled set, and refuses to combine with `--clone` / `--clone-all` (which would copy skills in anyway). Useful for narrow orchestrator profiles or sandbox profiles that should not inherit the full skill catalog. To toggle this on an already-created profile (including the default `~/.hermes`), use `hermes skills opt-out` / `hermes skills opt-in`. | +| `--no-skills` | Create an **empty** profile with zero bundled skills enabled. Writes a `.no-bundled-skills` marker into the profile so future `hermes update` runs won't re-seed the bundled set, and refuses to combine with `--clone`, `--clone-from`, or `--clone-all` (which would copy skills in anyway). Useful for narrow orchestrator profiles or sandbox profiles that should not inherit the full skill catalog. To toggle this on an already-created profile (including the default `~/.hermes`), use `hermes skills opt-out` / `hermes skills opt-in`. | Creating a profile does **not** make that profile directory the default project/workspace directory for terminal commands. If you want a profile to start in a specific project, set `terminal.cwd` in that profile's `config.yaml`. @@ -102,7 +102,10 @@ hermes profile create work --clone hermes profile create backup --clone-all # Clone config from a specific profile -hermes profile create work2 --clone --clone-from work +hermes profile create work2 --clone-from work + +# Clone everything from a specific profile +hermes profile create work2-backup --clone-from work --clone-all ``` ## `hermes profile describe` -
website/docs/user-guide/profiles.md@@ -50,7 +50,7 @@ You can also set or auto-generate the description later with `hermes profile des hermes profile create work --clone ``` -Copies your current profile's `config.yaml`, `.env`, and `SOUL.md` into the new profile. Same API keys and model, but fresh sessions and memory. Edit `~/.hermes/profiles/work/.env` for different API keys, or `~/.hermes/profiles/work/SOUL.md` for a different personality. +Copies your current profile's `config.yaml`, `.env`, `SOUL.md`, and skills into the new profile. Same API keys, model, and capabilities, but fresh sessions and memory. Edit `~/.hermes/profiles/work/.env` for different API keys, or `~/.hermes/profiles/work/SOUL.md` for a different personality. ### Clone everything (`--clone-all`) @@ -63,11 +63,17 @@ Copies **everything** — config, API keys, personality, all memories, skills, c ### Clone from a specific profile ```bash -hermes profile create work --clone --clone-from coder +hermes profile create work --clone-from coder +``` + +`--clone-from <source>` selects the source profile directly and implies a config/skills/SOUL clone. Combine it with `--clone-all` when you want a full copy of that source profile: + +```bash +hermes profile create work-backup --clone-from coder --clone-all ``` :::tip Honcho memory + profiles -When Honcho is enabled, `--clone` automatically creates a dedicated AI peer for the new profile while sharing the same user workspace. Each profile builds its own observations and identity. See [Honcho -- Multi-agent / Profiles](./features/memory-providers.md#honcho) for details. +When Honcho is enabled, clone operations automatically create a dedicated AI peer for the new profile while sharing the same user workspace. Each profile builds its own observations and identity. See [Honcho -- Multi-agent / Profiles](./features/memory-providers.md#honcho) for details. ::: ## Using profiles -
website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/cli-commands.md@@ -1175,7 +1175,7 @@ hermes profile <subcommand> |------------|-------------| | `list` | 列出所有 profile。 | | `use <name>` | 设置粘性默认 profile。 | -| `create <name> [--clone] [--clone-all] [--clone-from <source>] [--no-alias]` | 创建新 profile。`--clone` 从活跃 profile 复制 config、`.env` 和 `SOUL.md`。`--clone-all` 复制所有状态。`--clone-from` 指定源 profile。 | +| `create <name> [--clone] [--clone-all] [--clone-from <source>] [--no-alias]` | 创建新 profile。`--clone` 从活跃 profile 复制 config、`.env`、`SOUL.md` 和 skills。`--clone-all` 复制所有状态。`--clone-from` 指定源 profile,除非与 `--clone-all` 配合使用,否则会隐含 config 克隆。 | | `delete <name> [-y]` | 删除 profile。 | | `show <name>` | 显示 profile 详情(主目录、config 等)。 | | `alias <name> [--remove] [--name NAME]` | 管理快速访问 profile 的包装脚本。 | -
website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/faq.md@@ -626,7 +626,7 @@ Profiles 是构建在 `HERMES_HOME` 之上的托管层。您*可以*在每次命 ### Profiles 共享记忆或会话吗? -不共享。每个 profile 都有自己独立的记忆存储、会话数据库和技能目录,完全隔离。如果您想用现有的记忆和会话创建新 profile,请使用 `hermes profile create newname --clone-all` 从当前 profile 复制所有内容。 +不共享。每个 profile 都有自己独立的记忆存储、会话数据库和技能目录,完全隔离。如果您想用现有的记忆和会话创建新 profile,请使用 `hermes profile create newname --clone-all` 从当前 profile 复制所有内容,或添加 `--clone-from <profile>` 从指定源 profile 复制。 ### 运行 `hermes update` 时会发生什么? -
website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/reference/profile-commands.md@@ -79,12 +79,12 @@ hermes profile create <name> [options] | 参数 / 选项 | 描述 | |-------------------|-------------| | `<name>` | 新 profile 的名称。必须是合法的目录名(字母数字、连字符、下划线)。 | -| `--clone` | 从当前 profile 复制 `config.yaml`、`.env` 和 `SOUL.md`。 | -| `--clone-all` | 从当前 profile 复制所有内容(config、memories、skills、sessions、state)。 | -| `--clone-from <profile>` | 从指定 profile 克隆,而非当前 profile。与 `--clone` 或 `--clone-all` 配合使用。 | +| `--clone` | 从当前 profile 复制 `config.yaml`、`.env`、`SOUL.md` 和 skills。 | +| `--clone-all` | 从当前 profile 复制所有内容(config、memories、skills、cron、plugins)。会排除每个 profile 自己的历史数据:sessions、`state.db`、backups、state-snapshots、checkpoints。 | +| `--clone-from <profile>` | 从指定 profile 克隆 config/skills/SOUL,而非当前 profile。除非与 `--clone-all` 配合使用,否则会隐含 `--clone`。 | | `--no-alias` | 跳过 wrapper 脚本创建。 | | `--description "<text>"` | 一到两句话描述该 profile 的用途。供 kanban 编排器根据角色而非仅凭 profile 名称来路由任务。可跳过,稍后通过 `hermes profile describe` 添加。持久化保存在 `<profile_dir>/profile.yaml` 中。 | -| `--no-skills` | 创建一个**空** profile,不启用任何内置 skill。会在 profile 目录中写入 `.no-skills` 标记,使后续 `hermes update` 不再重新植入内置 skill 集,且拒绝与 `--clone` / `--clone-all` 组合使用(因为后者会复制 skill)。适用于不应继承完整 skill 目录的窄化编排器 profile 或沙箱 profile。 | +| `--no-skills` | 创建一个**空** profile,不启用任何内置 skill。会在 profile 目录中写入 `.no-bundled-skills` 标记,使后续 `hermes update` 不再重新植入内置 skill 集,且拒绝与 `--clone`、`--clone-from` 或 `--clone-all` 组合使用(因为这些选项会复制 skill)。适用于不应继承完整 skill 目录的窄化编排器 profile 或沙箱 profile。 | 创建 profile **不会**将该 profile 目录设为终端命令的默认项目/工作目录。如需让某个 profile 从特定项目目录启动,请在该 profile 的 `config.yaml` 中设置 `terminal.cwd`。 @@ -101,7 +101,10 @@ hermes profile create work --clone hermes profile create backup --clone-all # 从指定 profile 克隆 config -hermes profile create work2 --clone --clone-from work +hermes profile create work2 --clone-from work + +# 从指定 profile 克隆所有内容 +hermes profile create work2-backup --clone-from work --clone-all ``` ## `hermes profile describe` -
website/i18n/zh-Hans/docusaurus-plugin-content-docs/current/user-guide/profiles.md@@ -46,7 +46,7 @@ hermes profile create researcher --description "Reads source code and external d hermes profile create work --clone ``` -将当前 profile 的 `config.yaml`、`.env` 和 `SOUL.md` 复制到新 profile。API 密钥和模型相同,但会话和记忆是全新的。编辑 `~/.hermes/profiles/work/.env` 可使用不同的 API 密钥,编辑 `~/.hermes/profiles/work/SOUL.md` 可设置不同的人格。 +将当前 profile 的 `config.yaml`、`.env`、`SOUL.md` 和 skills 复制到新 profile。API 密钥、模型和能力相同,但会话和记忆是全新的。编辑 `~/.hermes/profiles/work/.env` 可使用不同的 API 密钥,编辑 `~/.hermes/profiles/work/SOUL.md` 可设置不同的人格。 ### 克隆全部内容(`--clone-all`) @@ -54,16 +54,22 @@ hermes profile create work --clone hermes profile create backup --clone-all ``` -复制**所有内容**——配置、API 密钥、人格、所有记忆、完整会话历史、技能、cron 任务、插件。完整快照。适用于备份或 fork 已有上下文的 agent。 +复制**所有内容**——配置、API 密钥、人格、记忆、技能、cron 任务、插件。会排除每个 profile 自己的历史数据(会话历史、`state.db`、`backups/`、`state-snapshots/`、`checkpoints/`),这些数据属于源 profile 且可能达到数十 GB。若要包含历史的完整备份,请使用 `hermes profile export` 或 `hermes backup`。 ### 从指定 profile 克隆 ```bash -hermes profile create work --clone --clone-from coder +hermes profile create work --clone-from coder +``` + +`--clone-from <source>` 会直接选择源 profile,并隐含执行 config/skills/SOUL 克隆。若要完整复制该源 profile,请与 `--clone-all` 组合使用: + +```bash +hermes profile create work-backup --clone-from coder --clone-all ``` :::tip Honcho 记忆 + profiles -启用 Honcho 后,`--clone` 会自动为新 profile 创建专属 AI 对等体,同时共享同一用户工作区。每个 profile 构建各自的观察记录和身份标识。详见 [Honcho——多 agent / Profiles](./features/memory-providers.md#honcho)。 +启用 Honcho 后,克隆操作会自动为新 profile 创建专属 AI 对等体,同时共享同一用户工作区。每个 profile 构建各自的观察记录和身份标识。详见 [Honcho——多 agent / Profiles](./features/memory-providers.md#honcho)。 ::: ## 使用 profile
-
1 PR closed without merging
From the close comments: merged via another change 1
-
#45558 feat(dashboard): clone new profile from any profile (dropdown instead of checkbox) · merged via another change →
#45630
Closed issues · 1
- Closed#23746[Feature]: Dashboard — clone new profile from any profile (dropdown instead of checkbox)
How this cluster was grouped
-
Link evidence (
structural links throughout): every member is connected by structural evidence: shared changed files, or a closing/superseded reference. -
Pairwise link density
0.67: the share of item pairs that carry a link, counting a structural link in full and a relevance-only link at 0.36.1.00means every pair is linked.
Pick up a Lab task
Check whether a fix covers the open issues here, with your own agent. Bring back evidence for maintainers.
Open the LabAt a glance
- Subsystem
- hermes cli
- Merged PRs
- #45630
- Closed issues
- 1
- Attempts
- 1 PR closed without merging
- Fix link checked
- 13 Jun 2026