From 9032dfa5c015fd5b629adce7f6494a1a7171dfd3 Mon Sep 17 00:00:00 2001 From: Bohdan Triapitsyn Date: Sat, 15 Aug 2026 10:16:24 +0300 Subject: [PATCH] fix(ui): show project names exactly as the folder is named Auto-derived project labels were title-cased, turning .ssh into .Ssh and opencode-claude into Opencode Claude. Show the folder name verbatim in the sidebar, window title, settings selector and notification templates, and migrate persisted legacy labels back to the folder name (manual renames are preserved). --- .../sections/shared/SettingsProjectSelector.tsx | 4 +--- .../ui/src/components/session/sidebar/utils.tsx | 7 ++----- packages/ui/src/hooks/useWindowTitle.ts | 4 +--- packages/ui/src/stores/useProjectsStore.ts | 17 ++++++++++++++--- .../lib/notifications/template-runtime.js | 5 ++--- 5 files changed, 20 insertions(+), 17 deletions(-) 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) => {