Skip to content

fix(web): hide unsupported settlement settings in search

MacroscopeApp / Macroscope - UI Consistency failed Aug 29, 2026 in 9m 46s

UI Consistency: 1 issue found

1 finding

  • apps/web/src/hooks/useThreadActions.ts — removing the canSettle pre-flight in settleThread leaves the Settle affordances enabled for ineligible threads (menu item gates on capability only; the sidebar hover button gates on settlementSupported only, unlike Snooze which uses canSnooze). Clicking Settle on a running/starting session or a thread with pending approval/user-input now surfaces the raw server invariant text in the failure toast instead of ThreadSettleBlockedError's wording, and that error class is now unreferenced while its snooze sibling remains in use.

An inline comment with a suggested single-hunk fix has been posted.

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 (files matching apps/web/src/**): components/ChatView.tsx, components/Sidebar.tsx, components/Sidebar.logic.ts, components/chat/ChatHeader.tsx, components/ThreadStatusIndicators.test.ts, components/settings/SettingsPanels.tsx, components/settings/SettingsSidebarNav.tsx, components/settings/settingsSearch.ts + test, hooks/useNowMinute.ts, hooks/useSettings.ts + test, hooks/useThreadActionMenu.ts, hooks/useThreadActions.ts.

Checked and clean

  • Prior run's finding (settings search catalog listing auto-settle rows that GeneralSettingsPanel no longer renders) is resolved: SettingsSearchItem.requiresThreadAutoSettlement plus the capabilities argument threaded from SettingsSidebarNav via primaryServerConfigAtom. Panel gate and search gate now read the same capability from the same environment config, and searchSettings is not called anywhere else.
  • Settled classification is consistent across surfaces after the migration: sidebar partition, SidebarThreadRow woke pill, ChatView banner, and useThreadActionMenu all resolve settledOverride === "settled" behind the threadSettlement capability.
  • mergeEnvironmentSettings precedence flip ({...clientSettings, ...serverSettings}) matches splitPatch, which already routes any key present in ServerSettings.fields to the server, so reads and writes agree on ownership.
  • No dead imports or orphaned props left behind: effectiveSettled/changeRequestAutoSettles/canSettle/threadLastActivityAt have no remaining references, ChatHeader's changeRequest prop was removed at its only call site, and pr/changeRequestSnapshotByKey remain used for PR badges.
  • No shared-primitive, Tailwind ownership, theme-variant, or scroll/mask changes in this diff; no new environment-scoped renderer actions.

Noted, not reported: Sidebar.tsx keeps nowMinute in the partition memo dependency list although the body now uses only preciseNow/snoozeWakeTick. react-hooks/exhaustive-deps is off repo-wide and the effect is only a once-per-minute recompute, so this is a harmless leftover rather than a finding.


🔍 Agent activity — 28 tool calls

💭 I'm reviewing the diff scope, which covers several web UI files including chat view, sidebar, header, and settings panel components. The prior flagged issue about the settings search catalog mismatch now looks fixed, since settingsSearch.ts and SettingsSidebarNav both consistently reference the new capability. I'm now checking whether SettingsPanels.tsx's capability gating aligns with the environment-routing rules, particularly whether environment-scoped capability checks resolve their target environment from explicit scope rather than implicit context. I want to verify how GeneralSettingsPanel uses primaryServerConfigAtom versus how multi-environment handling works elsewhere, like the mobile app's per-environment switch. I also need to check whether removing the canSettle guard in useThreadActions.ts left ThreadSettleBlockedError as an unused import, and whether other variables like changeRequestSnapshotByKey are now unused in ChatView.

1. git_tools — ThreadSettleBlockedError

2. git_tools — changeRequestSnapshotByKey

💭 I notice ThreadSettleBlockedError is now defined but seemingly unused elsewhere in the codebase, so I'm checking whether it's still referenced anywhere for error mapping or user-facing messaging before treating it as dead code. I also want to check whether removing the client-side canSettle guard leaves any UI feedback gap now that the server handles rejection instead, and confirm canSettle was cleanly removed from both client-runtime and mobile usage. Let me look at useThreadActions.ts near the removal to verify there aren't leftover unused imports tied to the dead error class.

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

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

💭 The unused ThreadSettleBlockedError class isn't a real issue, but the bigger concern is that settleThread no longer pre-checks eligibility before calling the server. If a user tries to settle a running thread now, they might get a raw protocol failure instead of the previously friendly error message. I want to trace whether the UI even surfaces that failure — settleThread doesn't seem to trigger a toast, so it depends on the caller. I need to check useThreadActionMenu to see if there's already a gate preventing Settle from being clickable on a running thread in the first place.

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

💭 I'm wondering if this is a regression where the friendly "still needs attention" message got replaced by a raw server error, and whether the settle menu item should already be disabled while the thread is running—need to check buildThreadActionMenuItems for that guard.

6. browse_code — In apps/web, show buildThreadActionMenuItems (thread action menu items builder): how does it decide whether to include/disable the "settle" item, and does it use isRunning or a canSettle-style check? Also show how the sidebar row decides whether to show Settle for a thread with pending approvals or a running session.

💭 I'm considering whether removing the client-side guard is actually a regression: now a settle attempt on a running session surfaces the raw server error ("thread has an active session") instead of a friendlier message, which could be seen as a genuine UX degradation rather than an acceptable tradeoff. I'm shifting focus to more concrete issues: whether the settings panel writes settlement config to the correct environment when multiple environments exist, and whether mergeEnvironmentSettings now applies server-over-client precedence to all settings keys rather than just the settlement-related ones, which would be an unintended scope expansion of that change.

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

💭 I'm confirming the merge order flip aligns with how splitPatch routes overlapping keys to the server, so that's not actually a problem. Now I'm checking whether the settings panel's gating atom matches what usePrimarySettings and updateSettings actually target.

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

💭 That's consistent. Now I want to check ChatView for leftover unused variables around the changed lines, and confirm the ChatHeader prop removal was applied cleanly at all call sites, so I'm going to look at ChatView around lines 4370-4480.

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

💭 I'm checking whether nowMinute is still referenced elsewhere in ChatView before removing it.

10. git_tools — nowMinute

💭 I'm checking whether nowMinute is actually used inside the partition memo body in Sidebar.tsx, since the diff removed the now variable and effectiveSettled call — if it's not used, it'd be a stale dependency causing unnecessary recomputation every minute. I want to look at lines 2060–2140 to verify this and also check if effectiveSnoozed references now internally.

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

💭 I notice nowMinute is now unused in this memo's body since it relies on preciseNow and snoozeWakeTick instead. It would trigger an exhaustive-deps lint warning and cause a harmless but unnecessary recompute each minute — checking the codebase, it's only referenced at those two lines, so it seems safe to remove. Now I want to check the other usage of nowMinute in ChatView around line 4816 to see what it's actually used for there.

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

💭 I'm double-checking that nowMinute and related variables are still legitimately used elsewhere, then verifying whether the ThreadStatusIndicators test file still needs its ProviderInstanceId and ThreadId imports after the edits.

13. git_tools — ThreadId|ProviderInstanceId|OrchestrationThreadShell|effectiveSettled

14. git_tools — effectiveSettled|changeRequestAutoSettles|canSettle|threadLastActivityAt

💭 No leftovers there. Now I want to check SidebarThreadRow in Sidebar.tsx since the autoSettleOnMerge prop was removed from both the component and its call site—I need to look at what a nearby condition checking settledOverride !== "settled" is actually computing, so I'll review the surrounding code around those two areas.

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

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

💭 I want to check whether the pr variable is still used elsewhere in that row after removing the auto-settle check, so I'm looking for its usage around the PR badge and the resolveDisplayedThreadPr assignment.

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

💭 I'm noticing a potential inconsistency: web only lets users configure auto-settle for the primary environment since useUpdatePrimarySettings writes there exclusively, while mobile renders a separate switch per connection, meaning multi-environment web users can't adjust settlement settings for non-primary environments the way mobile users can. Now I'm checking a truncated comment in the Sidebar around line 3074 that starts "Environments without" to see what it's actually trying to say before cleaning it up.

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

💭 That comment wrapping is cosmetic, not worth flagging. More importantly, I need to verify the searchSettings signature change is consistent—SettingsSidebarNav has been updated to pass capabilities, but I should check if there are other callers like the command palette that also need updating.

19. git_tools — searchSettings|SETTINGS_SEARCH_ITEMS

💭 I notice the line numbers don't quite match between what I found at HEAD and what the diff shows, so I want to view the actual file to confirm whether the search result was pulled from the right revision.

20. browse_code — apps/web/src/components/settings/SettingsSidebarNav.tsx

💭 Good, the view confirms the update went through despite the odd stale grep results. I don't see any other command-palette style search of settings elsewhere, so now I want to double-check the filter implementation in settingsSearch.ts for correctness.

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

💭 I'm checking other potential issues: the auto-settle rows in SettingsPanels could flicker briefly while the primary server config is loading, since it starts null before resolving capabilities—a minor edge case. I'm also starting to look at useThreadActionMenu's isSettled logic for similar consistency concerns. I confirm sidebar and ChatView both compute settled/snoozed status the same way, so that's consistent. But I notice a real regression: the client-side canSettle guard was removed from useThreadActions.settleThread, so now attempting to settle a thread with pending approvals or running sessions shows a raw server-side error toast instead of the friendly ThreadSettleBlockedError message that used to appear—and that error class is now dead code. This looks like a genuine UI regression from the migration, since the rubric calls for preserving interaction semantics. Comparing to snooze, which still retains its precondition guard and friendly blocked-error message via canSnooze, makes the inconsistency clear—settle lost its protection while snooze kept it. I want to check what the raw server error actually displays when someone tries to settle a running session, so I'm looking at the decider logic behind that toast message.

22. browse_code — apps/server/src/orchestration/decider.ts

💭 This is a user-visible regression: the toast now surfaces a raw internal error with thread ID instead of the friendlier "resolve or interrupt first" message. I want to flag it with a concrete fix—restore the client-side precondition check like the snooze pattern uses, or disable/hide the Settle button when it isn't settleable—but I need to check whether canSettle still exists in client-runtime since it may have been removed, complicating a simple restore. Maybe this is intentional — server now owns settle eligibility, and the sidebar's Settle button could still appear on running cards, so users might trigger it during a running thread. The resulting error message would likely be developer-facing text rather than something friendly, since it'd surface as a raw invariant error. I'll flag this as an inline comment on the removed lines in useThreadActions.ts, since I can't comment directly on deleted lines and need to anchor it to the nearby diff hunk. Since the hunk appears to be a pure deletion, I need to check whether GitHub lets me comment on context or left-side lines, so I should pull the actual patch for that file to see which lines are commentable.

23. github_api_read_only — repos/{owner}/{repo}/pulls/{pull_number}/files?per_page=100&page=2

💭 Since page 2 is empty and page 1 is too large, I'll comment on the left side of the deletion instead, using the base file's line numbers for the deleted lines in useThreadActions.ts (509-521) — I need to confirm the exact base content first.

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

💭 I'll post a comment on lines 509–521 of useThreadActions.ts. Next, I'm checking Sidebar.tsx for the nowMinute dependency — verifying it's actually unused now since preciseNow is what's used in the memo, confirming this is a valid finding. Beyond just being dead, this unused dep forces a full re-sort of threads every minute unnecessarily, and it's also a lint violation for exhaustive-deps. I should check whether the repo's eslint config treats that rule strictly enough that it would fail CI, since if so this is worth flagging more prominently rather than as a minor nit.

25. browse_code — Does the repo lint apps/web with eslint react-hooks/exhaustive-deps (or oxlint/biome equivalent), and is it configured as an error that fails CI? Show the lint config files and the relevant rule settings.

💭 The nowMinute dependency isn't a real lint failure, just a minor perf nit that's harmless given React Compiler is in use—I'll skip flagging it since the rubric wants concrete violations only, not noise about trivial unused deps. Checking ChatHeader, the changeRequest prop removal from useThreadActionMenu looks clean—both the declaration and usage were removed together, so it compiles fine. Sidebar still references changeRequestSnapshotByKey correctly too. Now I'm digging into a more substantive concern: with servers that predate threadAutoSettlement capability, the client used to compute auto-settle locally using client settings, but it seems the client no longer does that computation at all now that settlement is server-driven. So against an older server, auto-settle would silently stop functioning and its settings simply disappear from the UI—that's expected version-skew degradation rather than a genuine consistency bug, since the architecture now has the server own this behavior. I'm also checking whether SettingsPanels and the sidebar/ChatView gate consistently on the right capability flags, and they do—both correctly reference threadAutoSettlement and threadSettlement respectively. One more angle worth checking: on web, the settled shelf shows settled threads across every environment based on server settledOverride, but the settings UI only exposes auto-settle configuration for the primary environment, whereas mobile added per-connection switches for other environments—this asymmetry could be a real environment-routing inconsistency worth flagging against the rule that capability gating, action dispatch, and labels should all read from the same environment context. But then I reconsider: the panel reads and dispatches against the primary environment consistently, and the entire General panel on web is primary-only by design, so this is a product limitation rather than a bug. I want to verify primaryServerConfigAtom is properly exported from state/server since both files import it, so I'm searching for its definition.

26. git_tools — primaryServerConfigAtom

💭 I'm leaning toward this being a genuine finding rather than an "all clear," since removing the settle guard creates a real consistency risk. Let me double-check the settle path in the sidebar — confirming that Settle appears on running threads and that a failed attempt surfaces the raw server error as a toast.

27. git_tools — class OrchestrationCommandInvariantError

💭 This confirms a real regression: users now see an internal-looking invariant error message instead of the friendly one that used to appear, caused by the changed code in useThreadActions.ts. The smallest fix is to restore the pre-flight guard using the still-exported client helpers with inline checks. But using hasQueuedTurnStart requires adding a new import, which turns this into a two-hunk change rather than a clean single-hunk diff. I'll opt for the simpler version instead — limiting the guard to session status and pending flags without the queued-turn check — since that avoids the import and stays as a single, minimal hunk. I'm double-checking the type of activeThreadSettled in ChatView, confirming it resolves to a plain boolean since supportsSettlement is already a strict boolean check. I also verify AUTO_SETTLE_DEFAULT_DAYS and sidebarAutoSettleOnMerge are still referenced correctly elsewhere, then move to checking whether the reset buttons in SettingsPanels compare properly against the default settings object. I've decided there's one real finding here: removing the pre-flight canSettle check means the sidebar's Settle button and thread action menu item are still shown for running sessions or threads with pending approvals, so clicking them now exposes the raw server error message instead of the friendlier ThreadSettleBlockedError wording — and that error class is now unused while the sibling snooze path still has its own guard. I'm drafting an inline comment explaining this regression before flagging the review as a failure. I'm sketching a small diff that reinstates the client-side check, re-throwing ThreadSettleBlockedError when the thread has pending approvals, pending user input, or an active session, so I can attach it as a single-hunk suggestion with no new imports needed. Now I'm posting this as a github_pr_review comment with the review marked as requesting changes.

28. modify_pr — One finding: removing the client-side settle pre-flight leaves the Settle affordances enabled for ineligible threads, so users now see a raw server invariant string instead of the product-worded blocked message.


Agent Credits: 460 credits

Token usage: 104 uncached input · 3,935,511 cache-read input · 264,825 cache-write input · 30,079 output
Agent Credits may also include non-token charges from external tools such as web research.