Skip to content

New Tab: one scroller, soft edges - #19791

Merged
teamleaderleo merged 17 commits into
feat-cmux-nextfrom
fix/new-tab-scroll
Oct 12, 2026
Merged

teamleaderleo merged 17 commits into
feat-cmux-nextfrom
fix/new-tab-scroll

Conversation

@teamleaderleo

@teamleaderleo teamleaderleo commented Oct 12, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Leo, on New Tab in his build: two scrollbars (the page's own at the window edge and the Chats list's inside it), and the list sliced half a row right above the template dots.

  • One scroller. With the All chats list, the list takes the room left under the field and is the only scroller. In a short window it gives up rows instead of pushing the page past the window (before, its 240 px minimum did, and the page scrolled too). Only when there's no room for two rows of list does the page scroll, so nothing is ever out of reach.
  • Soft edges. The list fades out under its top and bottom edges instead of cutting a row, as Settings does (cmux-next: Settings scrolls in one place (cx-jh7kq) #19763). The fades sit over end padding, so the first and last rows are in full ink. The page itself never fades: the composer's model menu opens inside it.

The scrollbar's own look (thin, shown only while scrolling, app-wide) is a separate PR, because it changes the SCROLLBARS-FOLLOW-MACOS rule and its guard.

Testing

  • webviews/test/new-tab-scroll.test.ts (new, browser lane): the real gallery stages in Chromium.
    • many-chats at 900×360: one scroller (nt-all-list), the document never scrolls, the list fades, the page doesn't. Red at 84c2525 (two scrollers, no fade).
    • many-chats at 260 px: at least two rows of list, and the page can scroll to the rest. calm-default: at most one scroller, no page fade. Both added after review: red at f2af282 against the first fix (the list collapsed to nothing; the page fade dimmed the model menu).
    • Green at 5d9a03e.
  • bun run check passes. The New Tab tests (calm default, field focus, dictation, places) pass.
  • A review subagent measured the stages in Chromium; its three findings are fixed above.

Changelog

Fixed: New Tab scrolls in one place, and its lists fade out at their edges instead of cutting a row

Proof

The boundary test above (red, then green). The gallery's agent-pane.new-tab many-chats stage shows it in a short window.

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need. Autofix is disabled.


Summary by cubic

Fixes New Tab's double scrollbar: with an All chats list, the list is the page's only scroller and the document never scrolls; without the list (the calm default), the page itself scrolls. Either scroller fades out under its top and bottom edges instead of slicing a row, matching Settings behavior.

  • The list now keeps two full rows in a short window; below that the page itself scrolls, so nothing is out of reach.
  • Only the list fades; the page never fades, because a page fade would dim the composer's model menu (it opens inside the page). The fades sit in the list's end padding, so the first and last rows stay in full ink.
  • Adds a Chromium browser test (new-tab-scroll.test.ts) that stages the real gallery at 900×360 and asserts one scroller, no document scroll, and the edge fade for both many-chats and calm-default.

Written for commit 5d9a03e. Summary will update on new commits.

View guided diff Turn on auto-fix

teamleaderleo and others added 15 commits October 11, 2026 23:21
…to it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The page's focus effect depended on the input-ready callback, which the
App makes anew on each render, so every update and a click into the
focused field focused and selected it again: the caret blink restarted
and the text under the caret was selected. It runs once per input token
now (and on Cmd-L).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
… a real send button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…g around open menus

The default New Tab is the composer and the five newest chats as quiet rows, with All Chats for
the rest; the action row, Recently Closed and the All chats table leave the default (the other
templates keep theirs). Send is the composer's arrow-up disc, off while the field is empty.
An open menu, list or popover takes focus so its keys work; the global keyboard rings no longer
draw a square around it, and the focus-ring lint refuses a universal ring that would. Monaco's
find fields draw no ring of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…opovers drop it

Review: the Cloud machine list and the History list are themselves the focus target, so leaving
[role=listbox] out of the ring hid their keyboard focus. Drops the classic page's unused tools
and onRunAction props.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
The list variant set height 30px, but the card's 84px min-height won, so every row drew three
lines tall (fleet capture of the calm default; Threads and Console had it too). The calm-default
gallery play now refuses a row taller than one line: 84px before, 30px after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab-polish

# Conflicts:
#	webviews/src/agent-session/acpmux/newtab/NewTabScreen.tsx
#	webviews/src/agent-session/acpmux/newtab/screenActions.ts
#	webviews/src/agent-session/acpmux/newtab/templates.ts
…tton in it

The base's #19755 test rendered the tools row the calm default removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab-polish

# Conflicts:
#	webviews/src/agent-session/acpmux/newtab/NewTabScreen.tsx
Base's ghost buttons (#19771) restyled the New Tab tools row this branch removes: the
"Integrate a harness" note keeps the ghost classes, and the ghost balance test measures
the composer footer's folder button only.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab-polish

# Conflicts:
#	webviews/src/agent-session/acpmux/newtab/NewTabScreen.tsx
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…tab-scroll

# Conflicts:
#	webviews/src/agent-session/acpmux/newtab/NewTabScreen.tsx
@coderabbitai

coderabbitai Bot commented Oct 12, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: manaflow-ai/cmux/.coderabbit.yaml
  • Review profile: ASSERTIVE
  • Plan: Advanced
  • Run ID: 58b7ebb7-bad7-400d-8044-3dc0847d2b5b

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

teamleaderleo and others added 2 commits October 12, 2026 01:50
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Review: in a short pane the list collapsed to nothing behind the hidden page overflow, the
page's fade dimmed the composer's model menu (it opens inside the page), and the list's fades
covered its first and last rows. The list now keeps two rows and the page scrolls below that;
only the list fades, over end padding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@blacksmith-sh

blacksmith-sh Bot commented Oct 12, 2026

Copy link
Copy Markdown

Found 1 test failure on Blacksmith runners:

Failure

Test View Logs
test/chat-header-changes.test.tsx/chat-header-changes.test.tsx View Logs

Fix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

@github-actions

Copy link
Copy Markdown
Contributor

Unclear: web / react-apps-check failed: no known signature; failed step: Test webviews.

CI failed on 5d9a03eda3 (run 38226085273 attempt 1): 1 unknown.

Job Verdict Why
web / react-apps-check unknown no known signature; failed step: Test webviews

Not re-run automatically: web / react-apps-check is not a machine failure.

Written by scripts/ci/classify_failures.py (ci-failure-attribution.yml); signatures are its SIGNATURES table. A machine verdict is the runner's fault, not this PR's; yours means the failing file is one this PR changes, also red on main that main's latest full suite fails the same way, seen on other PRs that it failed on another pull request's run lately.

@github-actions

Copy link
Copy Markdown
Contributor

CI fast guards failed on 5d9a03eda3 (https://github.com/manaflow-ai/cmux/actions/runs/38226085273). It does not block the merge; a red guard merged into main breaks it for every open PR.
The log named no failed step; see the run.

Agents: python3 scripts/ci/guard_attribution.py fix applies the mechanical fixes locally. This comment is updated in place on each push.

@github-actions

Copy link
Copy Markdown
Contributor

Gallery

Review in feed

130 states changed: agent-pane.history/selected, agent-pane.history/chats, agent-pane.new-tab/template-threads, agent-pane.new-tab/template-console, agent-pane.new-tab/switcher-on, ... · 36 play checks failed

Diff page and renders (the run's gallery-pr artifact)

State Before · after
agent-pane.history/selected (Apple System Colors Light, webkit)
agent-pane.history/selected (Apple System Colors Light, chromium)
agent-pane.history/selected (Apple System Colors, chromium)
agent-pane.history/chats (Apple System Colors Light, chromium)
agent-pane.history/chats (Apple System Colors Light, webkit)
agent-pane.history/selected (Apple System Colors, webkit)

124 more on the diff page.

Live preview (follows this branch; start it with scripts/gallery-live.sh up <branch> from an hq checkout):

  • agent-pane.history/selected: /wt/fix-new-tab-scroll/?entry=agent-pane.history#/agent-pane.history/selected
  • agent-pane.history/chats: /wt/fix-new-tab-scroll/?entry=agent-pane.history#/agent-pane.history/chats
  • agent-pane.new-tab/template-threads: /wt/fix-new-tab-scroll/?entry=agent-pane.new-tab#/agent-pane.new-tab/template-threads
  • agent-pane.new-tab/template-console: /wt/fix-new-tab-scroll/?entry=agent-pane.new-tab#/agent-pane.new-tab/template-console
  • agent-pane.new-tab/switcher-on: /wt/fix-new-tab-scroll/?entry=agent-pane.new-tab#/agent-pane.new-tab/switcher-on
  • agent-pane.new-tab/recently-closed: /wt/fix-new-tab-scroll/?entry=agent-pane.new-tab#/agent-pane.new-tab/recently-closed
  • 48 more on the diff page

Play checks failed (0 px anchor movement, 0 layout shift, no frame over 33 ms unless the entry gives a reason):

  • agent-pane.composer-pickers/model-press-drag-release (Apple System Colors, webkit): play: no element for [role="option"][data-key="model:gpt-6-astra"]
  • agent-pane.composer/queued-send-remove (Apple System Colors, chromium): step 2 click .acpmux-queued:first-child button[aria-label="Remove queued prompt"]: layout shift 0.0106 (limit 0) · step 3 click button "Stop": layout shift 0.0074 (limit 0)
  • agent-pane.composer-pickers/effort-press-drag-release (Apple System Colors, chromium): step 3 up text "High": layout shift 0.0010 (limit 0)
  • agent-pane.header-tools/worktree-remove (Apple System Colors Light, chromium): step 1 click .acpmux-header-worktree: layout shift 0.0083 (limit 0)
  • agent-pane.header-tools/worktree-remove (Apple System Colors, chromium): step 1 click .acpmux-header-worktree: layout shift 0.0083 (limit 0)
  • agent-pane.transcript/thought-row (Apple System Colors Light, chromium): step 1 click button.cv-thought: layout shift 0.0004 (limit 0)
  • 172 more on the diff page

217 nondeterministic states differed from a second render of the same head (a clock or fixture leak) and are not counted as changes.

1541 unchanged · base bf3cac7 · head 5d9a03e

@teamleaderleo
teamleaderleo merged commit 496f28a into feat-cmux-next Oct 12, 2026
85 of 87 checks passed
@teamleaderleo
teamleaderleo deleted the fix/new-tab-scroll branch October 12, 2026 09:09
teamleaderleo added a commit that referenced this pull request Oct 12, 2026
* test(cmux-next): New Tab omnibar suggests reddit.com in a popover

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): New Tab suggestions are an omnibar popover

Typing in the New Tab field showed its rows inline, pushing the page down,
and "reddit" showed nothing. The rows now float in a popover anchored under
the composer on a solid theme surface. They complete addresses from visited
pages and top sites, match open tabs and workspaces, and end with "Search the
web for …" and "Ask <agent>". The top row is highlighted and taken by
Return. Blank browser tabs ("about:blank workspace-N") are never offered.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): keep blank tabs movable; complete addresses from 3 letters

The blank-page filter ran on the host context, which also feeds a split
page's Open Tabs list, so a fresh browser tab could not be moved into the
pane. Filter only the popover's rows. Two-letter words ("go", "no") stay
prompts instead of completing to google.com or notion.so.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: Opacity in Ghostty mode repaints live and leaves Material Ghostty (#19758)

* test: Opacity in Ghostty mode repaints live and leaves Ghostty

Red: with Material "Ghostty" (unset), moving Settings > Window Background >
Opacity through the page's preview does not repaint the window, and a write
leaves Material on Ghostty.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: Opacity leaves Material Ghostty and repaints the window live

With Material "Ghostty" (unset), the Opacity slider's preview never reached
the window: TerminalThemeSetting followed only the loaded settings for the
window background. It now follows the applied (previewed) background, so every
slider step repaints the window.

Writing Opacity while Material is unset now also names the material Ghostty's
background-blur gives (none, frosted, glass or clear glass), and removing either
key removes the other, back to Ghostty. The rule lives in
WindowBackgroundSetting.linkedEdits and runs in SettingsController.setSetting
and preview, so the page, the palette and the socket's settings.set agree.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: add the Ghostty blur hook to SettingsController

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: Opacity reset keeps a chosen Material; slider reloads coalesce

Red: resetting Opacity drops a Material the user chose, the flip reads the
window's resolved blur rather than Ghostty's own, and every slider step
reloads the Ghostty config. GhosttyRuntime now records the config files' raw
background-opacity and background-blur (configBackgroundOpacity/Blur), which
nothing reads yet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: Opacity reset keeps a chosen Material; coalesce slider reloads

Resetting Opacity now removes Material only when it is the one the flip
wrote (the material the config files' own background-blur gives); a material
the user chose stays. The flip and this check read Ghostty's raw blur, which
GhosttyConfigBackground records on every load of the user's config, instead
of the window's resolved blur.

A window background step that keeps the window translucent (or opaque) and
its material repaints the chrome from the theme at once
(ThemeBridge.repaintBackground) and reloads the Ghostty config 60 ms after
the last step (DemandTimer). Crossing opaque/translucent or changing the
material reloads at once, so the terminals follow.

The raw values move from GhosttyRuntime statics to GhosttyConfigBackground
(GhosttyRuntime's type was over the god-type budget), so the tests set that.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: build the app services before the stand-in Ghostty values

AppServices may load the machine's Ghostty config, which records its own raw
background values over the test's.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: Show more lines in the changes view (cx-ncc.85, 3/3) (#19780)

* test: Show more lines reads a git scope's file whole and expands between hunks (cx-ncc.85, red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: Show more lines in the changes view's git scopes (cx-ncc.85)

A git scope's file is read again as it comes into view, with every unchanged line
(git.diff paths + context_lines). With both versions known, Pierre's hunk separators
expand. A read whose changes differ from the shown diff (the file changed since) is not
used, nor is one for a rename.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: Show more lines ignores a read that does not start at the first line (cx-ncc.85, red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: Show more lines uses only a read from the file's first line (cx-ncc.85)

Review fix: a host that ignores or caps context_lines answers with the hunk's own context;
rebuilt from that, the file was too short and Pierre indexed past it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: a terminal whose shell the daemon cannot reach says so, not a silent cursor (#19773)

* test: a terminal whose host link the daemon gave up on is never a silent cursor

The daemon pushes no tab change when a terminal's host link is lost,
given up or back, and the app ignores terminal_state, so such a terminal
keeps a connected view with no label: the last screen, often a lone
block cursor, and keys that go nowhere. Red until the fix.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a terminal whose shell the daemon cannot reach says so

The daemon now pushes a terminal's tabs when its host link is lost,
given up or back, and the app labels a live terminal whose shell it
cannot reach (reconnecting, link lost, owned by another build) instead
of leaving a silent last screen. It re-attaches once the shell is
reachable again.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: push a resync reconnect's loss; the organization banner wins

Review follow-ups: a reconnect started by a resync that then fails moves
the terminal to reconnecting, which clients now hear about at once, and
a link an administrator turned off keeps its own label over a stall.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* fix(cmux-next): keep shortcut search keyboard parity (#19778)

* cmux-tui: a registry: tab's landed check builds on Windows (no bead) (#19775)

#19725 calls Mux::landed_agent_session from conversation_tabs_wire,
which builds everywhere, but agent_transfer (where it lives) is
#[cfg(unix)], so feat-cmux-next's cargo check (x86_64-pc-windows-gnu)
fails with E0599. Off unix no chat lands through agent-transfer, so the
check is false there.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: hideable chrome; Hide from the control, Show from its row (cx-wab7m) (#19783)

* test: right-click hides the mic, right-click its row shows it, across a relaunch (cx-wab7m, red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: hideable chrome; right-click a composer control to Hide it, its row to Show it (cx-wab7m)

ui/HideableChrome: a piece of optional chrome (id, Hide/Show labels, icon, persisted
shown/setShown) hides from its own right-click and comes back from its area's right-click.
ContextMenu items take an icon, and a menu inside another wins the right-click.
The composer's dictation mic and context ring use it in the agent composer and the New Tab
page: Hide Dictation Button / Hide Context Usage on the control, Show ... on the button row.
The choice stays the agentPane.showDictation / showContextUsage setting, written by the host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a popup's right-click keeps its own menu; New Tab offers Show only for what it draws (cx-wab7m, red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Agent pane: the button row's Show keeps out of popups and offers only what it draws (cx-wab7m)

ContextMenu targets="own" opens only for right-clicks on the host's own DOM, so the
project chooser's search field (a popup React events pass up from) keeps its own menu.
The New Tab row offers Show for the mic only; that page draws no context ring.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Chrome tab keys move between sidebar items from every focus (Leo 2026-10-12) (#19788)

* test(next): Chrome tab keys select sidebar items from any focus (Leo 2026-10-12)

Every Chrome-standard tab key (Ctrl-Tab, Cmd-Shift-] / [, Cmd-Opt-arrows,
Ctrl-PageDown / Up, Cmd-1…9) from a terminal, the New Tab field and an
agent composer, in one-tab and two-tab panes, compared with the sidebar
action from the same start. Leo's two Karabiner paths also go in
transition by transition with their timing through the new DEBUG
`debug.key` `sequence`, which posts CGEvent-built key events to the app's
event queue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): debug.key sequence schedules its steps with DemandTimer

check-concurrency bans asyncAfter; each step's timer posts its queued event.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(next): Chrome tab keys select sidebar items from every focus (Leo 2026-10-12)

Leo's Karabiner sends Cmd-Shift-] / [ and Ctrl-Tab, as Chrome, ChatGPT and
Claude use them. Here they ran Previous / Next, which steps the focused
pane's tabs when it has two or more, and in a terminal Ctrl-Tab and
Ctrl-PageDown / Up went to Ghostty's next_tab (the pane's tabs). Cmd-Opt
arrows focused panes.

Now Ctrl-Tab / Ctrl-Shift-Tab, Cmd-Shift-] / [, Cmd-Opt-Right / Left,
Ctrl-PageDown / Up (and Ctrl-Cmd-] / [) select the next / previous sidebar
item, and Cmd-1…8 / Cmd-9 the nth / last item. These Chrome tab keys sit
after the Ghostty config's keybinds and claims in the binding table, so a
terminal, a Ghostty keybind (super+[ included), a page and the agent
composer never take them; only cmux.json entries change them.

- Next / Previous Sidebar Item show Cmd-Shift-] / [ as their keys.
- Next / Previous Tab or Workspace keep no default key (palette only).
- Focus Pane Left / Right move to Ctrl-Cmd-H / L.
- Home's Cmd-Shift-[ / ] conversation keys go: Home is a sidebar item.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): regenerate action exports for the Chrome tab keys

scripts/measure/export-action-surfaces.sh on the fleet at the fix commit.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): chrome-tab-keys e2e reads older builds' sidebar selection and focus

Builds before the selection and focus readouts (Leo's DEV d0364e0) mark the
selected row and name no focus; the script falls back to those.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): regenerate action exports after the base merge

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* New Tab: a calm default with a real Send button; no caret restart; no ring around open menus (#19749)

* test: the New Tab field keeps its caret across updates and a click into it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the New Tab field takes the keyboard once, not on every update

The page's focus effect depended on the input-ready callback, which the
App makes anew on each render, so every update and a click into the
focused field focused and selected it again: the caret blink restarted
and the text under the caret was selected. It runs once per input token
now (and on Cmd-L).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the default New Tab is the composer and five recent chats, with a real send button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: an open menu draws no focus ring around itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a calm default New Tab with a real Send button, and no focus ring around open menus

The default New Tab is the composer and the five newest chats as quiet rows, with All Chats for
the rest; the action row, Recently Closed and the All chats table leave the default (the other
templates keep theirs). Send is the composer's arrow-up disc, off while the field is empty.
An open menu, list or popover takes focus so its keys work; the global keyboard rings no longer
draw a square around it, and the focus-ring lint refuses a universal ring that would. Monaco's
find fields draw no ring of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: lists that take Tab focus keep their ring; only open menus and popovers drop it

Review: the Cloud machine list and the History list are themselves the focus target, so leaving
[role=listbox] out of the ring hid their keyboard focus. Drops the classic page's unused tools
and onRunAction props.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the New Tab's recent chats are one-line rows

The list variant set height 30px, but the card's 84px min-height won, so every row drew three
lines tall (fleet capture of the calm default; Threads and Console had it too). The calm-default
gallery play now refuses a row taller than one line: 84px before, 30px after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the New Tab page has no tools row, so no Integrate a harness button in it

The base's #19755 test rendered the tools row the calm default removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cx-ncc.85: pass git diff paths and context_lines through the App (#19730)

* test: the pane's git diff reads pass paths and context_lines to the session host (cx-ncc.85, red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: the pane's git diff reads pass paths and context_lines to the session host (cx-ncc.85)

Show more lines asks for one file's patch with enough unchanged lines to hold
the whole file. The App decodes paths (relative, inside the repository) and
context_lines (0 to 1,000,000) and refuses anything else rather than dropping
it, which would be another read.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: import the pane module testably for its request decoder (cx-ncc.85)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* Recently Closed: leave out untouched New Tab pages and about:blank rows (#19774)

* test: closed.list marks a tab that reopens as nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: closed.list marks a tab that reopens as nothing

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: Recently Closed leaves out New Tab pages and names the rest

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: leave New Tab pages out of Recently Closed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: History and untitled chats keep about:blank out of Recently Closed

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep about:blank and New Tab pages out of History and Tab Search

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: closed chat fixtures carry no folder, as the daemon records them

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: name the closed-tabs helper's lifetime

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 5b12bae

- regenerated cmux-tui/crates/cmux-app-host/generated/cmux-app.d.ts

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38219624487

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

* cmux-next menus: highlight rows in the theme accent (#19784)

* test: themed menu rows draw the theme accent and run once

The workspace, tab and pane menus (#19754) highlight rows in the
system accent. Adds the failing behavior test for cmux-drawn rows: a
highlighted row draws the theme accent in a non-default theme scope,
and Return or a click runs the item's action exactly once. The API it
calls is a no-op stub here; the Palette accentFill/onAccent tokens it
reads are real.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: workspace, tab and pane menus highlight in the theme accent

NSMenu draws its highlight in the system accent whatever the theme.
NSMenu.themeRows(in:) swaps a menu's rows (and its submenus') for
ThemedMenuRowView, which draws the highlight in Palette.accentFill with
Palette.onAccent ink, in the scope of the view that was right-clicked.
Items keep titles, key equivalents, states and submenus; their action
goes through ThemedMenuDispatch, which validates via the original
target and runs once per opening whether it arrives from AppKit's
Return, a click on the row, or Return delivered to the row.

Applied to the three menus #19754 trimmed: the sidebar workspace and
tab menus and the strip's tab and pane menus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: Return on a themed submenu row reaches AppKit

Review of #19784 (cx-3pcgz): the row took Return whenever it was
highlighted, so Return on More or Move to Group was swallowed instead
of opening the submenu.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: themed menu rows run after tracking and redraw on arrows

Review of #19784 (cx-3pcgz):
- A click or Return on a row closes the menu and runs the action once
  the tracking loop has ended (RunLoop default mode), as a native row
  does. The selector form of RunLoop.perform keeps it Swift 6.0 safe.
- Each themed menu gets a delegate relay that redraws the rows losing
  and taking the highlight on menu(_:willHighlight:) and forwards to
  the menu's previous delegate (a choices submenu's coordinator).
- The row takes Return only for an enabled item with an action and no
  submenu; otherwise AppKit gets it.
- The row view is no accessibility element, so VoiceOver uses the
  native menu item.
- A release with no press on the row also runs it once the pointer has
  moved since the menu opened (right-drag-release).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: iconic public-domain paintings as backdrop art (cx-t2x.7) (#19776)

* test: backdrop art is iconic paintings in four collections (cx-t2x.7)

The picker groups bundled art into Dark Art, Museum Icons, Calm and Doré,
labels each "Title — Artist", ships every image at 1600x1000, and keeps
every id an earlier cmux.json saved choosing art. Fails on the current
set: two collections, title-only labels, mixed image sizes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: thumbnails for every work, a bundled dark starter set, verified downloads (cx-t2x.7)

Leo's list grew to about 170 works, so the full images no longer all fit
the app-size budget: every work ships a 480 px thumbnail, a starter set of
dark works ships its full 1600x1000 image, and the rest download once into
the backgrounds folder, kept only when the bytes match the manifest hash.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* feat: iconic public-domain paintings as backdrop art (cx-t2x.7)

Replaces the figure-drawing sketches and Met paintings with 169 well-known
public-domain works from Leo's list (cmuxterm-hq#1906): Goya, Doré,
Friedrich, Van Gogh, Bosch, Klimt, Munch and more, in four picker
collections (Dark Art, Museum Icons, Calm, Doré), labelled "Title — Artist".

- art/manifest.json is the source of truth: Commons source and license,
  copyright dates, framing, tone, measured range and SHA-256 per work.
  Gate: artist dead 70+ years and first published before 1931.
- Every image is 1600x1000 AVIF framed for a terminal backdrop (subject to
  one side, dark side for text) at <=150 KB. The app ships 480 px
  thumbnails of all works plus a 16-work dark starter set (~2 MB, down
  from 4.4 MB); the rest download once from files.cmux.com by hash
  (cmux-art-publish.yml) and are kept only when the bytes match.
- Each work sets its own default tint, and the legibility floor uses its
  measured darkest and lightest colors instead of assuming black-to-white.
- Ids saved by earlier builds map to a stand-in work.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: starter-set test reads metadata on the main actor; format the gallery fixture (cx-t2x.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate settings schema and MDM exports for the new art set (cx-t2x.7)

Generated by scripts/measure/export-settings-artifacts.sh on the fleet.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: keep non-art backdrop strings, all locales and true brightness extremes (cx-t2x.7)

Red on the current generator: it deletes backdrop.desktop/system strings,
writes art strings in 9 of the catalog's 21 locales, and measures the
brightest pixel from a 160x100 downscale, which loses a small moon.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep wallpaper strings, ship art strings in all 21 locales, measure true brightness extremes (cx-t2x.7)

- The generator no longer deletes backdrop.desktop/system strings; restored
  them from base with their 21 translations.
- Art titles, collections, label and "Public domain" now carry every locale
  the catalog does (bs, da, it, km, nb, pl, pt-BR, ru, th, tr, uk, vi added).
- darkest/lightest are the full-size extremes, so the legibility floor holds
  over moons and flames a 160x100 downscale averaged away.
- Merged the stacked JSDoc on the settings backdrops type.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate settings page strings after the base merge (cx-t2x.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate settings schema and MDM exports after the base merge (cx-t2x.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore: regenerate settings page strings from the refreshed schema (cx-t2x.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* CmuxNext: the chat header's Terminal honors split-inherit-working-directory (cx-zld.1) (#19799)

* test: the header's Terminal inherits the chat's folder only when splits inherit (cx-zld.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CmuxNext: the header's Terminal honors split-inherit-working-directory (cx-zld.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a chat of another machine keeps its folder for the header's Terminal (cx-zld.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CmuxNext: a chat of another machine keeps its folder for the header's Terminal (cx-zld.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* CmuxNext: keep AgentTabStore's length while wiring the header's local check (cx-zld.1)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* webviews: New Worktree shows the host's folder question instead of ending silently (cx-lyqio.7) (#19800)

* test: New Worktree hands back the host's folder question (cx-lyqio.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: type New Worktree's folder question, no behavior yet (cx-lyqio.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* webviews: New Worktree shows the host's folder question instead of ending silently (cx-lyqio.7)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux-next: rapid workspace create/close runs in press order (cx-7cit9) (#19757)

* test(next): rapid workspace create/close stress e2e (cx-7cit9)

30 Cmd-N at key-repeat speed with interleaved Cmd-Shift-W, some landing
on a workspace still being created; then the sidebar must match the
daemon, the new names must be consecutive, New Tab must suggest only
live tabs and workspaces, and no refusal HUD or toast may appear.

DEBUG readouts for it: debug.toast reports the refusal HUD's history and
debug.new_tab state the New Tab page's suggestions.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): rapid workspace e2e skips workspaces the sidebar never shows (cx-7cit9)

Home is in the daemon's list-workspaces but has no sidebar row; the
count and order checks compare only the workspaces shown at the start.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(next): queue workspace creates and closes per window (cx-7cit9)

Cmd-N at key-repeat speed started every create at once, so the daemon
numbered workspaces in arrival order with gaps, and a Cmd-Shift-W pressed
before a new workspace was mirrored closed the one the window still
showed. Each window's creates and closes now run one at a time, in the
order they were pressed (WorkspaceLanes). The workspace key is fixed at
the press, so a close pressed while a create waits or is in flight takes
exactly that workspace: one not started never runs, one in flight closes
once the daemon made it.

The create body moves from WindowManager to WorkspaceSpawner (the god-type
budget). Not-found refusals are quiet for keyboard and menu runs, so a
gone target is a no-op instead of a "no workspace <uuid>" notice; the
socket still answers not_found. The New Tab omnibar lists only terminal
and browser tabs, not every New Tab page as "about:blank".

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test(next): rapid workspace e2e checks press order under any placement (cx-7cit9)

New workspaces go to the top by default, so the sidebar is not in the
daemon's order; the new rows must instead be in press order. Home stays a
live workspace for the New Tab check.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix(next): capture the spawn command, not the shadowed spawn (cx-7cit9)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: 20 Cmd-N with closes in between keep every workspace in press order (cx-7cit9)

Real daemon and launch window: 20 named presses inside a second with
Close Workspace pressed after presses 5, 6, 12 and 18. Every workspace
not closed must be in the window's sidebar in press order, each close
must take exactly one, and no press may fail or show a notice.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* ci: publish art thumbnails and a public manifest to files.cmux.com (cx-t2x.8) (#19802)

* ci: publish art thumbnails and a public manifest to files.cmux.com (cx-t2x.8)

cmux.com/art and the in-app Browse page read the collection from
cmux-art/manifest.json: art/manifest.json plus each work's image and
thumbnail URL. Thumbnails upload write-once by their own SHA-256.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* ci: check ids before reading thumbnails; verify a served thumbnail (cx-t2x.8)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test: keys typed right after Cmd-N all land in the New Tab field (cx-kql5h) (#19797)

Real Chromium, both New Tab screens rendered the way the agent pane renders
them: a host update hands the screen new callbacks. Types "hello world 123"
a key at a time with a page render before each, as the bead's keys came.
Red on #19749's parent (both screens keep only "3"), green on base.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* cmux link sync pull: bring a Cloud folder back by a three-way merge (cx-rppd.4) (#19722)

* test: bring a VM's edits, commit and chat back to the Mac

Two boundary tests of cmux link sync pull against the fake fs-v1 socket
link_sync.rs already uses: an edit, a delete, a new file and a commit
made on the machine come back (the branch fast-forwards, git status
matches the machine's) while its hooks, config, ignored output and new
.env files stay there; and a file changed on both sides is a conflict
that changes nothing here, as is a branch both sides moved.

A Linux journey, the reverse of cloud_move_journey.rs, with real
processes: a Cloud-VM-shaped daemon whose chat runs in ~/work/app with
history, a running turn and a queued prompt, and a local daemon. An
edit and a commit made on the VM come back with link sync pull, then
link agent move takes the chat back under the Mac's web guard, where
its queued prompt runs once. A chat that approves everything is refused
with transfer.policy and stays open on the VM. The journeys share their
machine helpers in tests/journey_support.

cmux link sync pull does not exist yet, so these fail.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux link sync pull: bring a pushed folder back by a three-way merge

cmux link sync pull --socket LINK_SOCKET LOCAL REMOTE merges what a
push left on a Cloud machine back into the local folder, against the
push manifest as the base. Per file: the machine's still as pushed keeps
the local one, the local still as pushed takes the machine's (written,
or deleted), both changed alike settles, both changed differently is a
conflict. New machine files come back unless the working copy ignores
them, they are untracked .env files, or they are not regular files.

Any conflict changes nothing and answers
{"ok":false,"error_code":"sync_conflict","conflicts":[paths]}. Otherwise
the machine's files are first staged in a folder of the pull, the local
files are checked again, and each moves into place by rename, through no
symlink, with 0644, 0755, 0600 or 0700 permissions.

The git leg takes the same three-way rule for HEAD and for what is
staged. The push now records the branch, HEAD and a digest of
git ls-files --stage in its manifest. When the local branch is still at
the pushed commit, it fast-forwards to the machine's; a switched branch,
a rewritten history or both sides moving is a conflict. The machine's
objects (loose objects and pack data, never its pack indexes) go into a
quarantine repository whose object store borrows the local one; git
fetches only what the new commit and staged tree need from it with
transfer.fsckObjects, and the branch moves by update-ref compare and
swap. The machine's hooks, config, info and refs files never come back.

The manifest then records the merged state, so a second pull finds
nothing new. Unix only, like the rest of the link module.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: make the machine's hooks folder before writing a hook there

The push sends no .git/hooks folder, so the pull test made none to
write its machine hook into and stopped before the pull ran.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux link sync pull: clippy fixes; journeys bind install-host tabs (cx-k21v)

A registry: tab of a store takes its session only from
agent-session-start since cx-k21v, so the journeys' stand-in for a
client's chat tab records an install host.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the bring back journey starts its approve-all chat under an approving profile

Only the person's app may switch a chat to approve-all (cx-1l61), so the
journey starts that chat under a fake-approve harness profile instead.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a malicious machine can't plant a hook, climb out, half-apply or lie about the base

The fake fs-v1 machine can now plant hostile listing entries. A pull must
not bring back .GIT, git~1 or a nested repository, must refuse listed
names that climb (x/../.git/hooks/..., ../escaped.txt), must leave the
files, branch and index unchanged when it fails partway, and must use the
copy of the push record this Mac kept, refusing a missing or different
one. The names helpers are stubs with today's rules; their unit tests
cover git's HFS and NTFS .git spellings.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at f93b4f7

- cargo fmt in cmux-tui

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38208995763

* test: compare names, not paths, for a repository folder under another spelling

On a Mac's case-insensitive disk .GIT exists wherever .git does, so the
test looks for the planted names in the folder's listing.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* cmux link sync pull: treat the machine as hostile

- A listed name must be one plain component, or the listing is refused
  (sync_unsafe_name). No path that reads as .git under git's HFS and NTFS
  rules (.GIT, git~1, .git., ignorable code points) comes back, nor any
  folder below the top holding one (a nested repository).
- The base is this Mac's own copy of the push record, kept in its state
  folder by local folder and machine folder; a missing copy is
  sync_no_base and a machine record that differs is sync_base_mismatch.
- All or nothing: files are staged and the machine's commits fetched
  first; then files move into place through folder handles (openat,
  renameat, unlinkat, mkdirat with O_NOFOLLOW), each replaced or deleted
  file moved aside, and the branch (compare-and-swap) and index move last
  after this Mac's branch and index are checked again. Any failure puts
  back the files, and the branch if the index can't follow.
- A file's revision is read before and after its content, which streams
  to disk; pack and object files stream with a 4 GiB cap.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 47ca161

- cargo fmt in cmux-tui

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38209935609

* cmux link sync pull: fmt and clippy

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a hostile machine's repository shapes, hooks, fd load, races and endless answers

Red first for the re-review of the pull: a planted bare-repository shape
(HEAD, objects, refs, config), the working copy's hooksPath folder, 300
new folders under ulimit -n 128, a file edited after the plan, and answer
lines, ids and listings that never end.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 98b2197

- cargo fmt in cmux-tui

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38215666535

* fix: a pull never brings back a planted repository, hook folder or endless answer

- A folder that is a repository on either side (a .git, HEAD or
  commondir) is fenced off: nothing in it comes back or is deleted. A top
  with a repository's shape keeps out its repository names.
- The folder core.hooksPath names (and .husky above .husky/_) is fenced.
- The transaction keeps folders by path, so it holds no handle per file.
- A file is moved aside, then checked against the plan; one edited after
  the plan goes back and the pull answers sync_conflict. Files are placed
  and put back with a hard link, never over a file made meanwhile.
- Roll back reports what it could not put back and keeps the staging
  folder (also on panic).
- Answer lines, answers for other requests and listing pages are capped.
- The staged tree is rebuilt from the machine's entries in a new index,
  not taken from its cached trees. The .cmux-sync filter ignores case.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at c40e787

- cargo fmt in cmux-tui

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38216347988

* test: the hooks fence ignores case, and the git leg refuses hooks and repositories

Red first for the second re-review: core.hooksPath spelled .GitHooks,
and a machine commit or staged change that adds a hook or a bare
repository's shape.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: case-blind hook fence, and the git leg refuses hooks and repositories

- The hooks and fenced-folder checks compare names as a Mac's disk does
  (case and HFS-ignorable code points ignored).
- A machine commit or staged tree that adds or changes a path in a hooks
  folder, a .git under any spelling, or a HEAD or commondir in a
  subfolder is a conflict (.git/HEAD or .git/index).
- A repository here in a folder the push sent is fenced even when the
  machine no longer lists it.
- Roll back removes a placed file only if it is still the one placed.
- The staging folder has a name of its own and is never reused.
- All listings together are capped at 2,000,000 entries.
- core.hooksPath with ~/ or through a symlinked prefix is resolved.
- ENOTSUP and EOPNOTSUPP are one value on Linux (clippy there).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 19e2f4c

- cargo fmt in cmux-tui

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38220021551

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>

* webviews: chat-header-changes test expects the shortcut as the tooltip hint (#19805)

#19770 moved header shortcuts from the title into data-tooltip-shortcut; the
Changes test still expected "Changes (⌃⇧⌘G)" in the title.

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* New Tab: one scroller, soft edges (#19791)

* test: the New Tab field keeps its caret across updates and a click into it

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the New Tab field takes the keyboard once, not on every update

The page's focus effect depended on the input-ready callback, which the
App makes anew on each render, so every update and a click into the
focused field focused and selected it again: the caret blink restarted
and the text under the caret was selected. It runs once per input token
now (and on Cmd-L).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the default New Tab is the composer and five recent chats, with a real send button

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: an open menu draws no focus ring around itself

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: a calm default New Tab with a real Send button, and no focus ring around open menus

The default New Tab is the composer and the five newest chats as quiet rows, with All Chats for
the rest; the action row, Recently Closed and the All chats table leave the default (the other
templates keep theirs). Send is the composer's arrow-up disc, off while the field is empty.
An open menu, list or popover takes focus so its keys work; the global keyboard rings no longer
draw a square around it, and the focus-ring lint refuses a universal ring that would. Monaco's
find fields draw no ring of their own.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: lists that take Tab focus keep their ring; only open menus and popovers drop it

Review: the Cloud machine list and the History list are themselves the focus target, so leaving
[role=listbox] out of the ring hid their keyboard focus. Drops the classic page's unused tools
and onRunAction props.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: the New Tab's recent chats are one-line rows

The list variant set height 30px, but the card's 84px min-height won, so every row drew three
lines tall (fleet capture of the calm default; Threads and Console had it too). The calm-default
gallery play now refuses a row taller than one line: 84px before, 30px after.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the New Tab page has no tools row, so no Integrate a harness button in it

The base's #19755 test rendered the tools row the calm default removes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: New Tab scrolls in one place and fades at its edges

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: New Tab scrolls in one place and fades at its edges

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a short New Tab keeps two rows of list, and the page never fades

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* fix: keep two rows of New Tab list, fade only the list

Review: in a short pane the list collapsed to nothing behind the hidden page overflow, the
page's fade dimmed the composer's model menu (it opens inside the page), and the list's fades
covered its first and last rows. The list now keeps two rows and the page scrolls below that;
only the list fades, over end padding.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>

* test(gallery): typed-reddit allows the popover's own rows to reorder

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
teamleaderleo added a commit that referenced this pull request Oct 14, 2026
* test: the transcript's edges fade instead of cutting rows

Red: the transcript scroller has no mask, so rows are cut at the header
and above the composer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: fade the transcript's edges instead of cutting rows

Leo 10-14: rows were hard-cut under the header and above the composer.
The scroller now fades in under its top edge once it has left the start,
and out above the composer while more lies below (New Tab's soft edge,
#19791). A mask paints nothing, so rows dissolve into the pane's
background or the wallpaper and nothing moves; Reduce Transparency keeps
plain edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at fea26f6

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38543723901

* test: a followed reply never fades at the bottom (red)

The bottom fade switched on for a frame on each streamed chunk (24 times
in a 30-chunk reply) while the view was following it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: keep a followed reply's bottom edge plain (cx-tr80i)

The bottom fade read the offset the last scroll event left, so each
streamed chunk faded the line being written for a frame. Both edges now
read what the frame draws (the following state and its offset), and
Find in Chat's bar keeps the top edge plain so it stays in full ink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
teamleaderleo added a commit that referenced this pull request Oct 19, 2026
…1k) (#20633)

* test: a long chat opens on its tail and pages older turns in on scroll-up (cx-2lv1k)

Red: nothing calls loadOlder, so scrolling up never reaches the first turn.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: page older turns in as the reader scrolls up (cx-2lv1k)

The pane opened a chat on its newest 400 records and never asked for more:
nothing called loadOlder, and it read acpmux's paging flag as `more`
where acpmux sends `hasMore` (so missed-event catch-up also stopped after
one page). Now the transcript asks for the page before its oldest record
whenever the view is within two viewports of it, one request at a time
with a short backoff after a failure, and attach's hasMore says when the
start is already loaded. The reader's row holds its place as pages land,
at offset 0 too, and when a page completes a turn cut at its edge.

Each older page holds whole turns (the records before its first prompt
wait for the next page) and folds on its own above the held rows, so a
page costs the same however much history is loaded: re-folding every
held event cost 105 ms a page at 45k events and grew without bound.
Rows a rebuild makes again unchanged keep their objects, so drawn heights
and memoized rows survive it.

Opening asks for at most 128 KiB and each older page 256 KiB (maxBytes),
so a remote session's open stays small however large its tool output.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: the transcript's edges fade instead of cutting rows

Red: the transcript scroller has no mask, so rows are cut at the header
and above the composer.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: fade the transcript's edges instead of cutting rows

Leo 10-14: rows were hard-cut under the header and above the composer.
The scroller now fades in under its top edge once it has left the start,
and out above the composer while more lies below (New Tab's soft edge,
#19791). A mask paints nothing, so rows dissolve into the pane's
background or the wallpaper and nothing moves; Reduce Transparency keeps
plain edges.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a paged-in turn is timed from its own prompt (red)

An older page folded while the newer rows were still held, so a turn's
"Worked for" counted from the newest prompt and showed 0s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at fea26f6

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38543723901

* agent pane: time a paged-in turn from its own prompt (cx-2lv1k)

An older page folds below the held rows, so a turn ending there now
totals only the rows before them: its "Worked for" no longer counts
from the newest prompt. Review follow-ups: a steer opens no turn, so a
page never splits at one; a fork only an older page holds is kept;
a rebuild keeps an earlier row object only when its plain fields match
too; and a page in flight for the session before no longer blocks the
next session's first page.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* test: a followed reply never fades at the bottom (red)

The bottom fade switched on for a frame on each streamed chunk (24 times
in a 30-chunk reply) while the view was following it.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: keep a followed reply's bottom edge plain (cx-tr80i)

The bottom fade read the offset the last scroll event left, so each
streamed chunk faded the line being written for a frame. Both edges now
read what the frame draws (the following state and its offset), and
Find in Chat's bar keeps the top edge plain so it stays in full ink.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 6b20321

- regenerated schemas/defaults/cmux-next.json

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38545286296

* test: rows of an older page fade in once as they come into view (red)

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* agent pane: older turns fade in as they first come into view (cx-2lv1k)

Rows from a page of older history wait at 0.35 opacity out of view and
fade to full ink over the shared fade-in token the first time they
intersect the transcript view, once. Opened, live and already-seen rows
never take it; Reduce Motion draws them in full ink at once. Opacity
only, so anchoring and measured heights are untouched.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* chore(next): PR autofix at 8998da8

- vp fmt webviews/src/pages/home-channels/cloudHome.ts
- cargo fmt in Native/OptChat/optchat-chief

Autofix-Run: https://github.com/manaflow-ai/cmux/actions/runs/38651737990

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: cmux-autofix[bot] <cmux-autofix[bot]@users.noreply.github.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant