From 4d9b7718d96afda3a17c2382e56b52741510e643 Mon Sep 17 00:00:00 2001 From: Austin Pickett Date: Mon, 27 Jul 2026 20:53:19 -0400 Subject: [PATCH] fix(desktop): use shared create-profile dialog on Manage Profiles page The Manage Profiles page had its own local CreateProfileDialog/ RenameProfileDialog copies that predated the shared dialogs in create-profile-dialog.tsx / rename-profile-dialog.tsx. The local create copy lacked the SOUL.md textarea, so New Profile from the sidebar rail and New Profile from Manage Profiles rendered different modals. Delete both local duplicates and reuse the shared self-contained dialogs (they own the createProfile/renameProfile/updateProfileSoul calls), so both entry points show the same modal including SOUL.md. --- apps/desktop/src/app/profiles/index.tsx | 302 +----------------------- 1 file changed, 12 insertions(+), 290 deletions(-) diff --git a/apps/desktop/src/app/profiles/index.tsx b/apps/desktop/src/app/profiles/index.tsx index 6b30a0789f6..2c067dacc87 100644 --- a/apps/desktop/src/app/profiles/index.tsx +++ b/apps/desktop/src/app/profiles/index.tsx @@ -14,22 +14,11 @@ import { DialogHeader, DialogTitle } from '@/components/ui/dialog' -import { SanitizedInput } from '@/components/ui/sanitized-input' -import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select' -import { - createProfile, - deleteProfile, - getProfileSoul, - type ProfileInfo, - renameProfile, - updateProfileSoul -} from '@/hermes' +import { deleteProfile, getProfileSoul, type ProfileInfo, updateProfileSoul } from '@/hermes' import { useI18n } from '@/i18n' import { AlertTriangle, Save } from '@/lib/icons' import { profileColorSoft, resolveProfileColor } from '@/lib/profile-color' -import { slug } from '@/lib/sanitize' import { normalize } from '@/lib/text' -import { cn } from '@/lib/utils' import { notify, notifyError } from '@/store/notifications' import { $profileColors, refreshProfiles } from '@/store/profile' @@ -49,11 +38,8 @@ import { PanelSectionLabel } from '../overlays/panel' -const PROFILE_NAME_RE = /^[a-z0-9][a-z0-9_-]{0,63}$/ - -function isValidProfileName(name: string): boolean { - return PROFILE_NAME_RE.test(name.trim()) -} +import { CreateProfileDialog } from './create-profile-dialog' +import { RenameProfileDialog } from './rename-profile-dialog' interface ProfilesViewProps { onClose: () => void @@ -112,40 +98,15 @@ export function ProfilesView({ onClose }: ProfilesViewProps) { ) }, [profiles, query]) - const handleCreate = useCallback( - async (name: string, cloneFrom: null | string) => { - const trimmed = name.trim() - - if (!isValidProfileName(trimmed)) { - throw new Error(p.nameHint) - } - - await createProfile({ name: trimmed, clone_from: cloneFrom }) - notify({ kind: 'success', title: p.created, message: trimmed }) - setSelectedName(trimmed) + // The shared Create/Rename dialogs own the createProfile / renameProfile / + // updateProfileSoul calls; the panel just selects the resulting profile and + // re-pulls the list. + const selectAndRefresh = useCallback( + async (name: string) => { + setSelectedName(name) await refresh() }, - [p, refresh] - ) - - const handleRename = useCallback( - async (from: string, to: string): Promise => { - const target = to.trim() - - if (target === from) { - return - } - - if (!isValidProfileName(target)) { - throw new Error(p.nameHint) - } - - await renameProfile(from, target) - notify({ kind: 'success', title: p.renamed, message: `${from} → ${target}` }) - setSelectedName(target) - await refresh() - }, - [p, refresh] + [refresh] ) const handleConfirmDelete = useCallback(async () => { @@ -229,18 +190,13 @@ export function ProfilesView({ onClose }: ProfilesViewProps) { setPendingRename(null)} - onRename={async newName => { - if (pendingRename) { - await handleRename(pendingRename.name, newName) - setPendingRename(null) - } - }} + onRenamed={selectAndRefresh} open={pendingRename !== null} /> setCreateOpen(false)} - onCreate={async (name, cloneFrom) => handleCreate(name, cloneFrom)} + onCreated={selectAndRefresh} open={createOpen} profiles={profiles ?? []} /> @@ -471,237 +427,3 @@ function SoulEditor({ profileName }: { profileName: string }) { ) } - -function CreateProfileDialog({ - onClose, - onCreate, - open, - profiles -}: { - onClose: () => void - onCreate: (name: string, cloneFrom: null | string) => Promise - open: boolean - profiles: ProfileInfo[] -}) { - const { t } = useI18n() - const p = t.profiles - const [name, setName] = useState('') - const [cloneFrom, setCloneFrom] = useState('default') - const [saving, setSaving] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (!open) { - return - } - - setName('') - setCloneFrom('default') - setError(null) - setSaving(false) - }, [open]) - - const trimmed = name.trim() - const invalid = trimmed !== '' && !isValidProfileName(trimmed) - - async function handleSubmit(event: React.FormEvent) { - event.preventDefault() - - if (!trimmed || invalid) { - setError(invalid ? p.invalidName(p.nameHint) : p.nameRequired) - - return - } - - setSaving(true) - setError(null) - - try { - await onCreate(trimmed, cloneFrom) - onClose() - } catch (err) { - setError(err instanceof Error ? err.message : p.failedCreate) - } finally { - setSaving(false) - } - } - - return ( - !value && !saving && onClose()} open={open}> - - - {p.newProfile} - {p.createDesc} - - -
-
- - -

- {p.nameHint} -

-
- -
- - -

{p.cloneFromDesc}

-
- - {error && ( -
- - {error} -
- )} - - - - - -
-
-
- ) -} - -function RenameProfileDialog({ - currentName, - onClose, - onRename, - open -}: { - currentName: string - onClose: () => void - onRename: (newName: string) => Promise - open: boolean -}) { - const { t } = useI18n() - const p = t.profiles - const [name, setName] = useState(currentName) - const [saving, setSaving] = useState(false) - const [error, setError] = useState(null) - - useEffect(() => { - if (!open) { - return - } - - setName(currentName) - setError(null) - setSaving(false) - }, [currentName, open]) - - const trimmed = name.trim() - const unchanged = trimmed === currentName - const invalid = trimmed !== '' && !unchanged && !isValidProfileName(trimmed) - - async function handleSubmit(event: React.FormEvent) { - event.preventDefault() - - if (unchanged) { - onClose() - - return - } - - if (!trimmed || invalid) { - setError(invalid ? p.invalidName(p.nameHint) : p.nameRequired) - - return - } - - setSaving(true) - setError(null) - - try { - await onRename(trimmed) - } catch (err) { - setError(err instanceof Error ? err.message : p.failedRename) - } finally { - setSaving(false) - } - } - - return ( - !value && !saving && onClose()} open={open}> - - - {p.renameTitle} - - {p.renameDescPrefix} - ~/.local/bin - {p.renameDescSuffix} - - - -
-
- - -

- {p.nameHint} -

-
- - {error && ( -
- - {error} -
- )} - - - - - -
-
-
- ) -}