Repository navigation
UI Consistency: 1 issue found
apps/web/src/components/ChatView.logic.ts — the new resolveInteractionModeProviderSelection helper re-implements provider-instance selection instead of reusing the shared deriveProviderInstanceEntries / resolveSelectableProviderInstanceEntry contract. Its candidate chain now matches ChatComposer, but its fallback (selectableProviders[0]) ignores probe status, while the composer's selectedInstanceId fallback prefers a ready, then non-error, instance. An enabled-but-not-installed OMP instance (status: "error", showInteractionModeToggle: false) sorts before synthesized defaults, so on a thread with no matching candidate the composer can render the Plan toggle from a ready instance while ChatView normalizes Plan back to Default — a visible-but-dead toggle. The composer's locked-continuation-group constraint is also absent from the new helper.
Inline comment posted on the helper with the smallest suggested fix (resolve through the shared helpers / mirror the ready → non-error tiers).
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/ChatView.logic.ts (+test), components/Icons.tsx, components/chat/ChatComposer.tsx, components/chat/providerIconUtils.ts, components/settings/DiagnosticsSettings.tsx, components/settings/providerDriverMeta.ts (+ProviderSettingsForm.test.ts), composerDraftStore.ts, hooks/useHandleNewThread.ts, lib/contextWindow.ts (+test), providerModels.ts, session-logic.ts.
Checks that passed:
OmpIconfollows the existingOpenCodeIconconvention for theme-specific SVG shapes (dark:hidden/hidden dark:block), spreads consumerclassNameonto the<svg>, and introduces no global CSS; no new.darkselectors or theme-token changes.- Provider registration is consistent across
PROVIDER_ICON_BY_PROVIDER,PROVIDER_CLIENT_DEFINITIONS,PROVIDER_OPTIONS(pickerSidebarBadge: "new"matches the other recently added providers),composerDraftStoreprovider key lists, andformatProviderDisplayName. getProviderInteractionModeTogglewas removed fromproviderModels.tswith no remaining consumers; all interaction-mode surfaces inChatComposer(footer toggle,CompactComposerControlsMenuradio group,/planand/defaultslash items and their handler, Shift+Tab) are now gated by the same derivedcomposerProviderControls.showInteractionModeToggle, so no surface is reachable while the others are hidden. Prior findings about the ungated Shift+Tab shortcut and the thread-first capability chain are resolved.useHandleNewThreadresolves capability providers from an explicit environment scope (route target / carry-source draft) and degrades to preserving the current mode when that scope is unknown, so it does not fall back to the globally active environment.- No shared UI primitive was bypassed, and no primitive geometry/focus/hit-target overrides were introduced by the changed lines.
🔍 Agent activity — 16 tool calls
💭 I'm going through the list of changed web files in scope, including the chat view logic, tests, and related components like the composer and icons. For consistency, I want to check whether the OmpIcon's dark/light theming approach in Icons.tsx (using duplicated rects with dark:hidden classes) matches conventions used by other icons in the file, like currentColor fills, or whether it's introducing a new inconsistent pattern. Also, the interaction mode selection issue and the ChatComposer Shift+Tab gating flagged in prior runs both look resolved in the current code — the candidate ordering logic and the plan-mode toggle guard are both in place now. Time to actually look at the icon file itself.
1. git_tools — dark:hidden|hidden dark:block|currentColor
2. browse_code — apps/web/src/components/Icons.tsx
💭 The OmpIcon follows the same dark/light duplicated-shape convention as OpenCodeIcon, and spreading props onto the svg keeps className overrides working consistently. I do notice the viewBox is non-square (120x90) while other provider icons tend to differ in aspect ratio too, so that's probably not an issue worth flagging. Now I'm turning to the behavioral items in scope, starting with checking how ChatView.tsx handles interactionModeSelection and whether resolveInteractionModeProviderSelection properly addresses the prior comment about selectedInstance. I want to look at ChatComposer's showInteractionModeToggle logic, verifying selectedProviderShowsInteractionModeToggle handles cases where selectedInstanceId is undefined correctly — it seems the function defaults to true if no matching provider status is found, which looks safe.
3. browse_code — apps/web/src/components/chat/ChatComposer.tsx
4. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm comparing how ChatView and ChatComposer resolve provider selection—both filter by enabled status, availability, and locked provider, but ChatComposer adds continuation group filtering and a different fallback chain. There's a slight divergence in their fallback logic that's already been addressed in the reviewed fix, though a minor residual gap remains in that fallback path. Wondering if the availability check itself differs semantically—ChatView filters on
provider.availability !== "unavailable"while ChatComposer uses anisAvailableflag that might account for install/auth status too, which would make ChatView's filter looser and potentially select providers ChatComposer would exclude. I want to trace howproviderInstanceEntriesderivesisAvailableto confirm.
5. git_tools — isAvailable
💭 The availability check lines up with ChatView's filter, but I want to confirm whether deriveProviderInstanceEntries applies any extra filtering or just maps snapshots in order—I should check lines 150-210 of providerInstances.ts to verify ordering and filtering behavior, and also look at how ChatComposer handles continuationGroupKey filtering. ChatComposer's fallback also uses requestedDriverKind. At this point the divergence seems limited to that fallback tail, since the primary candidate chain now appears to match; given three prior review rounds on this issue, I want to be sure there's a genuinely concrete remaining difference before flagging anything, so I should look at deriveProviderInstanceEntries's ordering behavior next.
6. browse_code — apps/web/src/providerInstances.ts
7. browse_code — apps/web/src/providerInstances.ts
💭 I'm noticing the new local helper
resolveInteractionModeProviderSelectionduplicates the shared provider resolution logic but with a subtly different fallback behavior—picking the first enabled+available provider even if it's in error state, rather than preferring a ready or non-error entry like the shared helper does. This divergence seems worth flagging as an inconsistency worth reconciling. I need to check whether ChatView passesinteractionModedown to ChatComposer as a display prop — if so, the toggle would appear dead even though submission actually uses the composer's own selection state, which is the earlier flagged symptom resurfacing. There's also a secondary divergence: ChatComposer filters by continuationGroupKey when locked, while ChatView doesn't, so under lock they could pick different providers with the same driver but different continuation groups — a minor edge case worth noting. The main question now is whether the reimplemented helper's divergence fromresolveSelectableProviderInstanceEntryin the fallback tier is worth flagging as a fourth round of similar feedback, or if it's too narrow since it only bites when no candidates match at all — a case where the composer's own toggle visibility also depends on that same fallback resolution, so the desync is real but rare. A single inline comment flagging the fallback tier divergence and suggesting reuse of the shared resolver helper seems like the right actionable note. Let me also check useHandleNewThread.ts — it resolves sourceEnvironmentId explicitly before looking up providers, and if that's undefined it just falls back to an empty list rather than silently pulling from the wrong environment, so that path looks safe. I want to double check the edge case where an undefined sourceEnvironmentId might accidentally match an environment record. I'm also verifying the interaction mode gating in ChatView's submission logic looks fine, confirming no leftover references to the removed provider toggle helper, checking the diagnostics regex change, and reviewing the new provider option's sidebar badge for consistency with how other providers use badges.
8. git_tools — getProviderInteractionModeToggle|pickerSidebarBadge
💭 Good, that removal is fully clean with no leftover consumers. Now I'm checking that OmpIcon consumers render properly through the provider icon utility, noting the different viewBox sizes across icons like OpenCode's 32x40 versus the more square ones. OmpIcon just spreads its props including className directly onto the svg, similar to OpenCodeIcon, though other icons like GithubCopilotIcon use a cn helper for fill colors — that's consistent enough. Accessibility-wise none of these set aria-hidden, but that's handled by consumers. I want to check whether the customModels field being hidden in provider settings follows the same pattern as other providers, though that's in the contracts package and out of scope here. Now let me verify how ChatComposer gates the plan-related footer toggle — I should grep for showInteractionModeToggle and interactionMode usages there.
9. git_tools — showInteractionModeToggle|planModeUiEnabled|toggleInteractionMode
💭 Grep is stopping early because of binary detection, so I'll look directly at lines 1820-1900 and 2070-2110, and check where ComposerFooterModeControls receives the showInteractionModeToggle prop.
10. browse_code — apps/web/src/components/chat/ChatComposer.tsx
11. browse_code — apps/web/src/components/chat/ChatComposer.tsx
💭 Now I want to check the compact or mobile menu to see if the interaction mode toggle is also gated there, so I'll search for where the mode controls are referenced in the mobile "more" menu of ChatComposer.
12. browse_code — In apps/web/src/components/chat/ChatComposer.tsx at commit d45fe9e, list every place the plan/default interaction-mode UI is rendered or triggered (footer ComposerFooterModeControls props, any mobile/compact menu items, slash commands, keyboard shortcuts) and state for each whether it is gated by composerProviderControls.showInteractionModeToggle or only by planModeUiEnabled / settings.planModeEnabled. Quote the relevant lines with line numbers.
💭 Composer's fallback logic now matches ChatView's on all points except one subtle tail: ChatView falls back to the first selectable provider based on enabled/availability/lockedProvider checks, while Composer's fallback filters compatible entries slightly differently, and I want to pin down exactly how these two selection rules diverge in practice. The key risk case: when the requested driver kind (derived from the thread's provider) doesn't match the first-in-order selectable driver, the two components could pick different provider instances. This only matters visibly if those instances differ in
showInteractionModeToggle, which today only differs for omp. So I'm tracing through: if a thread's provider is omp but that specific omp instance becomes disabled or unavailable, Composer's candidate matching fails and it falls back to resolving a selectable driver through a different path than ChatView's ordering-based fallback -- this is where the two could genuinely disagree. Another divergence: if the first selectable-by-order instance has status "error" while a later one is "ready", ChatView takes the errored one but Composer prefers the ready one. This only causes a visible desync if one of them happens to be omp, since that's the only driver with different toggle capability -- for example, if a user explicitly configures an omp instance that lands first in the provider order, then opens a fresh thread with no existing candidates. ChatView would lock onto the errored omp instance and hide the Plan toggle, while ChatComposer picks a working codex instance and shows it -- creating a state where toggling Plan mode in the composer wouldn't match what ChatView renders. The fix is to have ChatView reuse the same shared instance resolution logic as the composer so both fall back consistently, or have the composer's resolved instance passed upward. I should also note that ChatView is missing the continuation-group constraint that the composer applies. I'm now weighing whether this warrants a failing check state, since there's a genuine finding here rather than a clean pass. Thinking through the actual reachability of this bug: an OMP instance that's enabled but not installed would have status "error" and undefined availability, meaning isAvailable is true even though showInteractionModeToggle is false — this is a very plausible real-world state for users who enable OMP before installing the binary. If that instance happens to be the explicitly configured one, this confirms the regression is genuinely reachable and worth flagging rather than dismissing. For the fallback candidates (composer draft, thread session, thread selection, project default) to all resolve to null, a fresh thread in a project without a default model would do it — so the sequence is: user enables OMP without installing it, opens a new thread with no default configured, ChatView picks the OMP entry for capability while ChatComposer picks a ready provider, and the Plan toggle flickers on then reverts to Build. This is a reasonable, reachable regression worth commenting on. Let me double check howcomposerActiveProvideris derived in ChatView, since it might never actually be null due to defaults.
13. git_tools — composerActiveProvider
💭 I'm noticing a contradiction - grepping at the same commit shows old code while my earlier file view showed the new resolveInteractionModeProviderSelection code at nearly the same lines. This makes me suspect the grep tool might be defaulting to a different revision than I expect. That earlier flagged comment on commit f3a3536 referenced the exact old ternary pattern matching what I'm now seeing, suggesting the grep result is actually pulling from that prior commit rather than HEAD. The diff clearly shows HEAD (d45fe9e) contains the new
resolveInteractionModeProviderSelectionlogic, matching what my tree-based view showed, so that should be the actual current state. It seems git_grep may have defaulted to a stale or cached commit rather than honoring the tree ref, so I should rerun it with the rev explicitly set to d45fe9e to confirm.
14. git_tools — interactionModeSelection
💭 The new helper is confirmed in HEAD, so now I need to decide whether to flag the fallback tier divergence between ChatView and ChatComposer as an issue, or mark it clear. ChatView falls back to the first enabled/available provider, while ChatComposer's logic depends on resolveSelectableProvider, so I want to check lines 820-845 to see what arguments that function actually receives. In a realistic scenario like OMP being enabled but not installed (status "error"), ChatComposer's fallback filters to entries that are ready, so it would prefer codex over an error-status provider like omp, while ChatView might land on omp if it's first in order. That means the divergence hinges on ordering — I want to check whether the server's provider ordering puts explicitly configured providerInstances first, which would explain why the two components could disagree. This creates a real regression risk: the Plan toggle might show as visible per ChatComposer's logic while ChatView's normalization keeps resetting the actual state to Build, since the two components are computing fallback providers differently. I'm deciding where to place my review comment — likely on ChatView.logic.ts near the fallback selection line, since that's where the tier logic that causes this mismatch is defined.
15. browse_code — apps/web/src/components/ChatView.logic.ts
💭 I'm noting that the fallback logic on line 54 of ChatView.logic.ts diverges from the shared provider-instance tiering used elsewhere, which could leave the toggle visible but pointing to a dead instance when the first enabled one is errored. I want to flag this as an inline comment and think through a fix. A full reuse of the shared helper would require changing the type signature to include full provider data, which cascades into test updates, so I'll suggest a simpler minimal fix: prefer a "ready" status provider, then a non-error one, mirroring the existing pattern, while noting this needs a type change too. Given this is a real but narrow-scope consistency risk, I'll mark the check as a failure rather than neutral and write up the comment concisely before posting it via github_pr_review on line 54.
16. modify_pr — One finding: the new interaction-mode instance resolution in ChatView.logic.ts re-implements provider-instance selection and diverges from the shared helper the composer uses for the same decision, so the Plan toggle can render while ChatView normalizes Plan back to Default. Everything else in the web scope (OMP icon following the existing OpenCodeIcon dark/light shape convention, providerIconUtils / providerDriverMeta / session-logic registrations, and the capability gating of the footer toggle, compact menu, /plan-/default items and Shift+Tab in ChatComposer) is consistent.
Agent Credits: 359 credits