Skip to content

fix(server): follow Effect error convention

MacroscopeApp / Macroscope - UI Consistency failed Aug 25, 2026 in 8m 9s

UI Consistency: 2 issues found

Two interaction-consistency issues were found in the new provider interaction-mode gating.

  • apps/web/src/components/chat/ChatComposer.tsx β€” the /plan and /default slash-command path is now gated by composerProviderControls.showInteractionModeToggle, but the Shift+Tab shortcut in onComposerCommandKey still uses only planModeUiEnabled. For a provider that hides the toggle (OMP), Shift+Tab still writes interactionMode: "plan" into the composer draft, which is invisible in the UI and re-activates Plan when the user switches to a plan-capable instance.
  • apps/web/src/components/ChatView.tsx β€” interactionModeSelection prefers the thread's stale modelSelection over the composer draft's activeProvider, the opposite of selectedProviderByThreadId and the candidateInstanceIds order that ChatComposer uses for selectedInstanceId. After switching a not-yet-started thread from an OMP instance to a plan-capable instance in the composer, the Plan toggle is shown but inert until the next send.

The remaining OMP web wiring (new OmpIcon, provider icon/meta/option maps, formatProviderDisplayName, draft-store provider keys, diagnostics label) follows existing patterns and raised no findings.

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: apps/web/src/**/*.ts, apps/web/src/**/*.tsx (no in-scope .css changes).

Methodology:

  • Compared the new OmpIcon against the existing theme-swapped icon pattern (OpenCodeIcon, GithubCopilotIcon): the dark:hidden / hidden dark:block duplication and non-square viewBox rendered at size-5 by ProviderInstanceIcon match existing precedent, so no finding.
  • Traced every consumer of the removed getProviderInteractionModeToggle helper; only ChatComposer used it and it now calls selectedProviderShowsInteractionModeToggle. No dangling references, and providerModels.ts imports remain used.
  • Verified the toggle render paths (ComposerFooterModeControls, CompactComposerControlsMenu), the slash-command menu list and its selection handler are all gated by the new capability flag, then found the Shift+Tab keyboard path (onComposerCommandKey, ~L2085-2093) still gated only by planModeUiEnabled.
  • Checked handleInteractionModeChange / toggleInteractionMode and composerDraftStore.setModelSelection to confirm the draft activeProvider is written on model selection while activeThread.modelSelection is not updated until send, which makes the precedence mismatch at ChatView.tsx L2311-2313 reachable.
  • Confirmed interactionMode has no use-before-declaration after being moved to L2314, and that the /plan plain-text fallback (standaloneSlashCommand) and submission normalization use the composer's own selection.
  • Environment routing: useHandleNewThread resolves provider snapshots from the explicit source environmentId rather than the active environment; ChatView reads serverConfig?.providers for its own thread scope. No findings.

πŸ” Agent activity β€” 22 tool calls

πŸ’­ I'm identifying which web files were changed within scope, including chat components, icons, and diagnostics settings, to figure out what needs review. The key UI-consistency question is the new OmpIcon in Icons.tsx, which duplicates SVG rects with dark:hidden and hidden dark:block classes to toggle fill by theme. I want to compare this against how other provider icons handle dark mode, and check whether the repo's rules around @variant dark versus raw .dark usage apply here, or whether a simpler fill="currentColor" approach would be more consistent.

1. git_tools β€” dark:hidden|hidden dark:block|currentColor β€” rejected: repo.url: required parameter is missing or empty;repo: required parameter is missing or empty

2. browse_code β€” apps/web/src/components/Icons.tsx

πŸ’­ The OmpIcon follows the existing dark/light toggle pattern consistent with the codebase, and its inner rects sit fine over the orange background in both themes. I'm double-checking whether the viewBox dimensions properly contain all the shape extents. I confirm the shapes fit within the viewBox, noting the icon has a non-square aspect ratio like OpenCodeIcon, which is an acceptable precedent. Now I'm reviewing the ChatComposer change where the provider interaction mode toggle lookup switched from being keyed by driver kind to being keyed by instance ID, and I need to verify that selectedInstanceId is properly scoped and typed correctly, since this changes the semantics from driver-based to instance-based lookup. I'm now checking whether ChatView's providerStatuses is correctly scoped to the thread's environment rather than just the active environment, given the environment routing patterns I saw elsewhere like the explicit sourceEnvironmentId lookup in useHandleNewThread.

3. git_tools β€” providerStatuses

πŸ’­ ChatView's serverConfig looks environment-scoped to the thread, so that seems fine. Now I want to check how ChatComposer handles selectedInstanceId, so I'm looking at the relevant lines there.

4. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ I'm checking ChatComposer next, which handles slash command gating for /plan and /default and the interaction mode toggle. Since I removed getProviderInteractionModeToggle from providerModels.ts, I need to make sure no other consumer still relies on it.

5. browse_code β€” In the t3code repo at commit e429f07, are there any remaining references to getProviderInteractionModeToggle anywhere (apps/web, apps/mobile, packages)? Also, in apps/web/src/components/chat/ChatComposer.tsx, where is the interaction-mode toggle button rendered and is its visibility gated by composerProviderControls.showInteractionModeToggle or still by planModeUiEnabled? Show the relevant code lines.

πŸ’­ The gating looks consistent since the footer already relies on composerProviderControls. Now I want to check the diff for a subtler issue around the slash-command list in ChatComposer. The gate for that list moved from planModeUiEnabled to composerProviderControls.showInteractionModeToggle, and the keyboard handler early-returns appropriately β€” that seems fine. Environment routing for new threads falls back to an empty array when the source environment isn't found, which defaults to preserving plan mode, which seems acceptable. The diagnostics settings regex addition for omp uses proper word boundaries so it won't accidentally substring-match against other provider names, and the OmpIcon follows the same duplication pattern as OpenCodeIcon for theme-dependent colors, so it stays consistent. The dark-mode styling rule about @variant dark/@variant light applies to CSS files rather than utility classes, so using dark:hidden here is fine. For the OmpIcon specifically, the inner eye/mouth shapes stay dark against the orange background in both themes, which works visually. I'm weighing whether the icon's repeated hidden/dark:block duplication is a real consistency violation, but since OpenCodeIcon uses the same approach for multi-fill icons while other monochrome icons use fill-black dark:fill-white, both patterns already coexist in the codebase β€” so this is more of a stylistic choice than an actual issue, though I want to double check whether display: block behaves as expected on SVG child elements like rects. Confirming that non-none display values render SVG elements normally, so block works fine here just as it does in OpenCodeIcon. The props spread pattern on the svg element also matches OpenCodeIcon's approach, so that's consistent. I'm also noting that since the OMP icon uses hardcoded fills rather than currentColor, it won't respond to text-color utility classes some callers might pass β€” but that's the same limitation OpenCodeIcon already has, not a new inconsistency. Now I'm considering the aspect ratio difference, since this icon's viewBox is wider than the roughly square dimensions of other provider icons. With preserveAspectRatio set to "meet" by default, the artwork will scale to fit both dimensions and end up rendering shorter than other provider icons at the same width, appearing as a wide short glyph rather than matching height. Since other icons already vary in proportions too, this discrepancy is probably not a strong enough finding to flag as a real violation, and I want to avoid raising false positives here. I should check how the provider icons are actually rendered β€” the sizing classes used and whether there's a shared wrapper component that assumes icons are square.

6. git_tools β€” PROVIDER_ICON_BY_PROVIDER

πŸ’­ I want to check how ProviderInstanceIcon handles sizing to understand its rendering.

7. browse_code β€” apps/web/src/components/chat/ProviderInstanceIcon.tsx

πŸ’­ I'm comparing how each provider icon's viewBox proportions fit within the size-5 square boxesβ€”the OMP glyph will appear smaller and off-center compared to icons like OpenAI, ClaudeAI, Cursor, and Grok, since their aspect ratios differ. Now I'm looking at a more substantive issue: the diff in apps/web/src/providerModels.ts removes the exported getProviderInteractionModeToggle function, and I want to check whether that removal leaves getProviderSnapshot and ProviderDriverKind as unused imports, which would trigger a lint or typecheck error.

8. browse_code β€” apps/web/src/providerModels.ts

πŸ’­ I'm checking whether moving the interactionMode definition further down in the component could cause a use-before-declaration error, since anything referencing it between the old and new location would break due to const temporal dead zone rules.

9. git_tools β€” \binteractionMode\b

πŸ’­ I'm checking that the interaction-mode toggle logic is properly guarded everywhereβ€”the keyboard slash command and ChatComposer visibility already handle providers lacking the toggle, and I'm now verifying that ChatView also gates another usage behind the provider support check.

10. browse_code β€” apps/web/src/components/ChatView.tsx

πŸ’­ Checking the plan follow-up prompt path β€” since an OMP thread can never be in plan mode, there's no proposed plan to trigger that follow-up prompt, so this should be a non-issue. I should also check whether the keyboard shortcut for toggling interaction mode in ChatComposer is gated behind showInteractionModeToggle, or if it could let an OMP user sneak into plan mode via keyboard even though the toggle UI is hidden. Even if it slips through, normalization would clamp it back to default on submission, and the displayed state stays on Build since ChatView computes from the normalized value β€” but I want to verify the shortcut is actually reachable and confirm this behavior directly in the code.

11. git_tools β€” toggleInteractionMode

πŸ’­ Since the grep got truncated, I should search differentlyβ€”looking for where toggleInteractionMode is bound to a keyboard shortcut like Shift+Tab in ChatComposer's keydown handler.

12. browse_code β€” In apps/web/src/components/chat/ChatComposer.tsx at commit e429f07, is toggleInteractionMode invoked from any keyboard shortcut handler (e.g. Shift+Tab) in addition to the footer toggle button? If so, is that keyboard path gated by composerProviderControls.showInteractionModeToggle? Quote the handler code.

πŸ’­ I'm noticing an inconsistency: the PR gated slash commands and the toggle button by provider capability, but the Shift+Tab keyboard shortcut is only gated by planModeUiEnabled, which contradicts the PR's stated goal of preserving consistent interaction semantics. I want to trace what Shift+Tab actually triggers in ChatView to confirm the effect of this gap.

13. browse_code β€” apps/web/src/components/ChatView.tsx

πŸ’­ So Shift+Tab on an OMP thread stores "plan" interaction mode into the composer draft, but the UI toggle stays hidden and ChatView normalizes the mode back to "default" β€” so the stored plan state is effectively invisible. That means pressing Shift+Tab looks like a silent no-op, even though it's quietly leaking hidden plan state into the draft that could carry over later. If the user then switches the thread's model to a plan-capable provider like Codex, that persisted "plan" draft state could unexpectedly reactivate β€” a subtle, minor-severity inconsistency worth flagging. The cleanest fix would be gating the Shift+Tab path with composerProviderControls.showInteractionModeToggle rather than planModeUiEnabled. I want to leave an inline comment pointing at the exact line, but I'm checking whether that region of ChatComposer actually falls within the diff's changed lines, since GitHub only allows inline comments on lines that are part of the diff. The added line at 1836 (the gating check for showInteractionModeToggle) is in the diff, so I can attach my comment there and reference the ungated Shift+Tab handler further down around line 2089. Let me confirm the exact content at line 1836 before finalizing the comment.

14. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ Let me double-check the exact code for the Shift+Tab handler to make sure it's correct.

15. browse_code β€” apps/web/src/components/chat/ChatComposer.tsx

πŸ’­ Good, that's confirmed as finding #1 since it's concrete and in-scope. The mobile equivalent is out of scope so I'll skip it. Now I want to check the ChatView's interactionModeSelection fallback logic to see whether composerActiveProvider is a ProviderInstanceId or a driver kind, since that affects whether this is another real finding. If composerActiveProvider is actually a driver kind rather than an instance id, then constructing a lookup object with it as the instanceId would only accidentally match the default instance when the instance id happens to equal the driver nameβ€”so I need to check composerActiveProvider's actual type to confirm.

16. git_tools β€” composerActiveProvider

πŸ’­ I'm noticing that composerActiveProvider is an instance id, but there's a precedence issue in ChatView: the code picks the thread's modelSelection first, falling back to composerActiveProvider only if that's null. This seems inconsistent, since elsewhere the composer draft's activeProvider takes precedence over the thread's modelSelection instead. This mismatch could cause the interactionMode toggle to appear stuck -- if the thread's modelSelection and the composer's active provider disagree on whether the model is codex or an OMP instance, clicking "Plan" might set the draft's plan state but then have interactionMode immediately recompute back to "default" against the wrong provider, making it look like nothing happened. I'm now checking whether this is actually reachable: switching provider in the composer for an existing thread that hasn't yet started a session (no locked provider) means the composer draft's activeProvider changes to codex, but the thread's stored modelSelection still reflects the original omp provider, so the interactionMode calculation and the toggle-visibility calculation would draw from different sources and disagree. The right fix seems to be having the interaction-mode selection prefer the composer's active provider first, falling back to the thread's modelSelection only when there's no active composer selection β€” mirroring how the composer already derives its selected instance ID with composer-first precedence. But I should second-guess this: it's a plausible bug but somewhat speculative, since I'm not certain composerActiveProvider behaves the way I'm assuming across local draft threads. Let me check whether setComposerDraftModelSelection also syncs activeProvider, since if selecting a provider always updates it in lockstep with the thread's modelSelection, this mismatch couldn't actually occur.

17. git_tools β€” setModelSelection:

18. browse_code β€” apps/web/src/composerDraftStore.ts

πŸ’­ So this confirms the mismatch is real: switching a composer's model creates a divergence where the thread's stored selection stays stale while the composer displays the newly picked instance, meaning the toggle can visually appear but not actually work since ChatView normalizes to the old selection instead of the new one. This is a moderate-severity regression risk worth flagging with an inline comment recommending composer-selection-first precedence. I should also verify whether a started session locks the provider (via lockedProvider) versus an unstarted thread, which seems to allow free switching since the block-reason check returns null when no session has started. I want to check whether picking a model for an unstarted thread optimistically updates the thread's stored modelSelection, which would make this mismatch only transient rather than persistent β€” need to look at the provider-model-select handler code around lines 6531-6600 to confirm.

19. browse_code β€” apps/web/src/components/ChatView.tsx

πŸ’­ I'm confirming that model selection only lives in the composer draft, not the server, so the thread's stored modelSelection stays stale until the next sendβ€”meaning the Codex/Plan toggle mismatch persists and the toggle stays inert since ChatView normalizes interactionMode against the outdated OMP selection. I'm now checking whether the reverse scenario, switching from Codex to an OMP model mid-thread, has a similar issue. For the fix, I'm suggesting an inline change to ChatView around lines 2311-2313, prioritizing the composer's active provider over the thread's stored modelSelection when resolving interactionModeSelection, so the toggle reflects the current composer state rather than the stale thread value. I also want to double-check the diff line numbers align with the hunk context so the review comment lands on the right lines, and I'm planning a second inline comment on ChatComposer around the Shift+Tab gating logic added there. I'm noticing session-logic.ts adds a PROVIDER_OPTIONS entry with a "new" badge, and want to check whether other providers already have that same badge to avoid duplication.

20. browse_code β€” apps/web/src/session-logic.ts

πŸ’­ The providerDriverMeta.ts entry for the omp driver looks consistent with icon and settingsSchema. I'm now checking whether other UI locations, like a provider card list in the settings dialog, also need to reference the new driver kind for consistency.

21. git_tools β€” "opencode"

πŸ’­ The model placeholder gaps for omp and grok look consistent with existing patterns, not an issue. I'm checking whether OmpIcon's spread of props onto the svg element could conflict with a caller-provided className, similar to how OpenCodeIcon handles its dark-mode child styling. I've now confirmed two solid findings worth reporting: the gating inconsistency where Shift+Tab bypasses the capability check that gates the button and slash commands, silently writing plan state the UI can't display, and a secondary precedence mismatch between the composer's capability gate and ChatView's thread-first normalization that could leave the Plan toggle visible but inert. I'll write up the first as the primary finding and the second as a lower-confidence suggestion. I'm drafting the review comment for the ChatComposer keyboard shortcut, explaining the invisible plan-state issue and proposing a fix that gates the Shift+Tab handler the same way the toggle button is gated, using the composer's provider controls instead of just the plan-mode-ui flag. For the second comment, I'm working out that interactionModeSelection should prefer the composer's active provider selection over the thread's stored model selection, since other resolutions in the file already do this β€” otherwise switching providers in the composer leaves the Plan toggle stuck referencing a stale provider, making clicks do nothing. I'm drafting the suggested code fix but double-checking the indentation before finalizing it.

22. modify_pr β€” Reviewed the in-scope web changes (apps/web/src/**). Two interaction-consistency issues in the new provider-capability gating; the rest of the OMP wiring (icon, provider option/meta lists, driver label maps, draft-store provider keys) matches existing patterns.


Agent Credits: 522 credits