Skip to content

fix(web): tighten the desktop-only browser notice copy

MacroscopeApp / Macroscope - UI Consistency failed Aug 15, 2026 in 5m 41s

UI Consistency: 1 issue found

apps/web/src/components/settings/IntegrationsSettings.tsx — the default-viewport row's control cluster cannot shrink: SelectTrigger keeps the variant's min-w-36 and sits next to a shrink-0 group of two w-24 number fields plus the rotate button (~240px), so the control has a hard minimum of roughly 390px. At phone widths (SettingsRow gets ~319px inside the page padding) and in narrow desktop windows the row overflows horizontally once a non-fill default is configured. The equivalent two-control row in SettingsPanels.tsx (font family + size) keeps its flexible control min-w-0 flex-1 beside a small fixed control, which is the pattern to follow. An inline suggestion adding flex-wrap to the wrapper and min-w-0 to the trigger was posted.

No other findings: the panel uses the shared Select, NumberField, Switch, Button (icon-sm / ghost-muted), Tooltip, and SettingsRow / SettingsSection / SettingResetButton primitives without overriding their core geometry or focus/disabled contracts; the desktop-only notice mirrors the existing Keybindings notice tokens (text-warning, InfoIcon); the ScreenRotationIcon extraction is behavior-neutral (size and icon color still come from the parent Button contract in both call sites); and the route, sidebar nav, and settings-search entries follow the established registration conventions.

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)

  • apps/web/src/components/settings/IntegrationsSettings.tsx (new)
  • apps/web/src/components/settings/SettingsSidebarNav.tsx, settingsSearch.ts
  • apps/web/src/routes/settings.integrations.tsx, routeTree.gen.ts
  • apps/web/src/browser/ScreenRotationIcon.tsx (new), BrowserDeviceToolbar.tsx, browserDefaults.ts (new), desktopTabLifetime.ts
  • apps/web/src/components/preview/PreviewView.tsx, PreviewAutomationHosts.tsx, openPreviewSession.ts, previewAutomationOpenReadiness.ts

Context used

  • apps/web/src/components/ui/{button,select,number-field}.tsx for primitive size/variant contracts (icon-sm, ghost-muted, selectTriggerVariants min-w-36, NumberField size="sm").
  • apps/web/src/components/settings/settingsLayout.tsx for SettingsRow grid (sm:grid-cols-[minmax(0,1fr)_minmax(10rem,auto)]) and control-slot geometry.
  • SettingsPanels.tsx, SourceControlWritingSettings.tsx, ConnectionsSettings.tsx for existing SelectTrigger className="w-full sm:w-40/44/56" and multi-control row precedents.
  • KeybindingsSettings.tsx for the existing non-Electron info-notice treatment.

Notes considered but not reported

  • Double dimming (opacity-64 wrapper over controls that already apply data-disabled:opacity-64) is an explicitly documented design decision in the new component; not reported as a finding.
  • commitDimension silently ignoring an over-area size relies on Base UI's number-field input resync on commit, so the displayed value is not left stale; no concrete regression demonstrable from the diff.
  • hideIndicator being passed on some SelectItems and not others is inert (the prop is unused by the primitive) and matches mixed existing usage.

🔍 Agent activity — 1 tool call

💭 I'm looking at the IntegrationsSettings.tsx file to verify the Tailwind classes being used. The opacity-64 utility should work fine in Tailwind v4 since it supports numeric opacity values from 0-100. I need to check how the existing settings panels are structured to ensure consistency. I'm noticing that SettingsRow expects a title prop, but in IntegrationsSettings.tsx the rows are spreading the result of searchableSetting("browser-default-viewport") without explicitly providing a title. I need to check what searchableSetting returns to see if it's already including the title. I see that searchableSetting gives me the id and title, so now I need to look at the full IntegrationsSettings file in the repo to understand what else might be hidden in the diff. I'm noticing that ScreenRotationIcon renders an SVG without explicit width, height, or class attributes, so it relies on parent styling to size it. In BrowserDeviceToolbar, it's wrapped in a Button that likely applies sizing through a CSS rule targeting unsized SVGs, while in IntegrationsSettings it's inside a Button with size="icon-sm". I need to check if the Button component has both the icon-sm and ghost-muted variants defined. I'm also verifying that text-warning and opacity-64 exist in the theme—the latter is already used elsewhere in the codebase like in NumberField's disabled state, so that's consistent. The NumberField's width handling looks fine too; when w-24 is applied alongside w-full, tailwind-merge should correctly resolve to w-24. I should check how other settings panels handle Select trigger sizing to maintain consistency across the UI. I'm noticing that hideIndicator gets prefixed with an underscore in SelectItem, which means it's being ignored, but that's fine since it's consistent with how other props are handled. Let me check if other settings panels use the same Select trigger patterns for things like size and width classes. Looking at how NumberField is used in SettingsPanels with the Decrement/Increment pattern, and verifying that text-warning exists as a token and opacity-64 is supported in Tailwind v4 through arbitrary numeric opacity. I'm also considering how the disabled state should look visually. The disabled Tooltip trigger button inside TooltipTrigger is fine from an accessibility standpoint. But there's a potential issue with BrowserViewportSetting's NumberField — it's controlled with value={presentedSize.width} and onValueCommitted only, which means without onValueChange, the input won't update as the user types since Base UI NumberField controls the input value from the prop directly, even though it has internal inputValue state for the raw string while typing. Looking at Base UI's implementation, there's a useEffect that syncs inputValue from the value prop when it changes, so typing works visually since the value prop doesn't change during input. After commit, updateSettings changes the value prop which then syncs. There's an edge case where if a commit is rejected as invalid, the input keeps the invalid value while the prop stays unchanged, creating a stale display — though Base UI's clamping to min/max on commit mostly mitigates this. Now I need to verify the rotate flow preserves the presetId correctly and check that the settings hooks in browserDefaults.ts have the right signatures, plus review PreviewView.tsx to ensure the useBrowserDefaults() hook has proper dependency arrays in any callbacks to avoid stale closures. The useClientSettings hook memoizes based on the selector and settings, so when useBrowserDefaults uses a stable module-level selector function, it returns the same object as long as settings don't change. But in IntegrationsSettings, passing an inline arrow function creates a new identity each render, causing the memoized computation to run repeatedly—though it still returns the same value from settings, so there's no actual issue. Now I'm examining the UI consistency, particularly around the ScreenRotationIcon. The icon's size is controlled by its parent Button component through CSS rules that set the svg size based on the button's size prop, so there's no inconsistency there. But the icon color is where things get interesting—the Button sets a CSS variable for the icon color, and the icon uses currentColor to inherit it, which should resolve correctly to the muted foreground color. I'm also checking whether opacity-64 is a valid Tailwind class in this project. Tailwind v4 supports arbitrary opacity values from 0–100, and I can see it's already used elsewhere in the UI components, so that's fine. I should verify that text-warning maps to an actual color token in the theme. There's an accessibility concern worth noting: when the component is disabled, both the container and individual controls get opacity-64 applied, which compounds the opacity and might drop the contrast below acceptable levels. I need to confirm that the Switch component accepts a disabled prop, and check whether the NumberField being used as a controlled component without an onValueChange handler might trigger warnings in the base-ui version being used. I'm checking the KeybindingsSettings component around line 1249 to see if there's a similar InfoIcon pattern that might indicate an existing shared component for "desktop only" notices that we could reuse instead of duplicating. The notice pattern aligns well with the existing keybindings approach. I need to check whether the Integrations entry was actually added to the settings sidebar navigation, since the diff only shows the icon mapping being updated. Let me look at the SettingsSidebarNav component to see if it pulls from SETTINGS_SECTION_LABELS. Looking at the UI consistency across the settings components, I'm noticing some layout differences in how the control wrappers are structured. The SettingsRow uses a flex container with specific responsive sizing, while BrowserViewportSetting adds its own wrapper with slightly different constraints—particularly on mobile where the select stretches full width while the number fields maintain fixed widths with shrink behavior. I'm checking the Tailwind merge behavior for the SelectTrigger: the base w-full min-w-36 combined with the responsive sm:w-44 should resolve correctly, with the min-width constraint working at small breakpoints and the larger width taking over at the sm breakpoint. The NumberField components with their w-24 sizing are merging properly with the flex column layout, though I'm now examining the height alignment between the select trigger (which has min-h-9 / sm:min-h-8) and the number fields to ensure they're visually consistent. Looking at the other NumberField usages in the repo, they include Decrement and Increment buttons, but this new one is just a plain numeric input without those controls — that's a reasonable variation. The more pressing question is whether NumberField with a value prop but no onValueChange handler is actually controlled in Base UI, so I need to check the base-ui version and how the component handles this scenario. Base UI uses useControlled to manage the value state, and when controlled, typing updates a separate inputValue local state while the actual value stays fixed. There's an effect that syncs the formatted value back to the input display whenever the prop changes. When you type, it calls setValue with the parsed number, but in controlled mode that doesn't update the internal state — it only fires onValueChange. So the display shows what you typed until blur, and on blur the value gets committed, but since the prop never changed, the input reverts to showing the old formatted value. The risky part is that if you type something invalid — like a dimension exceeding the max or an area that violates the constraint — Base UI clamps it on commit, but the clamping happens before onValueCommitted fires. So the value passed is already clamped. But here's the inconsistency: if the area constraint is violated, it's silently ignored, meaning the input keeps displaying the rejected value while the actual setting never updates. Since the value prop doesn't change, React won't re-render, leaving stale text in the field. This is different from the device toolbar behavior where applyCustomSize resets the field on invalid input — here there's no revert, so you end up with a display that shows a size that was never actually saved. I should verify Base UI's controlled mode behavior more carefully to make sure I'm not overstating this. Let me trace through what happens when the value prop is passed and the user blurs after typing something invalid. runs when value changes, so if the controlled value gets rejected and doesn't actually change, the input field keeps displaying whatever the user typed. Older versions of Base UI handle this differently with an allowInputSyncRef that gets disabled during typing and re-enabled on blur, which prevents the effect from overwriting the input while the user is still editing. I'm uncertain about the exact behavior in the current version though, so I should verify the actual implementation rather than guess. But there's a clearer issue I can point to: commitDimension silently ignores values that exceed the bounds. I'm thinking through how onValueCommitted works as a new API in Base UI 1.x and whether controlled components without onValueChange trigger warnings. Base UI's useControlled hook warns about switching between controlled and uncontrolled modes, but not specifically about missing onChange handlers. React itself would normally warn about an input with a value prop but no onChange, though in this case the input gets its value from internal state rather than directly from the prop, so React doesn't catch it. The real question is whether the input reverts to its previous state on blur when a value is committed. Looking at the Base UI number-field implementation, there's a layout effect that syncs the input value with the formatted number value whenever the controlled value changes, but only if allowInputSyncRef is true. During input changes, this flag gets set to false to prevent the effect from running. Then on blur, commitValue is called, which parses the input and sets the flag back to true, triggering the effect again on the next render to sync the display. So if the parent rejects the value, the display reverts to the stored value without any explicit feedback — it's silent rejection, which matches the revert semantics I see in the toolbar. The toolbar does add aria-invalid for live feedback, but that's arguably a separate concern. This behavior seems acceptable. Now I'm digging into the actual visual and layout consistency issues. Looking at the settings layout, the rows have their own padding that's preserved when wrapped in a bordered container, which is fine. The reset buttons are conditionally rendered based on disabled state. I'm noticing potential opacity compounding where opacity-64 might be applied alongside data-disabled:opacity-64 on various components like the Select trigger and NumberFieldGroup. The SelectItem components have an inconsistency with hideIndicator — some pass it, others don't — but since the parameter is ignored in the implementation, there's no visual impact. The rotate Button in BrowserViewportSetting is disabled and wrapped in a tooltip trigger, which means the tooltip won't actually display since disabled buttons don't fire pointer events, though this matches the existing pattern elsewhere. The viewport settings row has proper aria-labels on the NumberFields, which is good for accessibility. Looking at the Switch usage across settings, they're all using the correct Base UI props (checked and onCheckedChange). The ScreenRotationIcon extraction doesn't expose a className prop, so sizing relies on parent container rules — the icon gets 16px in IntegrationsSettings and 14px in the toolbar, which works fine. I'm checking whether the SVG itself has width/height attributes... The toggleDeviceToolbar function reads from the browserDefaults hook without memoization, which is fine. When users toggle the device toolbar and their default is a preset size, it switches to that preset rather than fitting the panel—consistent with the documentation. In IntegrationsSettings, preset viewports like iPhone allow the width and height fields to be edited, converting to freeform when the user types. The rotate button's aria-label correctly indicates the target orientation based on current dimensions. The SettingsSection with id "browser" matches the search targetId, and the Integrations route is properly registered in the settings route tree. Now checking the mobile layout behavior—the SettingsRow control container uses flexbox with full width on small screens and auto width on larger ones, with proper gap spacing between items. Looking at the actual dimensions on mobile, the select field has a minimum width of 144px, plus two NumberFields at 96px each, a button at 28px, and gaps adding up to roughly 394px minimum. With the row's horizontal padding, this exceeds a 375px viewport, causing the content to overflow since the select can't shrink below its minimum and the NumberFields are in a non-shrinking container. This horizontal overflow would either clip the content or cause the entire settings page to scroll horizontally, which breaks the intended layout on mobile devices. The math confirms the overflow: with the select's minimum width of 144px and the sized div taking up 242px plus the 8px gap between them, that's 394px total—exceeding the 319px available space by about 75px. The select can't shrink further, so this layout definitely breaks on a 375px viewport. The question is whether this matters for the actual use case, since the settings panel is disabled in the browser version, though it's still technically rendered and visible on mobile web clients that use the sm: breakpoints. Actually, wait—when the viewport is set to fill (the default for non-Electron), the sized value is null, which hides the number fields entirely. So on mobile web, users wouldn't typically see this overflowing row unless they'd previously configured the sizing on desktop and those settings synced across clients. The client settings are stored server-side and would be shared across any browser connecting to the same server, so a phone browser could potentially read the same settings as a desktop client. This is a plausible but somewhat speculative responsive issue—the row is new and the overflow risk is real but depends on a specific configuration state. Looking at comparable existing rows like the font-size selector in SettingsPanels, they use similar width constraints with shrink behavior. A straightforward fix would be adding min-w-0 to the Select trigger to allow it to shrink and wrap on mobile rather than overflow. But I want to find something more definitive than a speculative responsive edge case. Let me check whether the resolveResponsiveBrowserViewportSize import removal from PreviewView is actually safe—I need to verify it's not used elsewhere in that file, since the diff replaced the import line but I should confirm no unused import remains that would trigger a lint error. I'm noticing that browserResponsiveViewportForToggle returns the default viewport when it's not a fill mode, which could be a preset like "nest-hub" at 1024x600. There's a potential behavioral issue in PreviewView.handleToggleDeviceToolbar where if the user's default is a preset viewport and the panel is smaller than that preset, it previously fit within the panel constraints—which seems intentional. Now I'm trying to determine whether this behavior is actually problematic. I've identified three candidate issues: responsive overflow on narrow widths for the viewport row (needs min-w-0 or wrapping), double dimming, and missing invalid feedback on dimension inputs. Looking at the SelectTrigger default variant with w-full min-w-36 merged with responsive classes—tailwind-merge keeps min-w-36 separate from width utilities, so the minimum select width is 144px. The trigger is a flex item that normally shrinks, but the min-width constraint prevents it from going below 144px. Calculating the total minimum width: 144px for the select plus 8px gap plus roughly 241px for the other controls equals about 393px needed. But at a 375px viewport with page and row padding, only 319px is available—that's a 74px overflow. Even at 390px (iPhone 14 width), after accounting for padding, there's still overflow. This means any phone-width rendering with a sized default viewport will overflow, and the same issue could occur in the desktop app if the settings pane is narrow. The responsive behavior gets more complex on desktop: at the sm: breakpoint (≥640px viewport), the select switches to a fixed 176px width, but this is based on viewport width, not the actual container width. In a narrow desktop window with a sidebar taking up space, the settings content area might be only 348px wide at a 700px viewport—less than the 425px needed for the row—causing overflow there too. Existing rows in the settings use a single select that shrinks responsively, so this fixed-size cluster introduces a genuine responsive geometry problem. The overflow-y: auto declaration actually forces overflow-x to compute as auto too per the CSS spec, so the settings page would display a horizontal scrollbar. It's not ideal but not catastrophic either. I could fix this by allowing the cluster to wrap with flexbox and min-w-0 on the Select trigger, or constraining the number fields with a fixed width and shrink allowance. So I have a concrete finding: on narrow viewports the row can't shrink due to the minmax(10rem,auto) column and non-shrinkable items, causing overflow. I'm fairly confident about this, though the grid's auto sizing combined with the min-w-36 and shrink-0 constraints on the group makes it hard to shrink further. On mobile specifically, the SettingsRow switches to flex flex-col, so the control div becomes w-full at 319px with an overflowing child. The real question is whether this matters in practice — the default configuration uses a Select which fits fine, but once users configure it with number fields (the main use case), narrow layouts will overflow. I'll flag this as a suggestion in an inline comment rather than a blocker. I'm weighing whether this is a strong enough finding to fail the check. The guidance says findings lead to failure by default, but I want to make sure I'm not being overly strict about a layout issue that might be minor. Let me take another look at the responsive behavior across different screen sizes to confirm whether this is a genuine regression risk or just an edge case. 's a realistic scenario. I'm weighing whether to flag this as a failure—the instructions say to report "All clear" only when there are no findings, otherwise it defaults to failure. But I should reconsider if this is actually a real issue or a false positive. The number fields might only appear in certain contexts, and the design could be intentionally desktop-focused, so maybe this doesn't warrant flagging after all. Actually, looking more carefully at the codebase patterns, I see that settings pages do use responsive utilities like w-full sm:w-X to handle mobile layouts. The new row has that for the Select but pairs it with a fixed cluster that won't shrink on narrow screens. On mobile widths below 640px, the wrapper becomes full-width while the cluster stays fixed, which causes overflow. Since mobile web is clearly supported throughout the settings layout, this is a legitimate issue worth flagging. I'd suggest making the cluster responsive—allowing it to wrap on mobile and letting the Select shrink with a minimum width constraint, while keeping the fixed layout on larger screens. I'm refining the wrapper div to add flex-wrap and justify-end for better responsive behavior, plus adding min-w-0 to the SelectTrigger to prevent overflow issues with the flex layout. I need to verify the line numbers are correct for the diff comment since it's referencing the RIGHT side of the file, and for a newly added file line 176 should exist. Then I'm checking a few other in-scope files like the desktop tab lifetime, preview automation, and settings-related modules to see if there's anything else worth flagging, particularly looking at how the route files follow the existing conventions. Checking that other settings panels follow the same pattern with SettingsPageContainer and SettingsSection, and verifying how sections handle rows with consistent spacing. The disabled state introduces a bordered box as a new visual treatment, which is justified in the comments. I should also confirm the viewport dimension constant to ensure the number fields are sized appropriately. Looking at whether other rows with multiple controls already use similar minimum widths on mobile — I see the font size row uses a SelectTrigger with a fixed width and shrink behavior, so I want to check if that pattern is already established elsewhere, which would mean my approach is consistent with existing practice rather than introducing something new. I'm looking at how an existing two-control cluster handles responsive layouts—the flexible control uses min-w-0 flex-1 to shrink on narrow widths while the fixed control stays at 88px, and the wrapper applies the same flex gap pattern to keep everything compact. The new code breaks this pattern by using a non-shrinkable Select with min-w-36 plus a much wider 241px fixed cluster, which explains why the row overflows. I'll add an inline comment suggesting the Select should adopt the same min-w-0 flex-1 approach to match the established precedent in this file. Writing the comment... Actually, I'm reconsidering whether to add min-w-0 to the cluster as well. With flex-wrap enabled, the Select takes the full width on mobile while the 241px cluster wraps below it—that fits fine at 375px. At larger breakpoints, sm:w-auto makes the wrapper auto-sized, and with flex-wrap allowed throughout, the container naturally wraps only when needed rather than forcing a single line. This is actually cleaner than adding sm:flex-nowrap since wrapping adapts to any viewport width. Now I'm thinking through how grid column sizing works with auto max values. When the grid container gets too small, columns with auto max sizing can shrink down to their minimum content width. The flex container's min-content is determined by its widest child—either the 144px Select or the 241px cluster. Since the cluster has shrink-0 items, its min-content stays at 241px, which means the column would shrink to that width and the flex content would wrap onto two lines. That prevents overflow at smaller breakpoints without needing sm:flex-nowrap. For the alignment, I could add sm:justify-end to maintain right alignment when the content wraps. The suggestion would be to use flex w-full flex-wrap items-center gap-2 sm:w-auto sm:justify-end on the wrapper. I'll write this up as a comment and post the review. Given the check's default-to-failure policy and the concrete layout finding here, I'll mark this as a failure rather than neutral. Let me post the review with an inline comment.

1. modify_pr — One layout finding in the new Integrations settings panel. Everything else (primitive usage, variants, ScreenRotationIcon extraction, notice styling, route/nav wiring) matches the existing settings patterns.


Agent Credits: 241 credits