mirror of
https://github.com/paperclipai/paperclip.git
synced 2026-10-06 10:24:09 +02:00
## Thinking Path > - Paperclip is the open source app people use to manage AI agents for work. > - The browser must load its JavaScript before React can render a task. > - A failed import can stop that process before the React error boundary exists. > - The HTML entry then leaves an empty page with no recovery action. > - This PR adds a small recovery screen that works without React. > - The user can retry the same page and return to saved task content. ## Linked Issues or Issue Description Refs #13824 and #13895. This is a follow-up to their browser startup investigation. **What happened?** Interrupting the app bundle or a required import leaves an empty React root. A startup exception has the same effect. A React error boundary cannot handle these failures because React has not started. **Expected behavior** The page must explain the startup failure and offer a manual retry. A late successful load must dismiss the recovery message without a reload. **Steps to reproduce** 1. Open a saved task in the browser. 2. Abort the application bundle request, or make a required module return HTTP 503. 3. Observe the empty page before this change. With this change, use Reload page after the fault clears and verify the saved task and comment. **Paperclip version or commit** The failing regression baseline used master at `8781f06a8`. **Deployment mode** Local source build and compiled UI. Tests cover both initial navigation and a page controlled by the production service worker. The exact cause of the older intermittent Vite stall remains unconfirmed. Forty app loads and thirty replays of retained responses did not reproduce it. This PR fixes the missing recovery path; it does not claim to remove that historical cause. A normal HTTP 304 response is not a failure. ## What Changed - Add an inline startup guard and recovery screen in the HTML entry. It does not depend on the app module graph. - Show a manual reload action after a startup error or after 30 seconds without rendered root content. - Remove the notice, timer, observer, and error listeners when the app starts. Never reload automatically. - Keep the recovery screen outside the React root so it cannot satisfy app-readiness checks. - Add browser tests for interrupted imports, a stalled import, an evaluation error, service-worker-controlled retry, repeated offline retry, and cleanup after successful startup. - Return a static, uncached HTML retry screen when a service-worker-controlled navigation fails offline. It contains no task content. - Add a full-app test that retries an interrupted compiled bundle and checks the saved task, comment, composer, route, and absence of agent runs. - Document the coverage and the limits of the historical diagnosis. ## Verification - Red baseline: four recovery cases failed; the normal-startup case passed. After the change, all five recovery cases passed. The review found an offline retry gap; that additional case failed before the worker fix and passed afterward. - Full provider-free browser-support suite: 16 passed. - Compiled-app browser tests: four passed, including saved-task reload, interrupted-bundle recovery, slow-CPU service-worker reload, and sidebar navigation. - Expanded service-worker, offline response, PWA, and worker build-ID unit tests: 37 passed. The two old plain-text offline expectations were reproduced as failures and updated for the HTML retry contract. - UI production build, full local repository typecheck (`pnpm -r typecheck`), runner-E2E typecheck, and design token checks passed. - Manual browser check: a temporary server failed the compiled bundle once. The recovery screen appeared. Clicking Reload page restored the same saved task, comment, and composer. - Full local `pnpm build` passed. - Full local `pnpm test:run` was attempted with a bounded deadline and stopped after it timed out. Workspace runtime/cleanup tests reported timeouts on this host. The monolithic local run is not a pass. The focused tests above and the complete Linux CI run provide the successful verification. - Final-head [CI run](https://github.com/paperclipai/paperclip/actions/runs/36072201966) passed. All 53 check runs succeeded; the two Storybook jobs were intentionally skipped. The legacy security status also passed. - Greptile reviewed `f83e0f51fb760541d83353f2c1df4e182f3948f9`: 5/5. Both review findings are fixed and resolved. ## Risks - The guard only handles startup before React renders root content. Existing React boundaries handle later rendering errors. - A slow startup can show the message after 30 seconds. A later successful render removes it; the page does not reload by itself. - The fallback uses native HTML when the app stylesheet is unavailable. - The worker changes only its offline navigation response. It returns static HTML with a reload button and `Cache-Control: no-store`. Its cache allowlist, private-response protections, task state, provider prompts, and grading rules stay unchanged. - This does not establish or fix the unknown cause of the historical intermittent Vite stall. ## Model Used OpenAI GPT-6 through Codex. The session exposes the GPT-6 family but not an exact served model ID or context window size. Used reasoning, code editing, shell tools, and browser testing. No subagents were used. ## 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>
112 lines
5.4 KiB
HTML
112 lines
5.4 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="en" class="dark">
|
||
<head>
|
||
<meta charset="UTF-8" />
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0, viewport-fit=cover" />
|
||
<meta name="theme-color" content="#18181b" />
|
||
<meta name="apple-mobile-web-app-title" content="Paperclip" />
|
||
<title>Paperclip</title>
|
||
<!-- PAPERCLIP_RUNTIME_BRANDING_START -->
|
||
<!-- PAPERCLIP_RUNTIME_BRANDING_END -->
|
||
<!-- PAPERCLIP_FAVICON_START -->
|
||
<link rel="icon" href="/favicon.ico" sizes="48x48" />
|
||
<link rel="icon" href="/favicon.svg" type="image/svg+xml" />
|
||
<link rel="icon" type="image/png" sizes="32x32" href="/favicon-32x32.png" />
|
||
<link rel="icon" type="image/png" sizes="16x16" href="/favicon-16x16.png" />
|
||
<!-- PAPERCLIP_FAVICON_END -->
|
||
<link rel="apple-touch-icon" sizes="180x180" href="/apple-touch-icon.png" />
|
||
<link rel="manifest" href="/site.webmanifest" crossorigin="use-credentials" />
|
||
<script>
|
||
(() => {
|
||
const key = "paperclip.theme";
|
||
const darkThemeColor = "#18181b";
|
||
const lightThemeColor = "#ffffff";
|
||
try {
|
||
const stored = window.localStorage.getItem(key);
|
||
const prefersDark =
|
||
typeof window.matchMedia === "function" &&
|
||
window.matchMedia("(prefers-color-scheme: dark)").matches;
|
||
const fallback = prefersDark ? "dark" : "light";
|
||
const theme = stored === "light" || stored === "dark" ? stored : fallback;
|
||
const isDark = theme === "dark";
|
||
document.documentElement.classList.toggle("dark", isDark);
|
||
document.documentElement.style.colorScheme = isDark ? "dark" : "light";
|
||
const themeColorMeta = document.querySelector('meta[name="theme-color"]');
|
||
if (themeColorMeta) {
|
||
themeColorMeta.setAttribute("content", isDark ? darkThemeColor : lightThemeColor);
|
||
}
|
||
} catch {
|
||
document.documentElement.classList.add("dark");
|
||
document.documentElement.style.colorScheme = "dark";
|
||
}
|
||
})();
|
||
</script>
|
||
</head>
|
||
<body>
|
||
<div id="root"></div>
|
||
<!-- Keep recovery outside #root so it cannot be mistaken for a mounted app.
|
||
It must work even when the app's JavaScript or stylesheet cannot load. -->
|
||
<section id="paperclip-startup" role="status" aria-live="polite" hidden>
|
||
<div class="mx-auto flex min-h-screen max-w-2xl flex-col justify-center space-y-4 px-4 py-10">
|
||
<div>
|
||
<h1 id="paperclip-startup-title" class="text-lg font-semibold">Paperclip couldn’t start</h1>
|
||
<p id="paperclip-startup-message" class="mt-1 text-sm text-muted-foreground">
|
||
Part of the app failed to load. Check your connection and reload this page to try again.
|
||
</p>
|
||
</div>
|
||
<div>
|
||
<button id="paperclip-startup-reload" type="button" class="inline-flex items-center rounded-md border border-input bg-background px-3 py-1.5 text-sm font-medium shadow-sm hover:bg-accent hover:text-accent-foreground">
|
||
Reload page
|
||
</button>
|
||
</div>
|
||
</div>
|
||
</section>
|
||
<script>
|
||
// This guard is deliberately inline and independent of the module graph:
|
||
// React's error boundary cannot catch an import that prevents React loading.
|
||
(() => {
|
||
const root = document.getElementById("root");
|
||
const notice = document.getElementById("paperclip-startup");
|
||
const title = document.getElementById("paperclip-startup-title");
|
||
const message = document.getElementById("paperclip-startup-message");
|
||
const reload = document.getElementById("paperclip-startup-reload");
|
||
const retry = () => window.location.reload();
|
||
const finished = () => root.childElementCount > 0;
|
||
const cleanup = () => {
|
||
clearTimeout(timer);
|
||
observer.disconnect();
|
||
window.removeEventListener("error", onError, true);
|
||
window.removeEventListener("unhandledrejection", onFailure);
|
||
reload.removeEventListener("click", retry);
|
||
notice.remove();
|
||
};
|
||
const onFailure = () => {
|
||
if (finished()) return cleanup();
|
||
title.textContent = "Paperclip couldn’t start";
|
||
message.textContent = "Part of the app failed to load. Check your connection and reload this page to try again.";
|
||
notice.hidden = false;
|
||
};
|
||
const onError = (event) => {
|
||
// Ignore images and other incidental resources. Preserve the original
|
||
// error for the browser console; never display raw errors or URLs here.
|
||
if (event instanceof ErrorEvent || event.target instanceof HTMLScriptElement ||
|
||
(event.target instanceof HTMLLinkElement && event.target.rel === "stylesheet")) onFailure();
|
||
};
|
||
const observer = new MutationObserver(() => { if (finished()) cleanup(); });
|
||
observer.observe(root, { childList: true });
|
||
const timer = setTimeout(() => {
|
||
if (finished()) return cleanup();
|
||
if (!notice.hidden) return;
|
||
title.textContent = "Paperclip is taking longer to load";
|
||
message.textContent = "You can keep waiting, or reload this page to try again.";
|
||
notice.hidden = false;
|
||
}, 30_000);
|
||
window.addEventListener("error", onError, true);
|
||
window.addEventListener("unhandledrejection", onFailure);
|
||
reload.addEventListener("click", retry);
|
||
})();
|
||
</script>
|
||
<script type="module" src="/src/main.tsx"></script>
|
||
</body>
|
||
</html>
|