Skip to content

Build more than one app at the same time from the UI - #41

Merged
anurag merged 1 commit into
mainfrom
ag/parallel-builds
Sep 24, 2026
Merged

anurag merged 1 commit into
mainfrom
ag/parallel-builds

Conversation

@anurag

@anurag anurag commented Sep 24, 2026

Copy link
Copy Markdown
Contributor

Summary

The gateway already accepted three runs at the same time (maxConcurrentRuns), but the UI did not. It disabled Build and deploy while the selected run was running, and it polled only the selected run. Thus a second prompt was not possible, and the history did not show the progress of the other runs.

This change lets a user start a run while other runs build, and follow all of them.

Changes

UI (public/runs.js, app.js, table.js, both pages)

  • The submit button is disabled only while the gateway accepts a prompt ("Submitting…"). Then the form takes the next prompt.
  • The page reads GET /ui/apps every 5 seconds while a run or a delete is in progress. It renders the history and the selected run from that list, and it does not poll each run. When no run is in progress, it stops. A read that fails is tried again at the next interval.
  • The history and the sites table show the stage of a run that builds ("Building", "Waiting For Deploys"), not only "Running".
  • A refused prompt shows its reason below the prompt, and the prompt stays in the box. A delete that fails shows its reason in the delete dialog, which stays open for a retry. Each refresh renders the run panel again, so the run panel cannot keep these errors.
  • The run panel changes only when its run changes, so the live region does not read it again at each refresh. A focused control in the history keeps the focus when the history renders again.

Gateway (app/gateway.ts)

  • GET /ui/apps reconciles each run that a task owns, as GET /ui/apps/:runId does. Thus a run whose task failed or was canceled becomes failed and releases its concurrency slot, also when nobody selects it.
  • The 429 response has a detail: "The factory builds at most 3 apps at a time, for all users. Submit the prompt again when a run finishes."
  • The gateway imports the Render SDK statically. The dynamic import had no recorded reason, and Vitest 4.1 can give the real module to two dynamic imports of a mocked module that run at the same time. The SDK reads its environment in its constructor, not at import.

Store and workflow (app/store.ts, app/workflow.ts)

  • claimRunApp refuses an app name while a different run builds that app, under the same advisory lock as before. Two such runs would write the same directory, and each one would wait for the deploys of the other. The refused run ends as failed: "A different run is building demo/shop. Submit the prompt again when that run finishes."

Docs: AGENTS.md (UI, invariants, durability), docs/README.md, and docs/FAQ.md.

Tests

  • npm run check passes: Biome, tsc, and 391 Vitest tests. New tests cover the list reconciliation, the 429 detail, and the refusal of a second run of one app.
  • The claimRunApp SQL ran against a throwaway Postgres 18: a claim while a different run builds the app, a claim while a delete is in progress, a new claim after the first run finished, and two claims of one app at the same moment (one wins).
  • The UI ran in a browser against the real gateway, a throwaway Postgres, and a fake of the local task server that plays prompt-to-app and delete-app. Both views: three prompts in sequence while runs built, a fourth prompt refused at the cap with the message below the prompt, a run whose task failed changed to failed while not selected, polling stopped when no run was in progress, a refused delete (409) kept its dialog open and a retry deleted the app, focus stayed on a history item across refreshes, a failed read recovered at the next refresh, and the form error at phone width.

Notes

  • Not changed: the cap of three runs, which counts the runs of all users.
  • The demo talk track shows the 429 as "Request Failed" in the run panel. After this change, the message is below the prompt, so the Recovery row of the talk track needs an update when this merges.
  • During the browser check, a stop of Postgres crashed the gateway: the pg.Pool in app/store.ts has no error listener. That fix is in a separate session and PR.

🤖 Generated with Claude Code

The gateway already accepted three runs at the same time, but the UI
disabled the submit button while the selected run was running. It also
polled only the selected run, so the history did not show the progress
of the other runs.

- The UI disables the submit button only while the gateway accepts a
  prompt. It reads the list of runs every 5 seconds while a run or a
  delete is in progress, and it renders the history and the selected
  run from that list. The history shows the stage of each run that
  builds.
- GET /ui/apps reconciles each run that a task owns, as a read of one
  run does. Thus a run whose task failed releases its concurrency slot
  when the UI refreshes, also when nobody selects that run.
- The 429 response gives a detail that states the limit. The form shows
  it below the prompt, and the delete dialog shows the error of a delete
  that fails. Each refresh renders the run panel again, so the run panel
  cannot keep these errors.
- claimRunApp refuses an app name while a different run builds that
  app. The two runs would write the same directory, and each run would
  wait for the deploys of the other.
- The gateway imports the Render SDK statically. Vitest can give the
  real module to two dynamic imports of a mocked module that run at the
  same time.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@anurag
anurag enabled auto-merge (squash) September 24, 2026 15:47
@anurag
anurag merged commit c858246 into main Sep 24, 2026
3 checks passed
@anurag
anurag deleted the ag/parallel-builds branch September 24, 2026 15:47
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Development

Successfully merging this pull request may close these issues.

1 participant