Repository navigation
UI Consistency: 1 issue found
1 finding
apps/web/src/components/RightPanelTabs.tsx(L206-207) -surfaceShortcutTargetsTypingContextresolvesclosest("input, textarea, select, [contenteditable]")and then rejects the match whencontenteditable="false". Becauseclosest()stops at the nearest matching ancestor, a target inside acontenteditable="false"island nested in the editable composer (Lexical mention/skill chips inComposerPromptEditorarecontentEditable={false}decorators) is reported as not a typing context, so a launcher letter can still be claimed while the caret is in the composer - the hijack this PR fixes. Moving the exclusion into the selector ([contenteditable]:not([contenteditable="false"]), the idiom already used byComposerPendingUserInputPanel) satisfies both stated intents: nested islands resolve to the editable host, standalone non-editable widgets find no editable ancestor and stay shortcut-reachable.
An inline comment with the suggested selector has been posted on the PR.
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/components/RightPanelTabs.tsx(new exported helper, launcher capture-phase keydown handler)apps/web/src/components/RightPanelTabs.test.tsx(newsurface shortcut typing contextssuite)
Context checked beyond the diff
- Existing typing-context contracts in the app:
ChatView.tsxTYPE_TO_FOCUS_EDITABLE_SELECTOR([contenteditable="true"],[contenteditable="plaintext-only"],[role="textbox"], evaluated overcomposedPath()) andComposerPendingUserInputPanel.tsx(closest('[contenteditable]:not([contenteditable="false"])')). - Composer structure:
ComposerPromptEditor.tsxrenders LexicalContentEditable(contenteditable="true",role="textbox") withcontentEditable={false}chip decorators inside it;editor.setEditable(!disabled)flips the root tocontenteditable="false"for pending-approval / project-selection states. - Composer autofocus paths in
ChatView.tsx(focusComposeron thread activation and terminal close) confirm the core behavior change - letters no longer stolen from a focused empty composer - is the intended fix and does not make the launcher unreachable, sinceRightPanelEmptyStatefocuses its own container on mount.
Not flagged
- Widening the call-site guard from
HTMLElementtoElement(strictly safer;closestis defined onElement). - Treating a focused-but-empty editable as a typing context (the intended fix; consistent with
ChatViewtype-to-focus and the launcher docstring). - Absence of
[role="textbox"]in the helper's selector: no editable surface in the app relies on it withoutinput/textarea/contenteditable. - Shadow-DOM retargeting (
event.targetvscomposedPath()): pre-existing behavior, unchanged by this patch. - No styling, Tailwind ownership,
ScrollArea/virtualized-fade, or theme-variant changes are present in this diff.
🔍 Agent activity — 23 tool calls
💭 I'm comparing the old and new logic for detecting typing context: previously an empty contenteditable wasn't treated as typing, but now any focused editable element counts, using a broader selector that also checks the contenteditable attribute isn't explicitly "false". I'm checking whether this broader selector could accidentally match elements it shouldn't. I should check whether other shortcut handlers in the repo use a similar typing-context check, for consistency. Also worth noting: this change flips prior behavior where an empty composer allowed shortcuts — now typing there blocks them, so I want to verify that matches patterns elsewhere in the app before moving on.
1. browse_code — apps/web/src/components/RightPanelTabs.tsx
2. git_tools — isContentEditable|contenteditable
💭 I should check ChatView.tsx around lines 440-470 to see the existing typing-context selector contract, since that might be the shared canonical implementation.
3. browse_code — apps/web/src/components/ChatView.tsx
4. browse_code — apps/web/src/components/chat/ComposerPendingUserInputPanel.tsx
💭 I'm noticing a subtle edge case: the new helper mirrors the existing contenteditable-detection pattern, but using
closestto find the nearest matching ancestor could misfire if acontenteditable="false"island is nested inside a real composer—returning false and letting the shortcut fire even though the user is still inside the composer. This divergence from the repo idiom actually stops at the false island and returns false, letting letter shortcuts fire while the caret sits in a non-editable chip — but the comment explicitly frames this as intentional, so I need to weigh whether that's a real regression or a documented exception. Tracing through the old logic for a contenteditable=false target: isContentEditable is false, so it falls to closest("[contenteditable]"), which returns the chip itself, and then checks its trimmed text length. I'm also noticing there are now five slightly different implementations across the codebase for detecting typing context (ChatView, ComposerPendingUserInputPanel, KeybindingsSettings, SettingsSidebarNav, and this new RightPanelTabs helper). That's a consistency smell, but since the PR is a local bug fix rather than a repo-wide cleanup, I'm inclined not to flag it as a blocking issue. I'm now checking whether the composer gets auto-focused on mount whenever a thread opens, since if it does, the launcher letter shortcuts would never fire at all — which would defeat the whole point of the feature rather than just narrowing it to the intended case of "don't type into an active composer."
5. git_tools — autoFocus|.focus()
6. git_tools — apps/web/src/components/RightPanelTabs.tsx
💭 I recall that commit introduced the "empty contenteditable doesn't count" logic for the shortcut. Now I want to check whether the composer is focused by default, since that would make the launcher letters unusable—let me search for composer focus logic like useComposerFocus or focusComposer.
7. git_tools — focusComposer|focusEditor|composerRef.*focus|requestFocus
8. browse_code — apps/web/src/components/ChatView.tsx
9. browse_code — apps/web/src/components/ChatView.tsx
💭 I'm tracing the focus conflict: the composer auto-focuses on thread mount/change, while RightPanelEmptyState also grabs focus on its own mount. The launcher wins when the panel first opens with no surfaces, but a later thread-switch effect or click could steal focus back to the composer, breaking the launcher's keyboard shortcuts. I need to check whether shouldTypeToFocusComposer actually prevents default or just focuses the composer, since that determines whether the old capture-phase handler on the launcher ran first and intercepted the keystroke before ChatView's handler could. With the new behavior, typing while the composer is focused just types normally, which seems to be the intended fix rather than a regression — the launcher remains reachable whenever its own container has focus. I'm now looking at whether the helper correctly handles edge cases like select elements when checking for editable targets via closest(). I'm also considering whether the composer actually uses a contenteditable attribute or a React prop, since that affects whether the check works reliably, and whether broadening the type from HTMLElement to Element could catch SVG targets too. The remaining question is whether dropping the contenteditable="false" island check is truly intentional, since it changes how chip targets inside the composer are treated.
10. git_tools — contentEditable
💭 I'm noticing those contentEditable={false} spots are likely mention chips inside the composer, while the real editable host is elsewhere. I'm trying to figure out whether keydown events ever target those chips directly, or if focus always stays on the contenteditable host itself since that's typically what document.activeElement points to.
11. browse_code — apps/web/src/components/ComposerPromptEditor.tsx
12. browse_code — apps/web/src/components/ComposerPromptEditor.tsx
💭 I'm tracing what happens when focus lands on a remove button inside a chip—since the button's closest contenteditable ancestor is the chip span marked false, the helper would return false and let the launcher shortcut fire, which seems like reasonable behavior for standalone interactive widgets. But I realize the old code actually blocked this case, since it checked textContent non-empty on that same chip ancestor. I need to check whether Lexical's chip selection plugin makes chips focusable via tabIndex, since if selecting a chip focuses it, typing should replace the node selection rather than trigger shortcuts—though with NodeSelection the keydown target likely stays on the root contenteditable div anyway. Now I'm questioning whether the test's stubbed
closestactually mirrors real DOM behavior, since realclosestwith a multi-selector list matches the nearest ancestor satisfying any selector, while the stub only matches if the full selector list was passed verbatim. Working through it, though, the stub's behavior still lines up with real DOM semantics for the nested-chip case, so there's no actual divergence there—I need to look elsewhere for a genuine inconsistency, maybe in how other composer plugins handle this pattern.
13. git_tools — tabIndex|ComposerChipSelectionPlugin|onRemoveTerminalContext|<button
💭 Since chips aren't focusable and removal happens via backspace, keydown events really just target the contenteditable root, making the contenteditable=false scenario mostly a theoretical edge case. I'm also realizing Lexical sets contentEditable based on the editable state, so when the editor becomes non-editable—like while an agent is responding—the root div's contenteditable attribute actually flips to false. If the composer is disabled during streaming and focus sits on that non-editable root, the launcher helper would return false, letting a key like "b" trigger the Browser surface shortcut instead of being swallowed—though since typing does nothing in that state anyway, this might be a fairly benign gap rather than the real issue the second commit was meant to address. I'm second-guessing whether this is actually the finding worth flagging, and want to think more carefully about what a strict reviewer would catch, particularly around how the helper function takes its input. Checking the contenteditable inheritance case — the new code using just
closest("[contenteditable]")still correctly finds an ancestor editable host, so that edge case seems fine. I'm now tracing whether focus actually breaks in a realistic flow, like opening the right panel while the composer is focused, and whether RightPanelEmptyState's focus-on-mount behavior could conflict with later user interactions in the transcript. After pressing Escape or clicking in the transcript, focus could shift back to the composer, meaning letters typed while the launcher is visually present would go to the composer instead — but that actually seems to be the intended fix, not a regression. I'm checking whether the launcher's own keydown capture handler still intercepts keys correctly when focus is on its container div, and it looks like it does via preventDefault/stopPropagation, so I don't see a clear functional regression here. I'm shifting now to evaluate this from a UI consistency angle instead. I'm parsing the instructions: if no findings, the response should just be "All clear"; otherwise it should reflect failure. Let me also check whether other typing-detection surfaces in the app, like a terminal's textbox role, might diverge from what ChatView treats as "typing" versus what the launcher considers. Checking the terminal (xterm) — it uses a helper textarea, so that's already covered, and code editors like CodeMirror or Monaco rely on contenteditable, also covered. The files panel search input is a plain input, so that's fine too, and if the right panel has no terminal open there's nothing extra to worry about. Now I'm double-checking the contenteditable attribute check against real DOM behavior, since values like "false" or "inherit" need to be handled correctly per spec. I'm also considering edge cases like select elements (type-ahead is fine to block) and checkboxes or radio/button inputs — these get treated as typing contexts even though letter keys don't actually do anything when they're focused, which matches existing behavior so it's not a new issue. The more substantive concern is that the test's mockclosestdoesn't replicate real DOM matching for the composite selector, so the claim that contenteditable="false" islands are properly excluded is unverified — in a real DOM, if the composer root itself becomes contenteditable="false" while disabled, letter keydowns would incorrectly trigger launcher shortcuts while the caret is still inside the composer.
14. git_tools — setEditable|editable|disabled
💭 I'm recalling that Lexical's
setEditabletoggles the root element's contenteditable attribute directly, setting it to "false" when the editor is disabled and "true" otherwise. Even if focus isn't forcibly removed, the scenario matters: if the composer becomes disabled while the agent is responding, pressing "b" would hit a contenteditable="false" root, so the helper wouldn't treat it as typing and the launcher would open. But ChatView's own type-to-focus selector still includes[role="textbox"], so it might still consider the element editable — I need to check whether the composer is actually disabled during that state before drawing conclusions.
15. git_tools — disabled={|composerDisabled|editorDisabled
16. git_tools — ComposerPromptEditor
💭 I need to locate where ChatComposer passes the
disabledprop down to ComposerPromptEditor.
17. browse_code — In apps/web/src/components/chat/ChatComposer.tsx, what value is passed as the disabled prop to ? Show the JSX and the definition/derivation of that value (e.g. whether it is true while the agent is streaming, when disconnected, etc). Also confirm whether Lexical's ContentEditable renders contenteditable="false" and keeps tabIndex/role="textbox" when the editor is not editable in the version used by this repo.
💭 I'm reasoning that when the composer becomes non-editable during a pending approval, focus likely shifts to body rather than being lost entirely, so the shortcut would still fire the same way as in the earlier case—making these edge cases fairly marginal in practice. Thinking more about the launcher UX, I realize the real issue is ordering: since ChatView auto-focuses the composer whenever a thread becomes active, the right panel launcher's letter shortcuts become unreachable by default, since typing would just go into the composer instead of triggering a surface. That actually seems to be the intended behavior of this PR—preventing launcher shortcuts from hijacking text typed into an empty composer. Looking closer, the old behavior was genuinely broken: with an empty composer focused, pressing any letter like "b" would get intercepted by the launcher and open a surface instead of typing into the composer, meaning the first letter of every message could get swallowed. So this fix is legitimate and matches the documented intent that surface shortcuts only work "from anywhere outside a typing context." Now I want to check whether
surfaceShortcutTargetsTypingContextaccepting a duck-typed parameter instead of a properElementtype introduces any looseness worth flagging. I'm weighing whether this duck-typed helper creates a real inconsistency with the other typing-context detection logic elsewhere in the repo, like the composer's pending-input panel check, but comparing the actual editable surfaces in the app (xterm's textarea, CodeMirror's contenteditable) suggests there's no role="textbox" edge case being missed. I'm checking the test stub'sclosestmock to see if the selector matching logic correctly handles the "input, textarea, ..." selector string against a single matched tag. I realize the stub is too simplistic to distinguish a chip nested inside an editable composer from a standalone chip — two cases that should behave oppositely, since typing near a chip inside the composer shouldn't trigger the shortcut but a standalone chip might. This means the test's claim about chips staying "shortcut-reachable" isn't really validated against real DOM nesting behavior. I'm starting to lean toward "all clear" here, though the harness's default-to-failure framing nags at me to keep digging. Looking at the diff again, the swap from anHTMLElementcheck to anElementcheck with the helper's own null-safeclosestcall seems redundant but harmless given the call site already narrows the type. I'm also checking whether a non-Element event target (likedocumentorwindow) would skip the typing-context check and let the shortcut fire unexpectedly, and now I'm wondering about events originating from inside a shadow root. Since shadow DOM retargetsevent.targetto the host element, a custom element wrapping a textarea could letevent.targetresolve to the host, causingclosestto miss the actual input and the shortcut to fire while someone's typing. ChatView apparently handles this viacomposedPath(), so I want to check whether this app has any shadow-root editable surfaces by searching forattachShadoworcustomElements.defineusage.
18. git_tools — attachShadow|customElements.define|composedPath
💭 I'm wondering whether shadow DOM components like the diff viewer or font preview could contain editable inputs that the global keydown listener would still catch, even though the launcher itself only mounts when the right panel is empty. Since this shadow-DOM retargeting issue already existed in the prior code and isn't worsened by this patch, it's not something worth flagging. Instead, I'm noticing a more concrete inconsistency: different components use different selectors to detect contenteditable context — ComposerPendingUserInputPanel checks for any non-false contenteditable, while ChatView uses a more explicit selector, which could be a real divergence worth reporting. For a standalone false-island widget outside any editable context, the
:not()variant correctly returns null and preserves shortcuts, while the PR's version wrongly treats nested-in-composer cases as non-typing. This confirms the selector-based approach handles both scenarios correctly where the hand-rolled stub can't, since it never models nesting. Now I want to check where DiffCommentAnnotation and PullRequestReviewAnnotation actually render to see if they're ever nested inside an editable host.
19. browse_code — apps/web/src/components/diffs/DiffCommentAnnotation.tsx
💭 I notice DiffCommentAnnotation wraps a textarea in a contentEditable={false} container, so focus actually lands on the textarea inside. With the new helper, checking closest match against input/textarea/select/contenteditable elements should correctly identify that nested textarea as the real target. Tracing through, the closest lookup starts at the element itself, so it correctly resolves to the textarea and returns null for contenteditable, meaning it's properly treated as a typing context — no bug there. The only way this breaks is if a non-focusable element sits nested inside a false island inside an editable region, which seems like an edge case with essentially no real-world impact given how these annotations are actually mounted. Checking other angles: the launcher container isn't editable so shortcuts fire correctly there, and card buttons behave the same way — pressing a shortcut key while one is focused still works as expected. This looks like a UI Consistency check scoped to ts/tsx/css files, and since the fix doesn't touch styling or shared primitives, it's likely a clean pass. I do want to verify whether the PR's new describe block in RightPanelTabs tests conflicts with or should have updated any existing tests that asserted the old empty-composer behavior.
20. browse_code — apps/web/src/components/RightPanelTabs.test.tsx
💭 That test comment about non-editable islands inside an editable host highlights a real gap: the stub can't represent that nesting, and in actual DOM behavior,
closestwould match the island and incorrectly report "not typing" even though the caret sits inside the editable composer, letting launcher shortcuts fire when they shouldn't. I'm also weighing whether Base UI's TooltipTrigger might render the chip as a focusable element, which would let users tab into it while still inside the composer — meaning a keypress meant for the editor could trigger the launcher instead. This is plausible but unverified, so I need to decide whether that ambiguity alone justifies failing the check, especially given the guidance to ask for evidence rather than guess when ownership or equivalence can't be proven. Looking more carefully at the DOM structure,closest()walking up from a chip or its icon would match the chip itself under the existing selector list, returning "not typing" and letting the launcher shortcut fire — even though the caret is logically still inside the composer via a NodeSelection on the decorator, where typing should actually replace the selection. That suggests the helper's logic is wrong in this nested case, and the PR's second commit may have baked in this flawed behavior with a test that can't actually distinguish the correct outcome. The fix is to use the existing repo idiom ofclosest('input, textarea, select, [contenteditable]:not([contenteditable="false"])')without the extra getAttribute check, which correctly finds the composer ancestor and preserves both goals — keeping standalone widgets shortcut-reachable while letting nested chips still count as "typing." I should also verify whether the test setup actually supports real DOM nesting scenarios, so I'm checking if jsdom or happy-dom is used anywhere in the web app's tests, maybe by searching for document.createElement usage.
21. git_tools — document.createElement|innerHTML =
💭 I notice PreviewView.test.tsx uses document, so there's DOM available in tests. I should check the vite config or test setup to confirm whether it's running in a real browser environment or jsdom.
22. git_tools — environment|jsdom|happy-dom|browser:
💭 I've gathered enough signal now. Weighing whether to post a finding, I'm checking this against the check's focus on UI consistency, shared components, and behavioral constraints—since this diff involves keyboard-handling logic, the guidelines around requiring focused tests for interaction contracts seem relevant, and tests are indeed included here. The strongest candidate finding is that the helper's use of
closest(...)combined with checking the contenteditable attribute diverges from the repo's existing typing-context pattern, and would incorrectly treat a target as "not typing" when it's inside acontenteditable="false"island nested within an editable host—like a mention chip inside the composer. That's concerning because it could let a launcher shortcut hijack a keystroke meant for the composer, which is exactly the regression this PR is supposed to fix, and the test's stub can't model that nesting to actually verify the claimed behavior. I'm now trying to assess how realistic this trigger actually is, since it depends on whether Base UI's Tooltip.Trigger adds a tabindex to non-button render targets for these chips. But thinking through Chromium's focus behavior, clicking acontenteditable="false"span inside an editable region doesn't actually move focus there since the span isn't focusable—so the keydown target stays the editor root, which narrows the real-world risk considerably. Given the guidance to only report concrete violations and avoid guessing, I'm weighing whether this is worth flagging as a failure versus recognizing that the author's intent—letting typed characters replace a selected chip via Lexical—might make the current behavior defensible after all. Still, I land on this being a legitimate, low-risk consistency fix: excludingcontenteditable="false"nodes from theclosestselector matches the existing idiom already used inComposerPendingUserInputPanel, and both files are in scope. I'll post a single inline suggestion on the relevant lines of RightPanelTabs.tsx, updating the selector to excludecontenteditable="false", and briefly mention the test stub's inability to distinguish nesting within that same comment rather than adding a second one. I also realize the existing test case would need to flip since the fix changes behavior for a standalone[contenteditable="false"]target — I'll note that the test expectation should be updated alongside the function change, since a nested editable ancestor is a typing context but a standalone non-editable island still isn't.
23. modify_pr — One finding on the new typing-context helper: the "nearest match, then check the attribute" shape answers false for targets nested inside a contenteditable="false" island that itself sits inside the editable composer, which re-opens the keystroke-hijacking case this PR fixes. The repo already has an idiom for this (ComposerPendingUserInputPanel) that covers both goals in the selector.
Agent Credits: 479 credits