Skip to content

CmuxTabsKit: build only nearby grid cards; dormant hidden grid - #26818

Open
azooz2003-bit wants to merge 6 commits into
feat-cmux-next-tabs-kitfrom
feat-cmux-next-tabs-kit-smooth-grid
Open

azooz2003-bit wants to merge 6 commits into
feat-cmux-next-tabs-kitfrom
feat-cmux-next-tabs-kit-smooth-grid

Conversation

@azooz2003-bit

@azooz2003-bit azooz2003-bit commented Oct 28, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

CmuxTabsKit's tab grid builds only the cards near the screen and does no body work while it is hidden. These are the kit-only commits of the cmux-next iOS lists rebuild (manaflow-ai/cmux PR against feat-cmux-next from feat-cmux-next-ios-smooth-lists), proposed here so Packages/iOS/CmuxTabsKit stays identical on both branches.

  • Card residency (CardResidency, CardResidencyPlan): cards on screen are built at once, cards within 3 screens are built one per frame while the grid rests, cards past 4 screens are released. Scrolling never re-runs the grid's body.
  • Hidden grid (DormantGate): while a page shows, the grid stays mounted but holds its updates, and the active card's content is not built under its page.
  • overviewIdleChanged(_:): new source hook with a default no-op (additive; nothing public removed or changed). README updated.
  • Accessibility: a card outside the built band is a bare accessibility element in its slot (same label as the built card; activating opens the tab), so VoiceOver swiping reaches every tab.

Measurements (60 Hz iPhone 17 Pro simulator, iOS 26.5, list rig on aws-m4pro-6, before = feat-cmux-next at 4e28d9c, after = the app branch head, 2 runs each)

Case Scroll hitch ms/s (before / after) Main CPU ms/s Grid open, worst frame Memory
30 tabs 0 / 0-1.5 213-265 / 160-167 540-584 / 86-317 ms 185 / 59 MB
30 tabs, one chat streaming 173-396 / 0-1.7 930-1014 / 158-163 509-546 / 84 ms 217-222 / 61 MB
80 tabs 18-75 / 0-5.2 469-709 / 204-227 679-779 / 79-92 ms 395 / 66 MB

The after numbers include the app-side host changes (stored tabs, per-tab observation, agent cards as pictures), which stay in the app branch.

Testing

  • swift test in Packages/iOS/CmuxTabsKit on aws-m4pro-5: 62 tests in 13 suites passed (new CardResidencyPlanTests).
  • CmuxTabsKitDemo builds for the iOS simulator (aws-m4pro-6, Xcode 26.6).
  • In the app: WorkspaceTabsSourceListTests, WorkspaceTabsSourceUpdateTests on an iOS simulator, 16 passed.

Observed / Not run

  • Observed: rig frame logs, videos and stills in the app PR's report; the demo host was built, not run.
  • Not run: VoiceOver itself over the grid (the stand-in elements were checked in code only).

Changelog

none

🤖 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

The tab grid now builds only cards near the screen and stays dormant while hidden, so scrolling and page updates never re-run the grid's body.

  • Cards on screen build at once; cards within 3 screens build one per frame while the grid rests; cards past 4 screens are released.
  • Unbuilt cards remain in the accessibility tree as bare elements with the same label, so VoiceOver reaches every tab.
  • While a page shows, the grid stays mounted but holds parent updates (DormantGate), and the active card's content is not built under its page.
  • Adds overviewIdleChanged(_:) to TabsDataSource as an additive default no-op; nothing public was removed or changed.

Written for commit 19ec7fc. Summary will update on new commits.

View guided diff Turn on auto-fix

azooz2003-bit and others added 6 commits October 28, 2026 02:26
…en grid dormant

The tab grid built and laid out every card (full-page content under a
GeometryReader and scale, a shadow and a UIKit interaction view) in a plain
ZStack, on screen or not, and it stays mounted at opacity 0 while a page
shows, so every change on the page re-ran it.

- GridBand: the grid builds the cards within the visible rect plus a row
  of margin, snapped to whole rows (the band changes once per row
  scrolled, not per frame). Cards a gesture or the zoom holds (swiped,
  lifted for the menu, dragged, the zooming tab) stay built. Absolute
  layout, swipe to close, reorder, menus, Select Tabs and the zoom are
  unchanged.
- DormantGate: while the grid is hidden its parent's updates are held
  back (equatable gate); it wakes with current values when it starts to
  show.
- The active tab's card content is not built while its page is shown or
  zooming (the page draws it); a streaming chat's card re-rendered every
  token under the page.

Public API unchanged. Tests: GridBandTests (bounded band for 80 tabs,
one change per row, next row prebuilt).

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 40e42e7)
…nged

The first cull rebuilt the grid's body each time its band moved a row, so
every built card re-ran and new cards were built in scroll frames (rig:
30 tabs scrolled worse than with every card built). Residency replaces it:

- CardResidency: one observed slot per card, so building or releasing a
  card re-runs that card only; the scroll offset comes in from the scroll
  geometry callback and never re-runs the grid's body. Cards on screen
  are built at once while the grid shows (a zoom or a fling never shows a
  blank card); cards within 3 screens are built ahead one per frame while
  the grid rests; cards past 4 screens are released. A hidden grid builds
  only its on-screen cards. ResidentCard makes its card in its own body,
  so an unbuilt card costs the grid's body nothing (a built value per card
  was made for all 80 cards on every zoom frame).
- TabsDataSource.overviewIdleChanged(_:) (additive, default no-op): the
  grid is settled, not scrolling and has nothing left to build, the time
  for a source to draw costly cards. README documents it and residency.
- TabsScreen's overview is its own property (the body no longer
  type-checks in time with it inline).

Tests: CardResidencyPlanTests (80 tabs keep a bounded set with every
on-screen card built, scrolling back keeps built cards, on-screen first
then nearest); swift test on mini-cmuxs-mac-mini-3: 62 tests passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit b81d289)
…ames

overviewIdleChanged used the scroll phase, which does not report an
offset set from code (the list rig's scroll, a programmatic reveal), so a
source drew costly cards in scroll frames (rig, 30 tabs with a streaming
chat: 135-214 ms/s of hitches, main CPU 750-920 ms/s). CardResidency now
counts frames without an offset change (FrameClock, 6 frames) and reports
busy while moving or building ahead; the grid is idle only when settled,
not scrolling and not busy. Rig after: 0 ms/s hitches, CPU 160 ms/s.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit e518f67)
…rst frame

Review findings: TabOverview set CardResidency.onBusyChanged to a closure
that captured the view (and through it the source), held by the residency
it owned; and the callback was set after the first update, so a grid
shown from its first frame never reported idle. CardResidency is now
@observable with an observed busy flag that the grid reads in its idle
expression (onChange, initial: true); the callback is gone.

Tests: swift test on mini-cmuxs-mac-mini-3, 62 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 134307d)
…grid

The idle state (settled, visible, not scrolling, not moving, nothing to
build) was an observed property the grid's body read, so a scroll that
started or stopped and each build-ahead step re-ran the grid's body. It is
now computed in CardResidency (plain class) and sent to the source through
a sink that holds it weakly; a sink installed after the first update hears
the current state. The scroll phase goes to the residency directly.

Tests: swift test on mini-cmuxs-mac-mini-3, 62 passed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit 81df30c)
Review round 3 (low): cards outside the residency band were absent, so a
VoiceOver swipe through the grid stopped at the last built card. An unbuilt
card is now a bare accessibility element in its slot (no pixels, no
touches), labelled as its built card ("<title>, tab" or "<title>,
Pinned") with Open as its activation; scrolling to it builds the card.

Not covered by a unit test (a view's accessibility tree); swift test on the
kit still passes.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
(cherry picked from commit c80182f)
@coderabbitai

coderabbitai Bot commented Oct 28, 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

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.

@blacksmith-sh

blacksmith-sh Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

Found 14 test failures on Blacksmith runners:

Failures

Test View Logs
test_cmux_next_builds_the_web_bundles_before_the_swift_build (main.SwiftTestingSuit
eTimeoutTests.test_cmux_next_builds_the_web_bundles_before_the_swift_build)/
test_cmux_next_builds_the_web_bundles_before_the_swift_build (main.SwiftTestingSuit
eTimeoutTests.test_cmux_next_builds_the_web_bundles_before_the_swift_build)
View Logs
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='cmux-scheme-compile')/
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='cmux-scheme-compile')
View Logs
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='daemon-test')/
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='daemon-test')
View Logs
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='generated-files')/
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='generated-files')
View Logs
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='release-compile')/
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='release-compile')
View Logs
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='swift-test')/
test_every_mac_job_reads_the_placement (main.CmuxNextWiring.test_every_mac_job_read
s_the_placement) (job='swift-test')
View Logs
test_fork_pull_requests_into_manaflow_ai_never_reach_an_owned_runner_label (main.Fo
rkRunnerRoutingTests.test_fork_pull_requests_into_manaflow_ai_never_reach_an_owned_runn
er_label)/
test_fork_pull_requests_into_manaflow_ai_never_reach_an_owned_runner_label (main.Fo
rkRunnerRoutingTests.test_fork_pull_requests_into_manaflow_ai_never_reach_an_owned_runn
er_label)
View Logs
test_no_workflow_falls_back_to_blacksmith_outside_manaflow_ai (main.ForkRunnerRouti
ngTests.test_no_workflow_falls_back_to_blacksmith_outside_manaflow_ai)/
test_no_workflow_falls_back_to_blacksmith_outside_manaflow_ai (main.ForkRunnerRouti
ngTests.test_no_workflow_falls_back_to_blacksmith_outside_manaflow_ai)
View Logs
test_outsider_triggered_workflows_pin_a_hosted_runner (main.ForkRunnerRoutingTests.
test_outsider_triggered_workflows_pin_a_hosted_runner)/
test_outsider_triggered_workflows_pin_a_hosted_runner (main.ForkRunnerRoutingTests.
test_outsider_triggered_workflows_pin_a_hosted_runner)
View Logs
test_scans_only_test_sources (main.CommandLine.test_scans_only_test_sources)/
test_scans_only_test_sources (main.CommandLine.test_scans_only_test_sources)
View Logs

...and 4 more test failures. View all on Blacksmith: run 40664679414, run 40664679197

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

@cmux-bot

cmux-bot Bot commented Oct 28, 2026 •

Copy link
Copy Markdown

cmux status (one comment, edited in place; each line is updated by the job that owns it)

Probably yours: Fast static checks failed: a static check failed, FAILED localization (0.77s).

CI failed on 19ec7fc636 (run 40664679414 attempt 1): 3 code, 5 unknown.
macOS jobs did not run: compile and app tests were skipped: Fast static checks failure.

Job Verdict Why
Fast static checks code a static check failed
guards / workflow-guard-tests / app-host-cache unknown no known signature; failed step: Validate iOS video recording failures
guards / workflow-guard-tests / quality-determinism code a Python test failed
guards / workflow-guard-tests / ci unknown no known signature; failed step: Propagate failed independent fast guard
guards / workflow-guard-tests / release-ios unknown no known signature; failed step: Validate iOS simulator build-once contract
guards / workflow-guard-tests / app-host-execution code a Python test failed
guards / workflow-guard-tests / preflight unknown no known signature; failed step: Validate macOS runner guards
guards / workflow-guard-tests / release-tooling unknown no known signature; failed step: Validate retried Ghostty Zig package fetches
Matched log lines
Fast static checks: FAILED localization (0.77s)
guards / workflow-guard-tests / quality-determinism: FAIL: test_scans_only_test_sources (__main__.CommandLine.test_scans_only_test_sources)
guards / workflow-guard-tests / app-host-execution: FAIL: test_cmux_next_builds_the_web_bundles_before_the_swift_build (__main__.SwiftTestingSuiteTimeoutTests.test_cmux_next_builds_the_web_bundles_before_the_swift_build)

Not re-run automatically: Fast static checks, guards / workflow-guard-tests / app-host-cache, guards / workflow-guard-tests / quality-determinism, guards / workflow-guard-tests / ci, guards / workflow-guard-tests / release-ios are not machine failures.

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.

CI fast guards failed on 19ec7fc636 (https://github.com/manaflow-ai/cmux/actions/runs/40664679414). 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 branch has not been deployed

No deployments
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