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