Skip to content

fix(web): search community themes as you type

MacroscopeApp / Macroscope - UI Consistency failed Aug 20, 2026 in 4m 45s

UI Consistency: 3 findings

Three consistency/regression findings were posted as inline comments.

  • apps/web/src/components/settings/ThemeImportDialog.tsx (532-540): the dialog actions were moved out of DialogFooter into a raw flex items-center justify-end div inside DialogPanel, losing the pinned position outside the panel's ScrollArea, the footer separator/padding, mobile flex-col-reverse stacking, and the data-slot="dialog-footer" hook; the actions also no longer render in the conflict branch, so Cancel disappears there.
  • apps/web/src/components/settings/ThemeSearchSection.tsx (152-162, with 127): with the search now driven by a debounced effect, runSearch's setQuery(trimmed) rewrites the controlled input mid-typing (drops a just-typed trailing space and moves the caret); runSearch also clears results on every debounced keystroke, causing repeated list/height thrash.
  • apps/web/src/components/settings/ThemeSearchSection.tsx (340-348): the source link lost its text label and is now a bare 14px icon anchor, below the coarse-pointer hit target the Button primitive supplies; suggested rendering it via Button size="icon-micro" variant="ghost-muted".

The icon and label swaps in ThemeSettings.tsx and ThemeEditorPanel.tsx are internally consistent (Paintbrush for "Create theme", Plus for "Add theme") and no unused imports remain.

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.

Methodology: read the full post-change contents of ThemeImportDialog.tsx, ThemeSearchSection.tsx, and the changed regions of ThemeSettings.tsx / ThemeEditorPanel.tsx; compared against the shared primitives components/ui/dialog.tsx (DialogFooter, DialogPanel slot-driven padding), components/ui/button.tsx (size variants, coarse-pointer hit area, --control-icon-color), and components/ui/spinner.tsx; verified useDebouncedValue in apps/web/src/state/queries.ts; and checked repo-wide usage of DialogFooter, PlusIcon/DownloadIcon, and loading icons.

Not flagged (considered and dismissed):

  • Raw Loader2Icon with animate-spin inside InputGroupAddon mirrors the established PullRequestSearchInput pattern (components/pullRequest/PullRequestListFilters.tsx:87) and the aria-hidden avoids double-announcing with the existing role="status" region.
  • The selection:bg-accent selection:text-accent-foreground change in ThemeJsonEditor is coherent: the opaque selection background is painted by the z-10 textarea above the highlight <pre>, and dropping selection:text-transparent is required for the selected text to remain legible.
  • Sort-change re-search still works: runSearch depends on sortBy, so its identity change re-fires the debounced effect.
  • Grid minmax 17rem to 16rem and the icon/label copy changes are intentional visual choices.

🔍 Agent activity — 1 tool call

💭 I'm looking at the changed files for suspicious patterns. The ThemeImportDialog replaced the shared DialogFooter primitive with a raw div and moved it inside the scroll area, which changes the layout. I also want to verify that ThemeSearchSection's imports—useDebouncedValue, useEffect, useCallback, Spinner—are all actually present and used correctly. Another concern is the selection styling change from selection:bg-accent/30 to selection:bg-accent selection:text-accent-foreground, which drops selection:text-transparent. For a syntax-highlighted textarea overlay, that transparent-text-on-selection behavior is likely intentional to avoid doubling text with the highlight layer underneath, so removing it could be a real regression worth checking in the code. I should check DialogFooter and DialogPanel for border-top, sticky, and padding conventions, and confirm whether useDebouncedValue exists in state/queries. I'm also noticing the file already uses a shared Spinner primitive elsewhere, so using raw Loader2Icon with animate-spin in the InputGroupAddon instead would duplicate that existing component rather than reuse it. I'm also spotting a real behavioral issue: runSearch calls setQuery(trimmed), and since debouncedQuery derives from query.trim(), this means after 350ms the input value gets rewritten to its trimmed form while the user is still typing — which could delete a trailing space they just typed and jump the cursor. That's a regression from before, when runSearch only fired on submit, so it's worth flagging. I also notice handleSortChange only updates sortBy without triggering a new search run, even though runSearch depends on sortBy. Tracing further, since runSearch's identity changes whenever sortBy changes, the effect watching debouncedQuery and runSearch re-fires and triggers a new search with the updated sort — so it does work, just implicitly, which feels fragile. I also note the sort UI is hidden while isSearching is true, causing a layout jump, though that's a weaker finding. Now I'm looking at the ThemeJsonEditor's selection styling: previously the textarea text stayed transparent during selection so only the highlighted overlay showed through a translucent selection background, but now making the selected text opaque and visible breaks that layering. Actually this resolves fine: the opaque selection background sits above the pre layer, so selected text shows the textarea's own accent-foreground color while unselected text still shows through transparent to reveal syntax highlighting. It's a minor aesthetic tradeoff—losing highlight colors during selection—but not a real regression worth flagging strongly. I want to shift focus to the bigger finding: DialogFooter being replaced by a raw div, so I should check its definition in dialog.tsx. I also need to verify whether removing the DownloadIcon import from ThemeSettings leaves any dangling usage, and similarly check if PlusIcon is still referenced elsewhere in ThemeEditorPanel after being swapped for PaintbrushIcon, since either could cause an unused-import or compile error. I'm noticing that ThemeImportDialog swapped DialogFooter for a plain div, which breaks the sticky footer behavior, the mobile column-reverse stacking, and the border/background/padding styling that DialogFooter normally provides. Also, the conflicts branch previously always rendered the footer with Cancel/Add theme buttons, but now the footer only shows in the non-conflict branch, so the conflict state loses the Cancel button entirely (it only has "Back" now) — a real behavior change worth flagging. Separately, I'm seeing that a Loader2Icon with animate-spin was used instead of the existing Spinner primitive already imported in the file, and it drops the role="status"/aria-label that Spinner provides, though aria-hidden may have been intentional to avoid duplicate screen reader announcements. Actually, adding aria-hidden to Spinner removes it from the accessibility tree entirely, so there's no double-announcement issue — that confirms it's a clean fix worth flagging. Now I'm tracing a more concerning bug: the debounced search calls setQuery(trimmed), which rewrites the controlled input value after the user has already kept typing. If someone types "nord " with a trailing space, the debounce fires on "nord", trims it, and resets the input to "nord" — deleting the space they just typed and potentially jumping the caret mid-typing. This looks like leftover logic from an older submit-based flow that now conflicts with the controlled input. I should flag this and suggest removing setQuery(trimmed) from runSearch, then go check how useDebouncedValue is actually implemented to confirm the timing. Next I'm checking the ThemeSearch section where the license badge and "Source" label were removed, leaving just an icon link with an aria-label — that seems fine. Then I want to verify DownloadIcon and PlusIcon usage across ThemeSettings and ThemeEditorPanel, and I'm noticing a terminology inconsistency: the search cards say "Install"/"Installing" while the dialog itself uses "Add a theme" and "Add theme," with error copy saying "could not be added" — mixed vocabulary within the same dialog that's worth flagging as a copy-level consistency issue. I'm leaning toward skipping this since it seems intentional — the PR appears to deliberately distinguish "Install" for extensions versus "Add" for files, so I shouldn't flag it as an aesthetic nitpick. The grid minmax change from 17rem to 16rem looks fine too. Now I want to check the PlusIcon to PaintbrushIcon swap around line 1258 in ThemeEditorPanel to see what button that actually is — if it's meant for adding a color or token, a paintbrush icon might be a mismatch, so I need to look at the surrounding context before judging. Good, the "Create theme" icon usage in ThemeEditorPanel now matches ThemeSettings, both using PaintbrushIcon consistently. I'm checking the surrounding context in ThemeSettings around lines 860-885 to confirm. Imports look fine, with PaintbrushIcon properly used in ThemeSettings. The key finding is the DialogFooter removal, so I want to check other dialogs in the repo to confirm whether the DialogPanel-then-DialogFooter pattern holds, and also look at how ScrollArea interacts with DialogPanel's bottom padding rule when a footer is present. The real regression is on mobile: with bottomStickOnMobile, actions that used to stay pinned below the scroll area are now inside it, forcing users to scroll past the JSON editor to reach Cancel/Add theme, and losing the mobile stacking behavior and muted footer styling. I also notice that in the conflicts state there's no Cancel button anymore, only Update existing/Keep both/Back, which may be intentional but worth flagging. The DialogHeader padding rule and the ghost Cancel button styling seem unaffected by this change. Next I want to look at the Loader2Icon versus Spinner usage, and then check runSearch's query-setting logic for a potential write-back issue. I'm now tracing whether the suggestion row's visibility toggling during search causes layout shifts — since the popular/sort row unmounts whenever isSearching is true, changing sort could cause the Select the user just interacted with to disappear and reappear, costing focus. I need to verify whether this was already happening before the change or is a new regression introduced by debounced search triggering on every keystroke. I also want to check how InputGroupAddon handles icons elsewhere, to confirm the spinner placement pattern is consistent with existing usage. There's a discrepancy here—the grep results show DialogFooter at line 549 in ThemeImportDialog, but the file I viewed at head only has 554 lines with no DialogFooter present. I need to figure out why the grep might be referencing a different revision than what I actually viewed. I should re-run the grep with an explicit head revision to resolve this inconsistency before moving on. Meanwhile, I'm also noting that PullRequestListFilters.tsx conditionally renders a spinning loader icon or search icon based on a busy state. I'll skip flagging the Spinner/LoaderIcon usage since it matches an existing repo pattern for search inputs, so raising it would just be a nitpick. Now I'm moving on to compile the actual findings to report. My primary finding: replacing DialogFooter with a raw div inside DialogPanel breaks the shared footer contract — sticky placement, border/background styling, mobile stacked-button layout, rounded corners, and the data-slot hook other components rely on. Worse, the footer now only renders in the non-conflict branch, so Cancel disappears during conflicts, and on mobile the actions scroll with the editor instead of staying pinned. I suspect the author moved it to separate the paste-flow "Add theme" action from the search results section, which is reasonable intent, but the fix should still restore DialogFooter as a sibling of DialogPanel to preserve the composition contract. This is getting too complicated to fully resolve, so I'll just recommend restoring the footer outside DialogPanel as the established pattern, without an over-specified diff since the fix spans multiple hunks. For the second finding, I'm noting that runSearch still calls setQuery(trimmed), which now conflicts with the debounced search-on-keystroke effect — this write-back could rewrite the controlled input mid-typing and jump the caret. I should suggest dropping that setQuery call since it's no longer needed for the submit flow. Confirming the caret bug is real: setting a controlled input's value programmatically while focused moves the caret to the end, so typing "nord " followed by a pause would strip the trailing space and merge it with the next word typed, producing something like "nordtheme" instead of "nord theme" — a concrete, reproducible issue especially for multi-word queries like "tokyo night." Now reconsidering the selection styling change in ThemeJsonEditor more carefully. Removing selection:text-transparent and using an opaque accent background means selected text loses its syntax-highlight colors but stays legible — a mild visual downgrade rather than a clear regression, though I should check whether the overlay and textarea text still align properly during selection. I'll move on without flagging this. Next I'm looking at the removal of the license badge and "Source" text, leaving an icon-only link with an aria-label — I want to check whether the hit target is still adequate. I'm noting that the Button primitive already handles coarse-pointer hit targets and focus rings, but the source link was changed from a labeled text+icon anchor to a bare 14px icon with no text and no focus-visible ring, which shrinks the accessible hit target. The fix is to route the anchor through Button using its render prop with size="icon-micro" and variant="ghost-muted", adding an aria-label for the source link while keeping the icon as the child. I'm confirming that Button's render mechanism via useRender properly merges props onto the anchor, so the aria-label and href end up correctly applied, making this a clean inline fix for the affected lines. I'm now weighing whether to post this as an inline suggestion or a standalone diff, since the line range partially overlaps unchanged context and GitHub's suggestion ranges need to stay within the diff hunk — a fenced diff anchored near the changed line feels safer than a multi-line suggestion block. Combined with the earlier DialogFooter issue, I'm leaning toward failing the check, consistent with the default failure policy for clear consistency or regression risks, and I'm starting to enumerate the specific findings to post, starting with the raw div replacing DialogFooter in ThemeImportDialog.tsx. That removed text size no longer matters since there's no label text left. I should double-check the actual diff line numbers against the PR patch so the review comments land correctly on GitHub. I'm checking the diff hunk line ranges to find valid line numbers for placing a multi-line comment on the ThemeSearchSection component's source URL rendering block. Now I'm drafting an inline suggestion replacing the anchor element with a Button component that renders as a link, matching the 24-space indentation from the original code. Button is already imported, so this fix is self-contained. I'm checking whether icon-micro sizing is appropriate given the SourceLinkIcon's hardcoded dimensions, and confirming ghost-muted preserves the existing hover color behavior. For the ThemeImportDialog footer, the relevant lines are all part of a newly added block, so I'll note that the fix requires moving code across hunks rather than a simple diff suggestion. For the ThemeSearchSection debounce logic, I'm tracing through how setQuery inside runSearch could overwrite the controlled input while the user is still typing, and identifying that the actual fix belongs outside the current hunk range. I'm verifying that setQuery(trimmed) genuinely alters the input value in the common case of trailing whitespace, and confirming this doesn't trigger an infinite re-render loop since the debounced value stays the same after trimming. I'm also considering whether the isSearching state now causes the Popular/Sort row to unmount on every keystroke, creating a layout flash as results get cleared and the container collapses — a legitimate UX regression worth flagging. I'm weighing whether to fold this layout-flash observation into the existing debounce comment as a brief secondary note, rather than opening a separate comment thread, since it's a real but somewhat opinionated consequence of the live-search change. Now drafting the footer comment: swapping DialogFooter for a raw flex div loses the shared footer contract — it used to stay pinned outside the scroll area, carried the border/background/padding/stacking styles, and the data-slot hook other components key off. Inside the panel the buttons now scroll with the editor, lose the mobile stacking behavior, and Cancel disappears entirely in the conflict branch, so I should flag restoring DialogFooter. I'll post this as a GitHub PR review with a failure state since there are real findings, titled "UI Consistency: 3 findings."

1. modify_pr — Reviewed the theme settings UI changes for shared-primitive and interaction consistency. Three findings, all in apps/web/src/components/settings: the dialog actions no longer use the shared DialogFooter contract, the live-search rewrite lets runSearch write back into the controlled input, and the source link became a bare 14px icon target outside the button primitive. Icon/label swaps in ThemeSettings.tsx and ThemeEditorPanel.tsx look consistent with each other.


Agent Credits: 125 credits