authorgravatar for me@t3.ggTheo Browne <me@t3.gg> 2026-09-03 18:28:55-07:00
committergravatar for noreply@github.comGitHub <noreply@github.com> 2026-09-03 18:28:55-07:00
log07c4ab5077fa9c32bf34ab07dd2caa3907878dd9
tree14a0ead12ef7a1acec0cac471803d1dd02b5aa4f
parent0ba06a122bc73f705bfc18875366b4eeb1160992
signature Signed by PGP key B5690EEEBB952194

fix(web): keep automatic project icons consistent (#9535)


5 files changed, 42 insertions(+), 16 deletions(-)

apps/web/src/components/LegacySidebar.tsx+1-1
...@@ -2344,7 +2344,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec...@@ -2344,7 +2344,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec
2344 <ProjectFavicon2344 <ProjectFavicon
2345 environmentId={project.environmentId}2345 environmentId={project.environmentId}
2346 cwd={project.workspaceRoot}2346 cwd={project.workspaceRoot}
2347 projectName={project.displayName}2347 projectName={project.title}
2348 faviconPath={project.faviconPath}2348 faviconPath={project.faviconPath}
2349 projectIcon={project.projectIcon}2349 projectIcon={project.projectIcon}
2350 />2350 />
apps/web/src/components/Sidebar.tsx+38-12
...@@ -276,6 +276,7 @@ function terminalProcessLabel(count: number): string {...@@ -276,6 +276,7 @@ function terminalProcessLabel(count: number): string {
276function SidebarThreadTooltip({276function SidebarThreadTooltip({
277 thread,277 thread,
278 projectTitle,278 projectTitle,
279 projectDisplayName,
279 projectCwd,280 projectCwd,
280 projectFaviconPath,281 projectFaviconPath,
281 projectIcon,282 projectIcon,
...@@ -291,6 +292,7 @@ function SidebarThreadTooltip({...@@ -291,6 +292,7 @@ function SidebarThreadTooltip({
291}: {292}: {
292 thread: SidebarThreadSummary;293 thread: SidebarThreadSummary;
293 projectTitle: string | null;294 projectTitle: string | null;
295 projectDisplayName: string | null;
294 projectCwd: string | null;296 projectCwd: string | null;
295 projectFaviconPath: string | null;297 projectFaviconPath: string | null;
296 projectIcon: ProjectIconOverride | null;298 projectIcon: ProjectIconOverride | null;
...@@ -321,17 +323,17 @@ function SidebarThreadTooltip({...@@ -321,17 +323,17 @@ function SidebarThreadTooltip({
321 {thread.title}323 {thread.title}
322 </div>324 </div>
323 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">325 <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground">
324 {projectTitle ? (326 {projectDisplayName ? (
325 <div className="flex min-w-0 items-center gap-2">327 <div className="flex min-w-0 items-center gap-2">
326 <ProjectFavicon328 <ProjectFavicon
327 environmentId={thread.environmentId}329 environmentId={thread.environmentId}
328 cwd={projectCwd ?? ""}330 cwd={projectCwd ?? ""}
329 projectName={projectTitle}331 projectName={projectTitle ?? ""}
330 faviconPath={projectFaviconPath}332 faviconPath={projectFaviconPath}
331 projectIcon={projectIcon}333 projectIcon={projectIcon}
332 className="size-3 shrink-0"334 className="size-3 shrink-0"
333 />335 />
334 <div className="min-w-0 truncate text-foreground/75">{projectTitle}</div>336 <div className="min-w-0 truncate text-foreground/75">{projectDisplayName}</div>
335 </div>337 </div>
336 ) : null}338 ) : null}
337 {environmentLabel ? (339 {environmentLabel ? (
...@@ -497,6 +499,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {...@@ -497,6 +499,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
497 session: DraftSessionState;499 session: DraftSessionState;
498 composer: ComposerThreadDraftState;500 composer: ComposerThreadDraftState;
499 projectTitle: string | null;501 projectTitle: string | null;
502 projectDisplayName: string | null;
500 projectCwd: string | null;503 projectCwd: string | null;
501 projectFaviconPath: string | null;504 projectFaviconPath: string | null;
502 projectIcon: ProjectIconOverride | null;505 projectIcon: ProjectIconOverride | null;
...@@ -571,7 +574,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {...@@ -571,7 +574,7 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: {
571 className="size-4 shrink-0"574 className="size-4 shrink-0"
572 />575 />
573 <span className="min-w-0 flex-1 truncate text-xs font-medium text-secondary-label">576 <span className="min-w-0 flex-1 truncate text-xs font-medium text-secondary-label">
574 {props.projectTitle}577 {props.projectDisplayName}
575 </span>578 </span>
576 <span className="ml-auto flex h-5 min-w-5 shrink-0 items-center justify-end">579 <span className="ml-auto flex h-5 min-w-5 shrink-0 items-center justify-end">
577 <Tooltip>580 <Tooltip>
...@@ -609,6 +612,7 @@ interface SidebarDraftRowData {...@@ -609,6 +612,7 @@ interface SidebarDraftRowData {
609// subscription + closing divider) so per-keystroke composer updates612// subscription + closing divider) so per-keystroke composer updates
610// re-render only this block, never the whole sidebar. Vanishes at count 0.613// re-render only this block, never the whole sidebar. Vanishes at count 0.
611const SidebarDraftBlock = memo(function SidebarDraftBlock(props: {614const SidebarDraftBlock = memo(function SidebarDraftBlock(props: {
615 projectTitleByKey: ReadonlyMap<string, string>;
612 projectDisplayNameByKey: ReadonlyMap<string, string>;616 projectDisplayNameByKey: ReadonlyMap<string, string>;
613 projectCwdByKey: ReadonlyMap<string, string>;617 projectCwdByKey: ReadonlyMap<string, string>;
614 projectFaviconPathByKey: ReadonlyMap<string, string | null | undefined>;618 projectFaviconPathByKey: ReadonlyMap<string, string | null | undefined>;
...@@ -706,7 +710,8 @@ const SidebarDraftBlock = memo(function SidebarDraftBlock(props: {...@@ -706,7 +710,8 @@ const SidebarDraftBlock = memo(function SidebarDraftBlock(props: {
706 draftId={draftId}710 draftId={draftId}
707 session={session}711 session={session}
708 composer={composer}712 composer={composer}
709 projectTitle={props.projectDisplayNameByKey.get(projectKey) ?? null}713 projectTitle={props.projectTitleByKey.get(projectKey) ?? null}
714 projectDisplayName={props.projectDisplayNameByKey.get(projectKey) ?? null}
710 projectCwd={props.projectCwdByKey.get(projectKey) ?? null}715 projectCwd={props.projectCwdByKey.get(projectKey) ?? null}
711 projectFaviconPath={props.projectFaviconPathByKey.get(projectKey) ?? null}716 projectFaviconPath={props.projectFaviconPathByKey.get(projectKey) ?? null}
712 projectIcon={props.projectIconByKey.get(projectKey) ?? null}717 projectIcon={props.projectIconByKey.get(projectKey) ?? null}
...@@ -759,6 +764,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -759,6 +764,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
759 projectFaviconPath: string | null;764 projectFaviconPath: string | null;
760 projectIcon: ProjectIconOverride | null;765 projectIcon: ProjectIconOverride | null;
761 projectTitle: string | null;766 projectTitle: string | null;
767 projectDisplayName: string | null;
762 providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>;768 providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>;
763 timestampFormat: TimestampFormat;769 timestampFormat: TimestampFormat;
764 onThreadClick: (event: ReactMouseEvent, threadRef: ScopedThreadRef) => void;770 onThreadClick: (event: ReactMouseEvent, threadRef: ScopedThreadRef) => void;
...@@ -994,6 +1000,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -994,6 +1000,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
994 <SidebarThreadTooltip1000 <SidebarThreadTooltip
995 thread={thread}1001 thread={thread}
996 projectTitle={props.projectTitle}1002 projectTitle={props.projectTitle}
1003 projectDisplayName={props.projectDisplayName}
997 projectCwd={props.projectCwd}1004 projectCwd={props.projectCwd}
998 projectFaviconPath={props.projectFaviconPath}1005 projectFaviconPath={props.projectFaviconPath}
999 projectIcon={props.projectIcon}1006 projectIcon={props.projectIcon}
...@@ -1304,7 +1311,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1304,7 +1311,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1304 <ProjectFavicon1311 <ProjectFavicon
1305 environmentId={thread.environmentId}1312 environmentId={thread.environmentId}
1306 cwd={props.projectCwd ?? ""}1313 cwd={props.projectCwd ?? ""}
1307 projectName={props.projectTitle ?? thread.title}1314 projectName={props.projectTitle ?? ""}
1308 faviconPath={props.projectFaviconPath}1315 faviconPath={props.projectFaviconPath}
1309 projectIcon={props.projectIcon}1316 projectIcon={props.projectIcon}
1310 className="size-4"1317 className="size-4"
...@@ -1465,14 +1472,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {...@@ -1465,14 +1472,14 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: {
1465 projectIcon={props.projectIcon}1472 projectIcon={props.projectIcon}
1466 className="size-4 shrink-0"1473 className="size-4 shrink-0"
1467 />1474 />
1468 {props.projectTitle ? (1475 {props.projectDisplayName ? (
1469 <span1476 <span
1470 className={cn(1477 className={cn(
1471 "min-w-0 flex-1 truncate text-secondary-label text-xs",1478 "min-w-0 flex-1 truncate text-secondary-label text-xs",
1472 shouldRecede ? "font-normal" : "font-medium",1479 shouldRecede ? "font-normal" : "font-medium",
1473 )}1480 )}
1474 >1481 >
1475 {props.projectTitle}1482 {props.projectDisplayName}
1476 </span>1483 </span>
1477 ) : (1484 ) : (
1478 <span className="flex-1" />1485 <span className="flex-1" />
...@@ -1669,6 +1676,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1669,6 +1676,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1669 projectFaviconPath: string | null;1676 projectFaviconPath: string | null;
1670 projectIcon: ProjectIconOverride | null;1677 projectIcon: ProjectIconOverride | null;
1671 projectTitle: string | null;1678 projectTitle: string | null;
1679 projectDisplayName: string | null;
1672 environmentLabel: string | null;1680 environmentLabel: string | null;
1673 environmentMachine: EnvironmentMachineKind;1681 environmentMachine: EnvironmentMachineKind;
1674 providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>;1682 providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>;
...@@ -1735,7 +1743,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1735,7 +1743,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1735 aria-selected={props.isHighlighted}1743 aria-selected={props.isHighlighted}
1736 aria-current={props.isRouteActive ? "page" : undefined}1744 aria-current={props.isRouteActive ? "page" : undefined}
1737 aria-label={1745 aria-label={
1738 props.projectTitle ? `${thread.title}, ${props.projectTitle}` : thread.title1746 props.projectDisplayName
1747 ? `${thread.title}, ${props.projectDisplayName}`
1748 : thread.title
1739 }1749 }
1740 onMouseMove={props.onHighlight}1750 onMouseMove={props.onHighlight}
1741 onClick={props.onSelect}1751 onClick={props.onSelect}
...@@ -1751,7 +1761,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1751,7 +1761,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1751 <ProjectFavicon1761 <ProjectFavicon
1752 environmentId={thread.environmentId}1762 environmentId={thread.environmentId}
1753 cwd={props.projectCwd ?? ""}1763 cwd={props.projectCwd ?? ""}
1754 projectName={props.projectTitle ?? thread.title}1764 projectName={props.projectTitle ?? ""}
1755 faviconPath={props.projectFaviconPath}1765 faviconPath={props.projectFaviconPath}
1756 projectIcon={props.projectIcon}1766 projectIcon={props.projectIcon}
1757 className="size-4 shrink-0"1767 className="size-4 shrink-0"
...@@ -1764,6 +1774,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {...@@ -1764,6 +1774,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: {
1764 <SidebarThreadTooltip1774 <SidebarThreadTooltip
1765 thread={thread}1775 thread={thread}
1766 projectTitle={props.projectTitle}1776 projectTitle={props.projectTitle}
1777 projectDisplayName={props.projectDisplayName}
1767 projectCwd={props.projectCwd}1778 projectCwd={props.projectCwd}
1768 projectFaviconPath={props.projectFaviconPath}1779 projectFaviconPath={props.projectFaviconPath}
1769 projectIcon={props.projectIcon}1780 projectIcon={props.projectIcon}
...@@ -1995,6 +2006,12 @@ export default function Sidebar() {...@@ -1995,6 +2006,12 @@ export default function Sidebar() {
1995 ),2006 ),
1996 [projects],2007 [projects],
1997 );2008 );
2009 // Icons use saved titles. Group labels can include a repository owner or a different title.
2010 const projectTitleByKey = useMemo(
2011 () =>
2012 new Map(projects.map((project) => [`${project.environmentId}:${project.id}`, project.title])),
2013 [projects],
2014 );
1998 const projectDisplayNameByKey = useMemo(2015 const projectDisplayNameByKey = useMemo(
1999 () =>2016 () =>
2000 new Map(2017 new Map(
...@@ -3631,7 +3648,7 @@ export default function Sidebar() {...@@ -3631,7 +3648,7 @@ export default function Sidebar() {
3631 <ProjectFavicon3648 <ProjectFavicon
3632 environmentId={scopedProjectGroup.environmentId}3649 environmentId={scopedProjectGroup.environmentId}
3633 cwd={scopedProjectGroup.workspaceRoot}3650 cwd={scopedProjectGroup.workspaceRoot}
3634 projectName={scopedProjectGroup.displayName}3651 projectName={scopedProjectGroup.title}
3635 faviconPath={scopedProjectGroup.faviconPath}3652 faviconPath={scopedProjectGroup.faviconPath}
3636 projectIcon={scopedProjectGroup.projectIcon}3653 projectIcon={scopedProjectGroup.projectIcon}
3637 className="size-4"3654 className="size-4"
...@@ -3689,7 +3706,7 @@ export default function Sidebar() {...@@ -3689,7 +3706,7 @@ export default function Sidebar() {
3689 <ProjectFavicon3706 <ProjectFavicon
3690 environmentId={project.environmentId}3707 environmentId={project.environmentId}
3691 cwd={project.workspaceRoot}3708 cwd={project.workspaceRoot}
3692 projectName={project.displayName}3709 projectName={project.title}
3693 faviconPath={project.faviconPath}3710 faviconPath={project.faviconPath}
3694 projectIcon={project.projectIcon}3711 projectIcon={project.projectIcon}
3695 className="size-4 shrink-0"3712 className="size-4 shrink-0"
...@@ -3780,6 +3797,10 @@ export default function Sidebar() {...@@ -3780,6 +3797,10 @@ export default function Sidebar() {
3780 null3797 null
3781 }3798 }
3782 projectTitle={3799 projectTitle={
3800 projectTitleByKey.get(`${thread.environmentId}:${thread.projectId}`) ??
3801 null
3802 }
3803 projectDisplayName={
3783 projectDisplayNameByKey.get(3804 projectDisplayNameByKey.get(
3784 `${thread.environmentId}:${thread.projectId}`,3805 `${thread.environmentId}:${thread.projectId}`,
3785 ) ?? null3806 ) ?? null
...@@ -3902,6 +3923,10 @@ export default function Sidebar() {...@@ -3902,6 +3923,10 @@ export default function Sidebar() {
3902 null3923 null
3903 }3924 }
3904 projectTitle={3925 projectTitle={
3926 projectTitleByKey.get(`${thread.environmentId}:${thread.projectId}`) ??
3927 null
3928 }
3929 projectDisplayName={
3905 projectDisplayNameByKey.get(3930 projectDisplayNameByKey.get(
3906 `${thread.environmentId}:${thread.projectId}`,3931 `${thread.environmentId}:${thread.projectId}`,
3907 ) ?? null3932 ) ?? null
...@@ -3941,6 +3966,7 @@ export default function Sidebar() {...@@ -3941,6 +3966,7 @@ export default function Sidebar() {
3941 const items: ReactNode[] = [3966 const items: ReactNode[] = [
3942 <SidebarDraftBlock3967 <SidebarDraftBlock
3943 key="draft-sessions"3968 key="draft-sessions"
3969 projectTitleByKey={projectTitleByKey}
3944 projectDisplayNameByKey={projectDisplayNameByKey}3970 projectDisplayNameByKey={projectDisplayNameByKey}
3945 projectCwdByKey={projectCwdByKey}3971 projectCwdByKey={projectCwdByKey}
3946 projectFaviconPathByKey={projectFaviconPathByKey}3972 projectFaviconPathByKey={projectFaviconPathByKey}
apps/web/src/components/pullRequest/PullRequestListFilters.tsx-1
...@@ -78,7 +78,6 @@ export function PullRequestFilterOptionIcon<Value extends string>({...@@ -78,7 +78,6 @@ export function PullRequestFilterOptionIcon<Value extends string>({
78 projectName={option.label}78 projectName={option.label}
79 faviconPath={option.favicon.faviconPath}79 faviconPath={option.favicon.faviconPath}
80 projectIcon={option.favicon.projectIcon}80 projectIcon={option.favicon.projectIcon}
81 fallbackIcon={FolderGit2Icon}
82 className="size-3.5"81 className="size-3.5"
83 />82 />
84 ) : (83 ) : (
apps/web/src/components/settings/ProjectSettingsPanel.tsx+1-1
...@@ -857,7 +857,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {...@@ -857,7 +857,7 @@ function ProjectDetail({ group }: { group: SidebarProjectSnapshot }) {
857 <ProjectFavicon857 <ProjectFavicon
858 environmentId={representative.environmentId}858 environmentId={representative.environmentId}
859 cwd={representative.workspaceRoot}859 cwd={representative.workspaceRoot}
860 projectName={group.displayName}860 projectName={representative.title}
861 faviconPath={faviconPath}861 faviconPath={faviconPath}
862 projectIcon={projectIcon}862 projectIcon={projectIcon}
863 className="size-6"863 className="size-6"
docs/user/project-settings.md+2-1
...@@ -2,7 +2,8 @@...@@ -2,7 +2,8 @@
22
3T3 Code selects a project icon automatically. It checks `t3.json`, common favicon and app icon3T3 Code selects a project icon automatically. It checks `t3.json`, common favicon and app icon
4paths, and icon links in project HTML files. If it does not find an image, it chooses a built-in4paths, and icon links in project HTML files. If it does not find an image, it chooses a built-in
5icon from the project name.5icon from the saved project name. In web and desktop, this icon stays the same when the sidebar
6shows a repository label such as `owner/repo`.
67
7To choose a different icon or emoji:8To choose a different icon or emoji:
89