diff --git a/apps/web/index.html b/apps/web/index.html
index 828439e178a1..13518deb6e5d 100644
--- a/apps/web/index.html
+++ b/apps/web/index.html
@@ -439,13 +439,23 @@
object-fit: contain;
}
+ /* App CSS locks body and #root scrolling once main loads, so the error screen scrolls itself. */
+ #boot-shell:has(> #boot-error) {
+ height: 100%;
+ overflow-x: hidden;
+ overflow-y: auto;
+ }
+
#boot-error {
--boot-error-fg: #262626;
--boot-error-bg: #ffffff;
+ --boot-error-line: color-mix(in srgb, currentColor 12%, transparent);
display: flex;
flex-direction: column;
align-items: flex-start;
+ align-self: flex-start;
width: min(400px, calc(100% - 48px));
+ padding: clamp(24px, 22vh, 220px) 0 24px;
font-size: 13px;
line-height: 20px;
}
@@ -469,36 +479,134 @@
#boot-error p {
margin: 4px 0 0;
- opacity: 0.6;
+ opacity: 0.7;
overflow-wrap: anywhere;
text-wrap: pretty;
}
- #boot-error code {
+ #boot-error code,
+ #boot-error pre {
font-family: ui-monospace, "SF Mono", Menlo, Consolas, monospace;
font-size: 12px;
overflow-wrap: anywhere;
}
+ #boot-error a {
+ color: inherit;
+ text-decoration-line: underline;
+ text-decoration-color: color-mix(in srgb, currentColor 40%, transparent);
+ text-underline-offset: 3px;
+ }
+
+ #boot-error a:hover {
+ text-decoration-color: currentColor;
+ }
+
#boot-error button {
- border: 0;
+ height: 32px;
+ padding: 0 14px;
+ border: 1px solid var(--boot-error-line);
+ border-radius: 6px;
+ background: transparent;
+ color: inherit;
font: inherit;
+ font-weight: 500;
cursor: pointer;
}
- #boot-error button:focus-visible {
+ #boot-error button[aria-disabled="true"] {
+ opacity: 0.6;
+ cursor: default;
+ }
+
+ #boot-error :is(button, a, summary, pre):focus-visible {
outline: 2px solid var(--boot-error-fg);
outline-offset: 2px;
}
+ #boot-error-actions {
+ display: flex;
+ flex-wrap: wrap;
+ gap: 8px;
+ margin-top: 16px;
+ }
+
+ #boot-error #boot-error-reload {
+ border-color: var(--boot-error-fg);
+ background: var(--boot-error-fg);
+ color: var(--boot-error-bg);
+ }
+
+ #boot-error-reload:hover {
+ opacity: 0.9;
+ }
+
+ #boot-error #boot-error-update-note {
+ margin-top: 8px;
+ font-size: 12px;
+ line-height: 18px;
+ }
+
+ #boot-error details {
+ width: 100%;
+ margin-top: 32px;
+ }
+
+ #boot-error summary {
+ display: inline-flex;
+ align-items: center;
+ gap: 8px;
+ border-radius: 4px;
+ list-style: none;
+ opacity: 0.7;
+ cursor: pointer;
+ }
+
+ #boot-error summary::-webkit-details-marker {
+ display: none;
+ }
+
+ #boot-error summary::before {
+ content: "";
+ width: 5px;
+ height: 5px;
+ border-right: 1.5px solid currentColor;
+ border-bottom: 1.5px solid currentColor;
+ transform: rotate(-45deg);
+ }
+
+ #boot-error details[open] summary::before {
+ transform: rotate(45deg);
+ }
+
+ #boot-error summary:hover,
+ #boot-error details[open] summary {
+ opacity: 1;
+ }
+
+ #boot-error details > :not(summary) {
+ display: grid;
+ gap: 8px;
+ margin-top: 20px;
+ }
+
+ #boot-error details > a {
+ display: block;
+ width: fit-content;
+ }
+
+ #boot-error details p {
+ margin: 0;
+ font-size: 12px;
+ line-height: 18px;
+ }
+
#boot-error-command {
display: flex;
align-items: center;
gap: 8px;
- box-sizing: border-box;
- width: 100%;
padding: 4px 4px 4px 10px;
- border: 1px solid color-mix(in srgb, currentColor 10%, transparent);
+ border: 1px solid var(--boot-error-line);
border-radius: 6px;
}
@@ -506,49 +614,58 @@
flex: 1;
min-width: 0;
opacity: 0.8;
- user-select: all;
}
- #boot-error-copy {
+ #boot-error .boot-error-icon {
display: grid;
flex: none;
place-items: center;
- width: 24px;
+ min-width: 24px;
height: 24px;
+ padding: 0 4px;
+ border: 0;
border-radius: 4px;
- background: transparent;
- color: inherit;
- opacity: 0.6;
+ font-size: 12px;
+ font-weight: 400;
+ opacity: 0.7;
}
- #boot-error-copy:hover {
+ #boot-error .boot-error-icon:hover {
opacity: 1;
background: color-mix(in srgb, currentColor 8%, transparent);
}
- #boot-error-triage {
- width: 100%;
- margin-top: 40px;
+ #boot-error-details {
+ max-height: 200px;
+ margin: 0;
+ padding: 10px 12px;
+ overflow: auto;
+ border: 1px solid var(--boot-error-line);
+ border-radius: 6px;
+ font-size: 11px;
+ line-height: 16px;
+ white-space: pre-wrap;
+ opacity: 0.8;
}
- #boot-error-triage p {
- margin: 0 0 8px;
- font-size: 12px;
- line-height: 18px;
+ #boot-error-report {
+ display: flex;
+ align-items: center;
+ gap: 16px;
}
- #boot-error-reload {
- height: 32px;
- margin-top: 16px;
- padding: 0 14px;
- border-radius: 6px;
- background: var(--boot-error-fg);
- color: var(--boot-error-bg);
- font-weight: 500;
+ #boot-error-report button {
+ height: 28px;
+ padding: 0 10px;
}
- #boot-error-reload:hover {
- opacity: 0.9;
+ .boot-error-sr {
+ position: absolute;
+ width: 1px;
+ height: 1px;
+ overflow: hidden;
+ clip-path: inset(50%);
+ white-space: nowrap;
}
T3 Code (Beta)
diff --git a/apps/web/src/lib/bootError.test.ts b/apps/web/src/lib/bootError.test.ts
index ff02a25d3bff..e04e8bef2cfb 100644
--- a/apps/web/src/lib/bootError.test.ts
+++ b/apps/web/src/lib/bootError.test.ts
@@ -1,7 +1,12 @@
-import type { DesktopBridge, DesktopCliCommandState } from "@t3tools/contracts";
+import type { DesktopBridge, DesktopCliCommandState, DesktopUpdateState } from "@t3tools/contracts";
import { describe, expect, it, vi } from "vite-plus/test";
-import { resolveTriageCommand } from "./bootError";
+import {
+ type BootErrorEnvironment,
+ buildErrorReport,
+ resolveTriageCommand,
+ updateActionFor,
+} from "./bootError";
const desktopWith = (getState: () => Promise) =>
({ cliCommand: { getState } }) as unknown as DesktopBridge;
@@ -52,3 +57,77 @@ describe("resolveTriageCommand", () => {
).resolves.toBe("npx t3 triage");
});
});
+
+const environment: BootErrorEnvironment = {
+ version: "0.1.3-nightly.20261026.3707",
+ channel: "nightly",
+ surface: "web",
+ platform: "MacIntel",
+ userAgent: "Test",
+ time: "2026-10-26T00:00:00.000Z",
+};
+
+describe("buildErrorReport", () => {
+ it("drops hosts and tokens from URLs in the error", () => {
+ const error = new TypeError(
+ "Failed to fetch dynamically imported module: http://studio.tail1234.ts.net:3773/assets/a.js?pair=SECRET#token=SECRET",
+ {
+ cause: new Error(
+ "ws://studio.tail1234.ts.net:3773/ws?token=SECRET closed, main-a.js?sig=SECRET",
+ ),
+ },
+ );
+ error.stack = `TypeError: ${error.message}\n at http://studio.tail1234.ts.net:3773/src/main.tsx?t=1:10:5`;
+ const report = buildErrorReport(error, environment);
+ expect(report).not.toContain("SECRET");
+ expect(report).not.toContain("tail1234");
+ expect(report).toContain("module: /assets/a.js\n at /src/main.tsx:10:5");
+ expect(report).toContain("Caused by: Error: /ws closed, main-a.js");
+ expect(report).toContain("Version: 0.1.3-nightly.20261026.3707");
+ });
+
+ it("keeps the header when a Firefox stack frame starts with the error name", () => {
+ const error = new Error("boot failed");
+ error.stack = "ErrorBoundary@/assets/main.js:1:2";
+ expect(buildErrorReport(error, environment)).toMatch(
+ /^Error: boot failed\nErrorBoundary@\/assets\/main.js:1:2\n/,
+ );
+ const unnamed = new Error();
+ unnamed.stack = "ErrorBoundary@/assets/main.js:1:2";
+ expect(buildErrorReport(unnamed, environment)).toMatch(/^Error\nErrorBoundary@/);
+ });
+
+ it("stops at cause cycles and survives values that are not errors", () => {
+ const loop = new Error("outer");
+ loop.cause = loop;
+ expect(buildErrorReport(loop, environment).match(/outer/g)).toHaveLength(1);
+ expect(buildErrorReport("plain text", environment)).toMatch(/^plain text\n/);
+ expect(buildErrorReport({ code: 42 }, environment)).toMatch(/^\{"code":42\}\n/);
+ expect(buildErrorReport(Object.create(null), environment)).toMatch(/^\{\}\n/);
+ expect(buildErrorReport(undefined, environment)).toMatch(/^\(no error message\)\n/);
+ });
+
+ it("cuts long errors but keeps the environment", () => {
+ const report = buildErrorReport("x".repeat(20_000), environment);
+ expect(report).toContain("[truncated]");
+ expect(report.length).toBeLessThan(7_000);
+ expect(report).toContain("Time: 2026-10-26T00:00:00.000Z");
+ });
+});
+
+describe("updateActionFor", () => {
+ it("offers an action only when a newer build exists", () => {
+ const state = (status: DesktopUpdateState["status"], downloadPercent: number | null = null) =>
+ ({ status, downloadPercent }) as DesktopUpdateState;
+ expect(updateActionFor(state("available"))).toEqual({ kind: "download" });
+ expect(updateActionFor(state("downloading", 41.6))).toEqual({
+ kind: "downloading",
+ percent: 42,
+ });
+ expect(updateActionFor(state("downloaded"))).toEqual({ kind: "install" });
+ for (const status of ["disabled", "idle", "checking", "up-to-date", "error"] as const) {
+ expect(updateActionFor(state(status))).toBeNull();
+ }
+ expect(updateActionFor(undefined)).toBeNull();
+ });
+});
diff --git a/apps/web/src/lib/bootError.ts b/apps/web/src/lib/bootError.ts
index 4653c5144bb5..4b30fd7aa024 100644
--- a/apps/web/src/lib/bootError.ts
+++ b/apps/web/src/lib/bootError.ts
@@ -1,6 +1,15 @@
-import type { DesktopBridge } from "@t3tools/contracts";
+import type { DesktopBridge, DesktopUpdateState } from "@t3tools/contracts";
const FALLBACK_TRIAGE_COMMAND = "npx t3 triage";
+const DOWNLOAD_URL = "https://t3.codes/download";
+const ISSUE_URL = "https://github.com/pingdotgg/t3code/issues/new";
+const LAST_FAILURE_KEY = "t3code:boot-failed-at";
+const REPEAT_FAILURE_MS = 5 * 60_000;
+const MAX_CAUSES = 3;
+const MAX_ERROR_CHARS = 6_000;
+const COPY_BLOCKED = "Copy was blocked. The text is selected so you can copy it manually.";
+const UPDATE_DOWNLOAD_FAILED = "The update download failed. Try Download update again.";
+const UPDATE_INSTALL_FAILED = "The update install failed. Try Restart to update again.";
/** The Windows launcher is a `.cmd` run from PowerShell, which needs `&` to run a quoted path. */
function launcherCommand(launcherPath: string) {
@@ -28,6 +37,120 @@ export async function resolveTriageCommand(bridge: DesktopBridge | undefined) {
return FALLBACK_TRIAGE_COMMAND;
}
+/** The update button only appears when the desktop updater has a newer build to offer. */
+export function updateActionFor(state: DesktopUpdateState | null | undefined) {
+ switch (state?.status) {
+ case "available":
+ return { kind: "download" } as const;
+ case "downloading":
+ return { kind: "downloading", percent: Math.round(state.downloadPercent ?? 0) } as const;
+ case "downloaded":
+ return { kind: "install" } as const;
+ default:
+ return null;
+ }
+}
+
+function safeString(value: unknown) {
+ try {
+ return String(value);
+ } catch {
+ return "";
+ }
+}
+
+function describeOne(value: unknown) {
+ if (value instanceof Error) {
+ const header = value.message ? `${value.name}: ${value.message}` : value.name;
+ // V8 stacks start with the header; Safari and Firefox stacks do not.
+ const stack = typeof value.stack === "string" ? value.stack : "";
+ return stack === header || stack.startsWith(`${header}\n`)
+ ? stack
+ : [header, stack].filter(Boolean).join("\n");
+ }
+ if (typeof value === "string") return value;
+ try {
+ return JSON.stringify(value) ?? safeString(value);
+ } catch {
+ return safeString(value);
+ }
+}
+
+/** Hosts can name private machines, and pairing tokens live in query strings and hashes. */
+function redact(text: string) {
+ return text
+ .replace(/\b[a-z][\w+.-]*:\/\/[^\s/?#'"()]+/gi, "")
+ .replace(/[?#][^\s'"()]*=[^\s'"()]*?((?::\d+){1,2})?(?=[\s'"()]|$)/g, "$1");
+}
+
+export interface BootErrorEnvironment {
+ readonly version: string;
+ readonly channel: string;
+ readonly surface: "desktop" | "web";
+ readonly platform: string;
+ readonly userAgent: string;
+ readonly time: string;
+}
+
+/** Plain text that is shown and copied as-is, so users see exactly what they share. */
+export function buildErrorReport(error: unknown, environment: BootErrorEnvironment) {
+ const parts: Array = [];
+ const seen = new Set();
+ let current = error;
+ while (current != null && !seen.has(current) && parts.length <= MAX_CAUSES) {
+ seen.add(current);
+ const text = describeOne(current);
+ parts.push(parts.length === 0 ? text : `Caused by: ${text}`);
+ current = current instanceof Error ? current.cause : undefined;
+ }
+ let details = redact(parts.join("\n") || "(no error message)");
+ if (details.length > MAX_ERROR_CHARS) {
+ details = `${details.slice(0, MAX_ERROR_CHARS)}\n[truncated]`;
+ }
+ return [
+ details,
+ "",
+ `Version: ${environment.version}`,
+ `Channel: ${environment.channel}`,
+ `Surface: ${environment.surface}`,
+ `Platform: ${environment.platform}`,
+ `User agent: ${environment.userAgent}`,
+ `Time: ${environment.time}`,
+ ].join("\n");
+}
+
+function clientPlatform(bridge: DesktopBridge | undefined) {
+ try {
+ return bridge?.getClientPlatform?.() ?? navigator.platform;
+ } catch {
+ return navigator.platform;
+ }
+}
+
+function readEnvironment(bridge: DesktopBridge | undefined): BootErrorEnvironment {
+ const version = import.meta.env.APP_VERSION;
+ return {
+ version,
+ channel:
+ import.meta.env.VITE_RELEASE_CHANNEL || /-(nightly|preview)\./.exec(version)?.[1] || "stable",
+ surface: bridge ? "desktop" : "web",
+ platform: clientPlatform(bridge),
+ userAgent: navigator.userAgent,
+ time: new Date().toISOString(),
+ };
+}
+
+/** Opens More options by default when startup already failed in this tab a moment ago. */
+function isRepeatFailure(now: number) {
+ try {
+ const previous = Number(sessionStorage.getItem(LAST_FAILURE_KEY));
+ sessionStorage.setItem(LAST_FAILURE_KEY, String(now));
+ return now - previous < REPEAT_FAILURE_MS;
+ } catch {
+ return false;
+ }
+}
+
const icon = (paths: string) =>
``;
const COPY_ICON = icon(
@@ -35,65 +158,238 @@ const COPY_ICON = icon(
);
const CHECK_ICON = icon('');
+function element(
+ tag: K,
+ props: Partial = {},
+ children: ReadonlyArray = [],
+) {
+ const node = Object.assign(document.createElement(tag), props);
+ node.append(...children);
+ return node;
+}
+
+function selectContents(target: HTMLElement) {
+ const range = document.createRange();
+ range.selectNodeContents(target);
+ const selection = window.getSelection();
+ selection?.removeAllRanges();
+ selection?.addRange(range);
+}
+
+function copySelection() {
+ try {
+ return document.execCommand("copy");
+ } catch {
+ return false;
+ }
+}
+
+/** Plain http has no Clipboard API, but the legacy copy command still works on a selection. */
+function copyText(target: HTMLElement, onCopied: () => void, onBlocked: (hint: string) => void) {
+ const fallback = () => {
+ selectContents(target);
+ if (copySelection()) return onCopied();
+ onBlocked(
+ /Mac|iPhone|iPad/.test(navigator.platform) ? "Press ⌘C to copy" : "Press Ctrl+C to copy",
+ );
+ };
+ const clipboard = window.isSecureContext ? navigator.clipboard : undefined;
+ if (typeof clipboard?.writeText !== "function") return fallback();
+ clipboard.writeText(target.textContent ?? "").then(onCopied, fallback);
+}
+
+function mountUpdateButton(
+ bridge: DesktopBridge,
+ button: HTMLButtonElement,
+ note: HTMLParagraphElement,
+ announce: (text: string) => void,
+) {
+ let action: ReturnType = null;
+ const render = (state: DesktopUpdateState) => {
+ action = updateActionFor(state);
+ button.hidden = action === null;
+ // aria-disabled keeps keyboard focus on the button while the download runs.
+ button.setAttribute("aria-disabled", String(action?.kind === "downloading"));
+ if (action?.kind === "download") button.textContent = "Download update";
+ if (action?.kind === "downloading")
+ button.textContent = `Downloading update (${action.percent}%)`;
+ if (action?.kind === "install") button.textContent = "Restart to update";
+ const failure = !action
+ ? ""
+ : state.errorContext === "download"
+ ? UPDATE_DOWNLOAD_FAILED
+ : state.errorContext === "install"
+ ? UPDATE_INSTALL_FAILED
+ : "";
+ if (failure === note.textContent) return;
+ note.textContent = failure;
+ note.hidden = !failure;
+ if (failure) announce(failure);
+ };
+ button.addEventListener("click", () => {
+ const run =
+ action?.kind === "install"
+ ? bridge.installUpdate()
+ : action?.kind === "download"
+ ? bridge.downloadUpdate()
+ : undefined;
+ run?.then(
+ (result) => render(result.state),
+ (error: unknown) => console.warn("Could not update T3 Code.", error),
+ );
+ });
+ // A pushed state is newer than the initial read, so the read must not overwrite it.
+ let pushed = false;
+ try {
+ bridge.onUpdateState((state) => {
+ pushed = true;
+ render(state);
+ });
+ bridge.getUpdateState().then(
+ (state) => pushed || render(state),
+ (error: unknown) => console.warn("Could not read the desktop update state.", error),
+ );
+ } catch (error) {
+ console.warn("Could not read the desktop update state.", error);
+ }
+}
+
export function showBootError(error: unknown) {
console.error("T3 Code failed to start.", error);
const bootShell = document.getElementById("boot-shell");
if (!bootShell) return;
- const content = document.createElement("div");
- content.id = "boot-error";
- content.setAttribute("role", "alert");
+ const bridge = window.desktopBridge;
+ const environment = readEnvironment(bridge);
+ const offline = !bridge && navigator.onLine === false;
- const title = document.createElement("h1");
- title.textContent = "T3 Code could not load";
- content.append(title);
+ const status = element("span", { className: "boot-error-sr" });
+ status.setAttribute("aria-live", "polite");
+ // Clearing first makes screen readers announce a repeated message.
+ let announcement: ReturnType | undefined;
+ const announce = (text: string) => {
+ clearTimeout(announcement);
+ status.textContent = "";
+ announcement = setTimeout(() => (status.textContent = text), 100);
+ };
- if (import.meta.env.DEV && error instanceof Error) {
- const detail = document.createElement("p");
- detail.append(Object.assign(document.createElement("code"), { textContent: error.message }));
- content.append(detail);
- }
+ const message = element("div", {}, [
+ element("h1", { textContent: "T3 Code failed to start" }),
+ element("p", {
+ textContent: offline
+ ? "This browser appears to be offline. Check the connection, then reload."
+ : "We are not sure why. Try a reload first.",
+ }),
+ ]);
+ message.setAttribute("role", "alert");
- const reload = document.createElement("button");
- reload.id = "boot-error-reload";
- reload.type = "button";
- reload.textContent = "Reload";
+ const reload = element("button", {
+ id: "boot-error-reload",
+ type: "button",
+ textContent: "Reload",
+ });
reload.addEventListener("click", () => window.location.reload());
+ const update = element("button", { id: "boot-error-update", type: "button", hidden: true });
+ const updateNote = element("p", { id: "boot-error-update-note", hidden: true });
+ if (bridge) mountUpdateButton(bridge, update, updateNote, announce);
- const hint = document.createElement("p");
- hint.textContent = "If reloading does not help, run this in a terminal to find the cause:";
-
- const command = document.createElement("code");
- command.textContent = FALLBACK_TRIAGE_COMMAND;
- const commandRow = document.createElement("div");
- commandRow.id = "boot-error-command";
- commandRow.append(command);
- // The clipboard API exists only in secure contexts, such as HTTPS and the desktop app.
- if (window.isSecureContext) {
- const copy = document.createElement("button");
- copy.id = "boot-error-copy";
- copy.type = "button";
- copy.title = "Copy command";
- copy.setAttribute("aria-label", "Copy command");
- copy.innerHTML = COPY_ICON;
- copy.addEventListener("click", () => {
- navigator.clipboard.writeText(command.textContent ?? "").then(
- () => {
- copy.innerHTML = CHECK_ICON;
- copy.setAttribute("aria-label", "Copied");
- },
- (copyError) => console.warn("Could not copy the triage command.", copyError),
- );
- });
- commandRow.append(copy);
+ const command = element("code", { textContent: FALLBACK_TRIAGE_COMMAND });
+ const copyCommand = element("button", {
+ className: "boot-error-icon",
+ type: "button",
+ title: "Copy command",
+ innerHTML: COPY_ICON,
+ });
+ copyCommand.setAttribute("aria-label", "Copy command");
+ copyCommand.addEventListener("click", () =>
+ copyText(
+ command,
+ () => {
+ copyCommand.innerHTML = CHECK_ICON;
+ copyCommand.title = "Copied";
+ copyCommand.setAttribute("aria-label", "Copied");
+ announce("Copied to clipboard.");
+ },
+ (hint) => {
+ copyCommand.textContent = hint;
+ copyCommand.title = hint;
+ copyCommand.setAttribute("aria-label", hint);
+ announce(COPY_BLOCKED);
+ },
+ ),
+ );
+ void resolveTriageCommand(bridge).then((value) => (command.textContent = value));
+
+ let report = "(no error message)";
+ try {
+ report = buildErrorReport(error, environment);
+ } catch (reportError) {
+ console.warn("Could not describe the startup error.", reportError);
}
+ const details = element("pre", { id: "boot-error-details", tabIndex: 0, textContent: report });
+ const copyDetails = element("button", { type: "button", textContent: "Copy error details" });
+ copyDetails.addEventListener("click", () =>
+ copyText(
+ details,
+ () => {
+ copyDetails.textContent = "Copied";
+ announce("Copied to clipboard.");
+ },
+ (hint) => {
+ copyDetails.textContent = hint;
+ announce(COPY_BLOCKED);
+ },
+ ),
+ );
- const triage = document.createElement("div");
- triage.id = "boot-error-triage";
- triage.append(hint, commandRow);
+ const issue = new URL(ISSUE_URL);
+ issue.searchParams.set("template", "bug_report.yml");
+ issue.searchParams.set("title", "T3 Code failed to start");
+ issue.searchParams.set("area", "apps/web");
+ issue.searchParams.set("version", `${environment.version} (${environment.channel})`);
+ issue.searchParams.set("environment", `${environment.surface}, ${environment.platform}`);
+ const link = (href: string, textContent: string) =>
+ element("a", { href, textContent, target: "_blank", rel: "noreferrer" });
+ const nightly = environment.channel === "nightly";
- content.append(reload, triage);
- bootShell.replaceChildren(content);
+ const more = element("details", { open: isRepeatFailure(Date.now()) }, [
+ element("summary", { textContent: "More options if it keeps failing" }),
+ ...(bridge
+ ? [
+ link(
+ nightly ? `${DOWNLOAD_URL}?channel=nightly` : DOWNLOAD_URL,
+ nightly
+ ? "Download and reinstall the latest nightly build"
+ : "Download and reinstall T3 Code",
+ ),
+ ]
+ : []),
+ element("div", {}, [
+ element("p", {
+ textContent:
+ "Run this on the computer that runs T3 Code. Your agent can check the local logs and help you file a bug report.",
+ }),
+ element("div", { id: "boot-error-command" }, [command, copyCommand]),
+ ]),
+ element("div", {}, [
+ details,
+ element("p", {
+ textContent: "Details may include local file paths and names. Check before sharing.",
+ }),
+ element("div", { id: "boot-error-report" }, [
+ copyDetails,
+ link(issue.href, "File a bug report on GitHub"),
+ ]),
+ ]),
+ ]);
- void resolveTriageCommand(window.desktopBridge).then((value) => (command.textContent = value));
+ bootShell.replaceChildren(
+ element("div", { id: "boot-error" }, [
+ message,
+ element("div", { id: "boot-error-actions" }, [reload, update]),
+ updateNote,
+ more,
+ status,
+ ]),
+ );
}