← Files ModRetro Chromatic PluginARCHIVED FILE

dist/web-annotations/flash.js

48.1 KB · Oct 2, 2026 · 00:37 UTC

↓ Download file

/// <reference lib="dom" />
import { copyFlashFailureDetails } from "./flash-diagnostics.js";
import { DIALOG_STYLES } from "./view.js";
class DeviceRequestRejected extends Error {
}
class PreviewConnectionError extends Error {
}
const journalAccessMessage = "Installation history is unavailable. Open Setup for help.";
class JournalAccessError extends Error {
    constructor() { super(journalAccessMessage); }
}
/** Only diagnostic identifiers and measured boundaries enter the copied report. */
function errorReport(snapshot, readFailed, sourceRevision, statusErrorCode, pending) {
    // A lost reply may leave a snapshot of a different operation. Never attach its
    // closure or ROM to the new pending request.
    const rejectedBeforeDispatch = statusErrorCode === "PREVIEW_STORAGE_UNAVAILABLE" || statusErrorCode === "PREVIEW_REQUEST_REJECTED";
    const latestOperation = !rejectedBeforeDispatch && (!pending || snapshot?.operation?.requestId === pending.requestId) ? snapshot?.operation : undefined;
    // Match the displayed error's precedence. A current discovery failure must
    // not inherit a previous flash's identity, closure evidence, or ROM.
    const discovery = !rejectedBeforeDispatch && !pending && !!snapshot?.discoveryError && latestOperation?.state !== "running"
        && latestOperation?.state !== "unresolved" && latestOperation?.cartridgeWrite !== "outcome-unverified";
    const operation = discovery ? snapshot?.discoveryError?.operation : latestOperation;
    // A disconnected preview is not evidence of a device operation. Preserve
    // operation recovery whenever a request, recorded operation or uncertainty exists.
    const previewConnection = statusErrorCode === "PREVIEW_CONNECTION_LOST" && !pending && !snapshot?.operation
        && !snapshot?.discoveryError?.operation && !snapshot?.blocked && !snapshot?.requestedOperationUnknown;
    const code = (value) => typeof value === "string" && /^[a-zA-Z0-9_.:-]{1,128}$/u.test(value) ? value : undefined;
    const hash = (value) => typeof value === "string" && /^[a-f0-9]{64}$/u.test(value) ? value : undefined;
    const object = (value) => !!value && typeof value === "object" && !Array.isArray(value);
    const boundary = (value) => object(value) ? {
        ...(value.code === null || Number.isInteger(value.code) ? { code: value.code } : {}),
        ...(value.signal === null || code(value.signal) ? { signal: value.signal } : {}),
    } : undefined;
    const commands = (Array.isArray(operation?.diagnostics?.commands) ? operation.diagnostics.commands : []).slice(0, 16).filter(object).map(command => {
        const failureDetails = copyFlashFailureDetails(command.failureDetails);
        return {
            command: code(command.command), vendorCliVersion: code(command.version), target: code(command.target),
            ...(Number.isInteger(command.pid) ? { pid: command.pid } : {}),
            exit: boundary(command.exit), close: boundary(command.close), vendorCode: code(command.vendorCode),
            ...(typeof command.outputComplete === "boolean" ? { outputComplete: command.outputComplete } : {}),
            ...(failureDetails ? { failureDetails } : {}),
        };
    });
    return {
        schemaVersion: 1, source: "modretro-chromatic", statusReadFailed: readFailed && !rejectedBeforeDispatch && statusErrorCode !== "PREVIEW_RECOVERY_UNAVAILABLE", statusErrorCode: code(statusErrorCode),
        subject: previewConnection ? "preview-connection" : rejectedBeforeDispatch ? "request-not-dispatched" : discovery ? "discovery" : "operation",
        operationId: code(operation?.operationId), requestId: code(pending?.requestId ?? (!discovery ? latestOperation?.requestId : undefined)),
        requestScope: discovery || previewConnection ? undefined : "preview",
        command: code(operation?.command) ?? (pending ? pending.action === "flash" ? "flash" : "list_devices" : undefined),
        recordedState: code(operation?.state), stage: code(operation?.stage),
        errorCode: code(discovery ? snapshot?.discoveryError?.code : latestOperation?.error?.code),
        processCompletion: previewConnection ? undefined : rejectedBeforeDispatch ? "not-started" : code(operation?.diagnostics?.processCompletion) ?? "unconfirmed",
        cartridgeWrite: previewConnection ? undefined : rejectedBeforeDispatch ? "not-dispatched" : code(operation?.cartridgeWrite) ?? "unknown",
        rom: { sha256: discovery ? undefined : hash(latestOperation?.romSha256) },
        currentPreview: { sha256: hash(snapshot?.rom.sha256),
            ...(Number.isSafeInteger(snapshot?.rom.sizeBytes) ? { sizeBytes: snapshot.rom.sizeBytes } : {}),
            sourceRevision: typeof sourceRevision === "string" && /^(?:[a-f0-9]{40}|[a-f0-9]{64})$/u.test(sourceRevision) ? sourceRevision : undefined },
        commands,
    };
}
/** One visible-preview device observer and install/recovery controller, shared by both views. */
export function mountFlashControl(root, generation, current, verifyGame, sourceRevision) {
    const doc = root.ownerDocument;
    const node = (tag, text) => {
        const value = doc.createElement(tag);
        if (text)
            value.textContent = text;
        return value;
    };
    const button = node("button");
    const buttonLabel = node("span", "Install on Chromatic");
    button.type = "button";
    button.className = "cgv-install-button";
    button.disabled = true;
    button.dataset.action = "flash-to-device";
    button.setAttribute("aria-haspopup", "dialog");
    const icon = doc.createElementNS("http://www.w3.org/2000/svg", "svg");
    icon.setAttribute("viewBox", "0 0 24 24");
    icon.setAttribute("aria-hidden", "true");
    const iconPath = doc.createElementNS("http://www.w3.org/2000/svg", "path");
    const plugIcon = "M9 3v5m6-5v5M6 8h12M7 8v4a5 5 0 0 0 10 0V8m-5 9v4";
    const downloadIcon = "M12 3v12m-4-4 4 4 4-4M4 16v3a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2v-3";
    iconPath.setAttribute("d", plugIcon);
    icon.append(iconPath);
    button.append(icon, buttonLabel);
    const dialog = node("dialog");
    dialog.className = "cgv-dialog cgv-flash";
    dialog.dataset.previewFlash = "true";
    dialog.setAttribute("aria-labelledby", "cgv-flash-heading");
    dialog.setAttribute("aria-describedby", "cgv-flash-game");
    const heading = node("h2", "Install on Chromatic");
    heading.id = "cgv-flash-heading";
    heading.tabIndex = -1;
    const game = node("p", doc.title || "Current game");
    game.id = "cgv-flash-game";
    game.className = "cgv-flash-game";
    const select = node("select");
    select.id = "cgv-flash-device";
    select.setAttribute("aria-label", "Device");
    const picker = node("div");
    picker.className = "cgv-flash-picker";
    const deviceLabel = node("label", "Device");
    deviceLabel.htmlFor = select.id;
    const singleDevice = node("div");
    singleDevice.id = "cgv-flash-single-device";
    singleDevice.className = "cgv-flash-single-device";
    singleDevice.hidden = true;
    picker.append(deviceLabel, select, singleDevice);
    const warning = node("p", "This erases the cartridge’s current game, may erase saved progress, and makes no backup.");
    warning.id = "cgv-flash-warning";
    warning.className = "cgv-flash-warning";
    const review = node("button", "Install");
    review.type = "button";
    review.dataset.action = "review-install";
    const write = node("button", "Confirm Install");
    write.type = "button";
    write.dataset.action = "flash-game";
    const check = node("button", "Check status");
    check.type = "button";
    check.dataset.action = "flash-status";
    review.dataset.tooltip = "Review installation";
    write.dataset.tooltip = "Confirm installation on the selected cartridge";
    check.dataset.tooltip = "Check the original device operation";
    const close = node("button", "Close");
    close.type = "button";
    close.dataset.action = "close-flash";
    const actions = node("div");
    actions.className = "cgv-dialog-actions cgv-flash-actions";
    const newInstall = node("button", "Dismiss");
    newInstall.type = "button";
    newInstall.dataset.action = "dismiss-failure";
    newInstall.hidden = true;
    const progress = node("progress");
    progress.id = "cgv-flash-progress";
    progress.setAttribute("aria-label", "Device operation in progress");
    progress.hidden = true;
    const progressTrack = node("div");
    progressTrack.className = "cgv-flash-progress-track";
    progressTrack.hidden = true;
    progressTrack.append(progress);
    const progressNote = node("p", "Keep your Chromatic connected.");
    progressNote.id = "cgv-flash-progress-note";
    progressNote.className = "cgv-flash-progress-note";
    progressNote.hidden = true;
    const status = node("p");
    status.id = "cgv-flash-status";
    status.className = "cgv-flash-status cgv-dialog-status";
    status.setAttribute("role", "status");
    status.setAttribute("aria-live", "polite");
    status.setAttribute("aria-atomic", "true");
    status.hidden = true;
    const discoveryNotices = node("p");
    discoveryNotices.id = "cgv-flash-discovery-notices";
    discoveryNotices.className = "cgv-flash-status";
    discoveryNotices.setAttribute("role", "status");
    discoveryNotices.setAttribute("aria-live", "polite");
    discoveryNotices.hidden = true;
    const diagnosticText = node("pre");
    diagnosticText.id = "cgv-flash-diagnostic-text";
    diagnosticText.hidden = true;
    const copy = node("button", "Copy error");
    copy.type = "button";
    copy.dataset.action = "copy-error-diagnostics";
    actions.append(copy, close, check, newInstall, review, write);
    const handoff = node("p");
    handoff.id = "cgv-flash-handoff";
    handoff.className = "cgv-flash-reference";
    handoff.setAttribute("aria-live", "polite");
    handoff.hidden = true;
    const firmwareHelp = node("a", "Get ModRetro Updater");
    firmwareHelp.id = "cgv-firmware-help";
    firmwareHelp.setAttribute("href", "https://support.modretro.com/en_us/articles/chromatic-firmware-updater-ryhoYnzCx");
    firmwareHelp.setAttribute("target", "_blank");
    firmwareHelp.setAttribute("rel", "noopener noreferrer");
    firmwareHelp.className = "cgv-flash-reference";
    firmwareHelp.hidden = true;
    const content = node("div");
    content.className = "cgv-flash-content";
    content.append(game, picker, discoveryNotices, warning, status, progressTrack, progressNote, handoff, diagnosticText, firmwareHelp);
    dialog.append(heading, content, actions);
    const style = node("style");
    style.textContent = DIALOG_STYLES + `
.codex-game-view .cgv-install-button { display: flex; align-items: center; justify-content: center; gap: 6px; width: 100%; max-width: 100%; min-height: 44px; margin: 0; padding: 8px 10px; border: 0; border-radius: 8px; background: var(--cgv-ink); color: var(--cgv-background); font: inherit; font-size: 12px; line-height: 18px; font-weight: 550; white-space: nowrap; }
.codex-game-view .cgv-install-button span { overflow: hidden; text-overflow: ellipsis; }
.codex-game-view .cgv-install-button[hidden] { display: none; }
.cgv-flash-reference { color: #75757b; overflow-wrap: anywhere; white-space: pre-wrap; font-size: 12px; line-height: 18px; }
.cgv-flash a { width: fit-content; text-underline-offset: 3px; }
.cgv-flash #cgv-firmware-help { display: inline-flex; align-items: center; min-height: 36px; box-sizing: border-box; padding: 6px 10px; border-radius: 8px; background: #f1f1f3; color: #27272b; font-size: 13px; font-weight: 550; }
.codex-game-view .cgv-install-button:hover:not(:disabled) { filter: brightness(.92); }
.codex-game-view .cgv-install-button:disabled { opacity: 1; background: color-mix(in srgb, var(--cgv-ink) 12%, var(--cgv-background)); color: color-mix(in srgb, var(--cgv-ink) 60%, var(--cgv-background)); cursor: default; pointer-events: none; }
.codex-game-view .cgv-install-note { position: absolute; width: 1px; height: 1px; padding: 0; margin: -1px; overflow: hidden; clip-path: inset(50%); white-space: nowrap; }
.cgv-dialog.cgv-flash { height: min(340px, calc(100svh - 32px)); overflow: hidden; }
.cgv-dialog.cgv-flash[data-state="error"] { height: min(340px, calc(100svh - 32px)); }
.cgv-dialog.cgv-flash[open] { display: grid; grid-template-rows: auto minmax(0, 1fr) auto; gap: 14px; }
.cgv-flash-content { min-height: 0; overflow: auto; overscroll-behavior: contain; display: flex; flex-direction: column; gap: 12px; padding: 6px; margin: -6px; }
.cgv-flash .cgv-flash-game { color: #75757b; font-size: 13px; line-height: 20px; }
.cgv-flash .cgv-flash-warning { padding: 12px; border-radius: 10px; background: #faf6f4; color: #813d25; font-size: 14px; line-height: 21px; text-wrap: pretty; }
.cgv-flash-picker { display: flex; flex-direction: column; gap: 6px; }
.cgv-flash-picker label { color: #49494f; font-size: 12px; line-height: 18px; font-weight: 600; }
.cgv-dialog .cgv-flash-actions { gap: 4px; }
.cgv-dialog .cgv-flash-actions button { padding-inline: 6px; }
.cgv-flash-actions [data-action="copy-error-diagnostics"] { margin-right: auto; font-size: 12px; }
.cgv-flash select { width: 100%; margin: 0; }
.cgv-flash select[data-empty="true"] { color: #75757b; }
.cgv-flash-single-device { box-sizing: border-box; display: flex; align-items: center; min-height: 40px; padding: 8px 12px; border: 1px solid #dedee1; border-radius: 8px; background: #f8f8f9; color: #27272b; font-size: 13px; line-height: 20px; }
.cgv-flash .cgv-flash-status { color: #626268; font-size: 14px; line-height: 21px; white-space: pre-line; overflow-wrap: anywhere; }
.cgv-flash[data-state="error"] .cgv-dialog-status { border-color: #e5d8d2; background: #faf6f4; color: #813d25; }
.cgv-flash[data-state="running"] .cgv-flash-content, .cgv-flash[data-state="success"] .cgv-flash-content, .cgv-flash[data-state="error"] .cgv-flash-content { justify-content: safe center; }
.cgv-flash[data-state="running"] .cgv-dialog-status { padding: 0; border: 0; background: transparent; color: #27272b; font-size: 15px; font-weight: 600; }
.cgv-flash[data-state="success"] .cgv-dialog-status { display: flex; align-items: flex-start; gap: 10px; border-color: #cce4d2; background: #f1faf3; color: #28633b; }
.cgv-flash[data-state="success"] .cgv-dialog-status::before { content: ""; flex: none; width: 21px; height: 21px; border-radius: 50%; background: #d5efdb url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 24 24' fill='none' stroke='%2328633b' stroke-width='2.5' stroke-linecap='round' stroke-linejoin='round'%3E%3Cpath d='m5 12 4 4L19 6'/%3E%3C/svg%3E") center/13px no-repeat; }
.cgv-flash[data-state="success"] .cgv-flash-actions [data-action="close-flash"][data-primary="true"] { background: #2c7042; color: #fff; }
.cgv-flash[data-state="success"] .cgv-flash-actions [data-action="close-flash"][data-primary="true"]:hover { background: #245c36; }
.cgv-flash-progress-track { position: relative; height: 6px; overflow: hidden; border-radius: 999px; background: #e1eee5; }
.cgv-flash-progress-track::after { content: ""; position: absolute; inset: 0 auto 0 0; width: 38%; border-radius: inherit; background: #398455; animation: cgv-flash-progress 1.5s ease-in-out infinite; }
.cgv-flash-progress-track progress { position: absolute; width: 1px; height: 1px; margin: 0; opacity: 0; }
@keyframes cgv-flash-progress { from { transform: translateX(-100%); } to { transform: translateX(263%); } }
@media(prefers-reduced-motion:reduce) { .cgv-flash-progress-track::after { animation: none; transform: translateX(80%); } }
.cgv-flash .cgv-flash-progress-note { color: #75757b; font-size: 12px; line-height: 18px; }
@media(max-height:320px) {
  .cgv-dialog.cgv-flash[open] { display: flex; overflow: auto; gap: 12px; padding: 12px; }
  .cgv-flash-content { flex: none; overflow: visible; padding: 0; margin: 0; }
  .cgv-flash-actions { flex: none; margin-top: auto; }
}

`;
    const permissionNote = node("p", journalAccessMessage);
    permissionNote.className = "cgv-install-note";
    permissionNote.id = "cgv-install-permission";
    permissionNote.hidden = true;
    permissionNote.setAttribute("role", "status");
    const installSlot = root.querySelector(".cgv-install-slot");
    installSlot.setAttribute("role", "group");
    installSlot.append(button);
    root.append(style, dialog, permissionNote);
    const storageKey = `codex-preview-flash:${location.pathname}`;
    let disposed = false;
    let busy = false;
    let submittingFlash = false;
    let refreshOnOpen = false;
    let recoveryUnavailable = false;
    let unknown = false;
    let readFailed = false;
    let connectionLost = false;
    let accessDenied = false;
    let playerUnavailable = false;
    let captureBusy = false;
    let confirming = false;
    let dismissedSuccess;
    let snapshot;
    let statusErrorCode;
    let handoffRevision = 0;
    let devicesKey = "";
    let manualDevice = "";
    let pending;
    let timer;
    let connectionTimer;
    try {
        const saved = JSON.parse(sessionStorage.getItem(storageKey) ?? "null");
        if (saved && typeof saved.requestId === "string" && (saved.action === "discover" || saved.action === "flash"))
            pending = saved;
    }
    catch {
        recoveryUnavailable = true;
    }
    function say(message) {
        if (status.textContent !== message)
            status.textContent = message;
    }
    function reviewedOperation(reply = snapshot) {
        const review = reply?.newInstallReview;
        const operation = reply?.operation;
        return !!review && !!operation && review.operationId === operation.operationId && review.requestId === operation.requestId
            && (!pending || pending.action === "flash" && pending.requestId === operation.requestId)
            && review.generation === generation && operation.generation === generation
            && review.romSha256 === reply.rom.sha256 && operation.romSha256 === reply.rom.sha256;
    }
    function controls() {
        const freshDevices = snapshot?.available && !snapshot.blocked && !snapshot.discoveryError && !accessDenied
            ? snapshot.devices.filter(device => Date.parse(device.expiresAt) > Date.now()) : [];
        if (!freshDevices.some(device => device.token === select.value)) {
            const previous = select.value;
            select.value = freshDevices.length === 1 ? freshDevices[0].token : "";
            if (select.value !== previous)
                confirming = false;
        }
        const reviewed = reviewedOperation();
        const report = errorReport(snapshot, readFailed, sourceRevision, statusErrorCode, pending);
        const previewConnection = report.subject === "preview-connection";
        const installing = submittingFlash || snapshot?.operation?.command === "flash" && snapshot.operation.state === "running";
        const discovering = !snapshot || snapshot?.detecting || snapshot?.operation?.command === "list_devices" && snapshot.operation.state === "running";
        const successful = snapshot?.operation?.command === "flash" && snapshot.operation.state === "succeeded"
            && snapshot.operation.cartridgeWrite === "vendor-reported-success";
        const completed = !!snapshot && successful && snapshot.operation.generation === generation
            && snapshot.operation.romSha256 === snapshot.rom.sha256
            && snapshot.operation.operationId !== dismissedSuccess;
        const needsAttention = !reviewed && (pending?.action === "flash" || snapshot?.operation?.command === "flash" &&
            (snapshot.operation.state === "unresolved" || snapshot.operation.cartridgeWrite === "outcome-unverified"));
        // A setup failure is not a connected device. Keep existing operation
        // recovery reachable even when its journal can no longer be accessed.
        const setupRequired = accessDenied && !pending && !unknown && snapshot?.operation?.command !== "flash"
            && !(snapshot?.blocked && !discovering);
        // A retained background scan can predate the current write. The write's
        // live status takes precedence until it settles.
        const discoveryFailed = !!snapshot?.discoveryError && !installing;
        const diagnostics = discoveryFailed || !!snapshot?.discoveryNotices?.length || readFailed || !!snapshot && (!snapshot.available || snapshot.blocked && !discovering);
        const recovery = !setupRequired && !installing && (!!needsAttention || unknown || readFailed || !!snapshot?.blocked && !discovering);
        const settledFailure = !reviewed && !readFailed && !unknown && snapshot?.operation?.state === "failed" && snapshot.operation.diagnostics?.processCompletion === "closed";
        const activationRequired = settledFailure && snapshot?.operation?.diagnostics?.commands.some(command => !!command && typeof command === "object" && command.vendorCode === "feature.developer_mode_required");
        newInstall.textContent = reviewed ? "Choose device" : activationRequired ? "Check again" : "Dismiss";
        newInstall.hidden = readFailed || (reviewed ? !!snapshot?.devices.length : !settledFailure || !snapshot?.dismissalAvailable);
        newInstall.disabled = busy || captureBusy;
        const selecting = !setupRequired && !confirming && !installing && !completed && !recovery;
        const reviewing = !setupRequired && confirming && !installing && !completed && !recovery;
        const retainedInstall = installing || completed || settledFailure || needsAttention || unknown || !!pending;
        const detected = snapshot?.available && snapshot.devices.some(device => Date.parse(device.expiresAt) > Date.now());
        const canOpen = reviewed || setupRequired || retainedInstall || recovery || diagnostics || detected;
        button.disabled = playerUnavailable || !canOpen || captureBusy && !retainedInstall;
        iconPath.setAttribute("d", button.disabled ? plugIcon : downloadIcon);
        const description = setupRequired ? "Chromatic installation unavailable" : connectionLost ? "Preview connection lost" : installing ? "Installing on Chromatic…" : completed ? "Game installed" : settledFailure ? "Review installation failure" : unknown ? "Check device status" : captureBusy ? "Finish device capture to install" : diagnostics ? "Check device connection"
            : canOpen ? "Install on Chromatic" : discovering ? "Looking for your Chromatic…" : "Connect your Chromatic to install";
        buttonLabel.textContent = setupRequired ? "Setup" : connectionLost ? "Retry" : installing ? submittingFlash && snapshot?.operation?.state !== "running" ? "Preparing…" : "Writing…" : completed ? "Done" : settledFailure ? "Review" : unknown ? "Status" : captureBusy ? "Busy" : diagnostics ? "Check" : discovering && !canOpen ? "Finding Chromatic…" : "Install on Chromatic";
        button.dataset.tooltip = description;
        button.setAttribute("aria-label", buttonLabel.textContent === description ? description : `${buttonLabel.textContent}: ${description}`);
        // Disabled buttons cannot receive keyboard focus or our control tooltip.
        // Keep their explanation discoverable without enabling the install action.
        installSlot.tabIndex = button.disabled ? 0 : -1;
        if (button.disabled) {
            installSlot.dataset.tooltip = description;
            installSlot.setAttribute("aria-label", description);
        }
        else {
            delete installSlot.dataset.tooltip;
            installSlot.removeAttribute("aria-label");
        }
        permissionNote.hidden = !setupRequired;
        if (setupRequired)
            button.setAttribute("aria-describedby", permissionNote.id);
        else
            button.removeAttribute("aria-describedby");
        close.dataset.tooltip = completed ? "Done" : "Close device panel";
        const blocked = busy || captureBusy || unknown || readFailed || !snapshot || snapshot.blocked || !snapshot.available || !!snapshot.discoveryError;
        select.disabled = blocked || !freshDevices.length;
        select.dataset.empty = String(!select.value);
        select.hidden = freshDevices.length === 1;
        singleDevice.hidden = freshDevices.length !== 1;
        const singleLabel = freshDevices.length === 1 ? freshDevices[0].label : "";
        if (singleDevice.textContent !== singleLabel)
            singleDevice.textContent = singleLabel;
        review.disabled = write.disabled = blocked || !select.value;
        check.disabled = busy;
        // An in-flight write is necessarily unverified; only a settled or
        // unresolved outcome is an error, unless the status read itself failed.
        const errorVisible = readFailed || unknown || snapshot?.available === false || discoveryFailed || !reviewed &&
            (snapshot?.operation?.state === "failed" || snapshot?.operation?.state === "unresolved" ||
                snapshot?.operation?.state !== "running" && snapshot?.operation?.cartridgeWrite === "outcome-unverified");
        dialog.setAttribute("role", errorVisible ? "alertdialog" : "dialog");
        dialog.dataset.state = errorVisible ? "error" : completed ? "success" : installing ? "running" : "ready";
        progress.hidden = !installing || errorVisible;
        progressTrack.hidden = progress.hidden;
        progressNote.hidden = progress.hidden;
        discoveryNotices.hidden = installing || !snapshot?.discoveryNotices?.length;
        copy.hidden = !errorVisible;
        firmwareHelp.hidden = !activationRequired;
        const reportText = JSON.stringify(report, null, 2);
        if (diagnosticText.textContent !== (errorVisible ? reportText : "")) {
            handoffRevision++;
            handoff.textContent = "";
            handoff.hidden = true;
            diagnosticText.textContent = errorVisible ? reportText : "";
            diagnosticText.hidden = true;
        }
        check.textContent = previewConnection ? "Retry connection" : settledFailure ? "Refresh status" : "Check status";
        check.dataset.tooltip = previewConnection ? "Try reconnecting to this preview server"
            : settledFailure ? "Read this operation's recorded result again" : "Check the original device operation";
        picker.hidden = !selecting;
        game.hidden = !selecting;
        warning.hidden = !reviewing;
        review.hidden = !selecting;
        write.hidden = !reviewing;
        check.hidden = setupRequired || settledFailure && !discoveryFailed || !recovery && !discoveryFailed && !(installing && (readFailed || unknown));
        if (!discoveryFailed && (reviewing || selecting && successful || discovering && !readFailed && !unknown || !readFailed && !unknown && !snapshot?.blocked
            && snapshot?.operation?.command === "list_devices" && snapshot.operation.state === "succeeded" && snapshot.devices.length))
            status.hidden = true;
        heading.textContent = previewConnection ? "Game preview disconnected" : setupRequired ? "Chromatic installation unavailable" : installing && !errorVisible ? "Installing on Chromatic"
            : settledFailure ? activationRequired ? "Set up your Chromatic" : "Installation failed"
                : recovery || installing && errorVisible ? "Installation status" : discoveryFailed ? "Could not find your Chromatic"
                    : completed ? "Game installed" : reviewing ? "Confirm install" : "Install on Chromatic";
        dialog.setAttribute("aria-describedby", errorVisible ? status.id : selecting ? game.id : reviewing ? warning.id : status.id);
        close.textContent = completed ? "Done" : setupRequired || installing || recovery ? "Close" : "Cancel";
        close.hidden = settledFailure && !reviewed && !newInstall.hidden;
        const primary = setupRequired || completed ? close : settledFailure ? !newInstall.hidden ? newInstall : close
            : recovery || readFailed || unknown || discoveryFailed ? check : installing ? undefined
                : reviewing ? write : review;
        for (const action of [review, write, check, close, newInstall])
            action.dataset.primary = String(action === primary);
    }
    function remember(value) {
        if (value)
            sessionStorage.setItem(storageKey, JSON.stringify(value));
        else
            sessionStorage.removeItem(storageKey);
        pending = value;
    }
    async function request(payload) {
        const controller = new AbortController();
        const timeout = setTimeout(() => controller.abort(), 10_000);
        try {
            let response;
            let text;
            try {
                response = await fetch("./codex-device", {
                    method: "POST", mode: "same-origin", redirect: "error", cache: "no-store", signal: controller.signal,
                    headers: { "Content-Type": "application/json", "X-Codex-Preview-Device": "1" },
                    body: JSON.stringify({ ...payload, generation }),
                });
                text = await response.text();
            }
            catch {
                throw new PreviewConnectionError("Cannot reach the local preview server. Reopen the game preview to reconnect. This does not indicate that a Chromatic is connected.");
            }
            if (text.length > 65536)
                throw new Error("Device status was too large. Check the original operation in the plugin.");
            const reply = JSON.parse(text);
            if (!response.ok) {
                if (reply.code === "CHROMATIC_JOURNAL_ACCESS_DENIED")
                    throw new JournalAccessError();
                const message = reply.error || "The preview changed or device request was refused.";
                if (reply.admitted === false && (payload.action === "discover" || payload.action === "flash"))
                    throw new DeviceRequestRejected(message);
                throw new Error(message);
            }
            if (reply.generation !== generation || !Array.isArray(reply.devices))
                throw new Error("The device status belongs to a different preview.");
            return reply;
        }
        finally {
            clearTimeout(timeout);
        }
    }
    function render(reply) {
        snapshot = reply;
        discoveryNotices.textContent = reply.discoveryNotices?.some(item => item.code === "CHROMATIC_PLAYER_AMBIGUOUS") ? "Two Chromatics use the same player number. Choose different player numbers on the devices, then reconnect them." : reply.discoveryNotices?.length ? "Some devices could not be identified. Check their USB connections, or copy the error for help." : "";
        discoveryNotices.hidden = !reply.discoveryNotices?.length;
        statusErrorCode = undefined;
        readFailed = false;
        connectionLost = false;
        accessDenied = false;
        unknown = reply.requestedOperationUnknown === true;
        const key = JSON.stringify(reply.devices);
        if (key !== devicesKey) {
            devicesKey = key;
            const previous = select.value;
            const fresh = reply.devices.filter(device => Date.parse(device.expiresAt) > Date.now());
            const placeholder = node("option", fresh.length ? "Choose a device" : "No devices found");
            placeholder.value = "";
            select.replaceChildren(placeholder);
            for (const device of fresh) {
                const option = node("option", device.label);
                option.value = device.token;
                select.append(option);
            }
            select.value = fresh.some(device => device.token === manualDevice) ? manualDevice : fresh.length === 1 ? fresh[0].token : "";
            if (!fresh.some(device => device.token === manualDevice))
                manualDevice = "";
            if (select.value !== previous)
                confirming = false;
            if (reply.devices.length > 1)
                queueMicrotask(() => {
                    if (dialog.open && !disposed && current() && !select.hidden && !select.disabled && doc.activeElement === heading)
                        select.focus();
                });
        }
        const operation = reply.operation;
        status.hidden = !operation && !unknown && !reply.discoveryError && reply.available && !reply.blocked;
        const previousBuild = !!operation && (operation.generation !== generation || operation.romSha256 !== reply.rom.sha256);
        if (reviewedOperation(reply)) {
            status.hidden = false;
            say("Choose your Chromatic, then select Install to review the confirmation.");
        }
        else if (unknown)
            say(pending?.action === "flash" ? "The previous write’s outcome is unknown. Do not repeat it. Check the original operation in the plugin." : "The previous device request’s outcome is unknown. Check its original status in the plugin.");
        else if (operation?.state === "running") {
            say(operation.command === "list_devices" ? "Finding devices…"
                : operation.command !== "flash" ? "Checking device status…"
                    : operation.stage === "preparing" ? "Preparing installation…"
                        : operation.stage === "inspect-rom" ? "Checking the game…"
                            : operation.stage === "list-devices" ? "Checking your Chromatic…"
                                : "Installing the game…");
        }
        else if (operation?.state === "unresolved" || operation?.cartridgeWrite === "outcome-unverified") {
            const requiresUpdater = operation.diagnostics?.commands.some(command => !!command && typeof command === "object"
                && command.vendorCode === "feature.developer_mode_required");
            const cause = requiresUpdater ? operation.diagnostics?.processCompletion === "closed"
                ? "Install ModRetro Updater and activate Developer Mode on this computer. When finished, choose Check again to select your device. "
                : "Developer Mode could not be confirmed. "
                : "";
            if (operation.diagnostics?.processCompletion === "closed") {
                say(operation.state === "unresolved" ? `${cause}The command finished, but its final status is unresolved. Do not retry the write.`
                    : requiresUpdater ? `${cause}Installation was not confirmed.` : `${cause}The write’s effect on the cartridge is unverified.${reply.dismissalAvailable ? "" : " Do not retry the write."}`);
            }
            else {
                say(`${cause}${operation.cartridgeWrite === "outcome-unverified" ? "The command’s completion and effect on the cartridge are unconfirmed." : "The command’s completion is unconfirmed."} Keep your Chromatic connected and do not retry. Check status.`);
            }
        }
        else if (reply.discoveryError) {
            say(reply.blocked ? "Device search did not finish. Check status before trying again." : "Device search could not finish. Reopen this panel to try again, or copy the error for help.");
        }
        else if (operation?.state === "succeeded" && operation.command === "flash") {
            say(previousBuild ? "An earlier version was installed."
                : "Boot your Chromatic to play. Have fun!");
        }
        else if (operation?.state === "failed") {
            const requiresUpdater = operation.diagnostics?.commands.some(command => !!command && typeof command === "object"
                && command.vendorCode === "feature.developer_mode_required");
            say(requiresUpdater ? "Install ModRetro Updater and activate Developer Mode on this computer. When finished, choose Check again to select your device."
                : "The device operation could not finish. Copy the error for help.");
        }
        else if (!reply.available)
            say("Device installation is unavailable in this session. Ask Codex to check the plugin setup.");
        else if (reply.detecting)
            say("Looking for connected Chromatic devices…");
        else if (reply.blocked)
            say("Another device operation is pending. Check status before starting anything else.");
        else if (operation?.command === "list_devices") {
            say("");
            status.hidden = true;
        }
        else {
            say("");
            status.hidden = true;
        }
        const dismissedPending = reviewedOperation(reply) && pending?.action === "flash" && pending.requestId === operation?.requestId;
        if (dismissedPending || operation && operation.state !== "running" && operation.state !== "unresolved" && operation.cartridgeWrite !== "outcome-unverified") {
            try {
                remember(undefined);
            }
            catch {
                // Completion stops recovery in this page even if durable cleanup fails.
                // The retained ID can still recover the same terminal result on reload.
                pending = undefined;
            }
        }
        controls();
        if (dialog.open && (operation?.state === "running" || reply.detecting))
            timer = setTimeout(() => { void update(undefined, true); }, 500);
    }
    async function update(payload, background = false) {
        if (disposed || !current() || busy)
            return;
        if (recoveryUnavailable) {
            // An unreadable saved request is not proof that no write is pending.
            readFailed = unknown = true;
            statusErrorCode = "PREVIEW_RECOVERY_UNAVAILABLE";
            status.hidden = false;
            dialog.dataset.state = "error";
            say("Browser storage could not be read. A previous installation may still need recovery. Check the original operation in the plugin before starting another. Browser play is still available.");
            controls();
            return;
        }
        if (timer)
            clearTimeout(timer);
        if (!background && dialog.open)
            heading.focus({ preventScroll: true });
        submittingFlash = payload?.action === "flash";
        busy = true;
        controls();
        if (!background) {
            status.hidden = false;
            say(payload?.action === "flash" ? "Checking the game shown in this preview…" : "Checking device status…");
        }
        try {
            if (payload?.action === "flash") {
                try {
                    await verifyGame();
                    if (disposed || !current())
                        throw new Error("The game preview changed.");
                    if (!snapshot?.devices.some(device => device.token === payload.deviceToken && Date.parse(device.expiresAt) > Date.now()))
                        throw new Error("The device list expired. Reopen the install panel to refresh it.");
                }
                catch (error) {
                    throw new DeviceRequestRejected(`${error instanceof Error ? error.message : "The preview game could not be verified."} No device request was sent.`);
                }
            }
            const reply = await request(payload ?? { action: "status", ...(pending ? { requestId: pending.requestId } : {}) });
            if (!disposed && current())
                render(reply);
        }
        catch (error) {
            readFailed = true;
            connectionLost = error instanceof PreviewConnectionError;
            accessDenied = error instanceof JournalAccessError;
            statusErrorCode = accessDenied ? "CHROMATIC_JOURNAL_ACCESS_DENIED"
                : connectionLost ? "PREVIEW_CONNECTION_LOST" : error instanceof DeviceRequestRejected ? "PREVIEW_REQUEST_REJECTED" : "PREVIEW_STATUS_UNAVAILABLE";
            status.hidden = false;
            if (error instanceof DeviceRequestRejected) {
                try {
                    remember(undefined);
                }
                catch {
                    pending = undefined;
                }
                unknown = false;
                select.value = "";
                confirming = false;
            }
            else
                unknown = !!pending || accessDenied && unknown;
            if (accessDenied) {
                select.value = "";
                confirming = false;
            }
            dialog.dataset.state = "error";
            const writeNeedsRecovery = pending?.action === "flash" || snapshot?.operation?.command === "flash"
                && (snapshot.operation.state === "running" || snapshot.operation.state === "unresolved" || snapshot.operation.cartridgeWrite === "outcome-unverified");
            say(`${accessDenied ? "The installation record could not be read." : connectionLost ? "Connection to the preview was lost." : error instanceof DeviceRequestRejected ? "The request could not start. No device command was sent." : "The device status could not be checked."}${unknown || accessDenied && writeNeedsRecovery ? writeNeedsRecovery ? " The installation outcome is unknown. Keep your Chromatic connected and check status before trying again." : " Check status to recover the original request." : " Copy the error for help."}`);
            progress.hidden = true;
        }
        finally {
            busy = false;
            submittingFlash = false;
            if (!disposed) {
                controls();
                if (refreshOnOpen) {
                    refreshOnOpen = false;
                    const operation = snapshot?.operation;
                    const completed = operation?.command === "flash" && operation.state === "succeeded"
                        && operation.cartridgeWrite === "vendor-reported-success" && operation.generation === generation
                        && operation.romSha256 === snapshot?.rom.sha256 && operation.operationId !== dismissedSuccess;
                    const uncertainWrite = !reviewedOperation() && operation?.command === "flash"
                        && (operation.state === "unresolved" || operation.cartridgeWrite === "outcome-unverified");
                    // A click during a background read queues one scan. Existing scans and
                    // writes keep their original status and request identity.
                    if (dialog.open && current() && !captureBusy && !readFailed && !unknown && !pending && snapshot?.available
                        && !snapshot.blocked && !snapshot.detecting && operation?.state !== "running" && !completed && !uncertainWrite) {
                        void update({ action: "refresh" }, true);
                    }
                }
            }
        }
    }
    async function startInstall() {
        controls();
        if (busy || captureBusy || unknown || readFailed || !snapshot || snapshot.blocked || !snapshot.available || !select.value || !confirming)
            return;
        const requestId = crypto.randomUUID();
        try {
            remember({ requestId, action: "flash" });
        }
        catch {
            readFailed = true;
            statusErrorCode = "PREVIEW_STORAGE_UNAVAILABLE";
            confirming = false;
            status.hidden = false;
            dialog.dataset.state = "error";
            say("Browser storage is unavailable. No device request was sent.");
            controls();
            return;
        }
        await update({ action: "flash", requestId,
            deviceToken: select.value, acknowledgeErase: true, acknowledgeSaveLoss: true, acknowledgeNoBackup: true,
        });
    }
    function scheduleConnection(delay) {
        if (connectionTimer)
            clearTimeout(connectionTimer);
        if (!disposed && current() && !doc.hidden && !recoveryUnavailable) {
            connectionTimer = setTimeout(() => { void observeConnection(); }, delay);
        }
    }
    async function observeConnection() {
        if (disposed || !current() || doc.hidden || recoveryUnavailable)
            return;
        if (!dialog.open && !busy) {
            // A lost write response is recovered by its original ID, never by another write.
            await update(pending || unknown || captureBusy ? undefined : { action: "observe" }, true);
        }
        scheduleConnection(snapshot?.detecting || snapshot?.operation?.state === "running" ? 500 : 5_000);
    }
    const visibilityChanged = () => {
        if (connectionTimer)
            clearTimeout(connectionTimer);
        if (!doc.hidden)
            scheduleConnection(0);
    };
    doc.addEventListener("visibilitychange", visibilityChanged);
    doc.defaultView?.addEventListener("focus", visibilityChanged);
    review.addEventListener("click", () => {
        controls();
        if (review.disabled || review.hidden)
            return;
        confirming = true;
        controls();
        heading.focus({ preventScroll: true });
    });
    write.addEventListener("click", event => {
        // A second click on Install must not also confirm the newly revealed step.
        if (write.disabled || write.hidden || event.detail > 1)
            return;
        void startInstall();
    });
    check.addEventListener("click", () => { void update(); });
    newInstall.addEventListener("click", async () => {
        if (newInstall.hidden || newInstall.disabled || !snapshot?.operation)
            return;
        if (!reviewedOperation()) {
            const returnToInstall = newInstall.textContent === "Check again";
            await update({ action: "dismiss_failure", requestId: snapshot.operation.requestId,
                operationId: snapshot.operation.operationId, acknowledgePreviousOutcome: true });
            if (!disposed && current() && !readFailed && reviewedOperation()) {
                if (returnToInstall) {
                    confirming = false;
                    select.value = "";
                    await update({ action: "refresh" });
                }
                else
                    dialog.close();
            }
            return;
        }
        if (disposed || !current() || !dialog.open || readFailed)
            return;
        try {
            remember(undefined);
        }
        catch {
            pending = undefined;
        }
        confirming = false;
        select.value = "";
        await update({ action: "refresh" });
    });
    copy.addEventListener("click", async () => {
        if (disposed || !current() || !dialog.open || copy.hidden)
            return;
        const revision = ++handoffRevision;
        const report = diagnosticText.textContent ?? "";
        let copied = false;
        try {
            await navigator.clipboard.writeText(report);
            copied = true;
        }
        catch { /* Keep the same safe report selectable below. */ }
        if (disposed || !current() || !dialog.open || revision !== handoffRevision || report !== diagnosticText.textContent)
            return;
        if (copied)
            handoff.textContent = "Error copied.";
        else {
            diagnosticText.hidden = false;
            handoff.textContent = "Could not copy. Select and copy the error below.";
        }
        handoff.hidden = false;
    });
    select.addEventListener("change", () => { manualDevice = select.value; confirming = false; controls(); });
    close.addEventListener("click", () => dialog.close());
    dialog.addEventListener("close", () => {
        handoffRevision++;
        refreshOnOpen = false;
        handoff.hidden = true;
        diagnosticText.hidden = true;
        if (snapshot?.operation?.command === "flash" && snapshot.operation.state === "succeeded"
            && snapshot.operation.cartridgeWrite === "vendor-reported-success")
            dismissedSuccess = snapshot.operation.operationId;
        confirming = false;
        select.value = "";
        manualDevice = "";
        if (timer)
            clearTimeout(timer);
        if (!disposed && button.isConnected && !button.hidden)
            button.focus();
        scheduleConnection(0);
    });
    controls();
    if (recoveryUnavailable)
        void update();
    scheduleConnection(0);
    return {
        button,
        setPlayerUnavailable(value) { playerUnavailable = value; controls(); },
        setCaptureBusy(value) {
            if (captureBusy === value)
                return;
            captureBusy = value;
            controls();
            if (!value)
                scheduleConnection(0);
        },
        open() {
            if (disposed || !current() || dialog.open)
                return;
            if (connectionTimer)
                clearTimeout(connectionTimer);
            dialog.showModal();
            heading.focus({ preventScroll: true });
            refreshOnOpen = true;
            void update(undefined, true);
        },
        dispose() {
            disposed = true;
            if (timer)
                clearTimeout(timer);
            if (connectionTimer)
                clearTimeout(connectionTimer);
            doc.removeEventListener("visibilitychange", visibilityChanged);
            doc.defaultView?.removeEventListener("focus", visibilityChanged);
            dialog.close();
            dialog.remove();
            button.remove();
            permissionNote.remove();
            style.remove();
        },
    };
}
//# sourceMappingURL=flash.js.map

SHA-256: 85f61fb2b363aa265355847017b6797f4462b9fcbb4bea7017ff9356d51ed673