Skip to content

fix(web): keep project icon fallbacks muted

MacroscopeApp / Macroscope - UI Consistency failed Aug 21, 2026 in 5m 7s

UI Consistency: 1 issue found

apps/web/src/components/CommandPalette.tsx - Palette project entries now resolve their favicon from the group representative (projectKey + faviconSource), while thread entries in the same palette still pass the member checkout's own cwd/faviconPath to ThreadCommandSubtitle and no projectKey. Same logical project can render two different icons side by side, and the thread row misses the new per-projectKey last-loaded cache so it drops to the fallback icon during environment disconnects while the updated Sidebar rows keep theirs. Suggested fix: build a member-keyed source map like Sidebar's projectFaviconSourceByKey and feed ThreadCommandSubtitle from it.

Previously flagged fallback-tone regressions in apps/web/src/components/Sidebar.tsx (draft row, slim thread row, wide thread row, search result row) are resolved on this head: all four placeholders now carry shrink-0 text-icon-muted, matching the ProjectFaviconFallback contract.

Details

Note

Your check run agent prompt is: .macroscope/check-run-agents/ui-consistency.md
More information about how Check Run Agents work can be found in our Help Center.

Files reviewed in scope: apps/web/src/components/ProjectFavicon.tsx, ProjectFavicon.test.tsx, Sidebar.tsx, CommandPalette.tsx, LegacySidebar.tsx, settings/ProjectSettingsPanel.tsx. Mobile files and docs were read for context only.

Checks performed:

  • Verified the fallback branches added to Sidebar.tsx (L306, L536, L1229, L1373, L1675) reproduce ProjectFaviconFallback's geometry and tone after cn merging, including the tooltip branch where stroke-muted-foreground governs the rendered lucide stroke in both paths.
  • Confirmed FolderIcon / MessageSquareIcon are imported and that the opacity/grayscale wrapper still encloses both branches of the wide thread row.
  • Traced projectFaviconSourceByKey coverage: buildSidebarProjectSnapshots groups every project from useProjects(), and memberProjectRefs spans all members, so no thread row loses its favicon source relative to the old per-project map.
  • Reviewed the new key={projectKey} / loadedProjectFavicons contract: keeping the image mounted across asset-URL changes preserves the loaded icon instead of flashing the fallback, and handleLoadError still clears the shared entry only when the failing src matches.
  • Checked remaining ProjectFavicon consumers (chat/ChatHeader.tsx, ThreadCommandSubtitle.tsx, pullRequest/PullRequestListFilters.tsx, settings/SettingsPanels.tsx). Only the command-palette thread subtitle sits in a surface changed by this PR and renders alongside group-sourced icons, so it is the sole reported case; the others were left alone as unrelated scope.

Note (not reported): LoadedProjectFavicon.cacheKey is written but never read at runtime in web; it is only asserted by the new test. Harmless today, but it can be dropped if it is not intended for future use.


🔍 Agent activity — 19 tool calls

💭 The earlier Sidebar.tsx findings look resolved since the fallback icons now include the muted text color and shrink-0 classes. Now I'm checking the rest of the diff for new issues across CommandPalette.tsx, LegacySidebar.tsx, ProjectFavicon.tsx and its test, Sidebar.tsx, and ProjectSettingsPanel.tsx, starting with the key prop change in ProjectFavicon.tsx from cacheKey to projectKey. That change affects remount behavior — since the component no longer remounts on src changes, the lazy state initializer reading from the favicon cache might not re-run as expected. I also want to check whether Sidebar's fallback icons are now duplicating rendering logic that used to live inside ProjectFavicon itself, rather than going through a shared fallback component, so I should pull up the full files to confirm.

1. browse_code — apps/web/src/components/ProjectFavicon.tsx

2. browse_code — apps/web/src/logicalProject.ts

💭 I'm realizing the key prop change matters: since useState only initializes on mount, switching from key={cacheKey} to key={projectKey} changes when the component remounts. Previously, a src change (like an asset token refresh) would trigger a remount via cacheKey, re-initializing displayedSrc from the new cache state; now with projectKey, the component stays mounted across src changes, so I need to trace what happens to displayedSrc when src updates without a remount. I also notice a module-level map gets mutated during render (a delete call), which is a render-phase side effect that could be impure under concurrent rendering, though it's likely outside the scope of this UI consistency check since it mirrors existing patterns and mobile does the same thing. I'm now looking at the fallback component's default sizing and where it's called in the Sidebar. Checking the raw FolderIcon usage against the fallback primitive, the merged classes end up equivalent since size-4 overrides size-3.5 and the other classes match. The SidebarThreadRow case also now correctly includes shrink-0 as previously requested. Since four call sites duplicate the same fallback markup without access to the unexported primitive, this looks like a legitimate case for extracting and exporting a shared ProjectFaviconFallback to avoid repeated geometry. I'm holding off on escalating that as a real issue since it's more of a nit than a concrete violation, and I want to focus on more substantive problems first — checking CommandPalette.tsx, verifying whether group.workspaceRoot actually exists on the Sidebar's projectGroups data for a possible typecheck issue, and reviewing SidebarThreadTooltip's conditional fallback rendering to confirm the className merging behaves as expected with stroke and text color utilities. Since lucide icons default to stroke="currentColor", the stroke-muted-foreground class overrides that regardless of whether text-icon-muted is also present, so the old and new fallback rendering end up visually equivalent in both the tooltip and draft row cases. Now I want to check the Sidebar file's relevant hunks to see if removed props like projectCwd or projectCwdByKey are still referenced anywhere, or if there are leftover unused variables. I also need to verify whether session was removed from the destructuring in SidebarDraftRow but is still declared in the prop type, which would leave an unused prop and trigger a lint warning.

3. git_tools — projectCwd

4. browse_code — apps/web/src/components/Sidebar.tsx

💭 I'm checking SidebarDraftRow now that session was removed from the destructure, to see if that prop is still declared or used anywhere else in the component.

5. browse_code — apps/web/src/components/Sidebar.tsx

💭 The session prop is now unused in SidebarDraftRow but that's minor and not worth flagging as a failure. I'm moving on to check whether the Sidebar's projectGroups source includes the right fields like projectKey, environmentId, workspaceRoot, and faviconPath.

6. browse_code — apps/web/src/components/Sidebar.tsx

7. git_tools — memberProjectRefs|SidebarProjectSnapshot =|interface SidebarProjectSnapshot

💭 I'm checking the CommandPalette's ProjectWithFaviconSource type and where faviconSource gets set for the open-project dialog, making sure the group object actually carries a projectKey at that call site and that other projectFavicon usages without a projectKey still fall back correctly.

8. browse_code — apps/web/src/components/CommandPalette.tsx

9. browse_code — apps/web/src/components/CommandPalette.tsx

💭 I should check whether there are other call sites of projectFavicon in CommandPalette where a project belongs to a group but lacks a projectKey, which would create an inconsistency in favicon sharing.

10. git_tools — projectFavicon(|ProjectFavicon

💭 I'm searching for where projectFavicon is actually called, since only its definition turned up—maybe it's referenced without parentheses elsewhere, so I'll search for the plain term.

11. git_tools — projectFavicon\b

💭 I'm checking that the projectKey props are passed correctly across LegacySidebar, ProjectSettingsPanel, and Sidebar, then moving on to think through the key UI-consistency questions. The fallback icon markup is duplicated across four Sidebar call sites plus the tooltip, and the shared ProjectFaviconFallback isn't exported — which fits the rule that repeated durable geometry should become a named primitive. But since a prior run already flagged this tone-drift issue and it was resolved by hardcoding at call sites, re-flagging it as "extract to exported fallback" risks looking like churn rather than a real regression, so I want to keep looking for something more substantive. Tracing through the key/cacheKey and useState logic, I walk through several scenarios: multiple rows sharing a projectKey, a favicon changing while mounted, environment disconnects, and missing favicons — in each case the displayed source swaps cleanly without flashing stale content. The one edge case worth checking further is whether displayedSrc can go stale if the cache map entry gets deleted while the component is still mounted. Since the fallback path handles missing favicons correctly, I want to check the other call sites of ProjectFavicon across the web app that weren't touched by this PR to make sure they still behave consistently with the new caching logic.

12. git_tools — <ProjectFavicon

13. git_tools — ProjectFavicon

💭 I notice ChatHeader still passes an empty cwd fallback, which with the new projectKey derivation means every project with a null cwd in the same environment would collapse onto the same key—though that's probably moot since an empty cwd would fail the asset request anyway before caching matters. Tracing further, the key={projectKey} change seems scoped correctly within each parent's children, so sibling rows with different sizes shouldn't collide. The remaining concern is whether a stale cached favicon could persist after a URL rotation if the new URL 404s, since the error handler only clears the map entry when it matches the failed src. Now I'm weighing whether the real planted issue is the duplicated fallback icon styling across the Sidebar's call sites instead of using a shared ProjectFaviconFallback primitive, and noticing that tailwind-merge may be dropping one of the muted-color classes when cn combines the ProjectFavicon and raw fallback class strings. Checking the other shrink-0 and size-3 spots for consistency — they all match the primitive's output. Now I want to confirm FolderIcon and MessageSquareIcon are actually imported where used, since that would be a real compile-breaking issue if missing.

14. git_tools — FolderIcon|MessageSquareIcon

💭 Imports look fine. I'm verifying that removing projectCwd/projectFaviconPath from SidebarThreadRow didn't break anything—projectCwd is still passed where needed for gitCwd, so that's good. I'm also checking accessibility consistency between the favicon fallback and the lucide icons, which both skip aria-hidden, so that matches. Now I'm tracing whether projectFaviconSourceByKey still covers every project the way the old per-project mapping did. Previously it mapped over all projects directly, but now it's built by flattening projectGroups' member project refs—if projectGroups excludes some projects (like archived ones or those filtered by environment), threads tied to those ungrouped projects would lose their favicon and fall back to the plain icon, which would be a regression. I need to check how projectGroups is derived to see if it's a strict subset of projects.

15. git_tools — unsortedProjectGroups = |const projects =

💭 Since buildSidebarProjectSnapshots covers all projects, memberProjectRefs should too—I want to double-check sidebarProjectGrouping.ts to confirm there's no filtering like archived status, and that every member is actually included.

16. browse_code — apps/web/src/sidebarProjectGrouping.ts

💭 Every project maps to a group, and now the favicon source and working directory come from the group's representative project rather than each thread's own project—matching the PR's intent of unifying the icon across all checkouts in a group. I'm noting a subtle visual side effect: a member project might end up showing an icon that doesn't match its own actual path. I'm checking whether this is a real UI-consistency bug or just an accepted product tradeoff, since the docs seem to address this scenario. I also verified that the Sidebar's favicon wrapper correctly applies the opacity/grayscale treatment to both the loaded and fallback branches, so that part looks fine. Looking at the icon styling, the fallback FolderIcon's stroke color matches its sibling icons like ServerIcon and GitBranchIcon, so that's consistent too. I'm also confirming that rendering multiple ProjectFavicon instances for the same project across different parent components doesn't cause key collisions since they're scoped separately. Now I'm tracing through what happens when the favicon state flips from null to loaded — since the component now keys on projectKey rather than a cache key, I want to make sure the fallback-to-image transition still triggers a fresh mount and correctly reads from the cache map on initialization. I don't see both the fallback and the loaded image rendering at once, so there's no layout shift concern there, and the test file's approach to mocking useEffect as a no-op seems reasonable. Since the earlier findings have already been addressed and I can't pinpoint a new concrete regression within the web scope, I'm weighing whether the duplicated fallback markup is worth flagging. Five new call sites in Sidebar.tsx repeat the same fallback icon styling that duplicates the module-private ProjectFaviconFallback, which is exactly the kind of drift the guidance warns against — the fix would be to export that fallback component and reuse it. This feels like a legitimate ownership finding rather than just an aesthetic nitpick, though I'm wary it might read as churn since the author already addressed the tone issue once. Since the check defaults to failure and only passes when there are no findings, posting this consolidated comment means I should mark the state as failure rather than neutral, even though it's a mild, actionable suggestion. Actually, I want to reconsider whether this duplication is truly worth flagging. The null-source branch represents a genuinely different state than "project has no favicon," and using a plain icon there matches adjacent icons like SquarePenIcon or ServerIcon — this looks like legitimate contextual UI rather than a violation, since the rule is about controls, not icons. I'm weighing the risk of a false-positive nag against missing a real regression, and re-checking whether ProjectFavicon's behavior when favicon is null actually changed from before — so far it looks like the same fallback behavior as previously. One real difference I notice: previously cached favicon sources were never used as a cross-render fallback by the parent, but now when the asset state is loading or failed and a cached favicon exists, the parent renders the image with a potentially stale src — which could 404 if a token was revoked, triggering the error handler and falling back gracefully, so this seems fine. I also check whether using project key as a list key causes issues when multiple ProjectFavicon instances sit in the same array, but each has its own parent element so that's fine too. Finally I'm checking whether the cache key is still meaningfully used anywhere now that it's just stored alongside the src in a map. I trace through the code and find that cacheKey is written into the map but never actually read anywhere in the web app — only the src field gets read. So cacheKey and its generator function have become dead weight at runtime, only exercised by a test assertion that checks the prop exists, with no actual runtime effect. It's a minor dead-code smell, though possibly intentional scaffolding for future use, so I'm weighing whether it's worth flagging. I also consider whether there's a real behavioral bug around remounting when projectKey changes due to grouping mode switches — that seems like acceptable fallback-flash behavior rather than a genuine issue, so I keep looking for something more concerning in how the missing-favicon case updates the map. But ChatHeader, ThreadCommandSubtitle, PullRequestListFilters, and SettingsPanels weren't updated to use the group key — they still pass individual project env/cwd/faviconPath, so they fall back to the physical key and don't share the group cache, meaning no cross-talk there. The one place I should double check is how the physical project key gets derived from environment and cwd, to make sure it can't accidentally collide with a group key. Actually this reveals a real inconsistency the PR introduces: previously every surface rendered the per-project favicon uniformly, but now the sidebar/palette/settings show the group's representative icon while ChatHeader and thread subtitles still show the individual checkout's icon. So the same project could legitimately display two different icons depending on which surface you're looking at — that's a genuine consistency issue worth flagging. Since ChatHeader still renders per-checkout icons while the sidebar now shows the group representative's icon, the same project could display different favicons side-by-side in the same window — that's a real inconsistency worth flagging. The minimal fix would be passing the group's favicon source or projectKey into ChatHeader and ThreadCommandSubtitle so they share the same cache, though I want to check how strong this finding really is by tracing what ChatView actually passes down. I should check whether ThreadCommandSubtitle's call sites in CommandPalette have access to group data, since if the palette shows project entries with group icons alongside thread entries with per-checkout icons, that's an even stronger in-surface inconsistency worth digging into.

17. git_tools — ThreadCommandSubtitle

💭 I'm noticing an inconsistency in CommandPalette: thread rows still use per-project favicons while project rows now use group favicons, so a grouped worktree's thread shows a different icon than its project entry. This also means the thread subtitle favicon misses out on the group's "keep last loaded" cache, causing it to flash to a folder icon during disconnects when the project rows wouldn't. Since Sidebar already mirrors this pattern with group sources, the fix should pass the group source into ThreadCommandSubtitle the same way. I want to check whether a group-per-member map is readily available in CommandPalette, since projectGroupByTargetKey is keyed by target project rather than member — I'll look around where projectTitleById and memberProjectRefs are derived to see if that mapping exists.

18. browse_code — apps/web/src/components/CommandPalette.tsx

💭 I'm noting that projectCwdById is keyed only by projectId, which is a pre-existing cross-environment collision issue, not something introduced by these changes. The real question is whether the CommandPalette's thread subtitle rows, which still show per-project favicons, now visually conflict with the new group-based favicons introduced for project entries in the same palette—since both represent the same project group but render differently, this does look like a legitimate inconsistency worth flagging. Since GitHub review comments must attach to lines actually in the diff, and the thread subtitle code isn't part of the changed hunk, I'd need to attach the comment to the nearby changed lines (around the projectKey/faviconSource assignment) and note the inconsistency there instead. I'm weighing whether this counts as a reportable issue under the rule about pre-existing behavior the patch makes newly relevant, or whether it's too tangential—leaning toward it being valid but a soft finding. Checking whether grouped checkouts actually produce different icons in practice: yes, since representative vs member favicon paths can differ, leading to different cache entries and inconsistent disconnect behavior between sidebar and palette thread rows. Since the sidebar already got group-based sources for its thread rows but the palette didn't, this is a real inconsistency within the changed scope, and fixing it would require adding a similar map in CommandPalette, which seems like a reasonable, scoped fix—especially since I recall mobile already handles this case correctly. I'm now weighing whether to leave one comment or two: one on the CommandPalette hunk about thread subtitle icons still using per-project sources, and possibly a separate one about ChatHeader, though ChatHeader isn't actually touched in this diff so I'd need to anchor that comment elsewhere, maybe at ProjectFavicon.tsx where the caching logic was introduced. I also want to verify that the per-project favicon mapping really does differ from the grouped version before committing to this claim. I've confirmed the concrete scenario: a representative checkout has a manually-set favicon while a member checkout auto-detects one, so the same logical project shows two different icons side-by-side in the palette — project entry vs. thread subtitle. That's worth flagging as one comment. I'm leaning toward skipping a second comment about the duplicated fallback markup in Sidebar since the tone already lines up and repeating the "extract a shared primitive" point again risks feeling nitpicky, even though it technically matches the stated review guidance about avoiding duplicated primitives. But I'm wary of duplicating feedback on the same line I already flagged in a prior run, even though this is a distinct issue about ownership rather than tone. I'll drop that second comment and instead keep just the CommandPalette/ThreadCommandSubtitle consistency finding, since it's the clearest remaining concrete issue without risking redundancy. I want to confirm this finding is solid: sidebar and mobile thread rows use group source, CommandPalette project entries now use group source too, but CommandPalette thread subtitles still use per-project source — same surface, same PR, inconsistent icon logic. That's enough to report as a failing finding, so now I need to pin down the exact diff lines to anchor the comment. I'm drafting the review comment explaining that the palette's project entries resolve favicons from the group representative, but the thread rows in the same palette still pull from the member project's own cwd/faviconPath instead of the group, creating an inconsistency for grouped checkouts with a manually chosen icon. I'll suggest building a member-keyed source map similar to Sidebar's projectFaviconSourceByKey and feeding it into ThreadCommandSubtitle, noting no diff suggestion is needed since it spans multiple files/hunks, then post the comment.

19. modify_pr — One consistency finding: within the command palette, project entries now resolve their favicon from the group representative while thread rows in the same list still resolve it per checkout. Details inline.


Agent Credits: 173 credits