| author | |
| committer | |
| log | eb115063634c416c6362cc407f8572cb0c136ddf |
| tree | a4c4f1b0c0fb52f1a9b6e282075446e3c2b26428 |
| parent | d7a59c63c6a227ac295041660228a4aaa427ead0 |
| signature | Signed by PGP key B5690EEEBB952194 |
Co-authored-by: Claude Fable 5.1 <noreply@anthropic.com>12 files changed, 126 insertions(+), 342 deletions(-)
apps/web/src/components/ChatView.tsx+1-4| ... | ... | @@ -8093,10 +8093,7 @@ export default function ChatView(props: ChatViewProps) { |
| 8093 | 8093 | {...(routeKind === "draft" && draftId ? { draftId } : {})} |
| 8094 | 8094 | activeThreadTitle={activeThread.title} |
| 8095 | 8095 | isServerThread={isServerThread} |
| 8096 | activeProjectName={activeProject?.title} | |
| 8097 | activeProjectCwd={activeProject?.workspaceRoot ?? null} | |
| 8098 | activeProjectFaviconPath={activeProject?.faviconPath ?? null} | |
| 8099 | activeProjectIcon={activeProject?.projectIcon ?? null} | |
| 8096 | activeProject={activeProject} | |
| 8100 | 8097 | openInCwd={gitCwd} |
| 8101 | 8098 | activeProjectScripts={activeProjectScripts} |
| 8102 | 8099 | preferredScriptId={ |
apps/web/src/components/CommandPalette.tsx+5-31| ... | ... | @@ -178,16 +178,7 @@ import type { Project } from "../types"; |
| 178 | 178 | const EMPTY_BROWSE_ENTRIES: FilesystemBrowseResult["entries"] = []; |
| 179 | 179 | |
| 180 | 180 | function projectFavicon(project: Project) { |
| 181 | return ( | |
| 182 | <ProjectFavicon | |
| 183 | environmentId={project.environmentId} | |
| 184 | cwd={project.workspaceRoot} | |
| 185 | projectName={project.title} | |
| 186 | faviconPath={project.faviconPath} | |
| 187 | projectIcon={project.projectIcon} | |
| 188 | className="size-4" | |
| 189 | /> | |
| 190 | ); | |
| 181 | return <ProjectFavicon project={project} className="size-4" />; | |
| 191 | 182 | } |
| 192 | 183 | |
| 193 | 184 | function getEnvironmentBrowsePlatform(os: string | null | undefined): string { |
| ... | ... | @@ -927,18 +918,8 @@ function OpenCommandPaletteDialog(props: { |
| 927 | 918 | new Map<ProjectId, string>(projects.map((project) => [project.id, project.workspaceRoot])), |
| 928 | 919 | [projects], |
| 929 | 920 | ); |
| 930 | const projectFaviconPathById = useMemo( | |
| 931 | () => new Map(projects.map((project) => [project.id, project.faviconPath ?? null] as const)), | |
| 932 | [projects], | |
| 933 | ); | |
| 934 | const projectIconByKey = useMemo( | |
| 935 | () => | |
| 936 | new Map( | |
| 937 | projects.map( | |
| 938 | (project) => | |
| 939 | [`${project.environmentId}:${project.id}`, project.projectIcon ?? null] as const, | |
| 940 | ), | |
| 941 | ), | |
| 921 | const projectByKey = useMemo( | |
| 922 | () => new Map(projects.map((project) => [`${project.environmentId}:${project.id}`, project])), | |
| 942 | 923 | [projects], |
| 943 | 924 | ); |
| 944 | 925 | const projectTitleById = useMemo( |
| ... | ... | @@ -1174,12 +1155,7 @@ function OpenCommandPaletteDialog(props: { |
| 1174 | 1155 | ) ?? null; |
| 1175 | 1156 | return ( |
| 1176 | 1157 | <ThreadCommandSubtitle |
| 1177 | environmentId={thread.environmentId} | |
| 1178 | projectCwd={projectCwdById.get(thread.projectId) ?? null} | |
| 1179 | projectFaviconPath={projectFaviconPathById.get(thread.projectId) ?? null} | |
| 1180 | projectIcon={ | |
| 1181 | projectIconByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null | |
| 1182 | } | |
| 1158 | project={projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null} | |
| 1183 | 1159 | projectTitle={projectTitle ?? null} |
| 1184 | 1160 | branch={thread.branch} |
| 1185 | 1161 | worktreePath={thread.worktreePath} |
| ... | ... | @@ -1217,9 +1193,7 @@ function OpenCommandPaletteDialog(props: { |
| 1217 | 1193 | activeThreadId, |
| 1218 | 1194 | clientSettings.sidebarThreadSortOrder, |
| 1219 | 1195 | navigate, |
| 1220 | projectCwdById, | |
| 1221 | projectFaviconPathById, | |
| 1222 | projectIconByKey, | |
| 1196 | projectByKey, | |
| 1223 | 1197 | projectTitleById, |
| 1224 | 1198 | providerEntryByEnvironmentAndInstanceId, |
| 1225 | 1199 | threadContentMatchByKey, |
apps/web/src/components/LegacySidebar.tsx+1-7| ... | ... | @@ -2322,13 +2322,7 @@ const SidebarProjectItem = memo(function SidebarProjectItem(props: SidebarProjec |
| 2322 | 2322 | /> |
| 2323 | 2323 | )} |
| 2324 | 2324 | <span className="flex shrink-0"> |
| 2325 | <ProjectFavicon | |
| 2326 | environmentId={project.environmentId} | |
| 2327 | cwd={project.workspaceRoot} | |
| 2328 | projectName={project.title} | |
| 2329 | faviconPath={project.faviconPath} | |
| 2330 | projectIcon={project.projectIcon} | |
| 2331 | /> | |
| 2325 | <ProjectFavicon project={project} /> | |
| 2332 | 2326 | </span> |
| 2333 | 2327 | <span className="flex min-w-0 flex-1 items-center gap-2"> |
| 2334 | 2328 | <span className="truncate text-sm font-medium text-sidebar-foreground/90"> |
apps/web/src/components/ProjectFavicon.test.tsx+28-24| ... | ... | @@ -66,7 +66,14 @@ vi.mock("../state/assets", () => ({ |
| 66 | 66 | }, |
| 67 | 67 | })); |
| 68 | 68 | |
| 69 | import { ProjectFavicon } from "./ProjectFavicon"; | |
| 69 | import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; | |
| 70 | ||
| 71 | function makeProject( | |
| 72 | overrides: Partial<ProjectFaviconProject> & | |
| 73 | Pick<ProjectFaviconProject, "workspaceRoot" | "title">, | |
| 74 | ): ProjectFaviconProject { | |
| 75 | return { environmentId: "environment-test" as EnvironmentId, ...overrides }; | |
| 76 | } | |
| 70 | 77 | |
| 71 | 78 | type ProjectFaviconImageProps = { |
| 72 | 79 | readonly cacheKey: string; |
| ... | ... | @@ -91,9 +98,7 @@ function resolveImageComponent(): { |
| 91 | 98 | } { |
| 92 | 99 | hooks.beginRender(); |
| 93 | 100 | const element = ProjectFavicon({ |
| 94 | environmentId: "environment-test" as EnvironmentId, | |
| 95 | cwd: "/workspace-test", | |
| 96 | projectName: "workspace-test", | |
| 101 | project: makeProject({ workspaceRoot: "/workspace-test", title: "workspace-test" }), | |
| 97 | 102 | }) as ReactElement<ProjectFaviconImageProps>; |
| 98 | 103 | hooks.reset(); |
| 99 | 104 | |
| ... | ... | @@ -121,9 +126,7 @@ describe("ProjectFavicon", () => { |
| 121 | 126 | testState.faviconUrl = `https://environment.test/api/assets/token/${PROJECT_FAVICON_FALLBACK_MARKER}`; |
| 122 | 127 | |
| 123 | 128 | const element = ProjectFavicon({ |
| 124 | environmentId: "environment-test" as EnvironmentId, | |
| 125 | cwd: "/workspace/analytics-db", | |
| 126 | projectName: "analytics-db", | |
| 129 | project: makeProject({ workspaceRoot: "/workspace/analytics-db", title: "analytics-db" }), | |
| 127 | 130 | }) as ReactElement<{ |
| 128 | 131 | readonly colorClassName?: string; |
| 129 | 132 | readonly emoji?: string; |
| ... | ... | @@ -139,9 +142,7 @@ describe("ProjectFavicon", () => { |
| 139 | 142 | testState.faviconUrl = `https://environment.test/api/assets/token/${PROJECT_FAVICON_FALLBACK_MARKER}`; |
| 140 | 143 | |
| 141 | 144 | const element = ProjectFavicon({ |
| 142 | environmentId: "environment-test" as EnvironmentId, | |
| 143 | cwd: "/workspace/agent-runtime", | |
| 144 | projectName: "agent-runtime", | |
| 145 | project: makeProject({ workspaceRoot: "/workspace/agent-runtime", title: "agent-runtime" }), | |
| 145 | 146 | }) as ReactElement<{ |
| 146 | 147 | readonly colorClassName?: string; |
| 147 | 148 | readonly emoji?: string; |
| ... | ... | @@ -155,11 +156,12 @@ describe("ProjectFavicon", () => { |
| 155 | 156 | |
| 156 | 157 | it("renders a saved Lucide icon and color ahead of an uploaded favicon", () => { |
| 157 | 158 | const element = ProjectFavicon({ |
| 158 | environmentId: "environment-test" as EnvironmentId, | |
| 159 | cwd: "/workspace/test", | |
| 160 | projectName: "test", | |
| 161 | faviconPath: "brand/icon.svg", | |
| 162 | projectIcon: { kind: "lucide", name: "alarm-clock", color: "violet" }, | |
| 159 | project: makeProject({ | |
| 160 | workspaceRoot: "/workspace/test", | |
| 161 | title: "test", | |
| 162 | faviconPath: "brand/icon.svg", | |
| 163 | projectIcon: { kind: "lucide", name: "alarm-clock", color: "violet" }, | |
| 164 | }), | |
| 163 | 165 | }) as ReactElement<{ |
| 164 | 166 | readonly children: ReactElement<{ |
| 165 | 167 | readonly children: ReactElement<{ readonly name: string; readonly className: string }>; |
| ... | ... | @@ -174,11 +176,12 @@ describe("ProjectFavicon", () => { |
| 174 | 176 | |
| 175 | 177 | it("renders a saved emoji ahead of an uploaded favicon", () => { |
| 176 | 178 | const element = ProjectFavicon({ |
| 177 | environmentId: "environment-test" as EnvironmentId, | |
| 178 | cwd: "/workspace/test", | |
| 179 | projectName: "test", | |
| 180 | faviconPath: "brand/icon.svg", | |
| 181 | projectIcon: { kind: "emoji", emoji: "🦄" }, | |
| 179 | project: makeProject({ | |
| 180 | workspaceRoot: "/workspace/test", | |
| 181 | title: "test", | |
| 182 | faviconPath: "brand/icon.svg", | |
| 183 | projectIcon: { kind: "emoji", emoji: "🦄" }, | |
| 184 | }), | |
| 182 | 185 | }) as ReactElement<{ readonly emoji: string }>; |
| 183 | 186 | |
| 184 | 187 | expect(element.props.emoji).toBe("🦄"); |
| ... | ... | @@ -208,10 +211,11 @@ describe("ProjectFavicon", () => { |
| 208 | 211 | |
| 209 | 212 | it("requests a saved favicon path when one is set", () => { |
| 210 | 213 | ProjectFavicon({ |
| 211 | environmentId: "environment-test" as EnvironmentId, | |
| 212 | cwd: "/workspace-test", | |
| 213 | projectName: "workspace-test", | |
| 214 | faviconPath: "brand/icon.svg", | |
| 214 | project: makeProject({ | |
| 215 | workspaceRoot: "/workspace-test", | |
| 216 | title: "workspace-test", | |
| 217 | faviconPath: "brand/icon.svg", | |
| 218 | }), | |
| 215 | 219 | }); |
| 216 | 220 | |
| 217 | 221 | expect(testState.lastTarget).toMatchObject({ |
apps/web/src/components/ProjectFavicon.tsx+31-14| ... | ... | @@ -1,4 +1,5 @@ |
| 1 | import type { EnvironmentId, ProjectIconColor, ProjectIconOverride } from "@t3tools/contracts"; | |
| 1 | import type { ProjectIconColor } from "@t3tools/contracts"; | |
| 2 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; | |
| 2 | 3 | import { |
| 3 | 4 | getProjectFaviconResourceKey, |
| 4 | 5 | isProjectFaviconFallbackUrl, |
| ... | ... | @@ -94,21 +95,33 @@ const PROJECT_ICON_COLOR_BY_NAME: Record<ProjectIconName, ProjectIconColor> = { |
| 94 | 95 | web: "sky", |
| 95 | 96 | }; |
| 96 | 97 | |
| 98 | // The slice of a project that decides its icon. Every surface must pass the | |
| 99 | // project record itself (or a snapshot spread from it) so the saved title, favicon | |
| 100 | // and icon override always travel together. Passing a display label as the title | |
| 101 | // changes the automatic icon, which is how the command palette drifted once. | |
| 102 | export type ProjectFaviconProject = Pick< | |
| 103 | EnvironmentProject, | |
| 104 | "environmentId" | "workspaceRoot" | "title" | "faviconPath" | "projectIcon" | |
| 105 | >; | |
| 106 | ||
| 97 | 107 | export function ProjectFavicon(input: { |
| 98 | environmentId: EnvironmentId; | |
| 99 | cwd: string; | |
| 100 | projectName: string; | |
| 101 | faviconPath?: string | null | undefined; | |
| 102 | projectIcon?: ProjectIconOverride | null | undefined; | |
| 108 | project: ProjectFaviconProject; | |
| 103 | 109 | className?: string | undefined; |
| 104 | 110 | fallbackIcon?: ComponentType<{ className?: string }>; |
| 105 | 111 | }) { |
| 106 | const src = useAtomValue(projectFaviconUrlAtom(input)); | |
| 107 | if (input.projectIcon?.kind === "emoji") { | |
| 108 | return <ProjectFaviconFallback className={input.className} emoji={input.projectIcon.emoji} />; | |
| 112 | const { project } = input; | |
| 113 | const src = useAtomValue( | |
| 114 | projectFaviconUrlAtom({ | |
| 115 | environmentId: project.environmentId, | |
| 116 | cwd: project.workspaceRoot, | |
| 117 | faviconPath: project.faviconPath, | |
| 118 | }), | |
| 119 | ); | |
| 120 | if (project.projectIcon?.kind === "emoji") { | |
| 121 | return <ProjectFaviconFallback className={input.className} emoji={project.projectIcon.emoji} />; | |
| 109 | 122 | } |
| 110 | if (input.projectIcon?.kind === "lucide") { | |
| 111 | const colorClassName = projectIconColorClassName(input.projectIcon.color); | |
| 123 | if (project.projectIcon?.kind === "lucide") { | |
| 124 | const colorClassName = projectIconColorClassName(project.projectIcon.color); | |
| 112 | 125 | const iconClassName = cn( |
| 113 | 126 | "inline-flex size-3.5 shrink-0 items-center justify-center", |
| 114 | 127 | colorClassName, |
| ... | ... | @@ -118,7 +131,7 @@ export function ProjectFavicon(input: { |
| 118 | 131 | <span aria-hidden="true" className={iconClassName}> |
| 119 | 132 | <Suspense fallback={<DynamicProjectIconFallback />}> |
| 120 | 133 | <DynamicIcon |
| 121 | name={input.projectIcon.name as IconName} | |
| 134 | name={project.projectIcon.name as IconName} | |
| 122 | 135 | className={cn("size-full", colorClassName)} |
| 123 | 136 | fallback={DynamicProjectIconFallback} |
| 124 | 137 | /> |
| ... | ... | @@ -128,7 +141,7 @@ export function ProjectFavicon(input: { |
| 128 | 141 | } |
| 129 | 142 | const automaticIconName = input.fallbackIcon |
| 130 | 143 | ? null |
| 131 | : selectProjectIcon(input.projectName, input.cwd); | |
| 144 | : selectProjectIcon(project.title, project.workspaceRoot); | |
| 132 | 145 | const FallbackIcon = |
| 133 | 146 | input.fallbackIcon ?? |
| 134 | 147 | (automaticIconName?.kind === "lucide" ? PROJECT_ICONS[automaticIconName.icon] : undefined); |
| ... | ... | @@ -149,7 +162,11 @@ export function ProjectFavicon(input: { |
| 149 | 162 | ); |
| 150 | 163 | } |
| 151 | 164 | |
| 152 | const cacheKey = getProjectFaviconResourceKey(input.environmentId, input.cwd, input.faviconPath); | |
| 165 | const cacheKey = getProjectFaviconResourceKey( | |
| 166 | project.environmentId, | |
| 167 | project.workspaceRoot, | |
| 168 | project.faviconPath, | |
| 169 | ); | |
| 153 | 170 | |
| 154 | 171 | return ( |
| 155 | 172 | <ProjectFaviconImage |
apps/web/src/components/Sidebar.tsx+37-160| ... | ... | @@ -131,6 +131,7 @@ import { |
| 131 | 131 | } from "../threadRoutes"; |
| 132 | 132 | import { formatRelativeTimeLabel, parseTimestampDate } from "../timestampFormat"; |
| 133 | 133 | import type { SidebarThreadSummary } from "../types"; |
| 134 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; | |
| 134 | 135 | import { cn } from "~/lib/utils"; |
| 135 | 136 | import { EnvironmentMachineIcon } from "./EnvironmentMachineIcon"; |
| 136 | 137 | import { buildThreadActionMenuItems } from "./threadActionMenu.logic"; |
| ... | ... | @@ -193,7 +194,7 @@ import { |
| 193 | 194 | snoozeWakeLabel, |
| 194 | 195 | type SnoozePreset, |
| 195 | 196 | } from "./Sidebar.snooze"; |
| 196 | import { ProjectFavicon } from "./ProjectFavicon"; | |
| 197 | import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; | |
| 197 | 198 | import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon"; |
| 198 | 199 | import { getTriggerDisplayModelLabel } from "./chat/providerIconUtils"; |
| 199 | 200 | import { |
| ... | ... | @@ -295,11 +296,8 @@ function terminalProcessLabel(count: number): string { |
| 295 | 296 | |
| 296 | 297 | function SidebarThreadTooltip({ |
| 297 | 298 | thread, |
| 298 | projectTitle, | |
| 299 | project, | |
| 299 | 300 | projectDisplayName, |
| 300 | projectCwd, | |
| 301 | projectFaviconPath, | |
| 302 | projectIcon, | |
| 303 | 301 | environmentLabel, |
| 304 | 302 | environmentMachine, |
| 305 | 303 | providerEntry, |
| ... | ... | @@ -311,11 +309,8 @@ function SidebarThreadTooltip({ |
| 311 | 309 | terminalProcessCount, |
| 312 | 310 | }: { |
| 313 | 311 | thread: SidebarThreadSummary; |
| 314 | projectTitle: string | null; | |
| 312 | project: ProjectFaviconProject | null; | |
| 315 | 313 | projectDisplayName: string | null; |
| 316 | projectCwd: string | null; | |
| 317 | projectFaviconPath: string | null; | |
| 318 | projectIcon: ProjectIconOverride | null; | |
| 319 | 314 | environmentLabel: string | null; |
| 320 | 315 | environmentMachine: EnvironmentMachineKind; |
| 321 | 316 | providerEntry: ProviderInstanceEntry | null; |
| ... | ... | @@ -345,14 +340,7 @@ function SidebarThreadTooltip({ |
| 345 | 340 | <div className="grid gap-1.5 pl-0.5 text-xs text-muted-foreground"> |
| 346 | 341 | {projectDisplayName ? ( |
| 347 | 342 | <div className="flex min-w-0 items-center gap-2"> |
| 348 | <ProjectFavicon | |
| 349 | environmentId={thread.environmentId} | |
| 350 | cwd={projectCwd ?? ""} | |
| 351 | projectName={projectTitle ?? ""} | |
| 352 | faviconPath={projectFaviconPath} | |
| 353 | projectIcon={projectIcon} | |
| 354 | className="size-3 shrink-0" | |
| 355 | /> | |
| 343 | {project ? <ProjectFavicon project={project} className="size-3 shrink-0" /> : null} | |
| 356 | 344 | <div className="min-w-0 truncate text-foreground/75">{projectDisplayName}</div> |
| 357 | 345 | </div> |
| 358 | 346 | ) : null} |
| ... | ... | @@ -689,11 +677,8 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { |
| 689 | 677 | draftId: DraftId; |
| 690 | 678 | session: DraftSessionState; |
| 691 | 679 | composer: ComposerThreadDraftState; |
| 692 | projectTitle: string | null; | |
| 680 | project: ProjectFaviconProject | null; | |
| 693 | 681 | projectDisplayName: string | null; |
| 694 | projectCwd: string | null; | |
| 695 | projectFaviconPath: string | null; | |
| 696 | projectIcon: ProjectIconOverride | null; | |
| 697 | 682 | isActive: boolean; |
| 698 | 683 | onNavigate: (draftId: DraftId) => void; |
| 699 | 684 | onDiscard: (draftId: DraftId) => void; |
| ... | ... | @@ -751,14 +736,9 @@ const SidebarDraftRow = memo(function SidebarDraftRow(props: { |
| 751 | 736 | <div className="relative z-10 px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> |
| 752 | 737 | <div className="flex h-5 min-w-0 items-center gap-1.5"> |
| 753 | 738 | <SquarePenIcon aria-hidden className={draftPenClassName} /> |
| 754 | <ProjectFavicon | |
| 755 | environmentId={session.environmentId} | |
| 756 | cwd={props.projectCwd ?? ""} | |
| 757 | projectName={props.projectTitle ?? ""} | |
| 758 | faviconPath={props.projectFaviconPath} | |
| 759 | projectIcon={props.projectIcon} | |
| 760 | className="size-4 shrink-0" | |
| 761 | /> | |
| 739 | {props.project ? ( | |
| 740 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> | |
| 741 | ) : null} | |
| 762 | 742 | <span className="min-w-0 flex-1 truncate text-xs font-medium text-secondary-label"> |
| 763 | 743 | {props.projectDisplayName} |
| 764 | 744 | </span> |
| ... | ... | @@ -798,11 +778,8 @@ interface SidebarDraftRowData { |
| 798 | 778 | // subscription + closing divider) so per-keystroke composer updates |
| 799 | 779 | // re-render only this block, never the whole sidebar. Vanishes at count 0. |
| 800 | 780 | const SidebarDraftBlock = memo(function SidebarDraftBlock(props: { |
| 801 | projectTitleByKey: ReadonlyMap<string, string>; | |
| 781 | projectByKey: ReadonlyMap<string, EnvironmentProject>; | |
| 802 | 782 | projectDisplayNameByKey: ReadonlyMap<string, string>; |
| 803 | projectCwdByKey: ReadonlyMap<string, string>; | |
| 804 | projectFaviconPathByKey: ReadonlyMap<string, string | null | undefined>; | |
| 805 | projectIconByKey: ReadonlyMap<string, ProjectIconOverride | null | undefined>; | |
| 806 | 783 | scopedProjectKeys: ReadonlySet<string> | null; |
| 807 | 784 | routeDraftId: string | null; |
| 808 | 785 | onNavigateToDraft: (draftId: DraftId) => void; |
| ... | ... | @@ -896,11 +873,8 @@ const SidebarDraftBlock = memo(function SidebarDraftBlock(props: { |
| 896 | 873 | draftId={draftId} |
| 897 | 874 | session={session} |
| 898 | 875 | composer={composer} |
| 899 | projectTitle={props.projectTitleByKey.get(projectKey) ?? null} | |
| 876 | project={props.projectByKey.get(projectKey) ?? null} | |
| 900 | 877 | projectDisplayName={props.projectDisplayNameByKey.get(projectKey) ?? null} |
| 901 | projectCwd={props.projectCwdByKey.get(projectKey) ?? null} | |
| 902 | projectFaviconPath={props.projectFaviconPathByKey.get(projectKey) ?? null} | |
| 903 | projectIcon={props.projectIconByKey.get(projectKey) ?? null} | |
| 904 | 878 | isActive={draftId === props.routeDraftId} |
| 905 | 879 | onNavigate={props.onNavigateToDraft} |
| 906 | 880 | onDiscard={handleDiscard} |
| ... | ... | @@ -987,10 +961,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 987 | 961 | currentEnvironmentId: string | null; |
| 988 | 962 | environmentLabel: string | null; |
| 989 | 963 | environmentMachine: EnvironmentMachineKind; |
| 990 | projectCwd: string | null; | |
| 991 | projectFaviconPath: string | null; | |
| 992 | projectIcon: ProjectIconOverride | null; | |
| 993 | projectTitle: string | null; | |
| 964 | project: EnvironmentProject | null; | |
| 994 | 965 | projectDisplayName: string | null; |
| 995 | 966 | providerEntryByInstanceId: ReadonlyMap<string, ProviderInstanceEntry>; |
| 996 | 967 | timestampFormat: TimestampFormat; |
| ... | ... | @@ -1061,7 +1032,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1061 | 1032 | [clearComposerContent, threadRef], |
| 1062 | 1033 | ); |
| 1063 | 1034 | |
| 1064 | const gitCwd = thread.worktreePath ?? props.projectCwd; | |
| 1035 | const gitCwd = thread.worktreePath ?? props.project?.workspaceRoot ?? null; | |
| 1065 | 1036 | const linkedPullRequestStatus = useLinkedThreadPullRequest( |
| 1066 | 1037 | thread.environmentId, |
| 1067 | 1038 | thread.linkedPullRequest ?? thread.branchPullRequest, |
| ... | ... | @@ -1197,11 +1168,8 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1197 | 1168 | const detailsTooltip = ( |
| 1198 | 1169 | <SidebarThreadTooltip |
| 1199 | 1170 | thread={thread} |
| 1200 | projectTitle={props.projectTitle} | |
| 1171 | project={props.project} | |
| 1201 | 1172 | projectDisplayName={props.projectDisplayName} |
| 1202 | projectCwd={props.projectCwd} | |
| 1203 | projectFaviconPath={props.projectFaviconPath} | |
| 1204 | projectIcon={props.projectIcon} | |
| 1205 | 1173 | environmentLabel={props.environmentLabel} |
| 1206 | 1174 | environmentMachine={props.environmentMachine} |
| 1207 | 1175 | providerEntry={providerEntry} |
| ... | ... | @@ -1569,14 +1537,7 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1569 | 1537 | "opacity-40 grayscale group-hover/sidebar-row:opacity-100 group-hover/sidebar-row:grayscale-0", |
| 1570 | 1538 | )} |
| 1571 | 1539 | > |
| 1572 | <ProjectFavicon | |
| 1573 | environmentId={thread.environmentId} | |
| 1574 | cwd={props.projectCwd ?? ""} | |
| 1575 | projectName={props.projectTitle ?? ""} | |
| 1576 | faviconPath={props.projectFaviconPath} | |
| 1577 | projectIcon={props.projectIcon} | |
| 1578 | className="size-4" | |
| 1579 | /> | |
| 1540 | {props.project ? <ProjectFavicon project={props.project} className="size-4" /> : null} | |
| 1580 | 1541 | </span> |
| 1581 | 1542 | {draftIndicator} |
| 1582 | 1543 | {title} |
| ... | ... | @@ -1722,14 +1683,9 @@ const SidebarThreadRow = memo(function SidebarThreadRow(props: { |
| 1722 | 1683 | <div className="relative z-10 h-[4.875rem] px-[var(--sidebar-row-content-inset)] py-[var(--sidebar-content-inset)]"> |
| 1723 | 1684 | <div className="flex h-5 min-w-0 items-center gap-1.5"> |
| 1724 | 1685 | {draftIndicator} |
| 1725 | <ProjectFavicon | |
| 1726 | environmentId={thread.environmentId} | |
| 1727 | cwd={props.projectCwd ?? ""} | |
| 1728 | projectName={props.projectTitle ?? ""} | |
| 1729 | faviconPath={props.projectFaviconPath} | |
| 1730 | projectIcon={props.projectIcon} | |
| 1731 | className="size-4 shrink-0" | |
| 1732 | /> | |
| 1686 | {props.project ? ( | |
| 1687 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> | |
| 1688 | ) : null} | |
| 1733 | 1689 | {props.projectDisplayName ? ( |
| 1734 | 1690 | <span |
| 1735 | 1691 | className={cn( |
| ... | ... | @@ -1953,10 +1909,7 @@ function latestTurnDiff( |
| 1953 | 1909 | |
| 1954 | 1910 | const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 1955 | 1911 | thread: SidebarThreadSummary; |
| 1956 | projectCwd: string | null; | |
| 1957 | projectFaviconPath: string | null; | |
| 1958 | projectIcon: ProjectIconOverride | null; | |
| 1959 | projectTitle: string | null; | |
| 1912 | project: EnvironmentProject | null; | |
| 1960 | 1913 | projectDisplayName: string | null; |
| 1961 | 1914 | environmentLabel: string | null; |
| 1962 | 1915 | environmentMachine: EnvironmentMachineKind; |
| ... | ... | @@ -1973,7 +1926,7 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 1973 | 1926 | ); |
| 1974 | 1927 | // Same details tooltip as the regular rows: a search hit is still a thread, |
| 1975 | 1928 | // and the hover card is how you disambiguate identically-titled results. |
| 1976 | const gitCwd = thread.worktreePath ?? props.projectCwd; | |
| 1929 | const gitCwd = thread.worktreePath ?? props.project?.workspaceRoot ?? null; | |
| 1977 | 1930 | const gitStatus = useEnvironmentQuery( |
| 1978 | 1931 | leaseLiveStatus && (thread.branch != null || thread.worktreePath !== null) && gitCwd !== null |
| 1979 | 1932 | ? vcsEnvironment.status({ |
| ... | ... | @@ -2039,14 +1992,9 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 2039 | 1992 | /> |
| 2040 | 1993 | } |
| 2041 | 1994 | > |
| 2042 | <ProjectFavicon | |
| 2043 | environmentId={thread.environmentId} | |
| 2044 | cwd={props.projectCwd ?? ""} | |
| 2045 | projectName={props.projectTitle ?? ""} | |
| 2046 | faviconPath={props.projectFaviconPath} | |
| 2047 | projectIcon={props.projectIcon} | |
| 2048 | className="size-4 shrink-0" | |
| 2049 | /> | |
| 1995 | {props.project ? ( | |
| 1996 | <ProjectFavicon project={props.project} className="size-4 shrink-0" /> | |
| 1997 | ) : null} | |
| 2050 | 1998 | <span className="min-w-0 flex-1 truncate">{thread.title}</span> |
| 2051 | 1999 | <span className="shrink-0 text-xs text-muted-foreground/55 tabular-nums"> |
| 2052 | 2000 | {threadTimeLabel(thread)} |
| ... | ... | @@ -2054,11 +2002,8 @@ const SidebarSearchResultRow = memo(function SidebarSearchResultRow(props: { |
| 2054 | 2002 | </TooltipTrigger> |
| 2055 | 2003 | <SidebarThreadTooltip |
| 2056 | 2004 | thread={thread} |
| 2057 | projectTitle={props.projectTitle} | |
| 2005 | project={props.project} | |
| 2058 | 2006 | projectDisplayName={props.projectDisplayName} |
| 2059 | projectCwd={props.projectCwd} | |
| 2060 | projectFaviconPath={props.projectFaviconPath} | |
| 2061 | projectIcon={props.projectIcon} | |
| 2062 | 2007 | environmentLabel={props.environmentLabel} |
| 2063 | 2008 | environmentMachine={props.environmentMachine} |
| 2064 | 2009 | providerEntry={providerEntry} |
| ... | ... | @@ -2265,34 +2210,10 @@ export default function Sidebar() { |
| 2265 | 2210 | ), |
| 2266 | 2211 | [serverConfigs], |
| 2267 | 2212 | ); |
| 2268 | const projectCwdByKey = useMemo( | |
| 2269 | () => | |
| 2270 | new Map( | |
| 2271 | projects.map((project) => [ | |
| 2272 | `${project.environmentId}:${project.id}`, | |
| 2273 | project.workspaceRoot, | |
| 2274 | ]), | |
| 2275 | ), | |
| 2276 | [projects], | |
| 2277 | ); | |
| 2278 | const projectFaviconPathByKey = useMemo( | |
| 2279 | () => | |
| 2280 | new Map( | |
| 2281 | projects.map((project) => [`${project.environmentId}:${project.id}`, project.faviconPath]), | |
| 2282 | ), | |
| 2283 | [projects], | |
| 2284 | ); | |
| 2285 | const projectIconByKey = useMemo( | |
| 2286 | () => | |
| 2287 | new Map( | |
| 2288 | projects.map((project) => [`${project.environmentId}:${project.id}`, project.projectIcon]), | |
| 2289 | ), | |
| 2290 | [projects], | |
| 2291 | ); | |
| 2292 | // Icons use saved titles. Group labels can include a repository owner or a different title. | |
| 2293 | const projectTitleByKey = useMemo( | |
| 2294 | () => | |
| 2295 | new Map(projects.map((project) => [`${project.environmentId}:${project.id}`, project.title])), | |
| 2213 | // Rows read the project record for its icon and cwd. Group labels can include | |
| 2214 | // a repository owner or a different title, so they travel separately. | |
| 2215 | const projectByKey = useMemo( | |
| 2216 | () => new Map(projects.map((project) => [`${project.environmentId}:${project.id}`, project])), | |
| 2296 | 2217 | [projects], |
| 2297 | 2218 | ); |
| 2298 | 2219 | const projectDisplayNameByKey = useMemo( |
| ... | ... | @@ -3912,7 +3833,7 @@ export default function Sidebar() { |
| 3912 | 3833 | if (!thread) return; |
| 3913 | 3834 | const threadWorkspacePath = |
| 3914 | 3835 | thread.worktreePath ?? |
| 3915 | projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 3836 | projectByKey.get(`${thread.environmentId}:${thread.projectId}`)?.workspaceRoot ?? | |
| 3916 | 3837 | null; |
| 3917 | 3838 | // Un-settle pins the thread active until real activity clears the pin. |
| 3918 | 3839 | // Environments without |
| ... | ... | @@ -4135,7 +4056,7 @@ export default function Sidebar() { |
| 4135 | 4056 | handleMultiSelectContextMenu, |
| 4136 | 4057 | markThreadUnread, |
| 4137 | 4058 | openProjectSettings, |
| 4138 | projectCwdByKey, | |
| 4059 | projectByKey, | |
| 4139 | 4060 | serverConfigs, |
| 4140 | 4061 | startThreadRename, |
| 4141 | 4062 | updateThreadMetadata, |
| ... | ... | @@ -4392,14 +4313,7 @@ export default function Sidebar() { |
| 4392 | 4313 | > |
| 4393 | 4314 | {scopedProjectGroup ? ( |
| 4394 | 4315 | <span className="flex shrink-0"> |
| 4395 | <ProjectFavicon | |
| 4396 | environmentId={scopedProjectGroup.environmentId} | |
| 4397 | cwd={scopedProjectGroup.workspaceRoot} | |
| 4398 | projectName={scopedProjectGroup.title} | |
| 4399 | faviconPath={scopedProjectGroup.faviconPath} | |
| 4400 | projectIcon={scopedProjectGroup.projectIcon} | |
| 4401 | className="size-4" | |
| 4402 | /> | |
| 4316 | <ProjectFavicon project={scopedProjectGroup} className="size-4" /> | |
| 4403 | 4317 | </span> |
| 4404 | 4318 | ) : ( |
| 4405 | 4319 | <FolderIcon className="size-4 shrink-0" /> |
| ... | ... | @@ -4457,14 +4371,7 @@ export default function Sidebar() { |
| 4457 | 4371 | }} |
| 4458 | 4372 | > |
| 4459 | 4373 | {project ? ( |
| 4460 | <ProjectFavicon | |
| 4461 | environmentId={project.environmentId} | |
| 4462 | cwd={project.workspaceRoot} | |
| 4463 | projectName={project.title} | |
| 4464 | faviconPath={project.faviconPath} | |
| 4465 | projectIcon={project.projectIcon} | |
| 4466 | className="size-4 shrink-0" | |
| 4467 | /> | |
| 4374 | <ProjectFavicon project={project} className="size-4 shrink-0" /> | |
| 4468 | 4375 | ) : ( |
| 4469 | 4376 | <FolderIcon className="size-4 shrink-0" /> |
| 4470 | 4377 | )} |
| ... | ... | @@ -4539,21 +4446,8 @@ export default function Sidebar() { |
| 4539 | 4446 | <SidebarSearchResultRow |
| 4540 | 4447 | key={threadKey} |
| 4541 | 4448 | thread={thread} |
| 4542 | projectCwd={ | |
| 4543 | projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null | |
| 4544 | } | |
| 4545 | projectFaviconPath={ | |
| 4546 | projectFaviconPathByKey.get( | |
| 4547 | `${thread.environmentId}:${thread.projectId}`, | |
| 4548 | ) ?? null | |
| 4549 | } | |
| 4550 | projectIcon={ | |
| 4551 | projectIconByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 4552 | null | |
| 4553 | } | |
| 4554 | projectTitle={ | |
| 4555 | projectTitleByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 4556 | null | |
| 4449 | project={ | |
| 4450 | projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? null | |
| 4557 | 4451 | } |
| 4558 | 4452 | projectDisplayName={ |
| 4559 | 4453 | projectDisplayNameByKey.get( |
| ... | ... | @@ -4689,24 +4583,10 @@ export default function Sidebar() { |
| 4689 | 4583 | environmentMachine={ |
| 4690 | 4584 | environmentMachineById.get(thread.environmentId) ?? "server" |
| 4691 | 4585 | } |
| 4692 | projectCwd={ | |
| 4693 | projectCwdByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 4694 | null | |
| 4695 | } | |
| 4696 | projectFaviconPath={ | |
| 4697 | projectFaviconPathByKey.get( | |
| 4698 | `${thread.environmentId}:${thread.projectId}`, | |
| 4699 | ) ?? null | |
| 4700 | } | |
| 4701 | projectIcon={ | |
| 4702 | projectIconByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 4586 | project={ | |
| 4587 | projectByKey.get(`${thread.environmentId}:${thread.projectId}`) ?? | |
| 4703 | 4588 | null |
| 4704 | 4589 | } |
| 4705 | projectTitle={ | |
| 4706 | projectTitleByKey.get( | |
| 4707 | `${thread.environmentId}:${thread.projectId}`, | |
| 4708 | ) ?? null | |
| 4709 | } | |
| 4710 | 4590 | projectDisplayName={ |
| 4711 | 4591 | projectDisplayNameByKey.get( |
| 4712 | 4592 | `${thread.environmentId}:${thread.projectId}`, |
| ... | ... | @@ -4758,11 +4638,8 @@ export default function Sidebar() { |
| 4758 | 4638 | const items: ReactNode[] = [ |
| 4759 | 4639 | <SidebarDraftBlock |
| 4760 | 4640 | key="draft-sessions" |
| 4761 | projectTitleByKey={projectTitleByKey} | |
| 4641 | projectByKey={projectByKey} | |
| 4762 | 4642 | projectDisplayNameByKey={projectDisplayNameByKey} |
| 4763 | projectCwdByKey={projectCwdByKey} | |
| 4764 | projectFaviconPathByKey={projectFaviconPathByKey} | |
| 4765 | projectIconByKey={projectIconByKey} | |
| 4766 | 4643 | scopedProjectKeys={scopedProjectKeys} |
| 4767 | 4644 | routeDraftId={routeDraftIdForRows} |
| 4768 | 4645 | onNavigateToDraft={navigateToDraft} |
apps/web/src/components/ThreadCommandSubtitle.tsx+5-15| ... | ... | @@ -1,6 +1,6 @@ |
| 1 | import type { EnvironmentId, ProviderDriverKind } from "@t3tools/contracts"; | |
| 1 | import type { ProviderDriverKind } from "@t3tools/contracts"; | |
| 2 | 2 | import { FolderGit2Icon, FolderIcon, GitBranchIcon } from "lucide-react"; |
| 3 | import { ProjectFavicon } from "./ProjectFavicon"; | |
| 3 | import { ProjectFavicon, type ProjectFaviconProject } from "./ProjectFavicon"; | |
| 4 | 4 | import { ProviderInstanceIcon } from "./chat/ProviderInstanceIcon"; |
| 5 | 5 | import { cn } from "~/lib/utils"; |
| 6 | 6 | |
| ... | ... | @@ -34,10 +34,7 @@ function WorkspaceIcon(props: { variant: ThreadCommandSubtitleVariant; isWorktre |
| 34 | 34 | } |
| 35 | 35 | |
| 36 | 36 | export function ThreadCommandSubtitle(props: { |
| 37 | environmentId: EnvironmentId; | |
| 38 | projectCwd: string | null; | |
| 39 | projectFaviconPath?: string | null; | |
| 40 | projectIcon?: import("@t3tools/contracts").ProjectIconOverride | null; | |
| 37 | project: ProjectFaviconProject | null; | |
| 41 | 38 | projectTitle: string | null; |
| 42 | 39 | branch: string | null; |
| 43 | 40 | worktreePath: string | null; |
| ... | ... | @@ -68,15 +65,8 @@ export function ThreadCommandSubtitle(props: { |
| 68 | 65 | > |
| 69 | 66 | {projectLabel ? ( |
| 70 | 67 | <span className="inline-flex min-w-0 items-center gap-1"> |
| 71 | {props.projectCwd ? ( | |
| 72 | <ProjectFavicon | |
| 73 | environmentId={props.environmentId} | |
| 74 | cwd={props.projectCwd} | |
| 75 | projectName={projectLabel} | |
| 76 | faviconPath={props.projectFaviconPath} | |
| 77 | projectIcon={props.projectIcon} | |
| 78 | className="size-3 shrink-0" | |
| 79 | /> | |
| 68 | {props.project ? ( | |
| 69 | <ProjectFavicon project={props.project} className="size-3 shrink-0" /> | |
| 80 | 70 | ) : null} |
| 81 | 71 | <span className="min-w-0 truncate">{projectLabel}</span> |
| 82 | 72 | </span> |
apps/web/src/components/chat/ChatHeader.tsx+7-17| ... | ... | @@ -6,6 +6,7 @@ import { |
| 6 | 6 | type ThreadId, |
| 7 | 7 | } from "@t3tools/contracts"; |
| 8 | 8 | import { scopeThreadRef } from "@t3tools/client-runtime/environment"; |
| 9 | import type { EnvironmentProject } from "@t3tools/client-runtime/state/shell"; | |
| 9 | 10 | import { |
| 10 | 11 | isAtomCommandInterrupted, |
| 11 | 12 | squashAtomCommandFailure, |
| ... | ... | @@ -54,10 +55,7 @@ interface ChatHeaderProps { |
| 54 | 55 | activeThreadTitle: string; |
| 55 | 56 | /** Drafts have no server thread yet, so the title carries no action menu. */ |
| 56 | 57 | isServerThread: boolean; |
| 57 | activeProjectName: string | undefined; | |
| 58 | activeProjectCwd: string | null; | |
| 59 | activeProjectFaviconPath: string | null; | |
| 60 | activeProjectIcon: import("@t3tools/contracts").ProjectIconOverride | null; | |
| 58 | activeProject: EnvironmentProject | null; | |
| 61 | 59 | openInCwd: string | null; |
| 62 | 60 | activeProjectScripts: ReadonlyArray<ProjectScript> | undefined; |
| 63 | 61 | preferredScriptId: string | null; |
| ... | ... | @@ -126,10 +124,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 126 | 124 | draftId, |
| 127 | 125 | activeThreadTitle, |
| 128 | 126 | isServerThread, |
| 129 | activeProjectName, | |
| 130 | activeProjectCwd, | |
| 131 | activeProjectFaviconPath, | |
| 132 | activeProjectIcon, | |
| 127 | activeProject, | |
| 133 | 128 | openInCwd, |
| 134 | 129 | activeProjectScripts, |
| 135 | 130 | preferredScriptId, |
| ... | ... | @@ -161,6 +156,8 @@ export const ChatHeader = memo(function ChatHeader({ |
| 161 | 156 | }); |
| 162 | 157 | }, [panelAnimationDurationMs, panelAnimationsActive]); |
| 163 | 158 | const primaryEnvironmentId = usePrimaryEnvironmentId(); |
| 159 | const activeProjectName = activeProject?.title; | |
| 160 | const activeProjectCwd = activeProject?.workspaceRoot ?? null; | |
| 164 | 161 | const fileScripts = useT3ProjectFileScripts( |
| 165 | 162 | activeThreadEnvironmentId, |
| 166 | 163 | activeProjectScripts ? activeProjectCwd : null, |
| ... | ... | @@ -327,7 +324,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 327 | 324 | {/* The project always leads the header: knowing which project a |
| 328 | 325 | thread lives in is priority zero, and the thread title alone |
| 329 | 326 | doesn't answer it. */} |
| 330 | {activeProjectName ? ( | |
| 327 | {activeProject ? ( | |
| 331 | 328 | <> |
| 332 | 329 | <WorkspaceBreadcrumbItem className="shrink"> |
| 333 | 330 | <Tooltip> |
| ... | ... | @@ -341,14 +338,7 @@ export const ChatHeader = memo(function ChatHeader({ |
| 341 | 338 | /> |
| 342 | 339 | } |
| 343 | 340 | > |
| 344 | <ProjectFavicon | |
| 345 | environmentId={activeThreadEnvironmentId} | |
| 346 | cwd={activeProjectCwd ?? ""} | |
| 347 | projectName={activeProjectName} | |
| 348 | faviconPath={activeProjectFaviconPath} | |
| 349 | projectIcon={activeProjectIcon} | |
| 350 | className="size-3.5" | |
| 351 | /> | |
| 341 | <ProjectFavicon project={activeProject} className="size-3.5" /> | |
| 352 | 342 | <span className="max-w-40 truncate">{activeProjectName}</span> |
| 353 | 343 | </TooltipTrigger> |
| 354 | 344 | <TooltipPopup side="top">New thread in {activeProjectName}</TooltipPopup> |
apps/web/src/components/pullRequest/PullRequestListFilters.tsx+6-30| ... | ... | @@ -25,7 +25,7 @@ import { |
| 25 | 25 | import { type ElementType, useState } from "react"; |
| 26 | 26 | |
| 27 | 27 | import { getSourceControlPresentationForKind } from "~/sourceControlPresentation"; |
| 28 | import { ProjectFavicon } from "../ProjectFavicon"; | |
| 28 | import { ProjectFavicon, type ProjectFaviconProject } from "../ProjectFavicon"; | |
| 29 | 29 | import { InputGroup, InputGroupAddon, InputGroupInput } from "../ui/input-group"; |
| 30 | 30 | import { Button } from "../ui/button"; |
| 31 | 31 | |
| ... | ... | @@ -57,12 +57,7 @@ export interface PullRequestFilterOption<Value extends string> { |
| 57 | 57 | readonly label: string; |
| 58 | 58 | /** Uses the option's native icon tone. */ |
| 59 | 59 | readonly Icon: ElementType<{ className?: string }>; |
| 60 | readonly favicon?: { | |
| 61 | readonly environmentId: EnvironmentId; | |
| 62 | readonly cwd: string; | |
| 63 | readonly faviconPath?: string | null; | |
| 64 | readonly projectIcon?: ProjectIconOverride | null; | |
| 65 | }; | |
| 60 | readonly project?: ProjectFaviconProject; | |
| 66 | 61 | /** Why it cannot be chosen, carried onto the item as its title. */ |
| 67 | 62 | readonly unavailable?: string | undefined; |
| 68 | 63 | } |
| ... | ... | @@ -72,15 +67,8 @@ export function PullRequestFilterOptionIcon<Value extends string>({ |
| 72 | 67 | }: { |
| 73 | 68 | option: PullRequestFilterOption<Value>; |
| 74 | 69 | }) { |
| 75 | return option.favicon ? ( | |
| 76 | <ProjectFavicon | |
| 77 | environmentId={option.favicon.environmentId} | |
| 78 | cwd={option.favicon.cwd} | |
| 79 | projectName={option.label} | |
| 80 | faviconPath={option.favicon.faviconPath} | |
| 81 | projectIcon={option.favicon.projectIcon} | |
| 82 | className="size-3.5" | |
| 83 | /> | |
| 70 | return option.project ? ( | |
| 71 | <ProjectFavicon project={option.project} className="size-3.5" /> | |
| 84 | 72 | ) : ( |
| 85 | 73 | <option.Icon aria-hidden className="size-3.5" /> |
| 86 | 74 | ); |
| ... | ... | @@ -452,14 +440,7 @@ export function PullRequestFiltersMenu({ |
| 452 | 440 | serverOptions: ReadonlyArray<PullRequestFilterOption<string>>; |
| 453 | 441 | onServer: (server: EnvironmentId | undefined) => void; |
| 454 | 442 | /** The projects of every connected environment, each carrying the one its favicon is read from. */ |
| 455 | projects: ReadonlyArray<{ | |
| 456 | readonly id: ProjectId; | |
| 457 | readonly environmentId: EnvironmentId; | |
| 458 | readonly title: string; | |
| 459 | readonly workspaceRoot: string; | |
| 460 | readonly faviconPath?: string | null | undefined; | |
| 461 | readonly projectIcon?: ProjectIconOverride | null | undefined; | |
| 462 | }>; | |
| 443 | projects: ReadonlyArray<ProjectFaviconProject & { readonly id: ProjectId }>; | |
| 463 | 444 | projectId: ProjectId | undefined; |
| 464 | 445 | /** |
| 465 | 446 | * The server the selected project belongs to. A project id is only unique within its own |
| ... | ... | @@ -514,12 +495,7 @@ export function PullRequestFiltersMenu({ |
| 514 | 495 | value: pullRequestProjectKey(project), |
| 515 | 496 | label: project.title, |
| 516 | 497 | Icon: FolderGit2Icon, |
| 517 | favicon: { | |
| 518 | environmentId: project.environmentId, | |
| 519 | cwd: project.workspaceRoot, | |
| 520 | faviconPath: project.faviconPath ?? null, | |
| 521 | projectIcon: project.projectIcon ?? null, | |
| 522 | }, | |
| 498 | project, | |
| 523 | 499 | ...(unavailable.has(pullRequestProjectKey(project)) |
| 524 | 500 | ? { unavailable: unavailable.get(pullRequestProjectKey(project)) } |
| 525 | 501 | : {}), |
apps/web/src/components/settings/ProjectSettingsPanel.tsx+1-8| ... | ... | @@ -967,14 +967,7 @@ function ProjectDetail({ |
| 967 | 967 | } |
| 968 | 968 | control={ |
| 969 | 969 | <div className="flex items-center gap-2"> |
| 970 | <ProjectFavicon | |
| 971 | environmentId={representative.environmentId} | |
| 972 | cwd={representative.workspaceRoot} | |
| 973 | projectName={representative.title} | |
| 974 | faviconPath={faviconPath} | |
| 975 | projectIcon={projectIcon} | |
| 976 | className="size-6" | |
| 977 | /> | |
| 970 | <ProjectFavicon project={representative} className="size-6" /> | |
| 978 | 971 | <Button |
| 979 | 972 | size="sm" |
| 980 | 973 | variant="outline" |
apps/web/src/components/settings/ProjectsSettings.tsx+1-10| ... | ... | @@ -145,16 +145,7 @@ export function ProjectsSettings({ |
| 145 | 145 | options={groups.map((group) => ({ |
| 146 | 146 | value: group.projectKey, |
| 147 | 147 | label: group.displayName, |
| 148 | icon: ( | |
| 149 | <ProjectFavicon | |
| 150 | environmentId={group.environmentId} | |
| 151 | cwd={group.workspaceRoot} | |
| 152 | projectName={group.title} | |
| 153 | faviconPath={group.faviconPath} | |
| 154 | projectIcon={group.projectIcon} | |
| 155 | className="size-3.5" | |
| 156 | /> | |
| 157 | ), | |
| 148 | icon: <ProjectFavicon project={group} className="size-3.5" />, | |
| 158 | 149 | }))} |
| 159 | 150 | onChange={(value) => onScopeChange(value, machineId)} |
| 160 | 151 | /> |
apps/web/src/components/settings/SettingsPanels.tsx+3-22| ... | ... | @@ -2853,18 +2853,7 @@ export function ArchivedThreadsPanel() { |
| 2853 | 2853 | const projectsByEnvironmentAndId = new Map( |
| 2854 | 2854 | archivedSnapshots.flatMap(({ environmentId, snapshot }) => |
| 2855 | 2855 | snapshot.projects.map( |
| 2856 | (project) => | |
| 2857 | [ | |
| 2858 | `${environmentId}:${project.id}`, | |
| 2859 | { | |
| 2860 | id: project.id, | |
| 2861 | environmentId, | |
| 2862 | name: project.title, | |
| 2863 | cwd: project.workspaceRoot, | |
| 2864 | faviconPath: project.faviconPath, | |
| 2865 | projectIcon: project.projectIcon, | |
| 2866 | }, | |
| 2867 | ] as const, | |
| 2856 | (project) => [`${environmentId}:${project.id}`, { ...project, environmentId }] as const, | |
| 2868 | 2857 | ), |
| 2869 | 2858 | ), |
| 2870 | 2859 | ); |
| ... | ... | @@ -2983,16 +2972,8 @@ export function ArchivedThreadsPanel() { |
| 2983 | 2972 | <SettingsSection |
| 2984 | 2973 | key={project.id} |
| 2985 | 2974 | id={index === 0 ? searchableSetting("archive").id : undefined} |
| 2986 | title={project.name} | |
| 2987 | icon={ | |
| 2988 | <ProjectFavicon | |
| 2989 | environmentId={project.environmentId} | |
| 2990 | cwd={project.cwd} | |
| 2991 | projectName={project.name} | |
| 2992 | faviconPath={project.faviconPath} | |
| 2993 | projectIcon={project.projectIcon} | |
| 2994 | /> | |
| 2995 | } | |
| 2975 | title={project.title} | |
| 2976 | icon={<ProjectFavicon project={project} />} | |
| 2996 | 2977 | > |
| 2997 | 2978 | {projectThreads.map((thread) => ( |
| 2998 | 2979 | <SettingsRow |