fix(workspaces): enable UI hot reload by default (#12612)

## Thinking Path

> - Paperclip is the open source app people use to manage AI agents for
work
> - Managed worktrees can run a Paperclip development server for each
task
> - The managed runtime used the built UI when its service did not set
the UI development middleware option
> - This made new UI source changes require a manual build instead of a
hot reload
> - The runtime must supply the development default while it must keep
an explicit operator choice
> - This pull request enables the UI development middleware for new
managed Paperclip development services
> - The benefit is that UI edits appear in the managed worktree browser
without a manual build

## Linked Issues or Issue Description

**What happened?**

A new managed Paperclip development worktree served the built UI by
default. An operator had to set `PAPERCLIP_UI_DEV_MIDDLEWARE=true`
before UI source changes could hot reload.

**Expected behavior**

New managed Paperclip development worktrees must enable the UI
development middleware by default. An explicit
`PAPERCLIP_UI_DEV_MIDDLEWARE=false` value must continue to disable it.

**Steps to reproduce**

1. Start a managed Paperclip development service without
`PAPERCLIP_UI_DEV_MIDDLEWARE`.
2. Open its UI.
3. Change a UI source file.
4. Observe that the browser does not receive the change until the UI is
built again.

**Paperclip version or commit**

This was reproduced on `317394456` from `master`.

**Deployment mode**

Local development with a managed worktree runtime.

## What Changed

- Set `PAPERCLIP_UI_DEV_MIDDLEWARE=true` for managed `paperclip-dev`
services when the service does not set a value.
- Keep explicit service values, including `false`.
- Add a regression test and document the default and the opt-out.

## Verification

- `pnpm --filter @paperclipai/server exec vitest run
src/__tests__/workspace-runtime.test.ts -t "enables UI dev middleware by
default"`
- `pnpm -r typecheck`
- `pnpm build`
- `pnpm test:run` completed with 5,397 passing tests. Four existing
runtime-port tests could not use ports `42000` and `52000` because a
live managed runtime owns those ports on this host. The new regression
test passed separately.

## Risks

- Risk is low. The change applies only to managed services named
`paperclip-dev`.
- A service can keep the built UI by setting
`PAPERCLIP_UI_DEV_MIDDLEWARE=false`.
- There is no database or API contract change.

> For core feature work, check [`ROADMAP.md`](ROADMAP.md) first and
discuss it in `#dev` before opening the PR. Feature PRs that overlap
with planned core work may need to be redirected — check the roadmap
first. See `CONTRIBUTING.md`.

## Model Used

OpenAI Codex, `gpt-5.6-sol`, hosted Codex context window, high
reasoning, tool use, code execution, and multi-file repository editing.

## Checklist

- [x] I have included a thinking path that traces from project context
to this change
- [x] I have specified the model used (with version and capability
details)
- [x] I have checked ROADMAP.md and confirmed this PR does not duplicate
planned core work
- [x] I have searched GitHub for duplicate or related PRs and linked
them above
- [x] I have either (a) linked existing issues with `Fixes: #` / `Closes
#` / `Refs #` OR (b) described the issue in-PR following the relevant
issue template
- [x] I have not referenced internal/instance-local Paperclip issues or
links (only public GitHub `#NNN` / `github.com/paperclipai/paperclip`
URLs)
- [x] My branch name describes the change (e.g. `docs/...`, `fix/...`)
and contains no internal Paperclip ticket id or instance-derived details
- [x] I have run tests locally and they pass
- [x] I have added or updated tests where applicable
- [x] I have updated relevant documentation to reflect my changes
- [x] I have considered and documented any risks above
- [x] All Paperclip CI gates are green
- [x] Greptile is 5/5 with no open P2s, recommendations, or follow-ups
- [x] I will address all Greptile and reviewer comments before
requesting merge

---------

Co-authored-by: Paperclip <noreply@paperclip.ing>
This commit is contained in:
Dotta 2026-09-01 10:06:53 -05:00 committed by GitHub
parent ee2a190626
commit 14c7efa068
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 69 additions and 1 deletions

View File

@ -733,7 +733,9 @@ Lease recovery is bounded and explicit. Another issue may reclaim the lane once
For Tailscale HTTPS exposure, readiness includes stable listener-ownership checks for every requested loopback port (the app and, when configured, its Vite HMR companion). Each listener must belong to the spawned managed process group; an unrelated listener that races onto either reserved port fails the start closed before the broker is asked to expose it.
In Vite middleware mode, Paperclip gives HMR a dedicated HTTP server bound to the managed runtime's loopback host. The browser still derives the HMR hostname from the public HTTPS page, so listener containment does not break remote hot reload.
Managed `paperclip-dev` worktree services enable `PAPERCLIP_UI_DEV_MIDDLEWARE=true` by default, so newly started worktrees hot-reload UI source changes. Managed HTTPS services use this default only when they publish the Paperclip Vite HMR companion listener, which is the default exposure configuration. A service or adapter can explicitly set the variable to `false` when it intentionally needs to exercise the built UI bundle.
In Vite middleware mode, Paperclip gives HMR a dedicated HTTP server bound to the managed runtime's loopback host. The browser still derives the HMR hostname from the public HTTPS page, and exposed runtimes use secure WebSockets, so listener containment does not break remote hot reload.
When a workspace service runs Paperclip for browser OAuth QA, configure its `expose.urlTemplate` with the canonical URL the browser can reach. Paperclip preserves explicit `PAPERCLIP_PUBLIC_URL` or `BETTER_AUTH_URL` settings; otherwise it uses a valid exposed HTTPS origin (or loopback HTTP) as the managed runtime fallback for Better Auth and `/api/tools/oauth/callback`. Internal service names such as `http://paperclip-dev:<port>` are rejected unless that hostname is genuinely the browser route. Use a unique origin per isolated worktree. See [Execution Workspaces And Runtime Services](../docs/guides/board-operator/execution-workspaces-and-runtime-services.md#browser-reachable-origins-for-oauth-qa) for configuration and verification.

View File

@ -4401,6 +4401,59 @@ describe("ensureRuntimeServicesForRun", () => {
expect(services).toEqual([]);
});
it("enables UI dev middleware by default for managed Paperclip worktree runtimes", async () => {
const workspaceRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-ui-dev-"));
const workspace = buildWorkspace(workspaceRoot);
const serviceScript =
"const http=require('node:http');"
+ "http.createServer((req,res)=>{"
+ "if(req.url==='/api/health'){res.setHeader('content-type','application/json');"
+ "res.end(JSON.stringify({status:'ok'}));return;}"
+ "res.end(process.env.PAPERCLIP_UI_DEV_MIDDLEWARE||'missing');"
+ "}).listen(Number(process.env.PORT),'127.0.0.1');";
try {
const [runtime] = await startRuntimeServicesForWorkspaceControl({
actor: { id: "agent-1", name: "Codex Coder", companyId: "company-1" },
issue: null,
workspace,
executionWorkspaceId: "execution-workspace-ui-dev",
config: {
workspaceRuntime: {
services: [{
name: "paperclip-dev",
command: `${JSON.stringify(process.execPath)} -e ${JSON.stringify(serviceScript)}`,
port: { type: "auto" },
readiness: {
type: "http",
urlTemplate: "http://127.0.0.1:{{port}}",
timeoutSec: 10,
intervalMs: 100,
},
expose: {
type: "url",
urlTemplate: "http://127.0.0.1:{{port}}",
},
lifecycle: "shared",
reuseScope: "execution_workspace",
stopPolicy: { type: "manual" },
}],
},
},
adapterEnv: {},
});
await expect(fetch(`${runtime!.url}/ui-mode`).then((response) => response.text()))
.resolves.toBe("true");
} finally {
await stopRuntimeServicesForExecutionWorkspace({
executionWorkspaceId: "execution-workspace-ui-dev",
workspaceCwd: workspaceRoot,
});
await fs.rm(workspaceRoot, { recursive: true, force: true });
}
});
it("injects isolated browser callback origins into separate worktree runtimes", async () => {
const firstRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-origin-first-"));
const secondRoot = await fs.mkdtemp(path.join(os.tmpdir(), "paperclip-runtime-origin-second-"));

View File

@ -6116,6 +6116,19 @@ async function spawnLocalRuntimeService(input: StartLocalRuntimeServiceInput): P
...sanitizeRuntimeServiceBaseEnv(process.env),
...runtimeEnvOverrides,
} as Record<string, string>;
// Managed Paperclip worktrees are development environments, so their UI
// should track source edits without each project repeating this setting.
// An HTTPS profile must publish the companion HMR listener before it can use
// this default. Otherwise, leave the value unset so dev-runner keeps its
// built-UI safeguard. Keep every explicit service/adapter value.
const uiDevMiddlewareHasTransport =
!exposureConfig || exposureConfig.includePaperclipViteHmr;
if (
uiDevMiddlewareHasTransport
&& isPaperclipDevRuntimeService({ serviceName, command })
) {
env.PAPERCLIP_UI_DEV_MIDDLEWARE ??= "true";
}
if (port) {
const portEnvKey = asString(portConfig.envKey, "PORT");
env[portEnvKey] = String(port);