Skip to content

refactor(server): keep the settlement service in one module

MacroscopeApp / Macroscope - UI Consistency failed Aug 29, 2026 in 5m 19s

UI Consistency: 1 issue found

  • apps/web/src/components/settings/SettingsPanels.tsx (L1959-2030): the auto-settle rows are now gated on the primary environment's threadAutoSettlement capability, but SETTINGS_SEARCH_ITEMS still exposes auto-settle-merged-threads and auto-settle-inactive-threads unconditionally, so on a server without the capability a settings search result navigates to an anchor that is never rendered — the same mismatch the catalog's desktopOnly flag guards against. Suggested fix: mark those catalog entries and filter them at the searchSettings(query) call in SettingsSidebarNav.

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/**/*.{ts,tsx,css}): components/ChatView.tsx, components/Sidebar.tsx, components/Sidebar.logic.ts, components/ThreadStatusIndicators.test.ts, components/chat/ChatHeader.tsx, components/settings/SettingsPanels.tsx, hooks/useNowMinute.ts, hooks/useSettings.ts, hooks/useSettings.test.ts, hooks/useThreadActionMenu.ts, hooks/useThreadActions.ts.

Checked and found consistent:

  • Settled classification now reads settledOverride === "settled" in all three web surfaces (sidebar partition, ChatView parked banner and Woke pill, thread action menu), so the surfaces cannot disagree.
  • No shared-primitive, Tailwind ownership, theme-variant, or scroll/mask changes in this diff; no raw controls reconstructing Button/Input/Select/Switch.
  • Settings capability gate and mutation target the same environment: supportsAutoSettlement reads primaryServerConfigAtom, and useUpdatePrimarySettings routes the server keys to the primary environment via splitPatch, so gating, dispatch, and labels all refer to one server.
  • mergeEnvironmentSettings precedence flip ({...clientSettings, ...serverSettings}) is safe: ClientSettingsSchema and ServerSettings share no field names after the retired keys moved, so only stale untyped client data changes behavior, matching the added test.
  • ChatHeader's removed changeRequest prop has no remaining call sites; useThreadActionMenu callers updated.

Minor, not reported as findings: Sidebar.tsx keeps nowMinute in the partition useMemo dependency list although the body no longer reads a minute clock, and ThreadSettleBlockedError in hooks/useThreadActions.ts is now unused; neither changes rendered behavior.


🔍 Agent activity — 24 tool calls

💭 I'm scanning the changed web files - ChatView, Sidebar components, header, tests, and settings - to see what falls within scope for this review. I notice a potential issue in SettingsPanels.tsx: GeneralSettingsPanel gates auto-settle settings on the primary server config's capabilities, but settings might be per-environment, and there's a rule requiring capability gating to match the environment the action actually executes against. I need to trace which environment updateSettings targets. I'm also checking whether Sidebar.tsx's removed changeRequestSnapshotByKey dependency is truly unused now, and whether ChatView's memo dependencies for activeThreadWoke are still consistent with what's actually referenced.

1. git_tools — changeRequestSnapshotByKey

2. git_tools — primaryServerConfigAtom

💭 I'm looking at the GeneralSettingsPanel in SettingsPanels.tsx to see how updateSettings targets an environment, since the web app supports multiple environments while mobile uses per-environment switches — I want to check whether the web panel's useSettings/updateSettings only applies to the primary or active environment.

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

💭 The capability gate and update target both point to the primary environment, which is consistent. Now I'm wondering whether updateSettings correctly splits patches between server and client, since the sidebarAutoSettle settings moved from ClientSettings to ServerSettings — I need to check how useSettings.ts handles that routing.

4. browse_code — apps/web/src/hooks/useSettings.ts

💭 Settings routing looks consistent with the primary env, so that's fine. Now I'm shifting to check Sidebar.tsx and ChatView.tsx for unused variables, stale memo dependencies, or rendering issues, starting with whether nowMinute is actually used in the partition memo.

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

💭 I notice the memo still lists nowMinute in its dependency array even though it's no longer used inside—it now relies on preciseNow instead. That's a harmless leftover, though exhaustive-deps lint rules can sometimes flag unnecessary dependencies like this. This is minor and somewhat out of scope for the review focus. Let me verify that changeRequestSnapshotByKey is still properly used around line 4396 in ChatView, and check the surrounding code near lines 4370-4480 for any other unused imports or variables.

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

💭 I should check whether that nowMinute variable at line 4413 is still referenced elsewhere in ChatView before removing it.

7. git_tools — nowMinute

💭 The nowMinute dependency in that memo is technically unused now but still triggers a recompute each minute, which loosely keeps the snooze classification refreshed—harmless leftover, not worth flagging. Now I'm turning to useThreadActionMenu, where the changeRequest prop was removed from its input. I need to check whether callers still pass it, since that could trigger a type error—let me grep for usages of useThreadActionMenu.

8. git_tools — useThreadActionMenu|changeRequest=|changeRequest:

💭 I should also check how the sidebar's thread row handles its menu, since it might use a different mechanism—I want to look at where the autoSettleOnMerge prop was removed from SidebarThreadRow around lines 714 and 839, checking the surrounding context near 830-850.

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

💭 I'm noting that the PR chip still relies on that variable, and the settings gating question is trickier since the auto-settle visibility is tied to the primary environment's capabilities, but the web app can connect to multiple environments simultaneously. That said, the General panel is designed to be primary-only across other server settings too, so this behavior is actually consistent with the existing pattern rather than a new inconsistency. Now I want to check useThreadActions.ts for whether removing the client-side settle guard leaves any unused imports like ThreadSettleBlockedError or Cause, since the server now owns that validation.

10. git_tools — ThreadSettleBlockedError

💭 I should check if ThreadSettleBlockedError is still used elsewhere, like in toast error message mappings, so I'll search more broadly across the web codebase.

11. git_tools — SettleBlocked|settleThread|attemptSettle

💭 The ThreadSettleBlockedError class seems unused now, though it's still exported so no lint issue—just minor dead code. Since the server now rejects the settle attempt rather than the client blocking it, I want to check whether the sidebar's toast still surfaces the failure properly, then move on to look at settleThread in useThreadActions.ts.

12. browse_code — apps/web/src/hooks/useThreadActions.ts

💭 The cause value should still be used elsewhere for other blocked errors, so that's fine. Checking the ChatView settlement logic, I confirm the boolean typing works out correctly whether or not settlement is supported. For the sidebar, the banner now relies purely on the server's settledOverride without masking for pending approvals or live sessions like before — but the server likely auto-unsettles on real activity, so this is probably intentional. I also notice the nowMinute dependency is now unused in the sidebar's memo, which is minor, and I need to check whether useThreadActionMenu still needs its useClientSettings import now that the autoSettle-related lines were removed.

13. browse_code — apps/web/src/hooks/useThreadActionMenu.ts

💭 I'm checking whether the settings search registry could reference these conditionally-rendered rows and cause navigation issues, but that seems fine. I'm also noting the swapped merge order in mergeEnvironmentSettings, where client settings now get spread before server settings, potentially changing precedence. Since decode yields defaults for all schema keys, any key names shared between ClientSettingsSchema and ServerSettings would now have server winning instead of client -- I need to check the settings contracts file in useSettings.ts for actual overlapping field names to confirm this is a real behavioral change.

14. git_tools — ^export const (ClientSettingsSchema|ServerSettings) = Schema.Struct

15. browse_code — packages/contracts/src/settings.ts

💭 Checking whether any of these client keys overlap with fields in ServerSettings, since none of the remaining ones besides the removed sidebarAutoSettle* seem to match.

16. browse_code — packages/contracts/src/settings.ts

💭 Since there's no key overlap between client and server schemas, the different merge orderings end up equivalent, so that's fine. Now I'm checking for real UI regressions in the web scope, particularly around the ChatView parked banner and Sidebar rows where settled status is derived from server override in mixed environments with auto-settle stamping. I notice Sidebar's isWoke check ignores settlementSupported while the partition gates on it, but that's minor since settledOverride would never be set on a server lacking the capability anyway. ChatView's activeThreadSettled is also no longer gated on blockers, but this stays consistent with how Sidebar uses settledOverride alone. I'm also verifying that SettingsPanels' capability check and usePrimarySettings both correctly derive from primaryServerConfigAtom. One remaining concern: supportsAutoSettlement hides the whole auto-settle configuration when the primary environment is an old server, but the settings search index might still register those ids, and I need to think through what happens when a user has multiple environments where the primary lacks the capability but a secondary one has it. Now I want to grep the web codebase for any other places that reference sidebarAutoSettleAfterDays to make sure nothing still reads it from client settings, since the removed key would trigger a TypeScript error and fail the build -- but I should confirm that directly.

17. git_tools — sidebarAutoSettle

💭 I'm checking whether the settings summary panel and reset-all list around those line ranges still behave consistently once the capability gating for auto-settlement is applied, since they might reference those settings independently.

18. browse_code — apps/web/src/components/settings/SettingsPanels.tsx

💭 These references stay valid since the keys still live on UnifiedSettings. I'm noticing a possible UI issue though: hiding settings rows when a capability is absent could break the settings search feature, since the search index likely registers setting ids in a static list independent of visibility. I want to check how the search registry works to see if hidden rows would produce dead search results.

19. git_tools — auto-settle-merged-threads

20. browse_code — apps/web/src/components/settings/settingsSearch.ts

💭 The search catalog only has a desktopOnly flag, no capability-based filter, so search results for auto-settle settings would still link to a settings section that doesn't render when the server lacks that feature — this is a minor inconsistency similar to what the flag was meant to catch. It's plausible enough given the capability was just added and older servers won't have it, though it's a modest issue at best. I want to check how search results filter using isElectron to figure out the smallest possible fix.

21. browse_code — apps/web/src/components/settings/settingsSearch.ts

💭 I'm weighing whether this is significant enough to flag: the only filtering mechanism is desktopOnly, and adding capability awareness would require passing a filter through searchSettings's items argument, which is a multi-part fix rather than a simple one. Given the review guidance to report only concrete violations from changed lines, I think this qualifies as a real but low-severity issue worth noting rather than failing the check. I'm also scanning for other candidate findings: confirming ChatView's removed changeRequest prop doesn't break ChatHeader, checking that nowMinute and activeThreadSettled are still used correctly and type-safe, and reviewing the Sidebar's settled/un-settle behavior for consistency—so far everything else looks fine. Now I'm weighing whether the nowMinute dependency in Sidebar's memo is actually needed anymore, since snooze recomputation is now driven by snoozeWakeTick and thread changes rather than a minute-by-minute clock. Keeping it is harmless functionally but wasteful, and it could trigger an exhaustive-deps lint warning about an unnecessary dependency—worth flagging as a minor leftover from the removed logic, though it's a marginal concern rather than a clear UI consistency bug. I also want to double check that ChatView's activeThreadWokeVisible logic, now based on settledOverride === "settled", stays consistent with the sidebar row's isWoke check using settledOverride !== "settled"—these appear aligned. Next I should search the web app for any remaining references to "Auto-settle" text, like tooltips or headers mentioning auto-settle timing, to make sure nothing stale is left behind.

22. git_tools — [Aa]uto-settle

💭 I'm also noting that the restore defaults flow sends patch keys a legacy server might reject, and the confirmation list can show labels for settings the user can't even see—minor and pre-existing, not worth flagging. Then I'm thinking about how changedSettingLabels compares against DEFAULT_UNIFIED_SETTINGS, so if the server has a non-default auto-settle value configured, that label would show up in the list too. Checking the settled banner logic across ChatView, sidebar, and menu, they're all consistent in gating on supportsSettlement plus the settled override. The PR snapshot machinery feeding chips looks fine too. The useSettings merge order change is more interesting—hydrateClientSettings spreads persisted settings over defaults without decoding first, so retired keys could leak through, and reversing the merge order fixes that. But flipping it globally means client settings can never shadow server settings for any shared key, which seems fine since there's no overlap—though I want to check if there's a subtle edge case there. I'm also weighing whether the search gap in settings is enough to flag as a real problem. I think leaving an inline comment on the settings panel noting that search entries can point to rows that don't render, and suggesting a capability filter, is a reasonable fix to propose. Now I want to check where searchSettings gets called to see how feasible adding that filter would actually be.

23. git_tools — searchSettings|SETTINGS_SEARCH_ITEMS

💭 The fix would need to live in SettingsSidebarNav, filtering items by capability, which is a cross-file change so there's no single diff to offer — still, it's a legitimate if modest finding. I'm trying to parse the instructions on how the check should behave: it seems to default to a failure state when findings exist, and only respond "All clear" when there are none, though I need to reconcile that with the required JSON output format. I'll keep the JSON structure but set the summary field to "All clear" when the code is genuinely clean. Now I want to verify I'm not missing anything more substantive in the diff — checking that the removed changeRequest prop is gone from both sides of ChatHeader, confirming useClientSettings is still used after the import cleanup in Sidebar.tsx, and scanning for any other leftover unused variables. Everything checks out in Sidebar (removed props are consistently removed from both usage and interface), and the ThreadStatusIndicators test file is correctly updated to drop the stale assertion. Looking at useThreadActions.ts, I confirm the server-side decider still rejects settling running sessions, so the UI just shows a generic error toast instead of a friendly message — acceptable, and the menu already disables settle for running threads. That leaves me deciding whether the settings-search finding counts as introduced by this PR's changed lines, since the rows became conditional here while the catalog entry itself wasn't touched. I'll flag it as a concrete consistency violation: the catalog encodes a desktopOnly condition to avoid dangling anchors, but the corresponding row rendering is now unconditional. I'll leave a single inline comment on the relevant SettingsPanels.tsx line, and I'll skip flagging the leftover nowMinute dependency in Sidebar since useNowMinute() still forces a per-minute re-render regardless of memoization, so timestamps continue updating fine. I'm confirming the actual added line in the diff for the inline comment target, and since the underlying issue spans two files, I don't have a direct diff suggestion to offer—just a note pointing toward filtering the search catalog or passing an items filter into SettingsSidebarNav. I'm now weighing whether this issue is severe enough to warrant failing the check at all. Given it's a minor UX regression on legacy servers rather than a hard consistency break, I'm leaning toward reporting it as a single finding but not setting an overall failure state, even though the check defaults to failure when findings exist—the explicit guidance says a "clear" regression risk should fail it, and this doesn't quite meet that bar, so neutral feels more appropriate. Rereading the instructions more carefully: this check defaults to failure when findings exist, and my dangling-search finding is a genuine consistency issue rather than a mere aesthetic preference, so it should count as a real finding. I'll go with failure rather than neutral, since the customer explicitly set the default that way. This actually fits an existing invariant the codebase itself documents—the settings search catalog comment explicitly warns about anchors that don't exist. That gives me enough confidence to report it as a real finding rather than a false positive, so I'll write up an inline comment explaining that the search items list still references the auto-settle rows unconditionally even though those rows are now capability-gated behind threadAutoSettlement. For posting the review, I'm working through the line number details—since this is a single-line comment on line 1959, I need to figure out whether GitHub's API wants start_line to differ from line, since setting them equal might get rejected. I'll try covering both lines 1959 and 1960 as a multi-line range instead, since checking the diff confirms both are newly added lines within the same hunk, which should satisfy the API's requirements.

24. modify_pr — One UI consistency finding on the newly capability-gated auto-settle rows. Everything else in the web scope (settled state now read from settledOverride in Sidebar, ChatView, and the thread action menu, and the mergeEnvironmentSettings precedence flip) is internally consistent.


Agent Credits: 251 credits

Token usage: 48 uncached input · 2,999,494 cache-read input · 80,406 cache-write input · 15,318 output
Agent Credits may also include non-token charges from external tools such as web research.