fix: deduplicate project folders in sidebar across profiles

The all-profiles project tree merge used raw path strings as keys.
On Windows (case-insensitive fs), the same folder with different
casings across profiles (e.g. C:\Users\me\Project vs
C:\Users\me\project) produced duplicate top-level entries in the
sidebar Projects section.

Backend: normalize the merge key with os.path.normcase() (no-op on
POSIX, lowercases on Windows).

Frontend: deduplicate projectModel by id before rendering — a safety
net that catches any merge edge cases the backend fix doesn't cover.

Closes #92588
This commit is contained in:
David Metcalfe
2026-08-22 19:32:06 -07:00
committed by brooklyn!
parent 57d82f87ae
commit 426895d9e8
2 changed files with 11 additions and 2 deletions

View File

@@ -1008,7 +1008,15 @@ export function ChatSidebar({
// Layer the user's manual drag-order on top of the deterministic sort. Empty
// (default) returns `sorted` untouched; projects the user hasn't ordered yet
// keep their sorted position rather than jumping the hand-picked list.
return orderProjectsByIds(sorted, projectOrderIds)
// Deduplicate by id — safety net for backend merge edge cases
// (e.g. Windows path-casing differences across profiles).
const seen = new Set<string>()
const deduped = sorted.filter(project => {
if (seen.has(project.id)) return false
seen.add(project.id)
return true
})
return orderProjectsByIds(deduped, projectOrderIds)
}, [
projectTree,
dismissedAutoProjects,

View File

@@ -16,6 +16,7 @@ from hermes_cli.web_read_coalescing import coalesced_read
import inspect
import json
import logging
import os
import re
import subprocess
import sys
@@ -687,7 +688,7 @@ def _merge_profile_tree(
session["profile"] = profile
session["is_default_profile"] = profile == "default"
key = project.get("path") or project["id"]
key = os.path.normcase(project.get("path") or project["id"])
existing = merged.get(key)
if existing is None:
merged[key] = project