diff --git a/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx b/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx index 709d5c50..9acbd7b0 100644 --- a/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx +++ b/packages/ui/src/components/sections/shared/SettingsProjectSelector.tsx @@ -12,9 +12,7 @@ import { isVSCodeRuntime } from '@/lib/desktop'; import { cn } from '@/lib/utils'; import { useI18n } from '@/lib/i18n'; -const formatProjectLabel = (label: string): string => { - return label.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()); -}; +const formatProjectLabel = (label: string): string => label.trim(); export const SettingsProjectSelector: React.FC<{ className?: string }> = ({ className }) => { const { t } = useI18n(); diff --git a/packages/ui/src/components/session/sidebar/utils.tsx b/packages/ui/src/components/session/sidebar/utils.tsx index 05e57157..3d10eb4c 100644 --- a/packages/ui/src/components/session/sidebar/utils.tsx +++ b/packages/ui/src/components/session/sidebar/utils.tsx @@ -156,11 +156,8 @@ export const resolveArchivedFolderName = (session: Session, projectRoot: string return segments[segments.length - 1] ?? 'unassigned'; }; -export const formatProjectLabel = (label: string): string => { - return label - .replace(/[-_]/g, ' ') - .replace(/\b\w/g, (char) => char.toUpperCase()); -}; +// Folder names are shown exactly as they are on disk — no title-casing. +export const formatProjectLabel = (label: string): string => label.trim(); export const renderHighlightedText = (text: string, query: string): React.ReactNode => { if (!query) { diff --git a/packages/ui/src/hooks/useWindowTitle.ts b/packages/ui/src/hooks/useWindowTitle.ts index 87e64019..7fb31e4c 100644 --- a/packages/ui/src/hooks/useWindowTitle.ts +++ b/packages/ui/src/hooks/useWindowTitle.ts @@ -7,9 +7,7 @@ import { getRuntimeApiBaseUrl } from '@/lib/runtime-switch'; const APP_TITLE = 'OpenChamber'; -const formatProjectLabel = (label: string): string => { - return label.replace(/[-_]/g, ' ').replace(/\b\w/g, (char) => char.toUpperCase()); -}; +const formatProjectLabel = (label: string): string => label.trim(); const getProjectNameFromPath = (path: string): string => { const normalized = path.replace(/\\/g, '/').replace(/\/+$/, ''); diff --git a/packages/ui/src/stores/useProjectsStore.ts b/packages/ui/src/stores/useProjectsStore.ts index fbf279b3..0f37f137 100644 --- a/packages/ui/src/stores/useProjectsStore.ts +++ b/packages/ui/src/stores/useProjectsStore.ts @@ -166,14 +166,22 @@ const normalizeProjectPath = (value: string): string => { return normalized.length > 1 ? normalized.replace(/\/+$/, '') : normalized; }; +// Folder names are shown verbatim: title-casing them turned `.ssh` into `.Ssh` +// and made every project look like a name the user never chose. const deriveProjectLabel = (path: string): string => { const normalized = normalizeProjectPath(path); if (!normalized || normalized === '/') { return 'Root'; } const segments = normalized.split('/').filter(Boolean); - const raw = segments[segments.length - 1] || normalized; - return raw.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); + return segments[segments.length - 1] || normalized; +}; + +// Labels auto-derived by older versions were title-cased and persisted. Drop +// them back to the folder name; labels the user typed themselves are kept. +const legacyAutoProjectLabel = (path: string): string => { + const derived = deriveProjectLabel(path); + return derived.replace(/[-_]/g, ' ').replace(/\b\w/g, (c) => c.toUpperCase()); }; const sanitizeProjectIconImage = (value: unknown): ProjectEntry['iconImage'] | undefined => { @@ -261,7 +269,10 @@ const sanitizeProjects = (value: unknown): ProjectEntry[] => { }; if (typeof candidate.label === 'string' && candidate.label.trim().length > 0) { - project.label = candidate.label.trim(); + const storedLabel = candidate.label.trim(); + project.label = storedLabel === legacyAutoProjectLabel(normalizedPath) + ? deriveProjectLabel(normalizedPath) + : storedLabel; } if (typeof candidate.icon === 'string' && candidate.icon.trim().length > 0) { project.icon = candidate.icon.trim(); diff --git a/packages/web/server/lib/notifications/template-runtime.js b/packages/web/server/lib/notifications/template-runtime.js index b7233c3e..7400e24b 100644 --- a/packages/web/server/lib/notifications/template-runtime.js +++ b/packages/web/server/lib/notifications/template-runtime.js @@ -27,9 +27,8 @@ export const createNotificationTemplateRuntime = (deps) => { const formatProjectLabel = (label) => { if (!label || typeof label !== 'string') return ''; - return label - .replace(/[-_]/g, ' ') - .replace(/\b\w/g, (char) => char.toUpperCase()); + // Folder names are shown exactly as they are on disk — no title-casing. + return label.trim(); }; const resolveNotificationTemplate = (template, variables) => {