Repository navigation
UI Consistency: 1 issue found
Findings (1)
apps/web/src/hooks/useDefaultTheme.ts\u2014 environment-theme adoption callssetTheme(defaultTheme), writing the theme as the base preference. For the documented single-appearance published form,setThemeclears the stored light/dark halves andresolveThemeAppearancefalls back todefinition.appearance, so a client in explicit Light mode is flipped to dark while the Settings mode tiles still show Light (and render the light wireframe from dark preview colors). This is the same inconsistency the published-theme card'sonUsewas changed to avoid; suggest mirroring that rule (setThemeHalffor one-mode themes) or pairingsetThemewithsetAppearanceMode(definition.appearance).
Resolved since the previous run
apps/web/src/components/settings/ThemeSettings.tsx\u2014 the published-theme card now assigns only its own half for single-appearance themes, matching the saved-theme cards.apps/web/src/hooks/useEnvironmentTheme.ts\u2014managed: trueis now limited to the pure seeded form, so exported files and hand-tuned roles open Duplicate in the advanced editor.
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}): components/settings/ThemeSettings.tsx, hooks/useEnvironmentTheme.ts, hooks/useEnvironmentTheme.test.ts, hooks/useDefaultTheme.ts, routes/__root.tsx, state/server.ts, themePalette.ts. Server, contracts, and docs changes were read for context only.
Checks performed
- Shared primitives: the new published-theme cards reuse
ThemeLibraryCardwith the same props, tooltips,Button size="icon-xs" variant="ghost"actions, andpickedModesFor/handlePairPickwiring as the maintainer and custom cards; no locally reconstructed controls or primitive-overriding class strings were introduced. - Theme ownership:
lenientThemeColorOverridesis a single canonicalization path reused by stored, imported, and published colors; reserved ids are filtered before rendering;getThemeDefinitionresolves environment themes last so saved themes win an id collision (covered by the new tests). No global CSS, cascade, or@variantchanges in this PR. - Runtime palette:
setEnvironmentThemesreturns a structural-change flag anduseEnvironmentThemeSynconly callsrefreshTheme()on a real change, which resetslastAppliedThemeand repaints; appearance-contrast variables are independent of the palette variables, so no adjusted/unadjusted split is introduced. - Environment routing: the published set is read from
primaryServerEnvironmentThemesAtom(primary environment only) and default-theme adoption is keyed perenvironmentId, so no active-environment fallback is introduced. - No scroll/virtualization,
MenuPopupwidth, or emitted-selector surfaces are touched by this diff.
🔍 Agent activity — 22 tool calls
💭 I'm reviewing the current state of the PR's in-scope files—ThemeSettings.tsx, the theme hooks, __root.tsx, server.ts, and themePalette.ts—to check whether the two issues flagged in the prior review at commit 7ae7dc7 have since been addressed. Comparing against the current head at 1683fe4, both the onUse persistence logic in ThemeSettings.tsx and the managed flag handling in useEnvironmentTheme.ts now appear to be fixed. So I should move on to reviewing the actual files fresh for any new issues.
1. browse_code — apps/web/src/hooks/useEnvironmentTheme.ts
2. browse_code — apps/web/src/hooks/useDefaultTheme.ts
💭 I'm checking ThemeSettings.tsx to see how the new card renders alongside the existing custom theme cards for comparison.
3. browse_code — apps/web/src/components/settings/ThemeSettings.tsx
💭 With both prior findings resolved, I'm now scanning this section for new issues, starting with whether ThemeLibraryCard's omission of onDownload for environment themes is intentional or a consistency gap, then moving to check CustomThemeCollection next. Checking that MAINTAINER_THEMES also use ThemeLibraryCard consistently, and noting the dedup filter shadows the outer
themeprop name with a local variable—minor but worth flagging as a lint concern rather than a UI issue. Now I want to look at getThemeCardDefinition and getThemeModes to verify the environment theme card setup is correct.
4. git_tools — function ThemeLibraryCard|function getThemeCardDefinition|function getThemeModes|function CustomThemeCollectionCard
5. browse_code — apps/web/src/components/settings/ThemeSettings.tsx
💭 The environment theme card design matches maintainer and custom cards well. I notice environment themes aren't visually labeled as distinct from user-saved themes in the library, but that's just an aesthetic preference, not something worth flagging. The missing onDownload for environment cards also seems intentional given the docs only mention duplicating, so I won't raise that either. Now I'm shifting to checking themePalette.ts, specifically whether
setEnvironmentThemescorrectly usesEqual.equalsfor comparing plain arrays of plain objects. I need to verify whether Effect'sEqual.equalsdoes structural comparison on plain JS objects/arrays or just falls back to reference equality — checking Effect 3's actual implementation to see how it handles objects that don't implement the Equal interface. Following through, it seems plain arrays and objects would just compare by reference in Effect 3, meaning fresh arrays would always register as changed, causingrefreshTheme()to fire on every snapshot — exactly the kind of wasted-frame issue the codebase claims to guard against. Now I need to check which Effect version this repo actually targets, since the presence ofeffect/unstable/cliandTestConsolesuggests Effect 4, which may have added structural equality for plain objects/arrays instead. I'm also noticing the same pattern shows up elsewhere:primaryServerEnvironmentThemesAtomderives from a decoded schema and falls back to an EMPTY array, and the server-sideenvironmentTheme.tsdoes anEqual.equalscomparison on plain arrays too — same underlying question about whether that comparison is meaningful. I should check the package.json for the actual effect version to settle whether Equal.equals does structural comparison on plain arrays here.
6. git_tools — "effect":
7. git_tools — Equal.equals
💭 Existing code already relies on structural equality comparisons for plain objects, which confirms this Effect version supports that behavior, so I don't need to worry about it further. Now I'm turning to the more interesting question of how
setEnvironmentThemesin useEnvironmentTheme.ts handles comparisons for theme definitions. I'm checking whetherrefreshThemehas stable identity across renders — if not, the effect might re-run frequently, but the lastPublished guard should prevent unnecessary work. More importantly, I'm noticing thatuseEnvironmentThemeDefinitionsusesuseSyncExternalStorewith a getServerSnapshot that returns a new empty array on every call, which is a known anti-pattern that can trigger infinite-loop warnings during SSR or hydration since React expects that snapshot to be cached. I need to figure out whether this app actually does server-side rendering or hydration at all — it looks like a Vite SPA using TanStack Router, so if it's purely client-rendered via createRoot, getServerSnapshot would never actually be invoked, making this a non-issue. I want to check other useSyncExternalStore usages in the codebase to see if there's a consistent pattern for handling this.
8. git_tools — useSyncExternalStore
💭 I'm noticing that the server snapshot function creates a new array on every call instead of using a cached empty constant like useCustomThemes does—this could trigger React's "getServerSnapshot should be cached" error if it's ever invoked during SSR or hydration. I should check main.tsx to see whether the app uses createRoot or hydrateRoot to determine if this actually matters.
9. git_tools — hydrateRoot|createRoot
💭 Since this is client-only rendering, getServerSnapshot never gets called, so that's not actually a real bug and I can skip it. Now I'm turning to the themePalette.ts changes to check the cache/reserved-id handling and whether theme resolution stays consistent.
10. browse_code — apps/web/src/themePalette.ts
💭 I want to check getThemeDefinition to see the resolution order and confirm whether other consumers like theme editor seeding, installCustomTheme collision handling, theme file serialization, and sidebar artwork properly account for environment themes. Let me look around line 1440-1520.
11. browse_code — apps/web/src/themePalette.ts
💭 I'm checking whether the theme editor's "Duplicate" feature resolves its seed theme through
getThemeDefinition, which would let environment themes work as seeds, and looking at ThemeEditorPanel/useThemeEditor to see if it's instead limited togetCustomThemes(). I also want to verify that new theme IDs are generated to avoid colliding with existing environment IDs.
12. git_tools — seedThemeId
💭 The line numbers from grep don't match what I saw earlier in the file view, so I'm double-checking whether I'm looking at the right commit version of ThemeSettings.tsx. I should re-view a wider line range around 765-860 to reconcile the discrepancy and confirm exactly where the DESKTOP_SYSTEM_THEME_ID card and custom themes collection actually sit. I'm second-guessing whether the grep tool is actually reading from the PR head or some other ref, since the results seem inconsistent with what I saw earlier. Let me just re-view that section of the file directly again to resolve the discrepancy rather than trust the grep output. That math checks out — the grep result matches main's version with the desktop system card but without the environment block. So it seems git_grep may be defaulting to the main branch rather than honoring the specific tree reference I passed, which would explain why the line numbers don't line up with what I'm seeing in the file view.
13. browse_code — apps/web/src/components/settings/ThemeSettings.tsx
💭 The default-branch URL is showing the same content as the PR head, which is strange. I'm trying to figure out where that grep result mentioning DESKTOP_SYSTEM_THEME_ID came from since the line numbers don't seem to match up, so I want to double-check by grepping again.
14. git_tools — DESKTOP_SYSTEM_THEME_ID
💭 That earlier grep result must have been stale, so I'll trust the current view instead. I've already confirmed how ThemeEditorPanel handles seedTheme's managed flag for environment themes, and duplicate save names like "X copy" correctly generate ids like "x-copy" without collision issues. Now I want to check
useTheme's persistence behavior: if a client selects an environment theme that later stops being published, the stored preference becomes dangling andgetThemeDefinitionreturns null, falling back to the default look — that seems documented and expected. Similarly,useEnvironmentThemeSynccallsrefreshTheme()when the theme set changes, which repaints to default if the selected environment theme vanishes. One thing worth checking is whether other surfaces that enumerate themes viagetCustomThemes()— like a command palette theme switcher or onboarding picker — might miss environment themes entirely, creating an inconsistency. I should grep for whereuseCustomThemesis used to see if this gap actually matters or is out of scope.
15. git_tools — useCustomThemes(|getCustomThemes(
💭 The collision checks look fine since saved themes take priority and the environment card gets filtered out in that case—same pattern holds for the name-duplication check in the editor panel. I'm now checking whether the environment card is missing an export option that other theme cards have. More interesting:
useDefaultThemeAdoptioncallingsetTheme(defaultTheme)clears the light/dark half-mix per the codebase's existing rule, so I'm tracing through what happens if the environment's default theme only supports one appearance mode—does the light/dark fallback inresolveThemeAppearancehandle that gracefully? review didn't flag this case, and it's arguably deliberate: an environment-set theme is meant to be a full reset, not a half-assignment. Still, I want to check whethersetThemeon a dark-only theme while the user is in light mode would force an unwanted flip — that's a real subtlety worth verifying against how setTheme handles halves.
16. browse_code — apps/web/src/hooks/useTheme.ts
💭 I'm noting that setTheme keeps the user's chosen appearance mode intact when swapping themes, so a fresh system inference doesn't accidentally flip a dark UI to light. Now I'm tracing how applyTheme calls resolveThemeAppearance with the theme, system dark state, follow-system flag, appearance mode, and halves to figure out how single-appearance themes get resolved.
17. git_tools — export function resolveThemeAppearance
💭 So with a single-appearance published theme in light mode, the resolved appearance becomes the theme's own appearance (dark), which flips the whole app and discards the user's light/dark mix — since setTheme removes THEME_HALVES entirely. This seems intentional though, since the docs describe the environment theme as something that switches clients wholesale, and it's applied once per generation. But I'm noticing the card's
onUsedeliberately avoids this behavior for single-mode themes, following the same rule as saved-theme cards, while adoption calls setTheme directly. So a dark-only environment default silently flips a light-mode client to dark with no explicit user action — arguably worse than the card case since there's no click involved. Still, this is a judgment call since the documented intent is "clients switch to it," and I need to weigh whether this is a genuine inconsistency or just expected behavior. I also notice the mode preference itself isn't updated, so the UI would still show "Light" selected even while the app renders dark — that mismatch does look like a real inconsistency worth flagging. Given the ambiguity, I want to keep looking for more concrete issues rather than commit to this one. Let me check the other in-scope changes: the server-side atom in state/server.ts, and how the theme sync hook is placed within __root.tsx relative to the providers.
18. browse_code — apps/web/src/routes/__root.tsx
💭 I'm checking whether
ContrastAppearanceSync's contrast adjustments properly re-apply when environment theme changes update the palette's custom properties, since both systems touch the same runtime-adjusted role variables on the document root.
19. git_tools — applyAppearanceContrast
💭 Contrast is fine since it uses separate variables unaffected by palette changes. I'm now checking that
refreshThemein useTheme correctly handles repainting when environment themes change, verifying thatnotifyCustomThemeListenersand useTheme's own subscription mechanism stay properly separated so other theme consumers still refresh as expected. There's also alastAppliedThemeshort-circuit in applyTheme I need to trace — refreshTheme probably resets that value, so I should check its implementation.
20. git_tools — const refreshTheme
💭 I'm checking that the theme adoption hook correctly persists the applied generation, confirming mobile is out of scope, and noticing that published theme cards look identical to user-created ones—there's no visual cue distinguishing origin even though docs say they appear "alongside your own," which seems like a design gap worth flagging. Digging deeper into
environmentThemeDefinition, I trace how variant appearances fall back to stock default colors plus overrides rather than inheriting the base theme's accent—so a seeded dark theme's light variant ends up with a generic accent instead of the brand color. It's tested and documented behavior, so probably not worth changing, but it's a slightly surprising edge case worth noting. I check the role overrides, card ordering, and environment theme filtering logic, and none of it reveals a concrete UI-consistency violation beyond what I've already flagged. I'm now weighing whether the single-appearance flip in the default theme adoption hook is worth raising as an issue. Tracing a concrete scenario, though, exposes a real regression: if a user explicitly picks "Light" mode, then an environment sets a dark-only default theme, the app switches to rendering dark while the Settings panel still shows "Light" as selected. That's a genuine visible mismatch between the UI state and the actual rendered theme. I'm weighing whether the adoption path should mirror the same fix applied to the card path — assigning the missing half for single-appearance themes — but recognizing a tradeoff: doing so would make an environment's dark-only theme silently fail to change anything for a light-mode user, which might undermine the intent of switching clients. Still, that silent no-op seems less harmful than flipping the visual mode against the user's explicit setting while the UI keeps claiming they're in Light. I think the concrete issue is real: the tile selection and preview would show "Light" while the app actually renders dark, and there's already an established pattern (assignHalf) in the codebase for single-mode themes that this new adoption path doesn't follow. I need to check whether this situation actually differs from what happens today, since there's no existing default to compare against. I confirmed this is genuinely new behavior introduced by this PR, since thedefaultThemesettings field itself is being added here. I'll flag it as a single inline suggestion on the relevant line, pointing out the mismatch with the card path and the resulting inconsistency between Settings and the actual app appearance, and I'll need to verify the fix is feasible by checking whether useTheme exposes the half-setting function needed. Sketching a fix: get the definition for the default theme, check its modes, and if only one mode exists call setThemeHalf, otherwise fall back to setTheme, applying the generation write afterward. Though I realize using setThemeHalf for a light-only theme while the user's mode is dark still produces no visible change, so it's a genuine trade-off -- I'll phrase the suggestion as either matching the card's rule or otherwise ensuring the app reflects it consistently. Actually, thinking further, the cleanest fix might be to also call setAppearanceMode(definition.appearance) when the theme only supports one mode, so Settings shows the correct mode label and the rendered theme matches -- preserving the intent that environment settings switch clients wholesale. I should double check that useTheme actually exposes both setThemeHalf and setAppearanceMode before recommending this.
21. browse_code — apps/web/src/hooks/useTheme.ts
💭 I'm realizing this single-appearance case is likely the common scenario, since published environment themes typically seed with just one appearance mode. That means a client explicitly set to Light could easily get silently flipped to dark when the environment theme changes, while Settings still shows Light — a plausible real-world regression worth flagging. But then I'm second-guessing whether it's actually wrong for a dark-only theme to render dark even under System mode — that could be the intended behavior if the admin's chosen theme is dark-only, similar to a user manually picking a dark-only custom theme. The ambiguity is whether the Settings UI should visually reflect that override. This distinct "environment asserts a theme" act doesn't have a directly parallel convention, so the risk of a false positive here feels moderate rather than certain. Let me re-scan the remaining in-scope files once more for anything overlooked: server state, theme palette, theme settings, environment theme hook and its test, default theme hook, and root. I'm checking whether setEnvironmentThemes properly notifies listeners without needing to clear cached state itself, since the caller handles the repaint — that seems fine. I'm also verifying that the custom themes snapshot subscription correctly wires up its storage listener alongside the sync external store usage. I'm tracing through how re-renders trigger: environment theme changes produce a new array reference so components update, while custom theme changes via storage events leave the environment snapshot reference unchanged, correctly avoiding unnecessary re-renders. I don't see a problem with calling setEnvironmentThemes inside an effect while other consumers exist, and multiple subscribers coexisting seems fine too. I'm also confirming theme definitions don't have id collisions with reserved names and that name length constraints are respected, then starting to consider edge cases where a published theme's colors object might only include a partial set of keys. If a theme only overrides one color role, the base default palette fills in the rest, producing a reasonable result under the contract, so that's acceptable. Given the previous findings were already addressed, I'm now weighing whether this remaining case is worth flagging or if things are clear, and want to look once more at whether the adoption path has a cleaner alternative. The clearer issue is that adopting a default theme via
useDefaultThemeAdoptioncallssetTheme, which silently wipes THEME_HALVES — permanently destroying the user's configured light/dark mix through an automatic environment-driven action rather than an explicit user choice, unlike the card's "Use" button which erases halves as a deliberate action. The strongest, least debatable point is that adopting a single-appearance theme as the base forces that appearance on the app regardless of the user's Light/Dark/System setting, while the Settings mode tiles still show the old selection with previews rendered in the theme's one available appearance. I'm deciding the smallest fix is to mirror the card's rule usingassignHalf/setThemeHalf, or to also update the appearance mode so Settings stays consistent. Since the fix spans multiple hunks — new imports plus a change to the destructured effect body — I can't express it as a clean unified diff, so I'll post it as a single inline reasoning comment on line 67 of useDefaultTheme.ts instead. Given this is a real consistency/regression risk, I'll mark the overall check status as failure and finalize the review with that one finding.
22. modify_pr — One finding on the new environment-theme adoption path; the two issues flagged on the previous run (published-card onUse half assignment and the over-broad managed: true) are resolved in this revision.
Agent Credits: 235 credits
Token usage: 42 uncached input · 1,912,058 cache-read input · 130,909 cache-write input · 18,456 output
Agent Credits may also include non-token charges from external tools such as web research.