Source and history
Browse any branch, tag or commit, read history and diffs, compare refs, and edit files in the browser.
The Code item under an app in the sidebar opens /apps/<id>/source; the App item beside it goes back to the app’s overview. Everything there reads the push mirror, which holds every pushed branch — not only the deployed one.
Layout
Code mode has one bar: the branch picker on the left, History, Changes and Preview on the right. The files take the page. Each button opens a side panel on the right, a third of the width; on narrow screens the panel covers the files instead. One panel holds all three tabs, and switching between them keeps what each one holds: the preview keeps running, the commit form keeps the message and target you were typing, and History stays on its page. Click the active button again, or the panel’s close button, to hide the panel: every tab in it is let go, and the preview stops. The open panel is in the URL as ?panel=history, ?panel=changes or ?panel=preview, so refresh and deep links keep it. The pages below it (a commit, a compare) carry a link back to the files in place of the picker. Pulls have their own pages; see Pull requests.
Branch picker
Code mode opens on the default branch (main), the one browser edits are committed to. The branch picker is a dropdown that lists every branch. Once the app has deployed, the first entry is the deployed commit, by its short SHA with a deployed badge, to browse exactly what is running. The default branch carries a default badge in the same spot. Each other branch shows how far it is ahead of or behind main, and has a Compare link. Admins also get a Delete button, except on the default branch; deleting the branch you are browsing returns to the default branch. The URL carries the browsed ref as ?ref= (absent for the default branch) and the open file as ?file=, so a link to a file on a branch is shareable. Any tag or commit works in ?ref= too.
At the bottom of the dropdown, Create makes a branch from the ref you are browsing and switches to it. This needs the push role. A create or delete publishes exactly like a push: the tip bundle and manifest are written, and main moving deploys.
Browsing is read-only on anything but the default branch. Browser edits are made on the default branch (below).
Files and diffs
The tree lists the ref’s files with sizes; clicking one opens it in the editor. Binary files are detected and not rendered, blobs are capped at 256 KB, and a diff is capped at 1 MB. On the default branch, the tree marks every file you have added, changed or deleted but not pushed yet, and the folders that hold them. The editor’s own header shows the open file’s change the same way — the same letter (added, modified, moved, deleted) beside its name — from the same unpushed changes, so it stays right across a reload and follows a discard, an undo or another tab’s write.
Shortcuts
- Cmd/Ctrl+F opens find in the open file, and Cmd/Ctrl+Alt+F adds replace. Both are the editor’s own panel, not the browser’s.
- Cmd/Ctrl+click a symbol to jump to its definition (Code intelligence).
- F2 on the selected row renames it, and Delete or Cmd/Ctrl+Backspace deletes the selected rows after the confirmation (Browser edits).
- Ctrl+C at the terminal prompt stops the preview, the shell’s foreground job (Terminal).
Repository limits
Code mode checks out the whole ref in the browser, so the runner caps a checkout at 5000 files and 100 MiB. A repository over either cap is refused with 413 repository too large for Code mode: … instead of opening. Shrink the ref (or move large assets out of the repository) to bring Code mode back.
Code intelligence
TypeScript and JavaScript files get the editor help you would expect, computed in a web worker from the ref you are browsing:
- Diagnostics appear as inline markers on the offending token. They re-check a moment after you stop typing, using the repository’s own
tsconfig.json(ESNext /bundler/react-jsxdefaults when it has none). - Hover a symbol for its type and JSDoc. The tip opens once the pointer has rested on the symbol for 0.3 s and highlights the type like the editor does. On a marked token, the tip lists the diagnostic below the type.
- Cmd/Ctrl+click a symbol to jump to its definition, opening the other file at that line. A definition that lives in
node_modules, or in TypeScript’s own libs, has no row in the tree, so its declaration is shown right where you clicked instead. - Inline prediction offers the completion TypeScript ranks highest at the cursor, as ghost text.
Imports are resolved against the declaration files captured from the app’s last successful build, so the editor can still make sense of a repo while the next build is deploying.
Read-only refs (anything but the default branch) keep hover and Cmd/Ctrl+click, and never predict. They also show no markers: markers live inside an edit session, and a read-only ref deliberately has none.
History and commits
The History panel lists the browsed ref’s commits newest first: subject, author, time and short SHA. Newer and Older page through them (?skip=). With a file open, Only <file> narrows the list to that file. A commit opens at /apps/<id>/source/commit/<sha>, with its metadata and parents, the changed files with additions and deletions (binary files flagged), and the patch against its first parent.
Compare
The compare page (/apps/<id>/source/compare?base=&head=) shows two refs: their commits, the three-dot diff, and whether a squash merge would apply cleanly. Open it from a branch’s Compare link in the picker, or directly. The Open pull button starts one from the pair you are looking at (Pull requests).
Browser edits
Opening a file and changing it stages a web commit. You can edit as many files as you like: each keeps its draft while you open others. The Changes button shows how many files you have changed.
Right-click in the tree for New File, New Folder and Delete. On a folder they act inside it, on a file in its folder, and on the empty space below the files at the root. A new file is named in place and opens empty in the editor. Delete asks first; on a folder it removes every file in it. Ctrl/Cmd-click selects several rows: right-click one of them and Delete (N) removes them all after one confirmation. Git can’t store an empty folder, so a new folder lives only in your browser until it holds a file. On other refs the tree has no menu.
Drag a file or folder (or a whole selection) onto a folder to move it there. Dropping onto a file moves into that file’s folder, which is how you move something back to the root: drop it on a top-level file. A move onto a name that already exists is refused. The open file follows its move, edits included. Moves don’t read file contents, so binary and large files move too.
Rename (in the menu, or F2 on the selected row) edits the name in place; it is a move, so the same rules apply. Delete or Cmd/Ctrl+Backspace on the selected rows asks, then deletes them.
Upload files… in the menu, or dropping files and folders from your computer onto the tree, adds them where you dropped: in a folder, next to a file, or at the root for the empty space. Text files (UTF-8, up to 256 KB) become drafts you can edit; anything else is a binary file shown by its size. A push holds up to 100 files and 25 MB, and each upload up to 10 MB. Uploading onto a path that already exists asks whether to replace it.
The Changes panel shows every draft as a diff against the file as it was, and lists moved files (old → new) and deleted files, each with a Restore button. Discard on a change throws it away (an edit goes back to the committed text, an added file leaves the tree); Discard all resets everything. Both ask first. Its form at the bottom takes a commit message (left empty, it is Web edit: <paths>) and where to commit:
- The default branch, which deploys the commit and wakes a sleeping app, like any push to
main. - New branch…, with a name. The branch starts at the default branch’s tip, and the default branch stays as it was, so the open file goes back to its old text. The panel then links to a pull request from the new branch.
Push sends every draft, upload, move and deletion as one commit, with a validated set of paths. Then the panel says where it landed. If the push fails, the drafts and the form are kept, so you can retry.
Unpushed changes live in this browser, per app and branch. A reload, a visit to another page, or closing and reopening the tab doesn’t lose them, and they stay until you Push or Discard all — nothing expires. Tabs open on the same branch share them: an edit, upload, rename or discard in one tab shows up in the others as you make it, and every tab can edit. If two tabs change the same file at once, the last write wins. Each tab runs its own Preview. If the branch moved in the meantime, the panel says the changes are based on an older commit, and the push may conflict. Editing, uploading and discarding only ever touch your browser’s copy of the checkout; nothing reaches the server until you push.
- Edits start on the default branch. Other refs are read-only, so switch back to the default branch to edit.
- Empty repositories work, so you can seed a blank app from the browser.
- When the default branch is protected (
require_pron and you are not an app admin), it is greyed out in the form and only New branch… pushes (Branch protection).
Every commit Noite writes — browser edits, merges, template imports — is authored and committed as <your name> <your user id>@users.noreply.<domain>, never your email.
Preview
Preview is the third panel in the bar, beside History and Changes, and shows the app running in the browser on the checkout you are browsing; it is in the URL as ?panel=preview. Opening the panel starts the app, and closing it — the close button, or Preview’s own button — stops it; opening it again restarts it, quick because the packages it installed are cached in the browser. Switching to another tab leaves it running instead: the server, the frame and the app’s own state stay as they were. Nothing is pushed or deployed: Preview is a local run over the files as they are now, drafts included.
A preview is served from its own origin, one host per app, and never from the control UI’s origin. That is deliberate: app code must never be able to act as the signed-in viewer on the control UI. The workspace document lives there too, and the iframe loads the app through that origin’s service worker.
The workspace’s own code runs on that origin: the browser pod is a document on the preview host, and it creates the workers that run an install and its lifecycle scripts, the dev script and the bundler config, a build, and anything you type into the Terminal (see Terminal). The control UI keeps the editor, the file tree and every signed-in call, and only talks to that document. None of the pod’s code shares the control UI’s cookies, API or storage — your unpushed changes are saved under the preview origin now, out of the control UI’s reach and out of any other app’s. If the app’s preview host cannot be read, the workspace does not open at all (Code mode says why) rather than running its code on the control UI’s origin.
What the panel runs is decided from the checkout when it opens:
- The
devscript frompackage.json, when the app has a usable one and no dependency is native-only.@cloudflare/vite-plugin,wrangler,miniflareandworkerdcannot run in the browser, in dev or in build. - Otherwise a Worker, whose entry (from
cloudflare.config.tsorwrangler.json(c)) is bundled with Vite without plugins and served through srvx — the browser pod only strips the simplest TypeScript, so the entry is never run raw. - A Cloudflare Vite app that can build without the plugin has its client and Worker built the same way and served together; if its config cannot build without the plugin, Preview refuses with that reason instead of starting.
- An app with only static assets is served with its SPA fallback.
The serving and build tools (srvx, and Vite for a bundle) are the preview’s own: they are installed in the browser pod beside the generated server, never into the app. Your package.json and Changes stay exactly as you left them, and the app’s own dependencies are installed without being written back.
The bar at the top is an address bar with a status dot at its start — green when the app answers, yellow while it installs, builds or starts, red when it failed (hover it for the word) — and a restart button beside it. Type a path (/api/count) or a full URL and press Enter to open it in the frame; only the preview’s own host is accepted, anything else is refused. Any error is shown verbatim, the info icon next to the title explains how the preview runs, and the run’s output is in the Terminal, where the preview is the foreground job.
Three things to know about what a preview does not get:
- Secrets and environment variables from the app’s settings are never given to the preview — only plain
varsfrom the config file are. - Bindings run locally. D1, KV, R2 and Durable Objects are served in the browser by a local runtime (
@noitenow/poder): D1 and KV on SQLite, R2 as files, Durable Objects with their storage beside them. There is a single location and no output gates — an approximation the panel notes. Their data is kept in the browser and survives a reload: every write is mirrored synchronously, so counting, listing and reading carry on where they left off. Only a state past the mirror’s size cap (large R2 objects) falls back to the debounced save and can lose the last moment on a fast reload. WebSocket upgrades do not reach the preview host. - No preview from a secure page on the dev base. The
noite.localhostpreview host is plain http, which browsers block inside an https page — the panel says so. Open the UI on plain http (http://noite.localhost:9080) instead, or on an https name listed inCONTROL_EXTRA_HOSTS(such ashttps://noite.localbehind portless): the preview host then follows that name (https://app-0.preview.<slug>.noite.local). Either way the control UI and every app and preview host share one registrable domain — the same-site layout privacy browsers require for the frame’s IndexedDB and service worker.
Preview works on any ref, read-only ones included.
Terminal
Terminal, at the bottom of the file tree, opens a shell under the editor (and the right panel), in the URL as ?terminal=1. It runs in the same browser pod as the editor and Preview, at the checkout’s root (/home/project), so it sees exactly the files you are editing: a file it creates, changes or removes shows up in the tree, the editor and the Changes panel right away, and is pushed like any other change. Nothing runs on the server.
The running preview is the terminal’s foreground job, like npm run dev in a local terminal: its install, build and server output streams into the terminal, and Ctrl+C stops it and gives you the prompt back. When the preview starts again (the Preview panel’s restart button, or preview restart at the prompt), it takes the foreground again once the prompt is free. At the prompt, preview prints its state, URL and processes, and preview stop stops it. Closing the terminal keeps the shell and its scrollback for when you open it again. The terminal is drawn by ghostty-web.