fix(mobile): size a chat image's frame before its bytes arrive - #10199
Conversation
Thread transfer impact✅ Thread transfer remains within every enforced ceiling.
Baseline: unavailable · PR result: Scenario and decoded snapshot size10 historical turns, 5 command tools per turn, 878.9 KiB retained MCP result per historical turn, and a 1.05 MiB retained result in the measured turn.
Updated in place by a trusted workflow. PR artifacts are strictly validated and never executed. |
ApprovabilityVerdict: Approved at Macroscope's review found this PR approvable — This is a focused mobile layout bug fix that stabilizes existing markdown image frames using the feed width and already-supported image metadata. Its runtime impact is confined to image sizing and row layout, with no new capability, schema change, deployment change, or static-analysis override. No code changes detected at You can add or adjust custom eligibility rules. Learn more. |
a68cf3b to
791b74c
Compare
The image frame went through three sizes: full width on the first render, the measured width after its own onLayout, then the decoded size. Each one changed the row's height after LegendList had already placed the rows below it, so a screenshot could push the end of the thread behind the composer or leave the next tool call drawn under it. The feed now hands markdown its content width through context, so the placeholder is the right width on the first render, and the server's image dimensions size the frame exactly before the first byte arrives. An image the server could not measure still reserves a 16:9 slot. Co-Authored-By: Claude Code <noreply@anthropic.com>
… win A list item or blockquote indents its column below the feed width, so the measured width takes over once the row has laid out. The decoded size wins over the server's header hint so a wrong hint cannot stick. The padding constants use the mobile 14px rem. Co-Authored-By: Claude Code <noreply@anthropic.com>
791b74c to
cced7aa
Compare
Bugbot is paused — on-demand spend limit reachedBugbot uses usage-based billing for this team and has hit its on-demand spend limit. A team admin can raise the spend limit in the Cursor dashboard, or wait for the next billing cycle to continue. |
When the PR selected for `early` does not apply at the reconciled upstream boundary, the command stopped with a single line that gave nothing to act on: ```console $ node scripts/upstream-queue.ts early 11784 --through upstream/main Selected pingdotgg#11784 cannot be applied at the upstream boundary. ``` That happens when the PR builds on upstream work the fork has not taken in yet, and the planner only looks for dependencies in the changes it replays after a successful apply. The command now reports which of the PR's files conflict at the boundary and which earlier pending upstream PRs and direct commits change those files, in upstream order. Text output lists the first 20 and says how many remain; `--json` includes every source with its SHA, PR, and title. A plan that succeeds prints as before. The intake runbook's `early` paragraph describes the report. ## Validation - A new test in `scripts/upstream-early.test.ts` checks which sources the report lists: pending sources after the boundary and before the blocked PR whose files overlap the conflicting paths, including direct commits and directory prefixes, and not recorded or later sources. - `scripts/upstream-early.test.ts` and `scripts/upstream-queue.test.ts` pass (21 tests); lint, formatting, and the scripts typecheck are clean. - Against fetched fork `main` (`495b7cfd5`) and upstream `main` (`b2b43bef7`), the same command now prints: ```console $ node scripts/upstream-queue.ts early 11784 --through upstream/main Fork 495b7cf; reconciled through d28077e pingdotgg#11784 does not apply at the reconciled boundary. 16 of its files conflict there: apps/mobile/src/features/threads/ThreadFeed.tsx apps/mobile/src/features/threads/thread-work-log.tsx apps/mobile/src/lib/threadActivity.ts apps/server/src/orchestration/Layers/ProviderRuntimeIngestion.ts apps/server/src/orchestration/decider.ts apps/server/src/provider/Layers/ClaudeAdapter.test.ts apps/server/src/provider/Layers/ClaudeAdapter.ts apps/server/src/textGeneration/ThreadTitleContext.test.ts apps/server/src/textGeneration/ThreadTitleContext.ts apps/server/src/ws.ts apps/web/src/components/chat/MessagesTimeline.logic.ts apps/web/src/components/chat/MessagesTimeline.tsx apps/web/src/components/settings/SettingsPanels.tsx packages/client-runtime/src/state/environmentHttpAuth.test.ts packages/client-runtime/src/state/threadSnapshotHttp.ts packages/contracts/src/orchestration.ts Earlier pending upstream changes to those files: 110 PRs. Take them in first, or add their PRs to this plan. pingdotgg#9594 fix(connect): refresh HTTP credentials without reconnecting (pingdotgg#9594) pingdotgg#9894 fix(chat): show hours for long runs (pingdotgg#9894) pingdotgg#9933 fix(settings): share restart continuation across environments (pingdotgg#9933) pingdotgg#10092 fix(web): scale agent spawn rows with interface font (pingdotgg#10092) pingdotgg#9965 fix(web): keep the composer expanded until the thread can scroll (pingdotgg#9965) pingdotgg#9935 fix(web): align tool disclosure chevrons with expanded state (pingdotgg#9935) pingdotgg#10199 fix(mobile): size a chat image's frame before its bytes arrive (pingdotgg#10199) pingdotgg#10200 fix(web): size the chat image slot from server-reported dimensions (pingdotgg#10200) pingdotgg#10167 refactor(client-runtime): remove unused runtime exports and helpers (pingdotgg#10167) pingdotgg#10173 fix(mobile): restore live tool shimmer and add a Thinking row (pingdotgg#10173) pingdotgg#10210 fix(mobile): only make work rows expandable when the body adds something (pingdotgg#10210) pingdotgg#10211 fix(mobile): fold subagent lifecycle rows into one batch per spawn (pingdotgg#10211) pingdotgg#10212 fix(mobile): stop clipping expanded tool groups (pingdotgg#10212) pingdotgg#9875 feat: show provider usage limits with /usage-limits (pingdotgg#9875) pingdotgg#9561 fix(ui): unify loading and refresh feedback across clients (pingdotgg#9561) pingdotgg#10226 refactor(web): keep feature component helpers private (pingdotgg#10226) pingdotgg#9754 feat(settings): add shared project defaults and scoped overrides (pingdotgg#9754) pingdotgg#9895 feat(connections): balance new threads across connected machines (pingdotgg#9895) pingdotgg#10273 fix(mobile): stop the work log flickering during subagent runs and failing calls (pingdotgg#10273) pingdotgg#10271 fix(mobile): save linked media from chat (pingdotgg#10271) ... 90 more; --json lists every source. ``` `--json` returns the same 16 paths and all 110 sources. `early 5521 --through upstream/main`, which plans successfully, prints the same summary as before. --- Written by an agent (Claude Code, claude-opus-5-5).
Stack 2/3 (#10201). Depends on #10198.
Problem
An assistant image on mobile went through three sizes: full width on first render, the measured width after its own
onLayout, then the decoded size (up to 480px tall for a portrait screenshot). Each step changed the row's height after LegendList had already placed the rows below it. That's the "scroll area cut off under the composer" and "tool calls render under the media" reports.Fix
ThreadFeedhands markdown its content width throughMarkdownImageAvailableWidthContext, so the placeholder is the right width on the first render instead of afteronLayout. The context is an upper bound: a list item or blockquote indents its column, and the measured width takes over once known.ThreadMarkdownImageuses the server'simageDimensions(feat(server): report image dimensions with signed asset URLs #10198) to size the frame exactly before the first byte arrives. The decoded size still wins once it exists, so a wrong hint can never stick. An image the server couldn't measure keeps the 16:9 slot.px-3.5on the bubble,px-1on assistant rows).Evidence
iPhone 17 Pro simulator on macmini, seeded thread with landscape/portrait/wide screenshots, asset responses delayed 2.5 s through a proxy so the loading phase is observable. Frames captured every ~100 ms during thread open.
Before (main): portrait placeholder is 16:9; when the image decodes the row grows ~250px and everything below reflows.
After: the frame is portrait from first paint; rows below never move.
Verification
apps/mobiletypecheck clean; lint on changed files clean (pre-existing effect warnings unchanged).Model: Claude Fable 5 · Harness: Claude Code in T3 Code
🤖 Generated with Claude Code
Note
Size chat image frames before bytes arrive in mobile thread feed
ThreadFeedderives a markdown content width from the centered content width and row padding, then passes it torenderFeedEntryso user and assistant markdown rows have a width estimate before their ownonLayoutfires.ThreadMarkdownImagecombines the feed-provided width with a server-supplied image size to render an aspect-ratio frame before decode; after decode, decoded dimensions take precedence and the measured layout width constrains the estimate.readJpegnow skips later APP1 segments once a rotated EXIF orientation is detected, andIMAGE_DIMENSIONS_HEADER_BYTESis raised from 64 KiB to 256 KiB to reach JPEG frame headers after multiple metadata segments.Macroscope summarized cced7aa.