Skip to content

fix(providers): align OMP cleanup and fallback state

MacroscopeApp / Macroscope - UI Consistency failed Aug 25, 2026 in 7m 35s

UI Consistency: 1 issue found

1 finding

  • apps/web/src/components/ChatView.logic.ts (resolveInteractionModeProviderSelection, L43-L48): candidates are filtered on the streamed snapshot's enabled, while ChatComposer resolves the instance that owns the Plan toggle from settings-overlaid entries (applyProviderInstanceSettings). When settings disable or delete the instance the composer draft points at, the composer renders the toggle for a plan-capable instance while this helper still matches the stale-enabled instance and normalizes interactionMode back to "default", producing a toggle that snaps back to Build until probe reconciliation. Suggested fix: resolve from the same settings-overlaid entries and reuse resolveSelectableProviderInstanceEntry for the fallback tier.

No other in-scope issues found: the /plan and /default slash items, compact menu, footer toggle, Shift+Tab shortcut, and submission normalization are all gated consistently on composerProviderControls.showInteractionModeToggle; OmpIcon follows the existing OpenCodeIcon light/dark shape-swap convention; provider icon, label, driver-metadata, and draft-store provider key lists are all updated for the new driver.

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 examined (in scope): ChatView.tsx, ChatView.logic.ts, ChatView.logic.test.ts, chat/ChatComposer.tsx, chat/providerIconUtils.ts, Icons.tsx, settings/providerDriverMeta.ts, settings/DiagnosticsSettings.tsx, settings/ProviderSettingsForm.test.ts, composerDraftStore.ts, hooks/useHandleNewThread.ts, lib/contextWindow.ts(.test), providerModels.ts, session-logic.ts.

Context used: apps/web/src/providerInstances.ts (deriveProviderInstanceEntries, applyProviderInstanceSettings, sortProviderInstanceEntries, resolveSelectableProviderInstanceEntry, isProviderInstancePickerReady), packages/client-runtime/src/providerCapabilities.ts.

Checks performed

  • Verified all four Plan-mode entry points in ChatComposer (footer toggle prop L3545, compact menu prop L3528, Shift+Tab L2090, slash-command select L1836 plus item construction L1150) share the provider-capability-aware gate; the previously flagged raw-planModeUiEnabled paths are closed.
  • Compared resolveInteractionModeProviderSelection's candidate chain and fallback tiering against ChatComposer.selectedInstanceId: candidate order and the ready-then-non-error fallback now match; the remaining divergence is the enabled source (reported above). The lockedContinuationGroupKey difference cannot change the outcome because showInteractionModeToggle is driver-level and lockedProvider already pins the driver kind.
  • Confirmed providerModels.getProviderInteractionModeToggle has no remaining consumers after removal.
  • Confirmed useHandleNewThread resolves provider snapshots from an explicitly scoped sourceEnvironmentId (route target or draft) rather than the globally active environment, and that an unresolved environment yields [], which preserves the existing interaction mode.
  • Confirmed the new OmpIcon uses the same dark:hidden / hidden dark:block duplicated-shape pattern as the adjacent OpenCodeIcon, so no new theming-ownership divergence; no CSS files changed in this PR.

🔍 Agent activity — 20 tool calls

💭 I'm reviewing the changed files scoped to the web app, including chat components, icon utilities, and settings diagnostics files. The prior comment about ChatView.logic.ts's fallback provider selection appears to have been addressed—the code now prefers a "ready" provider before falling back to any non-error provider, rather than blindly taking the first item. Since that issue seems resolved, I shouldn't re-flag it, and I'm moving on to look for new concerns in the UI.

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

2. browse_code — apps/web/src/components/ChatView.logic.ts

💭 The OmpIcon matches the existing OpenCodeIcon pattern, so that's fine. The Shift+Tab gating issue flagged previously appears resolved now that the toggle check properly guards on both planModeUiEnabled and showInteractionModeToggle. I'm now moving on to check the ChatComposer changes further for remaining issues. The provider toggle lookup switched from a driver-based approach to an instance-based one using selectedInstanceId, so I want to verify that value is properly defined and included in dependencies. I also need to confirm no other consumers still reference the now-removed getProviderInteractionModeToggle function from providerModels.ts, and the DiagnosticsSettings regex change looks fine as-is.

3. git_tools — getProviderInteractionModeToggle

4. browse_code — apps/web/src/components/chat/ChatComposer.tsx

💭 I'm confused about whether that function was actually removed—the diff suggested it was gone, but grep still found it in the file, possibly at a different revision. Let me re-check the file directly to confirm its current state.

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

💭 The grep result was confusing since it pointed to a line that seemingly still has the function, but that might just be from a different revision or cached state. Checking the current view confirms the function is actually gone at head, so it seems fine. Now I want to verify how the interaction mode toggle for slash commands is used in ChatComposer, and whether that usage properly reflects the memoized provider controls with correct dependencies — that looks consistent. But I'm wondering if the footer's Plan toggle might still be gated only by planModeUiEnabled rather than the newer showInteractionModeToggle flag, which would create a UI inconsistency where something renders but is effectively dead. I should search for where planModeUiEnabled is used in ChatComposer to check this.

6. git_tools — planModeUiEnabled|showInteractionModeToggle|interactionMode

💭 The grep got cut off, so I need to look at other usages - things like the interaction mode toggle handler and the ComposerFooterModeControls component with its prop. Let me check specific line ranges around 1820-1900 and 2070-2110, plus wherever that controls component gets rendered.

7. browse_code — In apps/web/src/components/chat/ChatComposer.tsx at commit 50c5148, show all places where the interaction-mode (Plan/Build) toggle visibility is decided and where toggleInteractionMode / handleInteractionModeChange is invoked: (1) the props passed to <ComposerFooterModeControls ... showInteractionModeToggle={...}>, (2) any compact/overflow menu item for plan mode, (3) the Shift+Tab / keyboard handler, (4) the slash-command selection handler. Quote the exact lines with line numbers and state for each whether the gate uses planModeUiEnabled alone or composerProviderControls.showInteractionModeToggle.

💭 Now checking ChatView.tsx to see if it resolves the capability instance the same way ChatComposer does. It now uses the same candidate order — composerActiveProvider, session provider instance, thread model selection, then project default — matching ChatComposer, and the fallback tier prefers "ready" status instances first before falling back to others. I should verify against resolveSelectableProviderInstanceEntry whether the fallback semantics truly line up, and also check whether ChatComposer's candidate filtering considering lockedContinuationGroupKey (versus ChatView only considering lockedProvider) is a real divergence worth flagging, since prior comments already raised similar variants of this. Let me dig into resolveSelectableProviderInstanceEntry and isProviderInstancePickerReady next.

8. browse_code — Show the implementation of resolveSelectableProviderInstanceEntry and isProviderInstancePickerReady in apps/web (file path and full source), and explain the ordering/priority it uses (ready vs non-error vs first entry). Also show how providerInstanceEntries in ChatComposer.tsx is built (which snapshots it includes, whether enabled/isAvailable/continuationGroupKey are derived, and whether synthesized default instances are included and where they sort).

💭 I'm noticing a divergence: ChatView's helper checks enabled status from the raw provider snapshot, while ChatComposer overrides enabled via settings, so a provider disabled or deleted in settings could still be treated as enabled by ChatView's helper if the wire snapshot says otherwise. This looks like a genuine inconsistency introduced by the new helper, so I want to trace where ChatView passes providerStatuses to confirm it. I also notice ordering differs: ChatComposer sorts entries with default-first-per-kind logic and prioritizes requestedDriverKind/lockedContinuationGroupKey when falling back, but ChatView's helper doesn't account for either, so the two components could pick different provider instances when the ideal candidate is missing. That mismatch is the kind of "toggle visible but dead" or "toggle hidden but plan persists" bug this PR has already been flagged for multiple times, so I want to check whether this remaining divergence is significant enough to call out. Given the discipline of not overloading a focused PR with new demands, I'm weighing whether the continuation-group-lock scenario is realistically reachable and materially broken enough to raise again, versus letting the narrower remaining gaps (locking, settings-derived enablement, sort order) stand. The concrete failure I traced would produce a dead toggle: clicking Plan updates the composer draft, but ChatView's normalization would snap it back to Build visually since interactionMode gets recomputed against a different provider. I want to check whether lockedContinuationGroupKey can actually be set while composerActiveProvider sits outside that group, and how ChatView derives lockedProvider in relation to that group, before deciding this is worth flagging.

9. browse_code — apps/web/src/components/chat/ChatComposer.tsx

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

💭 I'm noticing another divergence in the enabled field: ChatView uses the raw snapshot value while ChatComposer applies settings overrides, so a custom instance deleted from settings but still present in the wire snapshot would be treated as disabled by ChatComposer but enabled by ChatView—meaning ChatView could end up selecting an instance that ChatComposer would consider invalid. There's also a mismatch in lockedContinuationGroupKey handling and in fallback ordering: ChatView keeps raw wire order while ChatComposer sorts with a default-first, requestedDriverKind-first preference. I'm weighing whether these are worth flagging given the check rules favor concrete violations over nitpicks—candidate order and fallback tier issues from prior runs are already fixed, so I'm focusing on whether the settings-derived enabled discrepancy has real practical impact, like ChatView normalizing to "default" based on a disabled instance while ChatComposer shows a different, plan-capable one. The transient window where composerActiveProvider still points at a since-disabled instance seems weak since settings changes propagate to the server snapshot eventually, making both views converge. The third divergence only surfaces in the rare case where no candidate matches at all—a fresh state where every provider option is unavailable, forcing each view to fall back to its own ordering logic (wire order vs. requestedDriverKind-first). But testing a scenario where the wire order's first entry is an explicit but non-ready custom instance and the second is a ready default: ChatView would pick the first ready one in wire order (the default), while ChatComposer's requestedDriverKind logic would follow the sorted-first entry's kind regardless of readiness—so the two views could genuinely disagree here. Following through the resolution chain more carefully though, ChatComposer eventually falls back through selectable entries and lands on the same ready default, so the two paths actually converge. Given several prior rounds of comments on this same axis and the code now mirroring ChatComposer's candidate/fallback logic, I'm inclined to consider this settled, but I want to scan the remaining in-scope web changes once more for genuine issues before concluding. I still need to check useHandleNewThread.ts, DiagnosticsSettings.tsx, providerDriverMeta.ts, session-logic.ts, composerDraftStore.ts, contextWindow.ts, and providerIconUtils.ts. One thing worth verifying: session-logic.ts's PROVIDER_OPTIONS marks the new omp entry with a "new" sidebar badge, so I should check whether any other provider is also currently flagged "new," which would create a duplicate badge. I also want to look at providerDriverMeta.ts's new omp definition to confirm it's ordered consistently and includes any fields (docsUrl, defaultEnabled, description) that other providers have.

11. browse_code — apps/web/src/components/settings/providerDriverMeta.ts

12. browse_code — apps/web/src/session-logic.ts

💭 The missing badgeLabel for omp seems like a weak finding since opencode also lacks one, so I'll not flag it as a real inconsistency. I'm now moving on to check the change in useHandleNewThread.ts and its import of useEnvironments from the state module. Checking the environment routing rule, sourceProviders resolves explicitly from sourceEnvironmentId rather than the globally active environment, and falls back safely to an empty array if undefined, which satisfies the contract. Next I want to review ChatView.tsx's second change around line 5411, particularly the added providerSupportsInteractionMode condition near line 5578, to see what exactly that guard controls.

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

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

💭 I'm noting that the submission path correctly uses the composer's authoritative model selection via sendCtx, while the displayed interaction mode still relies on ChatView's separately re-derived selection—so the divergence is really about display normalization versus composer toggle visibility, not the actual submission logic. Since ChatView can't synchronously access the composer's resolved instance during render, this duplicated resolution seems necessary; the more durable fix would be having ChatComposer report its resolved instance and toggle capability upward, which matches earlier feedback. I want to check whether the remaining concrete divergence—around the enabled derivation and lockedContinuationGroupKey—is actually reachable in practice, since that's the case that could produce a dead toggle in a fully non-transient state. I'm now tracing through when lockedContinuationGroupKey is non-null, which requires a locked provider, an active thread, and a resolvable locked instance ID, and checking how ChatView's helper filters candidate providers against the driver for a locked thread. Continuation group keys are unique per driver instance, so I'm considering whether the composer's active provider could point to a different instance of the same driver kind than what ChatView resolves. If the picker restricts choices to the locked continuation group, this shouldn't happen normally, but a stale draft state could still cause a mismatch — though since both instances share the same driver kind, they'd have identical toggle capability anyway, so the divergence wouldn't actually manifest as a bug in that scenario. Now I want to trace what happens when no candidate has matched yet, particularly for a fresh user with one ready omp instance and default codex/claude entries. Following the wire ordering logic, both ChatComposer and ChatView appear to independently arrive at selecting the same ready omp instance, since sorting groups by driver kind based on first appearance in the list. But when omp_work is only "warning" status (pending initial probe) while codex default is ready, ChatView picks the ready codex entry and stays in plan mode, while ChatComposer's resolution logic falls back to the warning-status omp instance since no ready candidate exists — causing the toggle to hide even though ChatView's interaction mode still says "plan." Since the toggle is hidden, this discrepancy stays invisible to the user, and submission normalization still correctly resets to default, so the practical impact seems minor. Overall I think I've converged on the core issue: it's a fallback-tier ordering nuance between requestedDriverKind preference and settings-derived enabled state, and the worst case is just this hidden-toggle mismatch that submission already handles correctly — which reads more like an edge-case aesthetic quirk than a real failure. I recall that applyProviderInstanceSettings re-derives enabled from settings since the probe snapshot's value can lag behind, but that staleness applies to status, not enabled itself. Following the docstring further, a non-default instance with no matching settings entry resolves to false, meaning it's effectively treated as deleted. Tracing through a concrete case: a user deletes a custom instance that a composer draft's active provider pointed at. ChatComposer sees the entry's derived enabled as false, skips it, and falls back to codex, showing the Plan toggle. But the ChatView helper still reads the raw snapshot where that same instance shows enabled=true and available, so its candidate matching normalizes the mode back to "default." So clicking Plan sets interactionMode to plan, ChatView recomputes preferredInteractionMode as plan, but then normalizes it back to "default" -- making the toggle appear stuck on "Build" again. That's a real, user-visible bug caused by the helper relying on raw snapshot enabled instead of the settings-derived enabled that ChatComposer uses. I'm weighing whether to flag this given it's a transient window until the server pushes a fresh snapshot, and I don't want to be noisy after several rounds of review -- but this is a distinct, narrower issue from the fallback-tier problem already fixed at d45fe9e, so it deserves its own note rather than being dismissed. Before deciding, I want to double check other web changes for correctness, like confirming that ServerProvider actually exposes an availability field the way ChatView.logic.test.ts assumes, which traces back to isAvailable derived from the snapshot. The Diagnostics regex pattern looks fine as written. Now I'm checking whether "omp" was added consistently to all the relevant provider key arrays in composerDraftStore -- the diff shows three lists updated, but I want to grep for other similar as const arrays elsewhere in the web app that might have been missed, since a missed list could silently drop draft model options for omp in the UI.

15. git_tools — "cursor", "opencode"

16. git_tools — claudeAgent", "cursor|claudeAgent",$

💭 With the three lists updated, I'm confirming that formatProviderDisplayName in contextWindow.ts now includes "omp" and checking whether other display-name maps in the web package, like formatProviderDriverKindLabel, also need it. The contracts PROVIDER_DISPLAY_NAMES entry looks good, and I'm also checking whether providerIconUtils needs an OmpIcon added. Now I want to search for OpenCodeIcon usages in the web codebase to see if there's a settings "add provider" list or onboarding screen showing provider logos that I might have missed.

17. git_tools — OpenCodeIcon

18. git_tools — "opencode"

💭 I'm noticing the grep results seem inconsistent — one search shows the list without "opencode" and another shows it present at the same line, so I need to figure out which version reflects the actual current state of the file. My file view calls used the actual tree/sha and confirmed head content, so that's authoritative regardless of what git_grep hit — main apparently has a newer "pi" provider added on top of an older merge base, but that's not something to flag since it's outside this PR's diff scope. Overall the in-scope web changes look consistent, and the only remaining item is the narrow ChatView/ChatComposer selection divergence I've already noted a few times, most of which is now fixed. I'm weighing whether the enabled-derivation divergence still warrants a flag. The prior four review passes each caught a genuine divergence that got fixed, and this one looks analogous: ChatView.logic's helper filters using the raw snapshot's enabled flag, while the composer instead runs applyProviderInstanceSettings, which exists precisely because the snapshot's enabled/status can lag behind actual settings. I recall there's a shared helper, resolveSelectableProviderInstance in providerInstances.ts, that already does this exact resolution from a raw ServerProvider array and returns just the id — so if that's accurate, the new resolveInteractionModeProviderSelection in ChatView.logic.ts is duplicating logic that already exists elsewhere, which favors consolidating on the smaller, already-correct shared contract. That said, resolveSelectableProviderInstance only takes a single instanceId, so it may not directly support the full candidate chain with lockedProvider — meaning the fix would likely be partial reuse, using resolveSelectableProviderInstanceEntry for the fallback tier and deriveProviderInstanceEntries plus applyProviderInstanceSettings for candidate filtering. I want to double check the actual signature before writing this up so the comment is accurate.

19. browse_code — apps/web/src/providerInstances.ts

💭 The docstring for applyProviderInstanceSettings confirms this: probe state can lag behind settings writes, so visibility must follow settings rather than probe reconciliation, which is exactly what the new helper violates by filtering on streamed enabled instead. This creates a concrete bug -- after disabling or deleting the instance the composer draft points at, the composer switches to a plan-capable instance and shows the Plan toggle, while ChatView still lags behind. I'll flag this as a single inline comment on ChatView.logic.ts around lines 43-48, proposing the fix: have the helper accept a settings input and filter through applyProviderInstanceSettings so it matches ChatComposer's behavior. Since this touches both the helper's signature and its call site in ChatView.tsx, it's a cross-file change, so I'll describe the fix without providing a diff. Given the real risk of a dead toggle control, I'll mark this as a failure — a moderate but genuine consistency/regression risk warrants it per the guidance.

20. modify_pr — One finding: the new interaction-mode provider resolution in ChatView.logic.ts filters on the streamed snapshot's enabled, while ChatComposer — which owns the Plan toggle's visibility — resolves its instance from settings-overlaid entries. That can leave the toggle rendered but inert. Details inline.


Agent Credits: 414 credits