Repository navigation
iOS: the team drawer and Jump to latest settle at once with Reduce Motion on - #27776
Conversation
Red first: the tests and an unused Reduce Motion seam on each controller. With Reduce Motion on, both still animate. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0113a9b59e8132f3f6d
|
Found 5 test failures on Blacksmith runners: Failures
|
|
Owned by an agent session · took over from an earlier session · session |
…atches the bug UIKit starts an animation from what was last committed. The tests set the start position (the transcript's top, the drawer's open rest) in the same turn as the jump or close, so the animation ran from the committed position to the same end and added nothing: the controls saw no motion, and the Reduce Motion jump test passed on unfixed code. Commit the start position first, as a reader's rest frames do, and close the drawer from an open drawer at rest rather than mid-open. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c7c892d83cd307433551
…ce Motion on The drawer always slid and parallaxed, and a long Jump to latest hid the transcript, faded it back and sprang, with no Reduce Motion check. Under Reduce Motion the drawer now writes its end geometry and runs its settle steps (corners, hidden sidebar, VoiceOver focus) in place, and a jump lands at once with the transcript shown. With Reduce Motion off both are unchanged, the long jump's stale-tile cut included. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-d0113a9b59e8132f3f6d
…orts it The control saw no animation keys after a long jump with Reduce Motion off. Read the drawn opacity and scroll position too, and look again one frame later (the fade back is delayed by 50 ms), and print the state on a failure. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c7c892d83cd307433551
…n frames after a commit Neither the layers' animation keys nor the drawn frames show the long jump's motion in the test host, even with Reduce Motion off: the control saw none. Record what the jump writes on its way instead (every scroll offset and transcript opacity, by KVO): the long jump cuts to one screen short and hides the transcript; under Reduce Motion it does neither. The drawn frames are still read, after a commit (reading before one saw the old frame and called it motion). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-c7c892d83cd307433551
|
Exact-head review at Astra (gpt-6-astra, high): BLOCK.
Sol (gpt-6.1-sol, high): The P3, a stronger motion assertion in |
|
Review (gpt-6.1-sol, high): SHIP at 57b130d This head is |
|
Kit run 41101909944 at
There was no crash and the run wasn't cut short. None of these tests touch the drawer or the transcript-jump code this PR changes.
|
|
The kit rerun at
All three also failed in attempt 1. This stays unlanded until those suites pass at this head, or until a fix for those flakes lands on base. Review status: Sol SHIP at this head. |
…-ios-reduce-motion
|
cmux status (one comment, edited in place; each line is updated by the job that owns it) Review App of this head (app.zip, kept 1 day)
Shipped in:
|
…lists One conflict, ChatKeyboardDock.scroll(to:animated:): kept both #27776's Reduce Motion seam (`reducesMotion`, a jump lands at once with Reduce Motion on) and this branch's find-scroll `settle` re-target. #27776's AgentChatScreen change (Jump to latest checks Reduce Motion) merged as is. #27776's JumpReduceMotionTests built the old one-hosting-view transcript (`content:`), which this branch replaced: it now seeds a real store into the collection-view transcript and checks the same two behaviors (with Reduce Motion the long jump lands at once with no cut and no spring; without it the jump still cuts one screen short and springs). The hidden-transcript-for-a-frame step no longer exists (reused rows draw in the cut's frame), so its opacity check is gone. Tests (fleet): iOS simulator CmuxMobileKit-Package on aws-m4pro-6: every suite passed except the two jump tests, fixed here and then 3/3 green with SmoothChatTests and TranscriptRowPortTests. macOS swift test on aws-m4pro-5: 1,544 tests, 4 failures that also fail on upstream on that host (PreviewInsufficientSpace: volumeAvailableCapacityForImportantUsage reads 0 over SSH). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…-mark ops queue in order - SlashSuggestionsReachTests and JumpReduceMotionTests (new on base, #27767 and #27776) built the transcript from hosted SwiftUI content and looked for ChatTranscriptScrollView. The carry's transcript takes its store and is a ChatTranscriptCollectionView, so the tests now give it a store (80 turns for the long jump) and read the collection view. The native transcript draws the rows a long jump cuts to in the same pass, so the "hidden for the cut" check is dropped; the cut, the spring and every Reduce Motion check stay. - FeedReadMarks: a save and a remove on different threads could number their operations in one order and queue them in the other, so the newer save wrote first and the older remove then deleted it. Each now numbers and queues under one lock, and a remove deletes only while it is still the file's newest operation. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
JumpReduceMotionTests (#27776) and SlashSuggestionsReachTests (#27767) built ChatTranscriptController with hosted content and looked for the old ChatTranscriptScrollView; #27087's list takes the store and draws rows in ChatTranscriptCollectionView. The jump test seeds a long store instead of a 9000 pt view; the list's jump never hides the transcript, so the hidden frame check in the without-Reduce-Motion oracle is dropped (the cut and spring checks stay). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…#27087) (#27664) * cmux-next-ios: list scroll rig for every vertical list (DEBUG only) ListStressRig seeds workspaces, tab grid, agent chat, diff, Home or the Feed, opens it and scrolls its UIScrollView top to bottom and back with one FrameIntervalLog window per direction. verify/listperf/rig.sh builds and runs it on a fleet Mac's simulator; matrix.sh is the phase-1 measurement set. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: transcripts scroll from the top, open windows, memory, sample mode, minis Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: lane brief Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios Feed store: no per-write scans or main-thread file writes Read marks are written on a background queue (load waits for queued writes), checked against a Set, and items are indexed by id, so item(id:), mutate and mergeHistory no longer scan. The Mac's reads land in one write, and the ledger's echo check is computed once per read (binary search per terminal row) instead of every entry against every other on each tree update. Tests (aws-m4pro-5, swift test --filter FeedStoreScale|FeedStoreChurn|FeedReadMarks|TerminalFeed|Persistence): 48 tests in 10 suites passed. New: FeedStoreScaleTests (index follows writes, one write for the Mac's reads, read marks reload in order, echo set equals the old pairwise check). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: red tests for stable section ids, one inbox build per change, lookup moves Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios home: red test for one Home list build per change and a result revision Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: diff pan, wrap and split modes, artifacts grid screen, stills ListStressRig gains LIST_PAN (sideways passes, <screen>-right/left-N windows), LIST_WRAP / LIST_SPLIT for the diff screen, LIST_HOLD_X with LIST_PASSES=0 for stills, and an artifacts screen (the chat's Files and Artifacts grid; each replayed turn names a screenshot, a page and a PDF). The mock source renders its screenshot and PDF once so the rig measures the app's decode. rig.sh gets DEVICE for sim and a shot mode; summarize.py pools the pan windows. DEBUG only. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CmuxTabsKit: build only the grid cards near the screen, keep the hidden 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> * cmux-next iOS tabs: stored kit tabs, per-tab observation, per-card lookups WorkspaceTabsSource.tabs mapped every tab through kitTab on every read (several times per pass), reading each chat's displayTitle (its turns), each page's title and progress and each terminal's status, so a token or a progress tick re-ran the tabs screen and the whole grid. Cards read source.workspace (any Mac publish re-ran every card). - tabs is a stored array rebuilt by refreshTabs() when the workspace changes and per tab when that tab's live inputs change; it is published only when a kit-visible field changes (unchanged tabs reuse their kit tab). - Cards, pages and accessories read their own TabEntry (one tab), not the workspace. - Agent cards show a turns copy (AgentCardFeed) that follows the chat only while the grid shows and is not scrolling. - Terminal cards: hosts and the first screen read are made when the tab is listed (prepareCard), and card(for:in:) returns one cached view per slot (no host, Task or new AnyView per body). - The grid header is its own view (the workspace title and tab count). - List rig: logs whether the tabs case's stream runs. Tests: WorkspaceTabsSourceListTests (no rebuild on another workspace's publish, one tab rebuilt on rename, per-tab entry observation). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios lists: Workspaces and Home rows skip unchanged rows; menus on demand; render-server shimmer Workspaces: the inbox is memoized once per change of its inputs (it was built about three times per body pass); rows are an Equatable view of plain values (WorkspaceRowModel), so an update re-renders only changed rows; the long-press menu is its own view read when shown (it scanned the Mac's rows and drew nine swatches per row per pass); swatch images and SF Symbol checks are cached; section ids no longer include the first row (a new top row tore its section down); Move Up / Down availability is a lookup; the list animates on memoized keys instead of id arrays; one list-level live check replaces a per-row onChange; the working shimmer is a Core Animation sweep (no TimelineView body per frame per row); RelativeStamp reuses its formatter. Home: the filtered list is memoized and animates on its revision; rows are an Equatable view with menus built on demand; MessagesStamp works out its day boundaries and styles once per stamp; the typing dots pulse on the render server. Devices: same-name tags and other-build rows once per pass, not per row. Drawer: the touch-down walk does not descend into a vertical list's cells. Tests (mini-cmux7s, swift test --filter): WorkspaceInboxIdentityTests, HomeListMemoTests, MessagesStampTests, WorkspaceGroupSectionsTests, WorkspaceInboxOrderTests, MacFilterTests, MacHomeTests: 49 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: FAMILY and PRODUCTS for an iPad build (diff Split stills) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios diff and artifacts: O(visible) diff list, prepared off main; cheap thumbnails and code preview Diff file screen: a UICollectionView (DiffCodeList) with one cell per line, split pair, hunk header, gap or "Expand N lines", laid out at heights prepared once per (file, options, code size, width) in DiffRenderModel (rows, attributed code with word highlights, wrap indents), built off the main thread for long files. The unwrapped unified code pans sideways as the collection view's own content: the gutter and band labels are pinned by moving them in visible cells only, so a pan frame runs no SwiftUI body (was ~50 ms frames). Per-line context menus are built on demand; expand context reconfigures only the bands that kept their id. Model: DiffFile counts additions/deletions and the viewed fingerprint once per content (DiffViewedStore.key no longer hashes every line per lookup; same key), memoizes the edits line diff per edit list, and hunks(context:) is one forward pass (the per-hunk backward declaration search was O(hunks x lines) and stalled the transcript 35-48 s on the rig's 2400-line rewrite). Changes list reads the viewed set once per body; rows are Equatable. Artifacts: thumbnails decode with ImageIO downsampling and draw PDFs with CoreGraphics off main, at most two HTML snapshot web views at once, in an NSCache keyed by artifact id and kind; a cancelled load is no longer recorded as a failure forever. The code preview splits and highlights once off main with a linear tokenizer (was O(L^2) per line in body); the image preview decodes once; browser and sheet derive shown items and chip counts once per body. Tests: DiffPrepareTests (memo counts, fingerprint parity, rows/hunks/split/ whitespace, linear hunks on 2400 lines, linear highlighter, filter counts, thumbnail memo keyed by id and retried after cancellation) pass on mini-cmux10s with DiffLayout, DiffViewedStore, Round3DiffArtifact, AgentChat and ArtifactExcerpt suites. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: a tree update re-hosts no toolbar menus and no shell Sampled live updates (200 workspaces, 8 changes/s, no scrolling): main busy 8790 -> 3617 samples in 30 s. ShellView's body read store.workspaces, activeWorkspaces and macData through onChange, so every Mac tree update re-ran it and rebuilt the drawer, the tabs and the toolbars: those observers are a modifier of their own now. The Workspaces toolbar items are Equatable views from plain values (filter entries, target, recents, creation options), so the re-host SwiftUI does on any preference change skips their menus (the menus' symbol lookups were about 1 s of every 20). Row list modifiers (insets, background, separator, swipe actions) live in the row view, and each Mac's display name is worked out once per pass. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios live: red test, a repeated terminal title is not activity Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: shot waits for the run's hold line, ARGS for launch arguments; LIST_WRAP keeps the argument domain LIST_WRAP replaced the argument domain, which dropped -cmuxMobile.setupComplete (the app opened on Choose a space); it now merges into it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: transcript as a collection view of reused rows; streamed text once per frame The agent chat transcript was one SwiftUI tree for every turn in one hosting controller: every row stayed alive, each streamed token re-ran the whole transcript's bodies (turn.rows, pending scans, a full Myers diff of every edit via changeSet and the snippet's unifiedPreview, a full markdown re-parse), and opening 200 turns took 46 s and 1.25 GB. - ChatTranscriptList: UICollectionView with a diffable data source of stable row ids (session line, prompt bubble, one row per answer block, tool/card rows, footer), cells via UIHostingConfiguration of the existing row views, heights cached per (row id, revision, width) in ChatTranscriptLayout (prefix sums, binary search), and only changed rows reconfigured. ChatKeyboardDock keeps the keyboard docking, follow-bottom, jump button, interactive dismissal and composer-height insets on it; a reader scrolled up keeps their row on inserts. - ChatTranscriptBuilder: rows memoized per turn (rebuilt only when the turn or its context changes); answers parsed by MarkdownStreamParse, which re-parses only the unfinished tail (equal to a full parse, tested). - AgentChatStore: text chunks coalesce to one store change per display frame (other events flush them first, so order holds); chunk appends are in place; hasTurns/isRunning/pendingPermission/pendingQuestion/runningPlanEntries/ firstPrompt/hasAgentEdits are stored and written only when they change, so the composer, toolbar and empty state no longer redraw per token. - ChatDiffCache (per store): each edit's line diff and snippet preview lines computed once, off the main thread; the edited-files card and Changes reuse them. Snippet preview lines come from a linear pass (unifiedPreview built every hunk header with a backward search: seconds on a 2400-line file). - The tab grid card (AgentChatPreview) draws the memoized tail rows that can show instead of laying out the whole newest answer every frame. - Shimmers (WorkingLabel, Chief status text and typing dots) are repeating animations instead of TimelineView(.animation) bodies per frame; attachment thumbnails decode off main; the live call captions no longer run an animated scrollTo per token; the subagent transcript is lazy. - Disclosure state (tool rows, groups, thoughts, long diffs) lives on the surface by row key, so reused cells keep it. - Rig: LIST_SEND=1 sends a prompt through the composer; LIST_OPEN_DELAY. Tests: SmoothChatTests (coalescing, order, derived state, per-turn memo, stable ids, incremental parse equality, one diff per edit) and EditPreviewTests, run on aws-m4pro-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CmuxTabsKit: card residency instead of a scroll band; overviewIdleChanged 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> * cmux-next iOS tabs: agent cards as pictures drawn while the grid rests An agent card's preview (a page of transcript: markdown, edits, attachments) took about 45 ms to build, so a card that scrolled into view missed frames. Terminal and browser cards are already pictures of their pages; agent cards become pictures too: - AgentCardRasterizer draws a card's preview with ImageRenderer at the card's pixel resolution, one per frame, only while the grid rests (overviewIdleChanged): built cards first, then the agent cards nearest the active tab up to 30 pictures (about 48 MB at most). A running turn is drawn live (its working label animates). - AgentTabCard shows the picture while its key (turns, color scheme, page size, insets, resolution) matches, else the live preview. - DEBUG: CMUX_MOBILE_LIST_PLAIN_CARDS=kind,... draws those kinds' cards as a plain fill (the rig's per-kind card cost). Tests: WorkspaceTabsSourceListTests agentCardPictureKey and cardScaleFollowsTheKitsCard; with the earlier list tests, 9 passed in the iOS simulator on mini-cmuxs-mac-mini-3 (xcodebuild test, eager module emission off: with it on, swift-frontend crashes emitting the untouched CmuxMobileTerminal module). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios: a repeated terminal title is not activity; list-row traits stay visible to List WorkspaceSync stamped activity and re-listed on every title-changed event, including a shell setting the same title again, so each one republished every workspace to the store. It keeps each surface's last title per connection and drops repeats (TitleActivityTests, red before this commit). The list-row traits (insets, row background, separator guide, swipe actions) move back outside the Equatable row views: set inside them, List did not see them and the rows took the default insets (base vs head screenshots now match pixel for pixel, apart from relative times and animation phase). Tests (mini-cmux7s): 54 passed in 15 suites, including TitleActivityTests. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: the open window waits one beat so a stalled open's long frame is logged A main thread blocked past the 2.5 s open window resumed in the rig's own task first and closed the window before the display link logged that frame, so a stalled open read as an empty window (base chat-40 r2 logged 0 ms). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CmuxTabsKit: the grid is idle only after its offset is still for 6 frames 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> * CmuxTabsKit: observed residency busy state, idle reported from the first 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> * cmux-next iOS tabs: review fixes (frozen titles, workspace identity, pictures, rig) - A chat store or local browser model made after its tab was built (in a page or card body, or a phone-made browser tab) rebuilds that tab once, after the update: agent titles and browser titles/progress no longer freeze. The phone's new browser tab gets its model before the list. - Pages, cards and accessories read listedWorkspace, which observes the workspace's identity (Mac, folder, title, branch), written only when it changes: a replaced workspace reaches them, an activity publish does not. A vanished workspace clears the per-tab entries. - Agent card pictures: asked for again when evicted; turns with attachments are drawn live (their thumbnails load in a .task, which a picture never runs). - Terminal cards: a host made in card(for:in:) asks for the Mac's screen after the update (a cooled or swapped host no longer waits for the next grid opening). - List rig: the tabs case scrolls the full-screen grid, not the active chat shown live on its card (one run scrolled the transcript for 180 s). Tests: WorkspaceTabsSourceListTests chatStoreAppearingRebuildsItsTab and workspaceIdentityChangeNotifiesBodies; 11 passed in the iOS simulator on mini-cmuxs-mac-mini-3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: red tests for the UIKit list's apply plan and quiet unchanged publishes Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CmuxTabsKit: idle computed inside CardResidency, not observed by the 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> * cmux-next iOS tabs: hold title rebuilds while the grid moves; draw cards with inline images - A chat store made while the grid scrolls (cards built as they near the screen) held its tab's title rebuild until the grid rests; each rebuild republished the list and re-ran the grid's body in a scroll frame. - The picture exclusion for attachments (review fix) made every mock agent card live; only an image whose bytes still load (a .task) keeps its card live now. Rig, 80 tabs: 362-394 ms/s of hitches after the review fixes, 1.8 ms/s with this. Tests: WorkspaceTabsSourceListTests titleChangesWaitForTheGridToRest and agentCardPictureKey (inline and loading images); 12 passed in the iOS simulator on mini-cmuxs-mac-mini-3. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: the list is a UIKit collection with a diffable source WorkspacesList's SwiftUI List is now WorkspacesCollection: a plain UICollectionLayoutListConfiguration list, a UICollectionViewDiffableDataSource <String, String> keyed by the stable section and workspace ids, and each item's SwiftUI content in a UIHostingConfiguration (rows: WorkspaceListRow with value inputs, in a registration of their own so a reused cell keeps its graph). - Updates coalesce to one apply per run-loop turn (ListApplyPlan): a snapshot only when the order or membership changed, animated only when rows came or went or the change was the person's (and never with Reduce Motion); changed rows on screen take their new value in place, others when they come on screen. - Heights are cached per row shape (two single-line texts below the accessibility sizes) or per value, width and text size. - Long-press menus are UIKit menus built when one opens (contextMenuConfigurationForItemsAt); trailing swipe actions come from trailingSwipeActionsConfigurationProvider; no leading swipes (the drawer). - Folds, Needs You, pins, groups, pull-down search, refresh, the press fill, accessibility actions and the close popover's FrameProbe anchor keep working. - AppStore.workspaces notifies nobody when set to an equal value. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: tabs-push window around opening the workspace Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: native transcript rows laid out off main; SwiftUI only for rare cards Round 1 left the transcript at 12-13 ms main-thread CPU p99 per frame while scrolling and 11-15 ms/s of hitches while an answer streams: every row was a SwiftUI cell that UIKit sized (systemLayoutSizeFitting) as it scrolled in or grew. - ChatNativeLayout / ChatNativeWidgets: the rows a transcript is made of (markdown blocks: paragraphs, headings, lists, quotes, rules, code cards, tables; the session line, the prompt bubble, finished tool rows and groups, closed thoughts, diff snippets, the edited-files card, the answer action row) as frames and attributed text measured once per (row revision, expanded state, width, text size, code wrap). The look follows the SwiftUI views they replace. - ChatNativeLayouts: layouts made off the main thread in batches (prefetch through UICollectionViewDataSourcePrefetching, and every row still on an estimate), kept per row; the newest screen on open and the streaming answer's changed block are laid out on the spot, so a token re-measures one block, not the answer. - ChatNativeRowCell: UIKit views reused per kind (one reuse pool per row kind); its height comes with the layout, so UIKit never sizes it. Links, Copy and Share of the message, the code card's copy and Show all, tables, the prompt's menu and Select Text, tool menus, opening diffs and files, viewed marks, the action row and its More menu all work as before. Open tool rows, groups and thoughts, running rows, math and accessibility-size action rows stay SwiftUI, in per-kind pools updated in place. - DiffViewedStore.isViewed(key:) so the card's viewed marks are not rehashed per draw. - DEBUG: CMUX_MOBILE_CHAT_ROWCOST=1 logs main-thread time per row kind (the rig greps it). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: native row labels draw asynchronously at commit, as the Feed's do Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: O(changed rows) live applies on the UIKit list - A live reorder applies as the moved items only (moveItem on the shown snapshot, found as the items outside the longest kept run); a new full snapshot only for bigger changes. Changed rows on screen take their value in place. - Cells are plain UICollectionViewCells with their own hairline: a list cell asked its hosting view for the separator inset on every layout, a SwiftUI layout pass per cell (a third of the main thread while scrolling). - Heights by row shape (two single-line texts below the accessibility sizes). - The inbox sorts on keys read once per row; the inbox memo keys on AppStore.workspacesRevision instead of comparing every workspace; each Mac's manage / arrange / group rights are worked out once per pass (WorkspaceCreator.rights(onMac:)); New Workspace's recent folders are one pass over the workspaces, cached per revision. Tests (mini-cmux7s, swift test --filter): 46 passed in 13 suites, including WorkspacesListPlanTests, WorkspacePublishTests and WorkspaceInboxSortTests. WorkspacesListMoveTests are iOS-only (they build with the app, not run by swift test on macOS). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: native table rows and prompt bubbles at SwiftUI's heights Parity stills against the original SwiftUI transcript (cec0c80b8ed7) showed data rows of a table 2.7 pt taller (an SF Mono run raised its line) and two-line prompt bubbles 3 pt taller (TextKit counts line spacing under the last line). Both now take n lines of the base font's height plus the spacing between lines, from a TextKit line count. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next iOS tabs: still agent cards, held grid transcript, lazy browser models Round 2 of the tabs lane, on the integration head. - Agent cards are stills: a card's copy of its chat changes only when the source catches it up (once a second while the grid rests, one card per frame), never per streamed token. A card shows its last picture while a newer one waits (as a terminal's last frame does), so building a card never lays out a page of transcript; a running turn is drawn too. - The active chat on the grid is its live page (no static card, nothing rebuilt at settle), and that page holds its transcript while the grid shows (AgentChatSurface.holdsTranscript): one reload a second while the grid rests, and one on leaving it. ChatTranscriptList skips reloads while held and applies them on refreshHeldTranscript(). - Opening a workspace makes no web view: BrowserTabModel is made when its page first shows; a browser card uses an existing model or the streamed one, never creates one. - DEBUG: CMUX_MOBILE_LIST_NO_CARD_PICTURES=1 turns agent card pictures off. Tests (iOS simulator, mini-cmuxs-mac-mini-3): WorkspaceTabsSourceList and Update suites, 15 passed (new: gridHoldsTheActiveTranscript, cardFeedCopiesOnlyOnCatchUp, browserModelsAreMadeOnFirstPage); the CmuxMobileAgentChat suites, 156, pass but PhoneFullAccessModeRuleTests, which fails the same way on the integration head (it looks for the repo root above the synced tree). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: reload, not reconfigure, a row whose reuse pool changed A streaming block keeps its row id while its kind can change (a paragraph that turns into a list moves from the text pool to the list pool), and UIKit refuses a reconfigure that dequeues from another registration (NSInternalInconsistencyException, seen in a LIST_STREAM run). Each row's pool is recorded when its cell is configured; a change of pool reloads the row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios workspaces: separator ends where the List's did; a UI test for the list's touches - The row hairline ends 16 pt from the trailing edge, as the plain List's separator did (measured at 3x: 1062 px from x = 96 px, 48 px short of the edge); its thickness (1 pt, 3 px) and color (.separator, #E8E8E8 in light) already matched. - WorkspacesListUITests (XCUITest, real touches on mock data): a row's long-press menu (Rename from it), the trailing swipe (Rename, then Close, which asks in its popover), an offline Mac's fold line, the pull-down search, and a tap that opens the workspace. Passed on mini-cmux7s (xcodebuild test, iOS 26.5 sim). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios: review fixes for Feed read marks, artifact thumbnails, Home typing dots - FeedReadMarks.load never waits on the disk queue: the newest marks (or a removal) are kept in memory until written, and an older queued write is skipped. - Artifact thumbnails: an "Edited" detail is part of the cache key, so a file the agent changed again gets a new thumbnail; HTML snapshots use the display's scale (off screen there is no window to read it from). - Home typing dots: still dots under Reduce Motion, restarted when it changes. Tests (aws-m4pro-5): FeedStoreScale|FeedReadMarks|FeedStoreChurn|Persistence| AccountSignOut|DiffPrepare|MacHome|HomeListMemo: 55 tests in 12 suites passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * CmuxTabsKit: unbuilt grid cards stay in the accessibility tree 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> * cmux-next iOS tabs: an older card picture never hides a loading image Review round 3 (high): AgentTabCard showed any picture of the same layout, and want() ignores a key that cannot be drawn (a turn with an image whose bytes are still loading, as Mac-recorded images arrive), so the card froze on its pre-image picture and the live preview never came back. AgentCardRasterKey.mayShow: a picture shows only when drawn for this key, or, while a newer one waits, when this key is drawable and the layout matches; a key that cannot be drawn drops the old picture and the card is live until it can be drawn. Test: WorkspaceTabsSourceListTests.olderPictureNeverHidesALoadingImage. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: review round 3 fixes (accessibility, AX sizes, shimmer, layout re-entry, borders, revisions) - Native rows are accessible without VoiceOver's help: every tap area is a button element with a label (Copy, Share, Retry, a tool row, Open diff with the file and counts, Review, each edited file with its viewed state, Show more); text runs carry explicit labels, links are custom actions ("Open <text>") on their text, list markers are not read alone; the code card and table expose their own elements. DEBUG CMUX_MOBILE_CHAT_AXLOG=1 logs each kind's tree for the rig. - Every kind is SwiftUI at accessibility text sizes (ChatNativeEligibility, portable and tested), as the native rows' contract says; ChatNativeLayout.make refuses AX sizes too. - WorkingLabel's shimmer and Chief's shimmer and typing dots run on repeating keyframe animators, which start from their first frame on every appearance; the repeatForever started in onAppear could be cancelled in the same update and freeze. - A width change no longer reloads from inside the collection view's layoutSubviews (a snapshot apply re-entering its layout): the controller asks from viewDidLayoutSubviews. - Card borders re-resolve their color when light and dark switch. - Row revisions come from one counter that only rises (ChatRowRevisions), so a row that leaves and returns (a turn's working label) never reuses an old height; measured heights are pruned with the rows. Tests: ChatNativeEligibilityTests (every kind SwiftUI at AX sizes; open rows and math stay SwiftUI; a returning row gets a new revision) pass on aws-m4pro-6. They are new types, so there is no red run on the old code. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: final before/after script Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios list rig: sync the specs the package tests read (schemas/, cmux-tui/spec) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios artifacts grid: no web snapshot under a moving grid; one shared make per thumbnail The final before/after run found the artifacts grid scrolling worse than before (44-48 ms/s hitches vs 2 ms/s): late frames every ~33 frames with the main thread idle. Each was a WKWebView loading and painting an HTML tile's snapshot under the moving grid; with the cache now keyed by id, the cancelled snapshots of tiles scrolled past restarted on every pass. - HTMLSnapshotter starts no snapshot while the grid moves (ArtifactGridMotion, rest after ~6 still frames), then serves the newest asks first (tiles on screen). - ArtifactMemo runs one make per key shared by every tile asking, and cancels it only when the last of them scrolls away (nothing recorded, so it retries). - Picture thumbnails are prepared for display off the main thread. - Rig: LIST_HOLD_Y rests a still at a fraction of the list. Rig (aws-m4pro-6, artifacts LIST_COUNT=40): head 0.0 ms/s hitch, max 16.7 ms in 3 of 3 runs (was 44.5/47.6 ms/s); base 1.6/1.7 ms/s. Stills at rest show the same HTML, image and PDF thumbnails as base. Tests: full CmuxMobileKit suite on aws-m4pro-5 (memoSharesOneMakeAndStopsItWhenNoTileWaits, 5 runs stable). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios chat: warm each native row kind's first draw after the screen appears The final before/after run had one 250-280 ms frame early in a chat's first scroll, with the main thread idle (~6 ms): the first cell of a row kind building its labels' GPU text path and glyph caches. One off-screen cell per kind is now configured and drawn into a small context once the transcript screen has appeared (viewDidAppear), one per run-loop turn and never while the list moves, as the Feed warms its rows. Rig (aws-m4pro-6): chat-40 scroll 0.0 ms/s, max 16.7 ms in 5 of 5 runs (was one 257-282 ms frame in 2 of 4); chat-40 streaming 0.0 ms/s in 2 of 2. The open window's worst frame is 103-105 ms in 4 of 5 runs (199 ms once), with nothing on screen moving, against 51-115 ms before the warm-up. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ios): list store changes before reading the tabs source in four base tests WorkspaceTabsSource (#27087) lists store changes on the next main-queue turn and reads each tab's listed copy. Four tests from base (#27084, #27322) changed the store and read the source in the same turn, so they saw the old tabs. They now call `source.refreshTabs()` first, as #27087's own WorkspaceTabsSourceListTests do: restartShellOnlyOnAnEndedShell, tabGroupsFromTheTabMenu, tabGroupPageActions and aBlankBrowserTabHasNoAddress (fleet kit run at 3f8df7b2, step 48cd3bbd368434dadcb35501; none fail on base c00c3219). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-89c8bbec072d313a51cd * chore(ios): drop an editor temp file the merge staged by mistake WorkspaceTabsSource.swift.tmp.729016.19be7d478e30 was an edit hook's temp copy left in the worktree while the merge's conflicts were resolved, and `git add` of the kit folder took it. It is no source file (SwiftPM builds only .swift); nothing references it. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-89c8bbec072d313a51cd * fix(ios): review P2s: a live reorder moves rows in place; opening a diff never diffs on the main thread Two findings of the independent slice reviews of #27664: - Workspaces list: `WorkspaceInbox.Memo.workspaceIDsKey` changed on a reorder too, and the list animates a full snapshot when that key changes, so a live reorder (a row going to the top while a Mac publishes) skipped the moved-rows path it was meant to take. The key now changes only when the set of workspace ids does. Test: memoBuildsOncePerChangeOfItsInputs reorders the rows and expects the key unchanged. - Agent chat: tapping an edit snippet (or its Open Diff menu row) before the turn's diff landed ran `changeSetNow`, the whole Myers diff, on the main thread (hundreds of ms on a large rewrite). The tap now awaits `ChatDiffCache.changeSetReady`: the cached diff, else the one computing, else one computed off the main thread. Test: openingChangesBeforeTheDiffLandsNeverDiffsOnTheMainThread. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-89c8bbec072d313a51cd * fix(ios): review P2s in the native chat rows and the artifacts grid Findings of the independent slice reviews of #27664 (all in #27087's native lists): - A markdown table's links did nothing when tapped and had no VoiceOver action: the cell hit-tested only its own text runs. `ChatTableView.link(at:)` finds the link under a tap, and each cell label gets the same "Open <link>" actions as the row's text. - Long-pressing the second file of a multi-file edit snippet opened, copied or showed the first file. The row's menu now gets the file of the card under the press. - With "Wrap code and diff lines" on, the native snippet still drew its diff lines unwrapped. Finished edit snippets now draw as the SwiftUI snippet (which wraps) while the setting is on. - VoiceOver skipped a native snippet's diff lines. Each line scroller is an element, read with "Added" / "Removed" for its markers. - ArtifactMemo: a cancelled make that ended late decremented the waiters of the make that replaced it, so cancelling one of two tiles stopped the other's thumbnail. Waiters are now per make (a generation), and each caller leaves once. Test: aLateCancelledMakeNeverStopsItsReplacement. - HTML thumbnails: a page whose load never finished held one of the two snapshot slots until it did, blocking every later HTML thumbnail. A load now ends after 5 s or when its tile goes away (stopLoading), and gives its slot back. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-89c8bbec072d313a51cd * test(ios): the main-thread diff test belongs in SmoothChatTests (its store fixture and main actor) openingChangesBeforeTheDiffLandsNeverDiffsOnTheMainThread was added after the closing brace of SmoothChatTests, into EditPreviewTests, which has neither Silent nor Self.turn and is not main-actor isolated: the kit tests did not compile (fleet step eb392c6e367a7737771570e1). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-89c8bbec072d313a51cd * test(ios): regressions for review findings at a9366668 Find in Chat's current match reads as selected to VoiceOver; an edited-files card's viewed key follows the file's content; a row queued for an off-main layout before an environment change is laid out for the new one; a read-marks save queued after a removal lands. Each fails at a93666681ed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * fix(ios): review P1/P2s after the base merge - Open on iPhone / Mirror from Mac rebuild the tab in the same turn, so the toolbar follows the page the person chose (NativeBrowserTabTests failed at a93666681ed: the tab list is a listed copy since #27087). - Find in Chat: the current match's row reads as selected to VoiceOver, as base's SwiftUI find marks did (native elements and hosted rows). - Edited-files cards: the viewed key is DiffViewedStore.key (the content hash is computed once per content); the cache keyed on counts could show a changed diff as viewed. - Native row layouts: a row queued before an environment change (rotation, Dynamic Type, code wrap) or for an older revision is queued again, not laid out on the main thread as it scrolls in. - Demo open-changes commands await the off-main diff; changeSetNow is gone, so no path diffs on the main thread. - Artifacts: a reused code preview never shows the previous file's lines; an HTML thumbnail waiter cancelled while the grid moves leaves the queue at once. - Feed read marks: queued file operations are numbered, so a write queued before a removal never takes the marks of a save queued after it. - listperf rig: seed-xcf refuses to copy a directory onto itself. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): a native heading reads as a heading; a tab moving screens updates its card chip Both fail at 1ad5d8f6377 (review findings). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * fix(ios): review P2s at 1ad5d8f6 (find reveal, native block parity, screen chips) - Find in Chat lays the matched row out before scrolling to it: a row far up may still wait for its off-main layout, and the reveal used its estimate. - Native answer rows: a markdown heading carries VoiceOver's heading trait; block and code menus offer Select Text (the SwiftUI blocks' text selection). - Grid cards: the observed workspace identity includes its screens, so a tab moving between the Mac's screens updates its card's screen chip. - Tests: the thumbnail-memo waits record an issue when they run out. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * fix(ios): a replaced artifact load writes nothing; the chat stress rig's later passes go down ArtifactPreview's load task checks cancellation after each wait, so a load replaced by a newer artifact or a retry never overwrites its preview (no unit seam: a SwiftUI view task). ListStressRig's chat passes after the first start at the top and now scroll down (they reached the top at once and measured nothing). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): the find-reveal layout test picks any native row still waiting for its layout Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): a closed chat's transcript releases its store and surface Regression test for a review finding: ChatTranscriptList's pending observation held the store and surface strongly. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): an unbuilt tab card offers Close to VoiceOver Regression test for a review finding: CardStandIn exposed only Open, while a built card offers Open and Close. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * fix(ios): an unbuilt tab card offers Close; the transcript's observation holds its store weakly CardStandIn now offers the built card's actions (Open, and Close for a regular card). ChatTranscriptList's observation callback captures the store and surface weakly, so a closed chat is released without waiting for one more change. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): the running-program close test waits for the Mac's answer closingATabWithARunningProgramAsks read the question's message as soon as it showed. When the simulator is busy the 250 ms quick answer is missed, the question shows as pending first, and the answer fills it in a moment later (the designed path); the test failed on that timing. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): a minified diff line is prepared off main; the tab grid builds ahead only at rest Regression tests for two review findings: DiffRenderer prepared any file of up to 300 lines on the main thread whatever its length in text, and CardResidency kept building off-screen cards one per frame while the grid scrolled. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * fix(ios): prepare a long-text diff off main; build tab cards ahead only while the grid rests DiffRenderer prepares a file in place only when it is under 300 lines and 24,000 characters, so a minified line no longer blocks the main thread. CardResidency builds ahead of the screen only while the grid is not scrolled by a finger or from code, and resumes when it is still. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> Agent-Session: https://cmux.com/deeplink/agent?id=aid-7aa94f16aeac121e03bf * test(ios): base's transcript tests build the list's controller JumpReduceMotionTests (#27776) and SlashSuggestionsReachTests (#27767) built ChatTranscriptController with hosted content and looked for the old ChatTranscriptScrollView; #27087's list takes the store and draws rows in ChatTranscriptCollectionView. The jump test seeds a long store instead of a 9000 pt view; the list's jump never hides the transcript, so the hidden frame check in the without-Reduce-Motion oracle is dropped (the cut and spring checks stay). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * cmux-next-ios tabs: a store change reaches the stored tab list in the same turn The stored kit tab list was rebuilt on the next main-actor turn after a store change, so code that changed the store and read the tabs (or a tab's entry) in the same turn saw the old list (CI's WorkspaceTabsSourceTests on #27087: restartShellOnlyOnAnEndedShell, tabGroupsFromTheTabMenu, aBlankBrowserTabHasNoAddress, closingATabWithARunningProgramAsks). 3981ade33a1d made those tests call refreshTabs() by hand; app code that does the same (change a tab, then read the list) still read the old one. A change now marks the list (or the one tab) stale synchronously and the next read rebuilds it; otherwise the rebuild still runs on the next turn. Still O(changed tabs). The tests' manual refreshTabs() calls are removed: they pass without them. From #27087 (b2e7a1ad56a9), onto #27664. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ios): iOS test build and the long-jump landing check - DiffRenderer.preparesInPlace and its limits are nonisolated: a pure size check, called by DiffInlineBuildTests from a nonisolated test, which did not compile on the iOS simulator (the macOS run skips the iOS-only suite). - withoutReduceMotionTheLongJumpStillAnimates: the jump re-targets after rows measured by its cut move the newest turn's bottom (5 pt here); the landing checks allow that. iOS simulator (aws-m4pro-6, iOS 26.5): CmuxMobileAgentChatTests 249 passed. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ios): a probed localhost port is returned only once its listener closed NativeBrowserTabTests.freePort() bound a port with a probe forwarder, stopped it and returned at once. The probe's listener holds the address until it reports cancelled, and the forwarder under test is another instance, so it did not wait for it: its bind lost the port ("Another app on this iPhone uses port ...") and aMacThatForwardsOpensItsLocalhostPageOnThePhone failed 4 of 4 on a busy host. The probe now waits for its own closing (LocalhostForwarder.closedEarlier, made public: a stopped port is free for another listener only once it returns). iOS simulator (aws-m4pro-6): NativeBrowserTabTests 13 passed, 5 of 5 runs. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(ios): CardViewedKeyTests and CodePreviewLinesTests are iOS-only Both test iOS-only types (ChatNativeRowActions, CodeView) but sat outside the file's #if os(iOS), so CmuxMobileAgentChatTests did not compile for CI's macOS swift test (the iOS simulator run passed). Moved into the iOS block next to DiffInlineBuildTests. macOS swift test (aws-m4pro-5): compiles; 1,566 tests, 4 failures that also fail on feat-cmux-next on that host (PreviewInsufficientSpace over SSH). Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cmux-next-ios): regressions for the #27664 review blockers and P1s (red) Tests first, against de6d2493 behavior, with test-only seams (no behavior change): - NativeRowFoldTests: reopened row takes its open height (blocker 1), a native line holds its place through open and close while following (blocker 2), a text size switch moves rows between native and SwiftUI cells (P1). - AgentCardPictureTests: a card picture shows an image attachment (blocker 3), a card drawn before its diff landed is drawn again (blocker 4), a card takes the grid's page size before any page shows (P1). - TitleActivityTests.aCoveredTitleEventDoesNotHideTheNextRealOne (P1). Seams: ChatTranscriptController.transcriptList/isHoldingFold, ChatTranscriptList cell(for:)/visibleRowIDs/row(_:), ChatNativeRowCell.perform internal, AgentCardRasterKey.current (the one key builder for built and ahead cards), WorkspaceSync.handle(_:sequence:) and an internal snapshotSequence. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): transcript rows re-measure on a disclosure change, native lines hold their place, text size switch swaps cell types Review of #27664 at de6d2493, blockers 1 and 2 and the ChatTranscriptList P1: 1. ChatRowCell.preferredLayoutAttributesFitting reused the cached height while the row's revision matched, and a disclosure is not part of the revision: a reopened or reclosed thought/tool row kept its other state's height. The layout's measured heights are now keyed on the row's disclosure states too (ChatTranscriptLayout.Item.fold, ChatRowLookup.fold), and a cell's known height stands in only for the first sizing pass after a configure (a later pass is the content changing size, which is measured). 2. Native rows toggled disclosures with ChatMotion.set and skipped the transcript's hold, so a chat following its newest row re-pinned and the tapped line jumped. Native taps now go through toggleDisclosure(context.foldHold) like the SwiftUI rows (ChatMotion is gone), and the hold lets go only after the list has applied the disclosure change (ChatTranscriptController.endFoldHold defers to the next main turn while ChatTranscriptList.disclosuresPending, then reloadIfDisclosuresChanged): a native tap animates nothing in SwiftUI, so its completion could come before the row's reload. P1. environmentChanged chose rows to reload after installing the new environment, so at an accessibility size no row counted as native and visible native cells stayed native. It now takes the rows native before the switch plus every shown row whose cell type the new environment changes. Red at 1745059a9969, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileAgentChatTests/NativeRowFoldTests red: aReopenedRowTakesItsOpenHeight (closed again 294 pt taller), aNativeLineHoldsItsPlace... (line moved while opening; still following), aTextSizeSwitchMoves... (native cells at AX size) green: 3/3 passed; JumpReduceMotionTests 2/2 passed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): agent card pictures wait for thumbnails and diffs, and take the grid's page size Review of #27664 at de6d2493, blockers 3 and 4 and the WorkspaceTabsSource P1: 3. Sent image thumbnails were decoded in SentAttachments' .task, which a synchronous ImageRenderer (AgentCardRasterizer.render) never runs: tab-card pictures showed the placeholder for good. Thumbnails now decode off the main thread into one cache (SentAttachmentThumbnails, per image and size); the live row still decodes in its .task and the body reads the cache synchronously, so a picture draws the decoded image. The card key waits for them (AgentCardRasterKey.pictures from AgentChatPreview.picturesReady, which starts the missing decodes and is observed), so the picture is drawn once they land. 4. A card drawn while the diff cache was cold kept the loading snippet: the key now carries the chat's ChatDiffCache.version (AgentCardRasterKey.diffs), so the picture is drawn again when diffs land (the older picture stands in meanwhile). P1. The page size came only from WorkspaceTabPage.onAppear: a grid shown first, or rotated with the grid up, drew cards at 402x874. WorkspaceTabCard reports the size the kit lays it out at (the full page, scaled down) to WorkspaceTabsSource.rememberPageSize, and the page size is observed so the cards follow it. AgentCardRasterKey.current builds the key for built cards and those drawn ahead alike. Red at 1745059a9969, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileWorkspacesTests/AgentCardPictureTests red: aCardPictureShowsTheImageAttachment (8 red pixels), aCardDrawnBeforeItsDiffLandedIsDrawnAgain (key unchanged, picture done), aCardTakesTheGridsPageSizeBeforeAnyPageShows (402x874) green: 3/3 passed; WorkspaceTabsSourceListTests 14/14 passed (The test helper now passes the turns to AgentCardRasterKey.current instead of setting them after it, so the readiness is read for those turns.) Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): a title event the snapshot covers no longer hides the next real one Review of #27664 at de6d2493, the WorkspaceSync P1: enqueue recorded a title (and its activity) before the snapshot-sequence filter dropped the event, so a stale titleChanged made the next real one with the same text look like a repeat. The filter now runs first. Red at 1745059a9969, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileLiveTests/TitleActivityTests red: aCoveredTitleEventDoesNotHideTheNextRealOne (activity set by the covered event; the real one ignored) green: 2/2 passed Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * feat(CmuxTabsKit): tell a card's content the page size it is laid out at Additive, kit-only: `EnvironmentValues.tabsKitCardPageSize` is set on grid cards (TabCard), the zoom's card and the swipe neighbor's card (the full page scaled into the card), and left nil where a card is laid out at its own size (its long-press menu preview, CardMenuPreview, and its drag preview). A host can then take the page size from the grid and never from a card's own geometry (review of #27664 at 2a38f796, item 1). Tests: CardPageSizeTests (aGridCardIsToldThePageSize, aCardsMenuPreviewIsToldNothing), run with xcodebuild test -scheme CmuxTabsKit on the iOS simulator on aws-m4pro-6. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cmux-next-ios): regressions for the 2a38f796 review (red) - AgentCardPictureTests.aCardsPreviewLeavesThePageSize: a card laid out at its own size (its long-press menu or drag preview) must not set the page size (item 1, P1). The grid-first test now hosts the card the way the grid does (tabsKitCardPageSize set). - SentAttachmentThumbnailsTests.bytesThatDoNotDecodeCountAsReady (item 2) and aBurstPastTheBoundSettles (item 3). - NativeRowFoldTests.theReadersOwnScrollReleasesAHeldLineAtOnce (optional item). Seams (no behavior change): SentAttachmentThumbnails byteLimit (not enforced yet), residentBytes, decodes. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): agent cards take the page size from the grid, never a card's own size Review of #27664 at 2a38f796, item 1 (P1, a regression of b55cb9b6): WorkspaceTabCard reported its own laid-out size to rememberPageSize, and a card is also laid out at its own size (about 177x249) by its long-press menu preview (CardMenuPreview) and its drag preview. A long press set the page to that size; the size is observed, so every agent card redrew with its transcript 177 pt wide (and the wrong bottom inset) until a page appeared. The card now takes the size TabsKit lays the grid's cards out at (tabsKitCardPageSize, 712fa07c), which those previews leave nil. Red at a042a3af431a, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileWorkspacesTests/AgentCardPictureTests red: aCardsPreviewLeavesThePageSize (page became the card's size) green: 4/4 passed (aCardTakesTheGridsPageSizeBeforeAnyPageShows too); WorkspaceTabsSourceListTests 14/14 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): the thumbnail cache is bounded by bytes, settles on bad bytes and never re-decodes in a loop Review of #27664 at 2a38f796, items 2 and 3 (P2): 2. Bytes UIImage(data:) cannot read stored nothing and bumped nothing, so ready() stayed false (the card a live preview for good) and every body started the decode again. A failed key is recorded and counts as ready (the picture draws the placeholder, as base did). 3. 96 thumbnails at about 1.9 MB each could hold about 180 MB, and an eviction bumped the shared revision, so every card re-read ready() and decoded again. The cache now keeps at most 32 MB of pixels (least recently used out, cost = pixel bytes). Readiness only goes forward: a thumbnail once decoded stays ready after an eviction, and the revision bumps only when a key first becomes ready. A picture about to be drawn checks residency (AgentChatPreview.picturesResident); an evicted thumbnail is decoded again for that card alone, which waits at the end of the rasterizer's queue. Red at a042a3af431a, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileAgentChatTests/SentAttachmentThumbnailsTests red: bytesThatDoNotDecodeCountAsReady (never ready, 29 decodes), aBurstPastTheBoundSettles (23,040,000 bytes kept, bound 4,194,304) green: 2/2 passed; AgentCardPictureTests 4/4 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): the reader's own scroll releases a held disclosure line at once Review of #27664 at 2a38f796 (optional): endFoldHold waits for the list to show a pending disclosure change before the line lets go; a drag, scrollTranscript (Jump to latest, Find) or a new session is the reader's intent and now releases it at once (endFoldHold(force:)). Red at a042a3af431a, green here (/tmp/smooth27664/ios.sh (xcodebuild test, CmuxMobileKit-Package, iOS sim on aws-m4pro-6)): -only-testing:CmuxMobileAgentChatTests/NativeRowFoldTests red: theReadersOwnScrollReleasesAHeldLineAtOnce (still holding after scrollTranscript) green: 4/4 passed; JumpReduceMotionTests 2/2 Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): a row keeps its height for each disclosure state The measured-height cache kept one disclosure state per row, so a row closed again fell back to its estimate until a cell measured it again: a jump below the row, and on CI's slower simulator NativeRowFoldTests.aReopenedRowTakesItsOpenHeight still saw the estimate after 30 frames (94 pt for a 32 pt line) at f8ed786eeaff. Each row now keeps its measured height per state (the newest four), so a close takes the closed height at once. Red: CI iOS simulator at f8ed786eeaff (aReopenedRowTakesItsOpenHeight, 94 vs 32). Green: NativeRowFoldTests 10/10 on aws-m4pro-6; full iOS simulator suite green; macOS swift test 1,583 with only the 4 host-only PreviewInsufficientSpace failures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cmux-next-ios): a row's height for a disclosure state is the one it settled at (red) CI at f8ed786e failed NativeRowFoldTests.aReopenedRowTakesItsOpenHeight in the full suite (closed again 94 pt, closed 32 pt), timing-dependent. A measurement trace of the open, close, reopen cycle (layout records with fold and revision, cell fits and configures) shows, on every cycle and also in passing runs: after a disclosure change the list installs the new state's layout items, and the snapshot apply's layout pass fits the visible cell while it still draws the old state (`fit measured 326 attrs 32`), records that under the new state's fold (`rec fold <closed> h 326`), and the reconfigure then hands it to the cell as its known height (`cfg known 326`, `fit known 326`). Only SwiftUI's own later size invalidation corrected it (`fit measured 32` -> `rec fold <closed> h 32`); when that invalidation lands mid-update or late (a loaded runner), the wrong height stays. aRowsHeightForAStateIsTheOneItSettledAt orders the reload and its layout pass before SwiftUI updates the row and checks the height kept for the state. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * fix(cmux-next-ios): a row's height for a disclosure state is measured from its settled drawing Root cause of NativeRowFoldTests.aReopenedRowTakesItsOpenHeight failing in CI's full suite at f8ed786e (closed again 94 pt, closed 32 pt). I traced the open/close cycle (layout records with revision and fold, cell sizing passes and configures, frame by frame) in the full iOS suite on aws-m4pro-6, where it fails about 2 runs in 3. The failing run's close of row c-c28: reload disclosures 2 shown 1 fit measured 94.0 attrs 32.0 shown 115/<open> (apply's pass; cell still configured open) inv c-c28 h 94.0 laid 32.0 item 115/<closed> cfg c-c28 known nil shown 115/<closed> fit measured 94.0 attrs 94.0 shown 115/<closed> (right after the reconfigure) inv c-c28 h 94.0 ... kept for <closed> frames 0..29: height 94 (no later sizing pass) ChatRowCell sized its hosted view (systemLayoutSizeFitting) before that view had laid out. UIHostingConfiguration applies a new configuration, and a disclosure change its views read themselves, in its own layout pass. Sized before it, it reported its old or half-applied drawing (94 = the line plus part of the closing output). That height was kept as the row's height for the new state, and the view's later update did not always size the cell again. The same trace in a deterministic ordering (the list's reload and layout pass before SwiftUI updates the row) showed the stale height kept for both states on every cycle (aRowsHeightForAStateIsTheOneItSettledAt, red at b54f0ea2). Fix: - ChatRowCell.preferredLayoutAttributesFitting lays its content view out before measuring. It measures the drawing the cell will show. - The cell's sizing pass carries the state the cell was configured with (ChatRowAttributes.shown, from ChatRowCell.shown). ChatTranscriptLayout keeps a measurement as the row's height for a state only when that is the row's current revision and disclosures, and no disclosure change is waiting for the list's reload (measuresSettle). A cell configured with the old state (the apply's pass before its reconfigure), or a SwiftUI row already drawing a change the list has not reloaded for, is laid out as drawn but not kept. - 638dec40's per-state heights (RowHeights) are kept: a row that returns to a state takes its settled height at once. Red, then green (/tmp/smooth27664/ios.sh: xcodebuild test, iOS 26.5 sim on aws-m4pro-6): aRowsHeightForAStateIsTheOneItSettledAt at b54f0ea2, -test-iterations 3: red 0/3 (open state kept 32 for 326; closed kept 326 for 32). Full suite at the previous fix attempt: aReopenedRowTakesItsOpenHeight failed 2 of 3 (94 vs 32). Here: see the PR comment for 10/10 alone and the full suite 3 in a row. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com> * test(cmux-next-ios): a SwiftUI row measured under the top safe area takes its own height (red) The root cause of the CI failure of NativeRowFoldTests.aReopenedRowTakesItsOpenHeight at f8ed786e (closed again 94 pt, closed 32 pt; failing 1 to 2 runs in 5 of the full suite here, never alone). The extra 62 pt is the window's top safe area. A full-suite trace of a failing run shows that at the close the row was re-pinned with its top above the screen (`MARK close line y -44`). In the following pass the hosted cell measured 94 twice. A settle-check probe that re-measured the content in place at the cell's position measured the open state 388 (326 + 62) and the closed state 94 (32 + 62), every time the row sat under the status bar. aRowMeasuredUnderTheTopSafeAreaTakesItsOwnHeight puts …
![Fix with [code]smith](https://pr-comments-assets.blacksmith.sh/codesmith/fix-with-codesmith-light-3x.png)
Fixes two internal reports about NEXT iOS ignoring Reduce Motion:
They share one fix pattern, so they are in one PR: each controller reads
UIAccessibility.isReduceMotionEnabledthrough areducesMotionseam that the tests set, and settles at once when it is on.Change
SidebarContainerViewController.settle: under Reduce Motion the drawer writes its end geometry at once and runs its settle steps in place, with no animator. Those steps were moved out of the animator's completion intodidSettle: model progress; a closed card's square corners and hidden sidebar; VoiceOver's screen-changed post to the opened sidebar. Unchanged: thepublishhit-testing and accessibility state, finger tracking during a drag (trackwrites the geometry per frame) and the animated path with Reduce Motion off.ChatTranscriptController.scroll(to:animated:): under Reduce Motion an animated jump becomes an immediate offset change. Nothing hides the transcript and there is no fade or spring. The long-jump cut, the hidden frame (the stale-tile-over-the-composer workaround) and the spring are unchanged with Reduce Motion off, and thewithoutReduceMotion…test still covers them.AgentChatSurface.scrollToBottom's SwiftUI fallback (used when no UIKit scroller is attached) also skips its 0.35 s animation under Reduce Motion.Tests
Each test reads the layers' running animations as well as the end point, because an animator writes the final model values at once and the end point alone would pass on the old code.
CmuxMobileShellTests/DrawerReduceMotionTests:isOpen, progress, the hidden sidebar, corners and accessibility hiding are all set at once.CmuxMobileAgentChatTests/JumpReduceMotionTests:Red/green (
cmux-next-ios kit tests, iOS simulator):cmux-next-ios-sim.sh test only=...JumpReduceMotionTests,...DrawerReduceMotionTests): 2 passed, 3 failed.aLongJumpLandsAtOnceWithTheTranscriptShown(the jump cut short to 7398 before 8272),openingAndClosingSettleInPlaceandaReleasedDragSettlesInPlaceAndTheDragFollowsTheFinger(animation running, sidebar not hidden, corners 44) fail; bothwithoutReduceMotion…controls pass, so the oracle sees motion.The controls failed on the first push because of how the tests were set up, not because of the fix. UIKit animates from the last committed frame, and a layout pass still pending when the jump starts snaps a following transcript to the bottom. So the tests now commit the start position first. The jump test records every offset and opacity the jump writes through KVO, and it jumps through the same animated scroll that
scrollTranscript(toContentY:)uses.No capture: Reduce Motion end frames match the animated end frames; tests assert no animation runs
UI: with Reduce Motion off, nothing changes. With it on, the drawer and the jump land in their final place, so a before/after still shows the same end frame; the tests above record the difference in motion.
Agent session: agent session
🤖 Generated with Claude Code
Need help on this PR? Tag
@codesmith-botwith what you need.Summary by cubic
Fixes NEXT iOS ignoring Reduce Motion: the team drawer always slid and parallaxed the sidebar on open and close, and Jump to latest from far up a transcript hid the transcript, faded it back, and sprang to the bottom. Both now settle at once when Reduce Motion is on, via a
reducesMotionseam each controller reads.Changes
SidebarContainerViewControllerwrites the drawer's end geometry and runs its settle steps (corners, hidden sidebar, VoiceOver focus) in place with no animator; drags still follow the finger.ChatTranscriptControllerturns an animated jump into an immediate offset change, keeping the transcript visible throughout.AgentChatSurface.scrollToBottomalso skips its animation under Reduce Motion.Written for commit 198d09d. Summary will update on new commits.