webviews: the pointing hand over everything clickable (links, PR bars, Stop, menus)
Leo-ask: ask-c4fb23e93c
Problem
Leo, 2026-10-21: "links when i hover them in the chat esp the pr things i need to have the cursor switch to the clickable look", "STOP BUTTON, I HOVER OVER IT, MY CURSOR SHOULD ALSO SEE IT AS CLICKABLE.. LOTS OF AREAS LIKE THAT SHOULD BE CHANGING MY CURSOR INDICATOR TO CLICKABLE", and "why hover over doesnt switch clicky indicator :(" over the transcript row "Subagent finished: Fix merge hook: refuse only real failures · 49m 38s". The cursor stayed an arrow over the pull request bars above the composer, the composer's Stop and Mic, the Subagent finished row, menu rows, the needs-you link and many other buttons.
Root cause
The web pages chose the arrow on purpose:
pages/shared/desktop.cssis the first stylesheet of every first-party page. It setcursor: defaultonbutton,a, thebutton/link/tab/menuitem/optionroles,label,summaryandselect(R139, the native arrow). Only.selectable a[href]got the hand.- About 170 surface rules repeated it:
.acpmux-pr-dock__bar(the PR bars, real<a href>links),.acpmux-composer-box button(Stop and the other composer buttons),.acpmux-mic,.ui-menu-item,.cv-link.is-needs-you,.acpmux-chat-inspector-pr__link,agent-session/shared/styles.css'sbutton, select, and others across every page. - Rows styled with
all: unsetresetcursortoinherit, which is the page's arrow. That covers the Subagent finished row and other.cv-toolrows,.cv-worked, artifact tabs, Edit, the ask toggle and the agents pill. A class rule beats the shared rule, so those rows would keep the arrow even after the base rule changed. - On a button chip (a path chip),
.selectable *setcursor: autoon the label, so it showed the I-beam even where the chip itself said pointer.
Reply link chips (.cv-chip, <a href>) already had cursor: pointer. The native side (NSButton and AppKit chrome) is #24009.
Fix
desktop.cssgives every clickable element and everything inside itcursor: pointer. That coversbutton,a[href], thebutton,link,menuitem*,tab,option,treeitem,switch,checkboxandradioroles,summary,label[for], a label around a checkbox or radio,select, and the clickableinputtypes. The rule has zero specificity (:where), so a surface's own cursor still wins: text in fields, resize and drag cursors, zoom-in on images.- Disabled things show the arrow, with
!important: a disabled control, anythinginertoraria-disabledand everything in it, and a disabled fieldset outside its first legend. A link or other clickable that still works there keeps the hand. So does a locked choice's label (Cloud's memory sizes), except a link or button in it. A surface that wantsnot-allowedsays so with!importanton its own:disabledrule. .selectable'sautomoved under:where, so it no longer overrides the hand.ui.csskeeps the arrow only on chrome (popups, toolbars, dialogs), not on.ui-buttonor drill rows.- In the agent pane and the shared ui components, every rule that set
defaultorautoon a clickable element is removed. Afterall: unset, clickable rows set the hand again (.cv-tool, which includes the Subagent finished row, plus.cv-worked,.cv-subsession__name, artifact tabs, Edit, the ask toggle, the agents pill and the permission card's links).
The other pages' cleanups (Settings, Files, Shortcuts, Cloud, Markdown, Home, Changelog and the rest) and the gallery cursor guard are on branch chat-link-cursor-pages, to open after this lands. Until then those pages keep their own arrow rules.
Tests
test/clickable-cursor.test.ts(new):- It builds the agent pane's real stylesheet in
build-agent-pane-web.shorder and renders the pane's clickable things in jsdom. That includes the Subagent finished row, the PR chip and bar, composer Stop, Mic and Send, menu and slash rows, the chips and the clickable input types; each must computepointer. Disabled Send, menu row, Subagent row and checkbox must computedefault, and so must disabled-fieldset content, anaria-disabledgroup, an inert button, an inert subtree's link, menu row and checkbox, and a locked choice. Labels and links of enabled controls beside them must computepointer. - No stylesheet in the agent pane,
ui/orpages/shared/may setcursor: default|autoon a clickable subject. Statement at-rules are skipped, and nested:not(…)lists parse. - A clickable rule with
all: unsetmust set a cursor again. - No clickable element in those sources may carry a
cursor-defaultorcursor-autoutility class. - Checks of the scan itself: the clickable input types count as clickable, and only a selector that is really disabled is exempt.
- It builds the agent pane's real stylesheet in
test/composer-chip-press.test.tsandtest/new-tab-omnibar-pointer.test.ts(browser lane) now expect the hand over the composer's chips and the New Tab field's model chip.
Observed / Not run
Run on cmux16. Each finding's test was committed first and was red on the head before its fix. Examples: 17 cursors wrong at base; the Subagent finished row read default before the all: unset fix; labels of enabled controls, an aria-disabled group and a disabled fieldset's legend each read wrong before their fixes. The inert case was red at f1fd72f88db (6 cursors read pointer). At 7840a0e8a71:
bun test test/clickable-cursor.test.ts: 6/6 (on huge-blue).CMUX_BROWSER_TESTS=1forcomposer-chip-pressandnew-tab-omnibar-pointer: 54/54 in Chromium and WebKit at f1fd72f88db; this round changed only the inert rule.bun run checkandscripts/cmux-next/prepush-gate.pypassed.cmux-ci compile-check --app: job 729f8f132ceefbf26b0079a9 passed.- A one-off check loaded the pane's stylesheet with the same fixture in real Chromium and WebKit; every expected cursor matched.
- The pre-split head 28d2af31 also passed the gallery cursor test 14/14. That test moves to the follow-up with its page stages.
Not run: the full bun test suite at this head (at d5ec560 it matched base's 97 failures exactly).
No capture: web-only CSS, and a still cannot show the pointer shape. The browser tests read the computed cursor instead.
Changelog
- Changed: Buttons, links, chips, transcript rows, menu rows and other clickable things in the agent chat and the shared controls show the pointing hand on hover; disabled ones keep the arrow.
🤖 Generated with Claude Code
Agent session: cosmic-gray