Discovery and Serving
How a Dev Preview session comes up: script discovery and command ranking, URL detection, port allocation, the stable proxy origin in front of your server, per-worktree sessions, and what happens to one when you close the panel.
Automatic discovery
Daintree scans the project for runnable commands. Detection covers nine sources:
package.jsonscripts: the runner follows your lock file (bun.lockorbun.lockbuses bun,pnpm-lock.yamluses pnpm,yarn.lockuses yarn, otherwise npm)- Makefile targets (non-private, non-PHONY)
- Justfile recipes (skips underscore-prefixed entries)
- Taskfile.yml tasks (only those with a
descfield) - Procfile: every
NAME: commandline, with the command body used verbatim - mise.toml:
[tasks]entries with arunfield, skippinghide = trueand_-prefixed tasks; surfaced asmise run [name] - Django
manage.py: common commands likerunserver,migrate,test, andshell composer.jsonscripts (non-lifecycle).devcontainer/devcontainer.json: thepostStartCommandin string, array, or object form, withnohup, a trailing&, andbash -cwrappers stripped
Detection results are cached for 60 seconds and refresh on project load or when you switch worktrees. Every discovered command also feeds the QuickRun autocomplete.
How a candidate is ranked
Ranking is framework-aware before it is name-based. If your dependencies name a framework Daintree recognizes (Next.js, Remix, React Router, Nuxt, SvelteKit, Astro, Create React App, or plain Vite) and that framework's canonical script exists and actually invokes it, that script wins. This is why a Create React App project offers start rather than being pushed down the list by a dev script that does something else.
After the framework default come scripts named dev, then start, then serve. If none of those matched, a .devcontainer postStartCommand is offered next, then every other detected runner.
--turbopack flag before spawning the dev server. The reason: webpack's style-loader injects CSS using unsafe-inline, which Electron's Content Security Policy blocks. Turbopack serves CSS as regular files, so styles render correctly in the embedded browser. The version is read from
node_modules/next/package.json, falling back to your package.json dependencies. On Next.js 14 or older, or when the version can't be determined, the flag is stripped rather than injected, since Turbopack only became stable for next dev in Next.js 15. Injection is idempotent and adapts to your command form.
next dev becomes next dev --turbopack. npm run dev / yarn dev / pnpm dev get -- --turbopack appended, and bun run dev gets --turbopack appended directly. Commands containing shell control characters (&&, ;, |, redirection, substitution) are left alone, because the flag would attach to the wrong program. Turn it off per project in Project Settings > Dev Server Command with the Auto-inject
--turbopack for Next.js 15+ projects checkbox (on by default). Disable it if your project relies on a custom webpack() config, a .babelrc, or loaders that aren't Turbopack-compatible (SVGR is a common one). With the toggle off, Daintree strips any --turbopack flag a previous run may have added.Port pinning
Daintree allocates a port for each session, and for the frameworks that ignore process.env.PORT it injects the port into the command rather than hoping to detect it afterwards. Vite and SvelteKit get --port plus --strictPort, so the server fails fast on a collision instead of quietly drifting to the next free port and leaving the readiness probe watching the wrong URL. Astro and Nuxt get --port only, since they don't forward --strictPort to Vite. A command that already specifies a port is left alone, as is any command with shell control characters in it.
For frameworks Daintree can't pin, it still knows the conventional default port (3000 for Next.js, Remix, Nuxt and Rails, 5173 for Vite and SvelteKit, 4321 for Astro, 8000 for Django and Laravel, 4000 for Phoenix) and uses it to aim the readiness probe.
How the preview is served
The webview never loads http://localhost:<devPort> directly. Daintree runs a small reverse proxy on a fixed port (43000, or an OS-assigned fallback if that one is taken) and gives every panel a stable origin of the form http://dp-<token>.localhost:<proxyPort>. Each request is forwarded to whatever port that panel's dev server is on right now, resolved live.
That indirection buys three things:
- Cookies and
localStoragesurvive a restart. Restart the dev server and it may come back on a different port, but the origin the page was loaded from does not change, so nothing scoped to that origin is lost. - Host checks pass. The proxy rewrites the
Hostheader to the upstream, so Vite and Next.js host validation sees what it expects, and it strips theDomain=attribute from upstreamSet-Cookieheaders so cookies bind host-only to the stable origin instead of being rejected. - IPv6-only and HTTPS dev servers work. The upstream is dialed as
localhost, not127.0.0.1, so a Vite server that bound IPv6-only still resolves; and a dev server running over TLS (Vite'sserver.https, Next.js--experimental-https, mkcert) is dialed over HTTPS rather than answered with a 502.
A stalled upstream is dropped after 30 seconds and surfaced as a classified 502, rather than leaving the webview spinning forever.
Opening the page in your real browser
Open in Browser does more than hand your default browser a URL. Daintree mints a short-lived, single-use, panel-bound HMAC token and opens a bootstrap URL carrying it. The external browser redeems the token once, picks up a session marker on the same stable origin, and lands on the page you were looking at, instead of an unauthenticated cold start. Tokens live for 60 seconds and never outlive the app process; the key is generated per run and is never persisted.
Session lifecycle
Each panel's dev-server session is keyed by project and panel, and survives panel remounts: docking, undocking or rearranging panels doesn't restart your server. If a panel remounts while the server is still starting, the last 300 lines of terminal output are replayed on the Output tab so you don't lose the context.
Across a relaunch
Running sessions are captured to a manifest at shutdown. The manifest holds spawn metadata only (working directory, command, environment, Turbopack setting) and no process handle: nothing is reattached, and the next start allocates a fresh port rather than reusing one that may now be in TIME_WAIT or taken by something else. On the next launch the panel comes back as Stopped (restored) and re-issues the command when you open it. Until then it stays visible in the dev-server dashboard as an explicit restart offer. Stopping a server yourself drops it from the manifest, so a deliberate stop stays stopped.
Lazy loading and memory
The embedded browser isn't created until the panel is first visible; background panels show a placeholder until you switch to them. A background panel may also have its webview released under memory pressure, in which case the panel shows a "Reclaimed for memory" placeholder and recreates the webview the next time you focus it.
Scroll position is saved when a panel unmounts and restored when the same URL loads again. Restarting the dev server clears the saved position, since the page content has changed.