cmux NEXT New Tab: one chat store for every page, drawn on the first frame

Merged
TE
16d ago
+797-13

Leo, 2026-10-19 12:15: after creating many chats and New Tabs fast, a torn-off window's New Tab listed different chats from the main window's. A New Tab page also drew no chats until its own daemon connection answered, and then the rows popped in.

Each page listed whatever its own daemon connection had last sent. A hidden window's connection hears changes late, so its pages kept the list they had when they connected. A new page's per-page snapshot cache (sessionStorage) starts empty.

One store for every page (webviews/src/agent-session/acpmux/newtab/chatStore.ts):

  • Every agent page shares one website data store (AgentPaneView.sharedDataStore), so this origin's localStorage lasts for the app run.
  • Each connected page writes the newest chat list it received to that store. A page that is still connecting, a gallery page and a debug page write nothing.
  • Every New Tab page lists the newest list any page wrote. Order is newest first, with ties broken by session id (#21828).
  • A page draws that list on its first frame, with no refetch and no empty list.
  • When another page writes, this page gets a storage event and updates its rows in place, keyed on session ids.
  • Unchanged writes don't re-render.
  • Only the newest 60 chats, plus any that wait on the user, are stored in full. Older chats keep only their ids, so their transcripts still don't come back as device chats.
  • If the store is blocked, or agent.pageHost gives each page its own store, each page lists its own chats as before.

Debug Settings newTab.privateChatLists (off): turning it on goes back to each page's own list. It lives in CmuxNextAgentPane, beside newTab.widgets, so CmuxNextApp doesn't change.

This branch includes #22008 (fix/newtab-settled-config, from a lane that died): the All chats list and the machine chip draw what the last page knew. With that included, this PR covers both halves of "paint New Tab from a snapshot". Once this lands, #22008 can close as included.

Testing

bun test test/new-tab-shared-chats.test.tsx (on Huge Blue):

  • Red at d360d76f (the test, with the store's seam passing each page's own list through). All 3 tests fail. In the 30-page case, page 2 (torn-off window) listed ["Chat 1"] where Chat 30, 28, 29, 26, 27, 24 was expected. A storage event changed nothing, and an unconnected write was not separated out.
  • Green at 863e1978. All 3 pass, 214 expects. The stress test opens 30 pages fast across two windows; the torn-off window's pages hear the daemon only once, when they connect. It checks:
    • every page lists one identical list, the newest the daemon reported;
    • every page after the first draws chats on its first frame;
    • every row is a chat's own title (no placeholder).

Also run:

  • bun test test/new-tab test/newtab: 92 pass, 11 skip, 0 fail.
  • vp check typecheck: clean.
  • vp lint and vp fmt on the changed files: clean.
  • New Swift test NewTabPrivateChatListsTests (key, default off, listed in Debug Settings, page context). It runs in CI.

Observed / Not run

  • Observed: the red and green runs above.
  • Not run: a tagged app build with two real windows. The sidebar's "workspace-78…108" placeholder workspace rows from the same repro are native (sidebar and workspace creation) and are not in this PR.
  • Not run: frame counts before and after.

Changelog

  • Fixed: Every window's New Tab page now lists the same chats, and a new New Tab page shows its chats immediately instead of popping them in.

🤖 Generated with Claude Code


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


Summary by cubic

Fixes New Tab pages listing different chats across windows and showing no chats until the page's own daemon connection answered. Every New Tab page now lists one shared chat store, drawn on its first frame instead of popping in.

Shared chat store

  • Every page on this Mac's daemon writes the newest chat list it received to one store in the app run's localStorage; every page reads the newest list any page wrote and updates rows in place on storage events.
  • A page lists the store's chats, else the chats its context carried (the warm snapshot), else its own.
  • Only a snapshot whose connection has read the daemon's list publishes; connecting, reconnecting, disconnected, gallery, debug, host-pushed, remote, peer, and mock-daemon snapshots never write.
  • Only the newest 60 chats, plus any waiting on the user, are stored in full; older chats keep their ids, folder, and host for the project picker.
  • Debug Settings newTab.privateChatLists (off) restores each page's own list; a page on another machine's daemon, or one whose daemon is down, turns it on from its context, before the handshake names any origin.

Settled New Tab config

  • The All chats list draws the rows the last page knew on its first frame.
  • A workspace close/open re-mounts the same page, which draws those rows and reads its first page only for a chat index push since; a brand-new page draws them, then reads its first page once.
  • The machine chip's pre-handshake cache is dropped; the machine's name comes in the page's context.