Skip to content

fix(clients): centralize reliable project icons

MacroscopeApp / Macroscope - UI Consistency failed Aug 22, 2026 in 5m 6s

UI Consistency: 2 issues found

Two consistency findings were posted as inline review comments on in-scope web files.

  • apps/web/src/state/projects.ts (L29-31): the new projectFaviconSourcesAtom feeds selectProjectFaviconSources, which groups projects with a hardcoded sidebarProjectGroupingMode: "repository" and empty overrides. Favicon grouping therefore ignores the grouping mode/overrides the sidebar and the Project settings panel honor: under Keep separate (or a per-project override) two clones of one repo remain separate rows yet share one favicon source, the Project icon row describes another checkout's path, and Reset fans out to both.
  • apps/web/src/components/settings/ProjectSettingsPanel.tsx (L385): the fan-out failure toast now falls back to the raw member.environmentId because the new members argument can carry plain project records without environmentLabel. Every other environment label in this panel renders a human string (environmentLabel ?? "This machine"), and the prior copy read the current environment.

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.

Scope reviewed (include patterns apps/web/src/**/*.{ts,tsx,css}): apps/web/src/components/ProjectFavicon.tsx, apps/web/src/components/ProjectFavicon.test.tsx, apps/web/src/components/settings/ProjectSettingsPanel.tsx, apps/web/src/state/projects.ts. Mobile, server, docs, and packages/client-runtime changes were read for context only.

Checks performed and found clean:

  • ProjectFavicon still renders through ProjectFaviconFallback / <img> with the existing size-3.5 shrink-0 text-icon-muted and rounded-sm object-contain treatments; no primitive was reconstructed and no call-site class string overrides a primitive contract. Fallback icon tone (text-icon-muted) is preserved for the no-favicon state, including the new "favicon missing" path.
  • The remount contract change (key={cacheKey} -> key={projectKey}) keeps the two-image preload pattern intact: the visible image holds the previously loaded src while the hidden className="hidden" image loads the new one, so no flash-to-fallback is introduced when the resolved URL changes, and the missing-favicon case unmounts to the fallback in the same render as the forgetProjectFavicon effect.
  • Shared store keys were verified to line up: deriveProjectGroupingOverrideKey === derivePhysicalProjectKey, so the faviconSources.get(deriveProjectGroupingOverrideKey(project)) lookup in the clear-icon path matches the map produced by selectProjectFaviconSources.
  • No Tailwind/global CSS, theme variant, MenuPopup width defaulting, ScrollArea, or virtualized fade behavior is touched by this diff.
  • useSyncExternalStore without getServerSnapshot was checked against SSR usage; only tests use react-dom/server, and none render ProjectFavicon, so this is not a runtime regression.

🔍 Agent activity — 18 tool calls

💭 I'm checking which files fall within the diff scope, noting the changed files touch ProjectFavicon and project settings components, and this set differs from what I'd expect based on earlier runs. I want to verify the imports carefully — checking whether ProjectFavicon correctly pulls in the path derivation helper and whether ProjectSettingsPanel properly imports the grouping override key function it references.

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

2. git_tools — deriveProjectGroupingOverrideKey|isWindowsAbsolutePath|projectFaviconSourcesAtom

💭 I'm checking for UI consistency issues in this mostly logic-focused change. The favicon component now keys remounting on projectKey rather than cacheKey, which changes when the image remounts versus just updating state—seems like an intentional design shift, but I want to verify how displayedSrc gets initialized to make sure there's no stale image flash. Behavior changes like external favicon gating and member selection logic for removeFavicon are functional, not styling, so they're less likely to be flagged by a strict UI consistency check unless they alter rendered structure. Comparing web and mobile favicon components, mobile sets an accessibility label while web leaves alt empty, which seems consistent with existing patterns. I want to check the toast message in the settings panel that now surfaces member.environmentId directly instead of a friendly label — showing a raw environment ID in user-facing text could be a real UI consistency issue worth investigating further.

3. browse_code — apps/web/src/components/settings/ProjectSettingsPanel.tsx

💭 I'm noticing the failure toast now falls back to showing the raw member.environmentId when a label isn't available, instead of the previous friendlier "the current environment" text — that's a user-facing copy regression worth flagging, exposing an internal ID to users. I want to check whether useProjects() actually exposes an environment label anywhere, or if the file only has access to raw environment IDs, and look at how other parts of the settings panel display environment names for comparison.

4. git_tools — environmentLabel

💭 I notice environmentLabelById is scoped to ProjectSettingsPanel, not accessible from ProjectDetail, but the existing convention elsewhere in the file uses environmentLabel ?? "This machine" as a fallback pattern I should follow for the member's environmentId case. Displaying a raw internal ID in the toast would break that convention, though it's more of a minor UX copy issue than a core primitive/layout violation, so I'll flag it weakly and keep looking. Moving to ProjectFavicon.tsx, I'm now checking whether changing the key from cacheKey to projectKey on ProjectFaviconImage affects remounting behavior when the favicon source changes, since the component previously reset its displayed source on remount but now may persist stale state. Tracing through the different favicon states — success, loading, missing, and failure paths — to confirm the fallback and cross-fade logic still behave correctly, and checking whether the shared favicon store being global across components introduces any stale-state issues when multiple components reference the same project key. I need to verify that derivePhysicalProjectKeyFromPath actually exists in the logicalProject module before relying on that import, and confirm that ProjectFavicon's prop signature hasn't changed for existing callers. I'm also noting that when a source exists, the component intentionally drops the input's faviconPath entirely in favor of the group-shared source, which keeps things consistent across settings previews. Since ProjectFavicon renders per sidebar row and may be virtualized, subscribing to the atom map per row should still be fine performance-wise. I want to double check accessibility on the visible image element to make sure alt text handling didn't regress. The hidden preloader image still uses display:none, which means browsers still load it in the background — that's pre-existing behavior. I'm tracing the loading state logic: when nothing has loaded yet, the fallback icon shows alongside the hidden preload image, and I'm matching this against the test's expected child indices for the hidden preloader element. Now I'm looking at the settings panel, where the favicon path now falls back to the representative's path when no group favicon source exists, and the gate for picking an external favicon was relaxed to only require the representative be on the primary environment rather than all group members. Working through the freshness resolution: if only the representative gets an external favicon while siblings keep their own overrides, shouldReplaceFaviconSource falls back to comparing update timestamps, so the most recently updated one wins. For clearing favicons, I'm checking how deriveProjectGroupingOverrideKey is used to key into favicon sources when filtering members with existing overrides.

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

6. git_tools — export function derive|export function buildProjectGroups|physicalProjectKey

💭 I notice selectProjectFaviconSources hardcodes the grouping mode to "repository" with empty overrides in client-runtime, ignoring the user's actual grouping settings — so favicon grouping could diverge from the sidebar grouping the user actually configured. Since that specific file is out of scope, I want to check whether the in-scope atom in state/projects.ts is the right place to flag this, so I'm looking at resolveProjectGroupingMode and the available grouping modes to confirm this is a genuine inconsistency worth noting.

7. browse_code — packages/client-runtime/src/state/projectGrouping.ts

8. git_tools — sidebarProjectGroupingMode

💭 I'm realizing the favicon logic hardcodes "repository" grouping and ignores the "separate" mode, so a user who's split checkouts into distinct sidebar projects would still see shared favicons across them—and clearing an icon on one could incorrectly affect sibling checkouts they consider independent. That seems like a real bug worth flagging rather than a minor edge case. Now I want to check the web ProjectFavicon component itself for a possible visual regression, looking at how it derives its source from the favicon asset state. The old version returned a fallback if the source was missing or matched a fallback URL, otherwise rendered the image keyed by a cache key. The new version pulls from the group's shared source and keeps a previously-loaded favicon visible while the connection drops, which seems intentional for smoother transitions. I'm now checking whether keying by project versus keying by resolved source causes stale icons to linger correctly during cross-fades, and looking at how handleLoadError calls into forgetProjectFavicon to clear a failed source for the visible image. Tracing through edge cases where the stored cache key might not exactly match the initial displayed source, and how the shared favicon store behaves when multiple rows reference the same project key — since a fresh load in one row notifies subscribers everywhere, but rows already showing a successful, distinct URL should keep priority over the shared update, which seems fine. Now I want to check other places ProjectFavicon is used across the web app, particularly whether dropping honoring the faviconPath prop breaks something like a settings preview panel that passes an explicit path to preview — so I should grep for its usages.

9. git_tools — <ProjectFavicon|useProjectFaviconAsset

💭 I'm searching for any imports of ProjectFavicon across the web app to check if it's actually used anywhere.

10. git_tools — ProjectFavicon

💭 That earlier note about Sidebar.tsx fallbacks and CommandPalette is outdated since that code was reverted, and Sidebar now uses ProjectFavicon again. I'm checking whether callers pass different className sizes like "size-4" versus "size-3.5" to ProjectFavicon, though that particular Sidebar section wasn't touched in this PR. The hidden preload img inside ProjectFaviconImage's fragment doesn't inherit the caller's className and stays display:none, so it won't affect flex gap spacing—this double-img pattern is pre-existing, not new. The real change is that the initial displayedSrc is now keyed by projectKey rather than cacheKey, and since cacheKey encodes environment, cwd, and a content hash, I need to trace through what that keying difference actually changes in behavior. Since ProjectFaviconImage is keyed by projectKey and each ProjectFavicon instance only renders once per project, sibling key uniqueness isn't an issue even with grouped project keys. But I'm now spotting a subtler problem: when the underlying asset URL changes for a project but the projectKey stays the same, the component won't remount since the key is unchanged, so I need to check whether the src update logic properly picks up the new URL in that case. Ignoring the individual faviconPath when a group source exists is actually the intended goal—making icons consistent across a project group rather than per-member, so callers like CommandPalette and Sidebar passing member-specific paths is expected to be overridden. I'm trying to pin down whether there's a genuine UI-consistency bug here rather than just expected behavior, revisiting the subscription pattern and effect logic in ProjectFavicon to see if there's something concrete worth flagging. The effect that clears the cached favicon when resolution falls back to the default URL could be too aggressive since it's shared across the whole project group, but since favicons are inherently group-shared, that's probably fine — missing is missing for the group. The edge case worth checking is instances where source is undefined, like when cwd is empty and the derived project key falls into its own isolated bucket rather than causing cross-contamination. There's another subtle inconsistency though: rows for a project whose environment is disconnected (so it's absent from projectsAtom) will have source undefined and fall back to their physical key, missing the remembered favicon that was cached under the group's canonical key while connected — causing the icon to look different than other rows for the same project. Actually the settings panel toast exposing the raw environmentId might be worth flagging. I'm also checking updateAllMembers' dependency array and setFaviconPath's deps — both look fine since they include everything referenced. Now I'm tracing whether ProjectDetail's faviconPath, when combined with a representative's cwd from a different checkout, could produce a mismatched path, and whether ProjectFavicon handles that internally. It resolves via source ?? input, so the request uses the source cwd correctly, but I still want to see whether the settings panel description text displays that path directly — if so, showing a path relative to a different checkout's workspace root could look slightly off, even if functionally fine. I'm going to read through that section of the panel to check.

11. browse_code — apps/web/src/components/settings/ProjectSettingsPanel.tsx

💭 The settings row shows the favicon path when set, with a reset option — and since faviconPath can come from a sibling checkout in the group, clicking reset there actually clears paths across all group members, which matches the intended "clearing fans out" behavior. But I'm noticing a potential edge case worth digging into further. Selecting a relative favicon path in the picker already fans out to every member of the group, which is pre-existing behavior tied to the representative's cwd and environment. But an external absolute path only applies to the representative itself, making it the newest override and thus the group's chosen favicon source — that seems consistent since we just updated its timestamp. Still, I want to check what happens to other members' relative paths after the representative gets an external favicon set. One concrete issue stands out: the toast text exposes a raw member.environmentId to the user, which breaks from the file's usual convention of showing friendly labels like "This machine". It's introduced directly by the changed line, so it's in scope even if it's more of a copy/consistency nit than a structural violation — I'll flag it as a minor finding. Meanwhile I'm tracing whether the favicon store keying could cause cross-environment flicker between sibling rows sharing a group key versus rows falling back to an env-specific key, but the derivation looks consistent since same-source rows compute identical URLs and differently-sourced rows land under separate keys, so no real cross-talk bug there. I check that the fallback folder icon still displays correctly during the loading/no-cache state, matching prior behavior, and confirm that the size-6/size-3.5 className merge at settings resolves correctly via tailwind-merge favoring size-6. I decide the mobile file is out of scope for this review and move to considering how the check should determine pass/fail based on whether findings exist. I lean toward treating the "All clear" instruction as describing the content of a success-state response rather than a literal exact string, since the output must still conform to the required JSON schema. I still need to verify whether an environment label is accessible from the ProjectDetail scope where the environmentId-in-toast issue would apply, so I want to look at the surrounding lines to check if environmentLabelById is reachable there.

12. browse_code — apps/web/src/components/settings/ProjectSettingsPanel.tsx

13. browse_code — apps/web/src/components/settings/ProjectSettingsPanel.tsx

💭 I notice the convention elsewhere is to fall back to "This machine" when environmentLabel is missing, but the changed toast line instead falls back to the raw environmentId, which would print something like a UUID or "local" — a real user-facing regression on an in-scope file that replaced the more readable "the current environment" text. I'm also checking the projects branch where members are typed as EnvironmentProject to see if the same issue applies there. The cleanest fix would be to resolve labels through something like a useEnvironments map and fall back to "This machine" consistently with the rest of the file, rather than showing raw IDs. I'm weighing whether this is within scope for the review — it's thin as UI copy, but similar tone/consistency issues were flagged legitimately before, so I think calling out this label regression is defensible and actionable. I want to double-check whether anything else in the diff changes rendered geometry, but nothing stands out — the favicon fallback swap unmounts cleanly and the effect handles forgetting properly, so that path seems fine. Now I'm considering whether useSyncExternalStore without a getServerSnapshot is actually a problem for the web app — that only matters if the app does SSR/hydration, so I need to check whether apps/web uses TanStack Start or renders purely client-side before flagging this as an issue.

14. git_tools — renderToString|react-dom/server|@tanstack/react-start|hydrateRoot

💭 I'm checking whether any tests render ProjectFavicon or Sidebar through renderToStaticMarkup, since useSyncExternalStore without getServerSnapshot would throw during SSR-style rendering in tests. The ProjectFavicon test mocks react entirely, so I need to check the Sidebar tests next. Actually, mobile passes a getServerSnapshot but web omits it — a minor inconsistency that only matters for SSR paths, and any test breakage from renderToStaticMarkup would surface in CI anyway, so this isn't worth flagging as a real issue. I'm settling on posting one inline comment about the raw environment ID appearing in the failure toast in ProjectSettingsPanel.tsx around line 385, though I want to double-check whether the old fallback behavior for environmentLabel being null actually differs from what's happening now. Looking closer, for snapshot members the code falls back to environmentId, but for the new plain projects[] members there's no environmentLabel field at all, so it always shows the raw ID — like "env_2f9c..." — while elsewhere the panel shows "This machine" for a missing label. That's a genuine inconsistency introduced by this diff. Since useEnvironments is already imported in this file, a fix is feasible: build a label map in ProjectDetail and fall back to "This machine" instead of the raw ID. I'll draft a short unified diff for this suggestion. That grouping mismatch could actually mislabel a remote environment as "This machine," so the more accurate fix is building an environmentLabelById map from useEnvironments() in ProjectDetail. Since that requires two separate hunks—adding the map and using it—I'll skip the diff format and just describe the fix in prose instead. I should also flag a real consistency issue: favicon grouping hardcodes "repository" mode while the sidebar respects the user's actual grouping mode and overrides, meaning that under "separate" or "repository_path" modes, checkouts shown as distinct projects in the sidebar would still share one icon, the settings panel could display a path from a different checkout, and Reset would wipe icons for projects the user intentionally keeps separate. Since that source atom hardcodes "repository" instead of deriving from the actual project grouping settings that already exist in the web client, this looks like a real bug worth flagging at that line, especially since a user with two clones of the same repo, each with explicit icon overrides, would end up with one clone's icon silently overriding the other's based on which was updated more recently. That's threaded through, so it's in scope — I'll comment on those lines. Though I want to check whether an atom can actually read client settings, since web typically reads them via a hook rather than directly, so let me verify there's a settings atom available before suggesting that fix.

15. browse_code — apps/web/src/state/projects.ts

16. git_tools — export function useClientSettings|clientSettingsAtom|settingsAtom

💭 I recall that useClientSettingsHydrated provides a server snapshot fallback for useSyncExternalStore, but the new web ProjectFavicon component doesn't supply that third argument. That's risky since React throws an error when SSR'ing components missing a server snapshot, so any test using renderToStaticMarkup on a tree containing ProjectFavicon would likely break. I want to check whether tests like ThreadCommandSubtitle, Sidebar, or CommandPalette actually render this component through renderToStaticMarkup.

17. git_tools — ThreadCommandSubtitle|PullRequestListFilters|ProjectFavicon

💭 Since no test actually renders ProjectFavicon through SSR, the missing getServerSnapshot isn't a real problem—mobile provides it and web doesn't need it, so I'll skip that finding. Turning to the grouping-mode claim, I confirm that with "separate" mode, deriveLogicalProjectKey uses the physical key, while hardcoding "repository" groups by the repo's canonical key, which validates my earlier point. I also want to check how sidebarProjectGroupingOverrides factors in as a per-project override. The per-project grouping override is also ignored, and since ProjectSettingsPanel exposes this exact setting to users, the favicon behavior directly contradicts a control shown in the same panel—that's a strong finding. Concretely, a user with "Keep separate" grouping and two clones with distinct icons would see both display the same newest icon, with the settings panel even showing the wrong clone's icon path and Reset clearing both. As for a fix, since client settings live outside the atom system in a separate external-store hook, making the favicon atom grouping-aware isn't trivial—the cleanest approach would be having the selector accept grouping settings and threading them in via a hook that reads from useClientSettings. Given this requires signature changes across files, I'll report it as a finding rather than a diff, pointing to the relevant lines in projects.ts. I also want to flag whether the hardcoded sharing is possibly intentional, but the added documentation explicitly ties icon sharing to "the project group," which the panel's grouping setting defines—so ignoring a changed grouping mode does contradict the documented behavior. I'll frame this as a suggestion: either add a clarifying comment if intentional, or thread the grouping settings through properly. That gives me two concrete comments to raise: the grouping-ignorant favicon logic, and the raw environment ID appearing in the toast message. I'm now settling on marking this review as failure, per guidance that a clear consistency or regression risk warrants it, and confirming the exact line numbers to attach each comment to before writing them out. For the favicon comment, I'm explaining that hardcoding the grouping mode causes two clones of the same repo to visually diverge from their actual sidebar grouping—rendering mismatched icons and confusing behavior when a user resets or overrides settings. For the second comment on the raw environment ID label, I'm sketching a possible fix that falls back through the group's member labels before defaulting to a generic "This machine" string, though I'm second-guessing whether that placeholder makes sense for an unrecognized remote environment. Since the old code's "current environment" phrasing was also vague, I think the better recommendation is to point toward resolving labels through useEnvironments() rather than provide a diff that could give a misleading fallback. I'll describe both approaches briefly without proposing a concrete code change, then move to posting the response.

18. modify_pr — Two consistency findings in the web changes. Details inline.


Agent Credits: 178 credits