Skip to content

fix(web): align window capture interactions

MacroscopeApp / Macroscope - UI Consistency failed Aug 24, 2026 in 8m 1s

UI Consistency: 4 issues found

Reviewed the web UI surfaces added by the window capture feature. The three findings from the previous run (global .animate-in/fade-in/zoom-in-95 override in index.css, call-site animation ownership in ChatComposer, and the missing data-keybinding-capture marker on the shortcut recorder) are resolved in this revision. Four new findings were posted as inline comments:

  • apps/web/src/components/chat/WindowCaptureAttachmentDetails.tsx — the app-name label inherits text-message-foreground from the user bubble while its own surface is bg-background/70; sibling chips in the same bubble re-anchor with text-foreground/85, so themes that set --app-theme-message-foreground independently can lose contrast.
  • apps/web/src/components/desktop/WindowCaptureOnboardingDialog.tsx — the first-run modal is gated on Boolean(window.desktopBridge) rather than this PR's own getDesktopWindowCaptureBridge() capability check, so on a desktop shell without the capture methods it promotes the feature and its CTA lands on a fully disabled settings page.
  • apps/web/src/components/desktop/WindowCaptureOnboardingDialog.tsx — the dialog body hand-rebuilds DialogPanel (mx-6 + footer mt-6), bypassing the primitive's padding-compensation slots and scroll container that every other dialog in the app composes.
  • apps/web/src/components/settings/WindowCaptureSettings.tsx — the capture sound and animation switches stay interactive without a capture bridge, while the enable, flash, and "Capture now" controls in the same section are disabled.

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: apps/web/src/** TS/TSX/CSS changes only; desktop main-process, contracts, and lockfile changes were read as context.

Checks performed:

  • Verified the new @keyframes window-capture-card-enter sits with the existing one-shot keyframes convention in index.css and that the call site owns the animation via animate-[...] with motion-reduce:animate-none (previous finding resolved).
  • Confirmed the shortcut recorder now carries data-keybinding-capture="", matching the contract AppSidebarLayout's capture-phase keydown handler relies on (previous finding resolved).
  • Compared the new settings page against settingsLayout primitives (SettingsPageContainer/SettingsSection/SettingsRow/SettingResetButton) and KeybindingsSettings; composition, searchableSetting anchors, SETTINGS_SECTION_LABELS/SETTINGS_SECTION_ICONS wiring, and size-4.5 icon sizing all match existing conventions.
  • Checked the composer and timeline attachment layouts: the composer container is flex flex-wrap, so the w-56 capture card is safe; the timeline grid card uses col-span-2 inside grid-cols-2, and the pr-8 inset preserves the absolute remove/warning affordances.
  • Compared the new command palette action item shape and toast usage (stackedThreadToast) with surrounding call sites — consistent.
  • Verified text-secondary-label is a real theme token with a contrast bridge, and traced --message-foreground / --app-theme-message-foreground to confirm the inherited-foreground risk in the timeline card.
  • Confirmed the useUpdateClientSettings return-type change to Promise<void> does not break existing call sites (typescript/no-floating-promises is disabled in vite.config.ts).

Not flagged (noted but judged out of scope or acceptable): the raw "t3:focus-composer" window event string duplicated across WindowCaptureCoordinator and ChatView instead of a typed bus module like previewActionBus.ts, and the decision to render the whole Window Capture section (disabled) in the browser rather than hiding it as desktopOnly rows do.


🔍 Agent activity — 34 tool calls

💭 Looking at the current PR state at commit f4f5a1e, the three previously flagged issues from commit 7f97b00 appear resolved — the keyframe naming, animation reference, and data attribute fixes are all in place. Now I need to check the rest of the diff for any new UI consistency issues within the web app's TypeScript, TSX, and CSS files. I'm going through the new and modified files: the window capture components (coordinator, onboarding dialog, attachment details), the updated settings sidebar and search, composer draft store, hooks, and route files. I want to verify the onboarding dialog follows the same padding/spacing conventions as other dialogs in the codebase, check whether text-secondary-label is a valid design token, and look at whether the settings rows are using the title prop and searchableSetting helper consistently across entries. I also want to check whether wrapping a Kbd inside a Button matches existing patterns elsewhere, review icon sizing conventions like size-4.5, and look at the capture status text before pulling up the relevant files to confirm all this.

1. browse_code — apps/web/src/components/settings/WindowCaptureSettings.tsx

2. browse_code — apps/web/src/components/settings/settingsLayout.tsx

💭 Let me check KeybindingsSettings.tsx to see how existing shortcut recording is implemented, to follow the same conventions.

3. browse_code — apps/web/src/components/settings/Keybinding*

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

💭 Let me verify that the text-secondary-label token actually exists in this project.

5. git_tools — secondary-label

💭 I should check how WindowCaptureAttachmentDetails inherits text color in the user message bubble context, then look at KeybindingsSettings' recorder to see how the capture button handles keyup/blur events and whether it uses Kbd for display conventions.

6. git_tools — data-keybinding-capture

💭 With prior findings addressed, I'm scanning the current diff for new issues — checking Button's size variants for consistency, whether Kbd's background might clash inside a Button, and how dialog padding is handled, particularly the onboarding dialog's use of mx-6 on a div. I want to check other dialogs for a shared content-wrapper convention instead of mx-6, and look at WindowCaptureOnboardingDialog's direct use of window.desktopBridge rather than the newer getDesktopWindowCaptureBridge() helper. That distinction matters because older desktop builds without window capture support would still trigger the onboarding flow, sending users to a settings page where the relevant controls are disabled — a real behavioral inconsistency worth flagging.

7. browse_code — apps/web/src/components/ui/dialog.tsx

8. browse_code — apps/web/src/components/ui/kbd.tsx

💭 I'm noticing the onboarding dialog uses a raw div plus footer margin instead of the standard DialogPanel convention that other dialogs follow, which normally handles padding and scroll behavior consistently. This deviation feels like a potential finding worth flagging against the established UI primitive conventions.

9. git_tools — DialogPanel

💭 I'm noticing this onboarding dialog skips the standard DialogPanel wrapper the rest of the repo uses, replacing it with a manual div and footer margin instead—which means it's also missing DialogPanel's padding compensation and header selector styling that other dialogs rely on for consistent spacing. More importantly, without ScrollArea in place, on small viewports the content may not be scrollable and could get clipped if the dialog popup uses overflow-hidden. I want to check the popup's class definitions to confirm overflow behavior, and also compare against ConnectOnboardingDialog to see if there's a similar established pattern for this kind of layout.

10. browse_code — apps/web/src/components/ui/dialog-styles.ts

11. browse_code — apps/web/src/components/cloud/ConnectOnboardingDialog.tsx

💭 I'm checking whether the popup body could clip its contents on a short viewport since it lacks overflow handling, but given the small amount of content it likely fits fine regardless. Using DialogPanel would still be the more conventional approach here. The manual mx-6/mt-6 body reconstructs DialogPanel's padding contract and skips the header's pb-3 compensation and ScrollArea, which I'd flag as a moderate finding worth an inline suggestion. Now I want to check the ChatComposer's window-capture tiles, which changed from a fixed h-16 w-16 to flex h-20 w-56, to see if the surrounding container is a grid with fixed columns that could break with the new width.

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

💭 The fixed width should be fine since the composer is wider than that even on mobile. I'm checking the remove button positioning and how the details element accounts for it with padding, then moving on to look at the MessagesTimeline layout with its column span and height settings.

13. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm checking the message bubble colors—the appName text inherits its color from the bubble's foreground rather than setting its own, which matters since the window capture card sits on a semi-transparent background surface nested inside. I need to make sure card, popover, accent, secondary, and message foreground colors are each tuned against their own specific surface rather than assumed globally. Also checking that avatar fallback styling, composer flex layout for the capture details, and whether the app already has a typed event convention for the custom "t3:focus-composer" window event used between the coordinator and chat view.

14. git_tools — "t3:[a-z-]*"

💭 I want to check whether this repo already has a convention for global custom events, so I'll search for existing dispatchEvent calls with CustomEvent or any established event name constants.

15. git_tools — dispatchEvent(new (Custom)?Event

16. browse_code — apps/web/src/components/preview/previewActionBus.ts

💭 I notice there's an existing typed event bus convention with dispatch/subscribe helpers, but the new code duplicates an untyped raw string event across two files instead of following that pattern. This feels more like an architectural consistency issue than strictly a UI one, so I'm weighing whether it fits within the scope of a UI consistency check focused on shared component systems. I'll lean toward keeping my focus tight on concrete UI regressions rather than this ad-hoc string event, since the rules are UI-specific. Looking at other issues instead, the WindowCaptureOnboardingDialog gates on Boolean(window.desktopBridge) rather than a capability helper, which means onboarding could show on older desktop builds where all controls are disabled — that's a real UX inconsistency worth flagging. I'm also checking whether the dialog's body layout bypasses the standard panel component, and whether other desktop-only components consistently use window.desktopBridge checks or the newer capability helper, to see if this is a pattern issue or a one-off.

17. git_tools — window.desktopBridge

💭 I notice the onboarding gate checks Boolean(window.desktopBridge) instead of getDesktopWindowCaptureBridge(), which means on older desktop builds the onboarding dialog still appears with a "Set up capture" CTA that leads to a settings page where all controls are disabled—a real inconsistency from the changed lines that needs fixing by switching to the proper bridge getter. The scenario is legitimate since the desktop app can load a remote-served web UI newer than the bundled shell, which is exactly why the PR added this capability check in the first place. I also want to check the new window-capture-card-enter keyframes added to the global CSS to confirm they follow existing conventions and are placed correctly.

18. browse_code — apps/web/src/index.css

💭 The CSS now matches convention. Now I want to check the CommandPalette action item I'm adding for consistency with existing items — verifying fields like icon, title, searchTerms, run, and whether others use keywords, group, disabled, or subtitle, so I'll look around lines 1500-1600 to compare.

19. browse_code — apps/web/src/components/CommandPalette.tsx

💭 The command palette item looks consistent — capture-now intentionally ignores the disabled shortcut setting, and it doesn't need a shortcutCommand since the capture trigger is a global OS shortcut, not a keybinding. Now I want to check whether stackedThreadToast is actually the right helper to use for non-thread errors, so I'm looking at its definition.

20. git_tools — stackedThreadToast

💭 Looking at WindowCaptureCoordinator, I notice the play-sound setting row isn't disabled when there's no bridge, unlike the flash and enabled rows which are — that inconsistency seems worth flagging. Sound and animation toggles remain interactive on web even though capture itself only works via the desktop bridge, so these controls are effectively inert without it while enabled/flash are correctly disabled. It's a minor consistency issue worth noting, though I'm weighing whether it's intentional since these could be renderer-side prefs set ahead of desktop use — but since web can't capture at all, flagging it still seems reasonable. I'm also checking whether getDesktopWindowCaptureBridge() calls during render are fine, and looking at how Kbd is used inside Button in WindowCaptureSettings versus the KeybindingPill pattern used in KeybindingsSettings for similar shortcut-editing buttons, to see if there's a more consistent component to reuse.

21. git_tools — formatShortcutLabel

💭 Now I'm checking whether adding the new settings path also requires updating a separate nav items list in SettingsSidebarNav, beyond just the SettingsPath union and labels in settingsSearch.ts.

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

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

💭 I'm checking whether the desktopOnly flag matters here—since Window Capture rows still render (just disabled) in the browser, their anchors exist, so that's not the issue. The real concern is whether the new Window Capture nav section itself is properly scoped for browser vs desktop. Existing convention hides desktop-only rows entirely in the browser rather than rendering disabled versions, so this PR's approach of showing an always-visible section with disabled controls and unfiltered search entries is a departure worth flagging—likely fixed by marking the search items desktopOnly and/or hiding the nav section on web.

24. git_tools — desktopOnly

💭 Since window capture rows are all rendered (disabled) in the browser, search items landing there is technically consistent, though I want to check how quit-confirmation's desktopOnly hiding is actually handled via hasDesktopBridge to confirm the pattern holds.

25. git_tools — hasDesktopBridge

💭 I'm noticing the existing convention hides desktop-only settings rows entirely rather than disabling them, whereas this new section keeps them visible but disabled with an "Available in the desktop app" label. Since the nav generation doesn't support hiding whole sections conditionally, the PR likely went with disabling instead of hiding as a practical workaround. That said, within this new section there's a real inconsistency worth flagging: sound and animation toggles remain interactive on web while the enable/flash/capture-now switches are disabled, even though the whole feature is unavailable without desktop. The simplest fix would be applying the same disabled condition to those two switches. I'm double-checking whether animations matter independently on web since attachment cards render in the message timeline there too, but the animation only triggers in the composer, which only exists on desktop for window-capture — so those toggles are genuinely inert on web and should be disabled as well. Now looking at the onboarding dialog itself, I want to review the whole file carefully. A few concrete issues stand out: it checks Boolean(window.desktopBridge) directly instead of using the capability helper, meaning the dialog could show onboarding CTAs pointing to a fully disabled settings page on older desktop builds without the capture bridge. The body also uses a plain div with margin plus a footer margin instead of following the repo's DialogPanel convention, which breaks the header's padding compensation and skips the ScrollArea — worth raising as a suggestion rather than a hard blocker. I also note this dialog renders at the root level for every desktop user regardless of thread context or whether the platform even supports window capture, functioning as a first-run modal for a single feature. Shifting to WindowCaptureCoordinator, I want to check a few UI-relevant details: the audio context gets recreated on every capture, there's a readFileAsDataUrl import from ChatView.logic, and a settings selector usage I need to verify against the useClientSettings hook signature. More importantly, the diff changes persistClientSettings and useUpdateClientSettings to return a Promise instead of void, so I need to check whether any callers pass the updater function directly as an event handler (like onCheckedChange) — that could now trigger no-misused-promises lint issues where a void-returning callback is expected.

26. git_tools — updateClientSettings|useUpdateClientSettings

💭 I'm noticing that in ProjectSettingsPanel, a call that now returns a Promise is invoked inside a useCallback without being awaited or voided, which could trigger the no-floating-promises lint rule given the changed return type. I'm wondering whether CI would have already flagged this for the PR author. I should check other call sites like Sidebar and LegacySidebar for similar unhandled updateSettings calls, though this is really a lint/typing concern rather than a UI consistency issue, so it may fall outside the scope of what I'm reviewing here. Still, since the PR's type change directly created these floating promises, I want to quickly verify whether the repo actually enforces that lint rule before deciding how much weight to give this.

27. git_tools — no-floating-promises|no-misused-promises

💭 Good, no lint failure there. Now let me go back and re-read the onboarding dialog in full to finalize my findings.

28. browse_code — apps/web/src/components/desktop/WindowCaptureOnboardingDialog.tsx

29. browse_code — apps/web/src/components/desktop/SshPasswordPromptDialog.tsx

💭 I notice the sibling dialog in the same folder follows the DialogHeader/DialogPanel/DialogFooter pattern, while the new dialog instead uses a raw div with manual margin and a footer with top margin—bypassing DialogPanel's padding contract and ScrollArea, and hand-reproducing the horizontal spacing instead. Beyond that, I'm noting the capability gate inconsistency between using Boolean(window.desktopBridge) versus getDesktopWindowCaptureBridge(), and that sound/animation switches aren't disabled without the bridge unlike sibling rows in the same section. I should also verify size-4.5 is valid under Tailwind v4's dynamic spacing scale and check whether other files use it, then look at how message-side annotation cards handle foreground color conventions—I'll check the element-context chips around line 1069-1100 for that pattern.

30. git_tools — size-4.5

31. browse_code — apps/web/src/components/chat/MessagesTimeline.tsx

💭 I'm checking the UserMessageElementContextChip component to see if cards within message bubbles explicitly set foreground colors against their background surfaces.

32. git_tools — function UserMessageElementContextChip

💭 I notice the existing convention sets explicit foreground opacity on chips rather than inheriting the message foreground color, but the new WindowCaptureAttachmentDetails component inherits text-message-foreground on both the timeline and composer surfaces — which breaks that pattern and could look off with custom themes where the message foreground color diverges from the default. I want to check index.css to confirm whether --message-foreground and --foreground actually diverge under any theme, since if they do, this is a real contrast bug worth fixing by switching to text-foreground on the app name.

33. git_tools — --message-foreground

💭 I'm confirming that custom themes can make message-foreground contrast poorly against the timeline and composer backgrounds, so the chip's text-foreground/85 convention should be followed, while composer text outside the bubble already inherits the right color and text-secondary-label stays safe since it's a background-surface token. The fix is to add text-foreground to the details wrapper in both the timeline and composer call sites for consistent contrast, since both surfaces use bg-background-ish tones — a small, self-contained change. I also check the composer card now includes text where it previously had none, verify the focus-scheduling effect in ChatView is fine, and note the decorative image alt text is appropriately empty. Then I start listing findings, beginning with the onboarding dialog's raw bridge usage causing a capability inconsistency. I'll suggest adding foreground text color for the app name line to fix contrast, and propose an inline fix for the sound switch adding a disabled state tied to bridge availability, taking care to match exact indentation for the diff suggestion. I'm also questioning whether disabling only some rows when the bridge is unavailable is inconsistent, and deciding to raise it as a suggestion rather than a hard requirement. I'm confirming line numbers for comments correspond to the diff's right-side lines, then drafting a suggestion to guard the onboarding dialog's open condition on both settings hydration and bridge availability plus dismissal state. Since that fix depends on adding an import, I'm switching to a small unified diff for the gate line while noting the import addition separately, rather than a self-contained inline suggestion. For the DialogPanel wrapper change, I'm noting the edit spans a contiguous block of lines replacing the outer div with a DialogPanel and adjusting DialogFooter margin. The simplest fix is wrapping the shortcut card in DialogPanel (imported from the ui/dialog module) and removing the mx-6/mt-6 spacing classes, so I'll skip the diff since it spans multiple hunks plus an import change. Now I want to double check the message-foreground claim for comment 3 — the card sits inside a bg-message text-message-foreground bubble in the timeline, and since the card uses bg-background/70, the foreground color should indeed be inherited correctly. I'll finalize concise comment bodies and post the review. I'm drafting the actual comment text now: one flags that the app name inherits message-foreground color while sitting on a background-token surface, which can lose contrast under custom themes since --app-theme-message-foreground and --foreground are set independently — sibling chips already work around this by re-anchoring to text-foreground/85, so I'll suggest anchoring the label to the card's own surface, which also applies to the composer card. I'm preparing a second comment for the onboarding dialog file as well, then I'll post all four comments together and note the overall review status as a failure with a short summary of findings. Now I'll attach comment D to the Switch component lines, adding a suggestion that includes a disabled prop to prevent interaction when the feature is unavailable.

34. modify_pr — UI consistency review of the web-side window capture surfaces. Four findings, all on newly added lines; the three items flagged in the previous run (global .animate-in override, call-site animation ownership, and the missing data-keybinding-capture marker) are resolved in this revision.


Agent Credits: 372 credits