Contrast brings its own bundler
Contrast bundles React Native apps with its own bundler now, in the browser and on a compile CDN, and this week it went from bundling pieces to a whole router app. Follow mode moves your workspace onto whatever branch an agent is on, the iPhone app got a launch-quality pass and is on TestFlight, and rollup aggregates keep themselves correct.
Platform
Follow an agent and your workspace goes with it
Turn follow on and previews, the editor and the stage all move onto the branch the agent is actually on. Its chat stays anchored at the front of the row instead of sliding away each time the agent changes what it is doing, its cursors appear on the pane you are looking at, and the strip of people and agents became a hover popover holding the follow toggle.
Follow never fights you. Pin a preview or switch follow off and the stage is yours until you resume, and coming back is calm rather than a yank mid-thought. Runs an agent starts follow along by default.
The Contrast app on your iPhone
The iPhone app was reworked end to end and is on TestFlight. A long run of agent activity draws as one thin timeline with a node per beat instead of a stack of coloured cards; a wide delegation lists at most five names with an overflow count; a paused run settles rather than churning. Production is built around your deployment list, newest first, with each deploy’s duration and what went into the build.
The design map fills the viewport, its controls moved into the shared bottom deck, and the inspect highlight is drawn into the board, staying glued to an element as you pan and zoom. Plan progress floats in a collapsible panel, the run control flips the instant you tap and holds until the server agrees, and the floating preview drags and docks without losing the running app.
Production, deploy and health in one place
Deploying and watching production used to be two workspaces; now they are one. Deploys go out in stages, and the screen shows what is live, what is being promoted and how traffic splits, with a page per deployment. The status pane became a Health view: big live numbers for requests, errors and sync, a 24-hour traffic trace with errors marked, and honest copy for local or undeployed projects instead of empty charts.
Every deployment carries its iOS preview, so opening a build puts the native app right there, no separate open-in-simulator step. The Production tab on your phone reads the same state.
Rollups that keep themselves correct
Declare a rollup next to your data and it stays correct without anyone recomputing it. A comment count on the post itself, or a daily spend-per-category table summing transactions: either way the database maintains the aggregate as the underlying rows change, and it syncs to every connected client like any other data. Two shapes: writing onto a row you already have, or maintaining a separate grouped table.
The finance example’s dashboard and budgets screens were rebuilt on it, the app template ships a post comment count, and the agents that build apps in Contrast know the pattern, so a generated app reaches for it unprompted.
Choose how much horsepower the agents get
Chat has a model control with four sizes. Each size routes every agent role, the manager, the workers doing the building and the reviewer, to a model that suits it, so you pick one thing rather than fill in a table. A badge in the composer shows what the thread is running, each agent’s composer names its model and effort, and the choice applies on desktop and phone. Luna joined the lineup, its highest reasoning tier selectable rather than left to the router.
Pro+ and Enterprise now build several of your projects in parallel instead of one at a time, quoted the same everywhere the plan is described. It stays one build per project on purpose: two drivers on one project would answer the same prompt twice.
Tech
Contrast bundles React Native apps itself
@contrast/contrast-bundler bundles React Native source in the browser: real metro-resolver over a virtual filesystem, sucrase for transforms, genuine Metro __d/__r output that metro-runtime executes. No Babel, no hermes-parser, no wasm in the client, because packages arrive pre-transformed as CommonJS and are emitted byte-for-byte. The serializer follows Metro’s five stages, polyfills go through Metro’s own wrapPolyfill IIFE, and hot updates emit exactly upstream’s payload. require.context was the hard part, since One builds its route table from it: the bundler follows a name bound once to a literal and refuses an argument that does not fold. On the game-pet example: 4,528 modules, zero reachable failures, and 13 route modules no import statement mentions.
Packages compile once to a CDN, addressed by package@version plus a hash over babel and preset versions, the dev flag and a pipeline revision, so a toolchain upgrade lands on a new address instead of silently serving old bytes. Local and hosted builds must emit identical bytes, structurally: the transform takes source content and returns bytes with no filesystem access, after the first builder leaked a home directory into react-native’s artifact through transform-react-jsx-source. Speed came from noticing acorn parsing is roughly 90% of a graph walk. Discovery resumes instead of restarting the graph on each package it finds, 164 full walks down to one, and parses are shared with the build walk: a template’s build walk went from 488ms to 47ms, its whole bundle from 5.4s to 2.2s of CPU, byte-identical.
Cutting the Cloudflare bill by four fifths
Last week’s work capped spend. This week went after what generated it. The five-minute sweep opened every project’s Durable Object just to ask whether it had work, roughly 28,800 wakes a day on an account with no users, and each wake re-ran namespace provisioning, so a deploy invalidated schema readiness and the sweep re-migrated the whole fleet, about 750k rows per deploy. Project work rows live inside each project’s own database, invisible to the control plane, so a new control-plane index has work announce itself before it is written and the sweep reads only that index. Marking first means the worst case is one project woken for nothing, never work that nothing reconciles.
Measurement reordered the rest. One production delete is 208 billable rows for 15 logical rows, 0.07% of the write budget, while provisioning measures 14,857 rows, 71 times a delete, so the throttle guarding deletes was guarding a cost that does not exist. Anonymous projects stopped materializing a database until their first real write, and the hourly prune folded its edited-work rule into the query rather than applying it afterwards, where one edited account at the head of the order had been eating the whole batch every hour.
What an agent call actually pays for
An audit of 44 wave databases hunted for everything an agent pays for and never uses. A worker fetching package type declarations put about 1.5MB into its live conversation, and six consecutive calls each re-paid roughly 390k uncached input tokens, about a third of one wave’s cost; stored results were already capped at 8k, while the live transcript had no cap at all. Several tool schemas shipped on every call with zero invocations by the roles carrying them. And the agent’s name was the first line of its system prompt, so no two agents ever shared a cached prefix; identity moved to the tail, making everything before it byte-identical per role.
Tools stopped re-sending what the model already holds: a skill read answers with a marker when the body is already inlined, and a byte-identical repeat of a file read does the same. Reassignment was the largest source of redundant re-reads at 57%, about 80 induced turns per wave, because a reused agent’s model began empty even though its thread held the prior work. Measuring skills, about 30% of everything sent, found one at 61.9 KB against a 50 KB cap, so every read silently deleted about 12 KB from its middle and no agent ever saw it whole. A run_script verb now lets hands-on roles chain a read, a decision and an edit in one turn: across 35 trials it halved tool calls, 5.1 against about 13, and cut output tokens 43% at 10 of 10 correctness.
The simulator sleeps when nothing moves
A small looping animation used to force the compositor to re-render and resubmit the whole canvas every frame. A stable animated region is promoted to a retained in-surface pass now: its static background is recorded once as a picture, then a fast frame clips to the damage rect, replays it, and draws only the live animated roots. Perpetual animations move onto the compositor’s clock as per-slot deterministic loop graphs, so the shell stops stepping them at all. At rest the loop parks after two idle frames and wakes on a host or tree message, or on a demand epoch bumped by any shared-array write and read before the frame body, so a missed write resumes rather than being swallowed. Idle ticks went from 60.1 to 0.3 per second.
Animated styles stopped travelling as messages too. The shell used to write each value to the mirror, forward it to the compositor’s shared slot, and post a per-node per-frame message the tenant applied and republished. The value was in shared memory the whole time, so the tenant reads it there and the shell posts a binding only when a slot’s shape changes: tenant tree deltas during one drag fell from 258 to 22. Large-sigma blurs moved to downsample-blur-upsample, equivalent at 0.05% mean pixel delta, dropping compositor p95 from 33.3ms to 18.5ms, with retained pixels capped at 32 MiB.
Previews are reused, and fully released
Two leaks made a long editing session get heavier the more you edited. Every remount-class change recreated the visible panes’ iframe elements through a React key, and removing an iframe whose document still has in-realm activity leaks the whole frame Blink-side with no JS retainer for V8 to see, sawtoothing the heap until the renderer died. Iframe elements are created once and navigated for the rest of their life now, and an ordinary merge applies as an in-realm cold mount; only a storage-scope change needs a fresh document.
Second, a retired preview’s workers are terminated outright rather than asked to wind down. Graceful shutdown force-terminates through a 250ms backstop timer owned by the realm’s own window, but retirement gave the realm 1000ms and then navigated its document away regardless, destroying that timer, so a realm that missed its window leaked its tenant worker and CanvasKit heap permanently. It compounded, each orphan making the next disposal likelier to miss: one run climbed 1, 2, 3, 8, 13, 26 live tenant workers past 6GB.
Editor and IDE
- One durable link opens the editor, used by the header, footer, invite links and the CLI; arriving keeps the showcase and editor shell alive.
- Type errors come back while you type: a whole-project check no longer blocks the file in front of you.
- Type a pane’s name in the command menu, including panes not on screen, and it reveals and focuses.
- Floating panes carry their title bar in the inverse theme, so they read as above the workspace rather than blending in.
- Panes scrolled off the side of a row stop rendering, taking an eleven-pane row from eleven active panes to two.
- The composer mounts on first paint while the agent runtime loads behind it, so you can type before the machinery is ready.
- Bringing an agent’s work into your project applies it in place instead of rebooting the preview.
- A preview that cannot come up shows the actual failure, and the data preview names an out-of-date schema instead of an empty table.
- The Media pane went from four competing add actions to one per section, and tiles come to full strength under the pointer.
- Tool cards flattened, and plain and formatted messages share one type scale, so a long conversation reads as a stream.
- Questions the agent asks keep their raised card, and your answer to a poll stays on screen.
- The assistant that works alongside you in the IDE is now Lux.
- Pasting a bare task identifier takes you to that task’s branch, and a dead link says so clearly.
- Tooltips stop flickering across adjacent buttons, and let go when a click takes focus or the pointer enters a preview.
- Filtering the command menu keeps the row you had highlighted, so enter lands where you expect.
- Chat groups tool calls into a short summary while an assistant drives your app, and a quiet stream times out rather than hanging.
- The IDE desktop window moved into the main signed app, picking up notarization, auto-update, tray and single-instance behavior.
- Starting a project times each phase, so a slow first boot tells you which step was slow.
Design
- The dark palette was rebuilt so page background, surfaces and cards separate cleanly instead of flattening into one shade.
- Card shadows compacted onto one scale across cards, chat rows, toasts, pickers and tiles, and horizontal rows keep theirs unclipped.
- Buttons moved to the body font, primary dialog actions use the inverse theme, and eleven dialog surfaces got roomier insets.
- The docs detail column resizes with a real gutter and minimum width, and animates into place instead of snapping.
- The mark is left-weighted again, with a new banner, avatar and favicon, and the mobile icon and splash regenerated at every density.
- Generated app themes derive their ramps from the palette’s own hue and chroma, so a saturated palette no longer collapses to mud at the ends.
Agents and the build system
- Screens land while the build is still running: a checkpoint submit runs the same review bar, merges, and rebinds the task to a fresh branch.
- Screens arrive reachable, since tab registration moved into each screen’s own task and the runtime enforces the ordering.
- Agent code search pulls matches in batches of 32 files; a broad no-match grep had measured 2m59s across 500 sequential reads.
- QA lands a small fix it already holds rather than filing a bug and paying for a fresh worker, refusing when another task owns the file.
- Branches for agent work take their name from the task title instead of an opaque id.
- Manager startup runs as durable stages, so a reconnect mid-startup resumes at its actual stage rather than restarting.
- Dispatch became one atomic call claiming every ready task at once, so a partial failure is recoverable.
- The manager asks its one taste question up front and dispatches the first worker before working through implementation detail.
- A continued task resumes with the conversation and skill bodies it already had, instead of starting the model from nothing.
- Route ownership comes from the template contract, after one run spent 16 of its 33 minutes on reads and greps before the first screen worker started.
- Five graded design defects closed in guidance: meter fills and labels survive the contrast floor in both themes, sample rows show every status variant, empty states keep the headline metric glanceable, backdrops contain their content, and previews use a relative date.
- Skill routing stopped dragging data-authoring and copywriting guidance into design tasks that only need fixtures.
- Navigation work no longer depends on every screen design, which used to let one unfinished design leave every merged screen unreachable.
- Undecided escalations stay listed until resolved rather than aging out of context after 90 seconds.
- Designers get one final verification pass with a re-run-only-the-failed-step rule, after 23 rule-required re-verification stalls in a single wave.
- QA waits for the merge that would invalidate it, so one pass covers both halves.
- Route navigation reports the route it actually reached instead of declaring success whenever a dispatcher existed.
- A model that stalls emitting reasoning tokens is reported with its last reasoning text, elapsed time and token counts.
- An interrupted planning turn renders as interrupted and gets a recovery nudge, instead of reading as idle from three directions at once.
- Wave behavior is asserted against frozen evidence: each invariant names a directory where it must fire and one where it must stay quiet.
- The pair agent delivers canonical deduped skill excerpts, and can send an excerpt and escalate on the same trigger.
- Your first message lands on a ready machine: the runtime relocates in the background and project creation finishes before warm-up starts.
- Spec authoring separates a product’s actions from its screens, so an action belongs on the surface showing its result.
SootSim
- Grab a list mid-bounce and it catches under your finger at the overscrolled offset; let go and the bounce resumes and settles.
- Taps that interrupt a bounce, cancelled touches and pointers handed to another gesture all end where iOS would end them.
- A paged carousel always lands on a page instead of parking between two, and paged views publish their settled offset.
- Pinch-zooming a scroll view keeps up with your fingers instead of trailing a frame behind.
- Focus an input near the bottom of a scroll view and the view lifts it into place as the keyboard comes up.
- Pressing a key draws the connected balloon iOS actually draws, one shape whose bottom lobe is the pressed key’s own footprint.
- Selecting text in an input inside the simulator shows the selection, and deep trees snapshot cleanly.
- Two-finger trackpad swipes trigger back and edge gestures, so you can drive an app without a touchscreen.
- Glass refracts, brightens and catches light like a device running iOS 26, with softer rims and no doubled contours mid-transition.
- Dragging up from the home screen fans the switcher cards in with the backdrop blurring alongside, and a returning app waits for real pixels.
- Long-press context menus lay out like the real iOS menu when labels wrap onto more than one line.
- Buttons match native pressed and disabled styling, submenu highlights clear as a menu collapses, and a tab indicator settles after a cancelled drag.
- WebViews behave like a device: native overlays stay above embedded content, the placeholder goes when the page is live, and reload really navigates.
- Staggered entrances driven by object shared values keep every intermediate update, so Tamagui enter sequences play as written.
- ScrollView and VirtualizedList refs carry the Fabric host-instance fields React Native code reaches for, so libraries work unmodified.
- expo-ui menus render their labels correctly and open when a child responder claims the long press.
- Reanimated 4.5 and worklets 0.10 run on a demand-driven finalizer queue, since upstream’s per-frame loop would keep the simulator awake forever.
- 3D mode surfaces pick a physical material, matte, glossy, glass or balloon, each with color, roughness, metalness and light response, shared by extruded text and icon tiles.
- 3D mode gained shape primitives sized in world units, screen layers placing a captured app screen on a bezel-less panel, and connector ribbons whose bend adapts to the drop.
- A still 3D shot keeps integrating the same lens the export uses once you stop touching the scene, and fonts plug in as files.
- Four CLI releases went out across the week, 0.1.255 through 0.1.258.
Website
- The homepage runs a real showcase app in an office-monitor scene that scales with the window, and the sections around it stage as you scroll.
- The hero sits in front of a slowly turning arrangement of glass and chrome, lit from above and reflecting the page, raytraced once ahead of time and shipped as 44KB light and 77KB dark stills rather than run in every visitor’s browser.
- The showcase renders a still capture of itself on the server, picking light or dark so only one image is fetched, then cross-fades to the live shell after hydration.
- The hero-to-overview transition runs on scroll timelines plus one deterministic
calc()projection with zero runtime measurement, retiring a 433-line showcase generator. - The phone on the SootSim homepage boots the real simulator as the page loads, holding a recorded frame until the live canvas draws, with a working Retry if a boot fails.
- The SootSim section became a full ambient scene in the brand blue, marketing type and color moved onto one system across both sites, and each site got a footer.
- A cloth simulation moves with the page behind the overview section, sharing one renderer with the rest of the site instead of its own clock.
- The prompt box runs full width on a phone with a 16px input floor so tapping it does not zoom, and the placeholder types real prompts until you start typing.
- Visiting the site while signed in no longer bounces you straight into a workspace.
- The capabilities page became seven grouped categories of short concrete capabilities in plain text, adding the design map, its screenshot composer and integrations.
- The hero office illustration halved to 141KB, sized exactly to the box it occupies, and phones skip it entirely, saving 276KB a load.
- Opening the nav sheet moved off React state at the top of the header tree, cutting a close from 415ms to 247ms of scheduler time at 6x CPU throttle.
- On iOS Safari the deploy diagram had rendered as a grey void because WebKit computes
atan2()wrong when a container-query unit is an operand; registering the ratio as a length fixes it. - Safari samples the page background to tint its status bar, which any full-viewport fixed portal defeats, so the page now hands it the hero band’s real color.
- Layout and motion pass, mostly on phones: the orbiting devices grew and gained a grounding shadow, the overview ribbon stops standing vertical, the production diagram uses the card’s full width.
- The film page mounts SootSim’s real 3D stage and seeks its timeline from page scroll rather than scrubbing a pre-rendered atlas, at a median seek of 1.3ms.
Mobile
- The iOS beta is on TestFlight, branded, with universal links, so tapping a Contrast link opens the app instead of the browser.
- On an iPhone the download page is a single install button; anywhere else it puts you through sign-in first, so your projects are waiting when the app opens.
- Sign in with GitHub hands off to the system browser and brings you back signed in.
- Attach a photo to a message from the phone, and agent run surfaces are available in the app again.
- A Feedback action on docs and running tasks tucks the card into the pip stack, lands you in chat, and brings the subject as a context chip.
- The update toast appears when the download starts, shows progress, stays until you act on it, and offers a restart button.
- A brand-new project’s chat is usable before its data finishes coming up, waiting for readiness instead of dropping the message.
- Glass panels enter by transform and the pager header hides by scale, because iOS drops the liquid glass material the moment a view’s opacity is animated.
- The animated brandmark gets its own stacking context so its halves stop flickering mid-flip, and the parked Plan card stays put while the pager shifts.
- Over-the-air updates are fingerprinted against a committed Podfile.lock, so an update ships against the native module set it was built for.
Deploy and infrastructure
- A production route is claimed only after the deployment rows commit, so a deploy that fails partway cannot leave a public address pointing at nothing.
- CLI-triggered builds go through that same durable path, collapsing two deploy code paths into one and deleting a standalone slug registry.
- Deploy readiness is verified against the stable origin after promotion, and a failure restores the previous version, since Cloudflare never mints a preview for a Worker declaring its own Durable Object classes.
- Each artifact upload is bounded by a deadline scaled to body size, and publishing skips an artifact whose etag already matches, ending four redundant re-uploads per deploy.
- Namespace backups walk a deterministic rotation offering every namespace a check once per six-hour cycle, against Cloudflare’s 1,000 subrequests per invocation.
- Cross-origin isolation moved into one owner module used by the app middleware, deploy path and SPA entries, since a preview iframe qualifies only if every ancestor carries the headers.
- A new build lands quietly on public pages instead of raising an update toast over marketing and docs, and the reload guard is per-tab.
- Compiled packages publish to an immutable content-addressed route whose uploader refuses to replace an address holding different bytes, which is what makes a one-year cache header safe.
Data, sync and accounts
- The sync stack moved to Zero 1.8 and Orez 0.12.3 at once across the root, the tool runtime, four examples and five templates, so a generated app cannot drift from its host.
- The client transport settled on polling by default, because a preview holds many short-lived connections and each racing to wake the sync host saturated the pull queue.
- A session read retries a Durable Object reset only, on an allowlist, and never replays a non-read, so a deploy under in-flight requests does not sign people out.
- Reading the session without handling the unavailable case no longer typechecks, replacing 27 call sites that had swallowed it with a catch returning null.
- An unresolved session parks the anonymous identity in its own field rather than flipping the whole sync-backed subtree, which had been remounting 48 times in 20 seconds.
- Sync recovery clears only the state a reason invalidates, instead of wiping local stores in your other open tabs over a code-version mismatch.
- The login dialog asks which sign-in providers a deployment can actually finish and shows only those.
- Every path that mutates a project requires an editor grant checked in one place, and invite acceptance became a single transaction.
- An invite link shows the project name, icon and who invited you before you sign in, and a pending invite survives an Apple or GitHub sign-in on the way through.
GitHub
- What lands in GitHub when you push is a repo anyone can clone, install and run, built by the same code as the downloadable export.
- Pulling changes back subtracts the pieces Contrast contributes and restores your manifest, so the project does not slowly vendor a copy of itself.
- A push to the default branch that would roll back commits the remote already has is refused, and settings report push access correctly for App installs.
- Connect a project to a repository you just created and left empty, and the first push initializes it, from local and from production alike.
- The persistent sync clone is bare, and branch state is compared against the remote instead of the default branch against itself.
CLI
contrast openbrings up the background service, this directory’s dev runtime and the window, waiting for a stack that answers rather than a process that started.- Local development commands moved to the top level:
contrast web,contrast ios,contrast open,contrast shot, plus a newcontrast status. contrast devprints the URL a browser loads next to the desktop command, and takes single keys while it runs: o for the IDE, w for the web app, h for help, q to quit.contrast ios --nativebuilds and runs your project on Apple’s own iOS Simulator alongside the browser-native one, choosing the iOS version and checking the packager first.- The CLI checks your Node version before it starts and names the versions the project supports.
- Manifest fetch, sha256 verification, release channels, version pinning and auto-update moved into one shared delivery engine behind both CLIs.
Templates and examples
- Every template and example moved to Expo SDK 57, React Native 0.86.2, One 1.23.1 and Tamagui 2.7.1: 405 pins across 28 manifests, read from Expo’s
bundledNativeModules.json. - Native menus come from one shared backend across the mobile app, the default template and every example, replacing zeego in all eight entry points.
- 21 Expo packages now ship raw TypeScript entries and SDK 57’s exports maps hide internal paths, both handled at the resolution layer rather than per template.
- A project’s auth client is about 26 lines instead of 141: bearer plugin, bearer store, per-platform files and demo-user helper in one packaged helper.
- Each example declares its demo identity in one place with a matching auth-server hook, and the skills that generate seeded apps teach the same contract.
- The floating tab bar’s height is an exported constant the layout consumes, so a rewrite that drops the coupling fails typecheck rather than hiding content under the bar.
- Templates and examples converged on one application SQL client, with a priority test pinning the resolution order.
- A new check compares each template’s imports against the preview bundle’s import map, so an unresolvable import fails validation up front.
Design capture
- Capture records whether a screen is presented modally and which screen covers the view, so a shot taken while a modal is up cannot be mislabelled.
- A capture pass counts as complete only when a retry could not do better, so a partial pass cannot replace a more complete artifact.
- Every call crossing into a guest realm carries a deadline, and the lifecycle sweep skips design sessions and leased panes.
- A cold preview boot gets its own 90-second budget instead of borrowing the warm pool’s 30, and a readiness failure names the gate that failed.
- Captured design trees replay through the Skia paths SootSim implements rather than assuming the whole React Native Skia surface.
Fixes & polish
498 fixes and 28 performance improvements landed this week. Three worth calling out. Bringing an agent’s work into your project applies the change to the running app in place, so the preview keeps going instead of restarting from the first screen. The design map keeps rendering when a typeface fails to load or a screen throws while recording, so one bad screen no longer takes the whole tab with it. And sending a message while a run is paused simply resumes it, with no confirmation dialog and no lifecycle card left in the thread.