webviews: the pointing hand over everything clickable (links, PR bars, Stop, menus)

Merged
TE
9d ago
+616-173

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.css is the first stylesheet of every first-party page. It set cursor: default on button, a, the button/link/tab/menuitem/option roles, label, summary and select (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's button, select, and others across every page.
  • Rows styled with all: unset reset cursor to inherit, which is the page's arrow. That covers the Subagent finished row and other .cv-tool rows, .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 * set cursor: auto on 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.css gives every clickable element and everything inside it cursor: pointer. That covers button, a[href], the button, link, menuitem*, tab, option, treeitem, switch, checkbox and radio roles, summary, label[for], a label around a checkbox or radio, select, and the clickable input types. 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, anything inert or aria-disabled and 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 wants not-allowed says so with !important on its own :disabled rule.
  • .selectable's auto moved under :where, so it no longer overrides the hand. ui.css keeps the arrow only on chrome (popups, toolbars, dialogs), not on .ui-button or drill rows.
  • In the agent pane and the shared ui components, every rule that set default or auto on a clickable element is removed. After all: 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):
    1. It builds the agent pane's real stylesheet in build-agent-pane-web.sh order 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 compute pointer. Disabled Send, menu row, Subagent row and checkbox must compute default, and so must disabled-fieldset content, an aria-disabled group, 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 compute pointer.
    2. No stylesheet in the agent pane, ui/ or pages/shared/ may set cursor: default|auto on a clickable subject. Statement at-rules are skipped, and nested :not(…) lists parse.
    3. A clickable rule with all: unset must set a cursor again.
    4. No clickable element in those sources may carry a cursor-default or cursor-auto utility class.
    5. Checks of the scan itself: the clickable input types count as clickable, and only a selector that is really disabled is exempt.
  • test/composer-chip-press.test.ts and test/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=1 for composer-chip-press and new-tab-omnibar-pointer: 54/54 in Chromium and WebKit at f1fd72f88db; this round changed only the inert rule.
  • bun run check and scripts/cmux-next/prepush-gate.py passed.
  • 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