From 2b967792b7d232e77923e234e75d8fa2c2af30e2 Mon Sep 17 00:00:00 2001 From: Stuart Date: Wed, 23 Sep 2026 04:30:31 +1000 Subject: [PATCH 1/2] feat(STU-379): preserve previous results through refinement and build-error regeneration Refinement and pasted-build-error regeneration now hold the prior successful result (selected artifact, full bundle, prompt, review, selection) until a replacement generation AND review both succeed. A generator or reviewer failure restores that result behind a persistent error + retry banner - never a transient toast or a blanked Results surface. - Snapshot/restore shared pipeline result state around replacement runs - Commit the new result atomically only after generation + review succeed - Reconcile artifact selection to the new bundle (no stale tabs) - Add persistent, keyboard-accessible error + retry banner over the results - Restyle the pasted-build-errors panel with shared controls; scope labels - Guard duplicate clicks (no double charge), keep telemetry/usage contracts --- app.js | 137 ++++++++- e2e/stu377-pipeline-progress.spec.js | 30 +- e2e/stu379-refinement-regeneration.spec.js | 332 +++++++++++++++++++++ index.html | 121 +++++++- 4 files changed, 580 insertions(+), 40 deletions(-) create mode 100644 e2e/stu379-refinement-regeneration.spec.js diff --git a/app.js b/app.js index f1563b3..406a45c 100644 --- a/app.js +++ b/app.js @@ -3688,6 +3688,88 @@ function updateModelInfo(selectedModel) { console.log(`Step 3 (Code Review): ${getModelLabel(CODE_REVIEW_MODEL)}`) } +/** + * Snapshot the current successful generation result so a failed refinement or + * build-error replacement can restore it verbatim. The selected artifact, full + * bundle, prompt, review and selection are held untouched until a replacement + * generation AND review both succeed. + */ +function snapshotGenerationResult() { + return { + step1Result: pipelineState.step1Result, + step2Result: pipelineState.step2Result, + step3Result: pipelineState.step3Result, + artifactBundle: pipelineState.artifactBundle, + bundleReview: pipelineState.bundleReview, + selectedArtifactId: pipelineState.selectedArtifactId, + }; +} + +/** + * Bring a prior successful result back into shared state and reconcile the + * selection to an id that still exists in the restored bundle (so the prior + * result never points at a stale artifact that the failed replacement no + * longer defined). + */ +function restoreGenerationResult(snapshot) { + if (!snapshot) return; + Object.assign(pipelineState, snapshot); + const ids = new Set((pipelineState.artifactBundle?.artifacts || []).map((a) => a.id)); + if (!ids.has(pipelineState.selectedArtifactId)) { + pipelineState.selectedArtifactId = + getPrimaryArtifact(pipelineState.artifactBundle)?.id || null; + } +} + +const RESULTS_REPLACEMENT_ERROR_ID = "results-replacement-error"; + +/** Persistent error + retry UI that sits over the retained previous result. */ +function showReplacementFailure(error, { stage = 2, runId, retry }) { + if (!isCurrentPipelineRun(runId)) return; + const banner = document.getElementById(RESULTS_REPLACEMENT_ERROR_ID); + if (!banner) return; + const failure = classifyPipelineError(error); + const titleEl = document.getElementById("results-replacement-error-title"); + const messageEl = document.getElementById("results-replacement-error-message"); + const retryEl = document.getElementById("results-replacement-error-retry"); + if (titleEl) titleEl.textContent = `${PIPELINE_STAGE_LABELS[stage] || "Regeneration"}: ${failure.title}`; + if (messageEl) messageEl.textContent = failure.message; + banner.hidden = false; + if (retryEl) { + retryEl.onclick = () => { + hideReplacementFailure(); + if (typeof retry === "function") retry(); + }; + retryEl.textContent = "Retry"; + // Keyboard accessible: a failed replacement lands focus on its retry. + retryEl.focus({ preventScroll: true }); + } +} + +function hideReplacementFailure() { + const banner = document.getElementById(RESULTS_REPLACEMENT_ERROR_ID); + if (banner) banner.hidden = true; +} + +/** + * Shared completion path for a replacement run (refinement / build-error fix): + * the previous successful result is restored verbatim, the Results surface is + * repainted from it, and a persistent error + retry banner is shown. A toast + * alone is insufficient - the old code and review must stay inspectable and + * copyable while the user decides whether to retry. + */ +function restoreAndShowReplacementFailure(error, { previous, stage, runId, retry }) { + if (!isCurrentPipelineRun(runId)) return; + if (error.isUsageLimit) updateUsageDisplay(); + restoreGenerationResult(previous); + hidePipelineProgress(); + setGenerationStageVisible(true); + showResultsView(); + updateSelectedArtifactPanels(); + showReplacementFailure(error, { stage, runId, retry }); + updateDeployButtonVisibility(); +} + async function runRefinement() { console.log("runRefinement called"); @@ -3702,6 +3784,10 @@ async function runRefinement() { callEndpoint('standardRegenerate', pipelineState.step2Result, pipelineState.step1Result) const btns = document.querySelectorAll(".btn-refine-action"); + // Hold the prior successful result verbatim until a replacement generation + // AND review both succeed; a failure restores it below. + const previousResult = snapshotGenerationResult(); + btns.forEach((btn) => { btn.disabled = true; btn.innerHTML = ` @@ -3711,7 +3797,9 @@ async function runRefinement() { }); try { + hideReplacementFailure(); const selectedArtifact = getSelectedArtifact(); + const refinedArtifactId = selectedArtifact.id; const refinementPrompt = buildArtifactRegenerationPrompt({ bundleSpec: pipelineState.step1Result, artifactBundle: JSON.stringify(pipelineState.artifactBundle || pipelineState.step2Result), @@ -3763,6 +3851,16 @@ async function runRefinement() { updateBundleReviewFromReviewResult(); completePipelineStage(3, runId); + // The whole replacement (generation + review) succeeded, so the new result + // is committed. Reconcile the selection onto the re-refined artifact if it + // still exists, otherwise fall back to the new bundle's primary — either + // way the tabs are rebuilt from the new bundle, so none go stale. + if (pipelineState.artifactBundle?.artifacts?.some((a) => a.id === refinedArtifactId)) { + pipelineState.selectedArtifactId = refinedArtifactId; + } else { + pipelineState.selectedArtifactId = getPrimaryArtifact(pipelineState.artifactBundle)?.id || null; + } + const auditOutput = document.getElementById("step3-output"); auditOutput.textContent = pipelineState.step3Result; @@ -3782,14 +3880,15 @@ async function runRefinement() { review: 3, }); - if (error.isUsageLimit) { - updateUsageDisplay(); - } - - selectWorkflowStep(errorStep); - showStepLoading(errorStep, false); - showPipelineFailure(error, { stage: errorStep, runId, retry: runRefinement }); - updateStepIndicator(errorStep, "error"); + // Restore the previous successful result and keep it visible and copyable + // with persistent error + retry UI; the failed replacement never owns the + // screen. + restoreAndShowReplacementFailure(error, { + previous: previousResult, + stage: errorStep, + runId, + retry: runRefinement, + }); } finally { // The control's own busy affordance is restored even for an abandoned // run — the run guard only protects shared pipeline state. @@ -3865,7 +3964,12 @@ async function regenerateFromPastedErrors() { Fixing…` } + // Hold the prior successful result verbatim until the bundle replacement + // generation AND review both succeed; a failure restores it below. + const previousResult = snapshotGenerationResult() + try { + hideReplacementFailure() const refinementPrompt = buildBundleRegenerationPrompt({ bundleSpec: pipelineState.step1Result, artifactBundle: JSON.stringify(pipelineState.artifactBundle || pipelineState.step2Result), @@ -3921,14 +4025,15 @@ async function regenerateFromPastedErrors() { review: 3, }) - if (error.isUsageLimit) { - updateUsageDisplay() - } - - selectWorkflowStep(errorStep) - showStepLoading(errorStep, false) - showPipelineFailure(error, { stage: errorStep, runId, retry: regenerateFromPastedErrors }) - updateStepIndicator(errorStep, "error") + // Restore the previous successful result and keep it visible and copyable + // with persistent error + retry UI; the failed replacement never owns the + // screen. + restoreAndShowReplacementFailure(error, { + previous: previousResult, + stage: errorStep, + runId, + retry: regenerateFromPastedErrors, + }) } finally { if (btn) { btn.disabled = false diff --git a/e2e/stu377-pipeline-progress.spec.js b/e2e/stu377-pipeline-progress.spec.js index 867e52c..9317369 100644 --- a/e2e/stu377-pipeline-progress.spec.js +++ b/e2e/stu377-pipeline-progress.spec.js @@ -488,28 +488,26 @@ test.describe("Stateful transitions stay isolated per run", () => { await page.locator("#hero-send").click(); await expect(page.locator("#results-view")).toHaveClass(/visible/); - // The refinement's generator call fails: the run must land on the - // persistent failure panel, not a transient toast and a blank stage. + // The refinement's generator call fails. STU-379: the run must NOT blank + // the previous result — it stays visible and copyable behind a persistent + // error + retry banner, never a transient toast over an empty stage. await routePipelineStages(page, { responses: { generator: providerError } }); await page.locator("#btn-refine-header").click(); - await expect(failure(page)).toBeVisible(); - await expect(failure(page)).toHaveAttribute("data-kind", "generic"); - await expect(failure(page).locator('button[data-action="retry"]')).toBeVisible(); - await expect(stage(page, 2)).toHaveAttribute("data-state", "failed"); - await expect(page.locator("#pipeline-progress")).toHaveClass(/visible/); - await expect(page.locator("#results-view")).not.toHaveClass(/visible/); + await expect(page.locator("#results-view")).toHaveClass(/visible/); + await expect(page.locator("#results-replacement-error")).not.toBeHidden(); + await expect(page.locator("#results-replacement-error-retry")).toBeVisible(); await expect(page.locator("#hero-send")).toBeEnabled(); // Well past the hide window the failure is still on screen — a toast - // alone would have left an empty stage behind. + // alone would have left behind only the floating result. await page.waitForTimeout(700); - await expect(failure(page)).toBeVisible(); + await expect(page.locator("#results-replacement-error")).not.toBeHidden(); // Retry re-enters the same refinement flow (Generator then Review), not // a brand-new pipeline run from the Architect. const retryRun = await routePipelineStages(page); - await failure(page).locator('button[data-action="retry"]').click(); + await page.locator("#results-replacement-error-retry").click(); await expect(page.locator("#results-view")).toHaveClass(/visible/); expect(retryRun.bodies.map((body) => body.step)).toEqual(["generator", "review"]); }); @@ -556,11 +554,11 @@ test.describe("Stateful transitions stay isolated per run", () => { await page.locator("#ff-error-paste-input").fill("Widget build failed: missing return"); await page.locator("#btn-fix-from-errors").click(); - await expect(failure(page)).toBeVisible(); - await expect(failure(page)).toHaveAttribute("data-kind", "generic"); - await expect(failure(page).locator('button[data-action="retry"]')).toBeVisible(); - await expect(stage(page, 2)).toHaveAttribute("data-state", "failed"); - await expect(page.locator("#pipeline-progress")).toHaveClass(/visible/); + // STU-379: a failed replacement keeps the previous result visible and + // copyable behind a persistent error + retry banner. + await expect(page.locator("#results-view")).toHaveClass(/visible/); + await expect(page.locator("#results-replacement-error")).not.toBeHidden(); + await expect(page.locator("#results-replacement-error-retry")).toBeVisible(); }); }); diff --git a/e2e/stu379-refinement-regeneration.spec.js b/e2e/stu379-refinement-regeneration.spec.js new file mode 100644 index 0000000..34092d5 --- /dev/null +++ b/e2e/stu379-refinement-regeneration.spec.js @@ -0,0 +1,332 @@ +import { test, expect } from "@playwright/test"; +import { + applyDefaultRoutes, + ENDPOINTS, + ok, + providerError, + reviewPassed, + oneArtifact, + freeSubscription, +} from "./fixtures/apiFixtures.js"; + +/** + * STU-379 — Preserve previous results through refinement and build-error + * regeneration. + * + * Refinement ("Refine & Regenerate") and pasted-build-error regeneration + * ("Add FlutterFlow Build Errors & Regenerate") must hold the prior + * successful result — selected artifact, full bundle, prompt, review — until + * a replacement generation AND review both succeed. A generator failure or a + * reviewer failure must restore that previous result and keep it inspectable + * and copyable behind a persistent error + retry affordance; a toast alone is + * not enough. A successful replacement swaps in the new result and reconciles + * selection without stale tabs, and cancel/empty build-error input preserves + * the surrounding context. + * + * Every route is deterministic and hermetic: nothing is timed, no wall clock, + * no live endpoint — each stage advances only when its intercepted response + * is fulfilled. + */ + +const ARTIFACT_ID = "custom-action-greet-user"; +const WEEK = new Date().toISOString().slice(0, 7); + +function makeBundle(code) { + return { + schemaVersion: "1.0.0", + id: "bundle-test-379", + title: "Deterministic STU-379 bundle", + description: "", + artifacts: [ + { + id: ARTIFACT_ID, + artifactType: "CustomAction", + artifactName: "GreetUser", + fileName: "greet_user.dart", + code, + dependencies: [], + imports: [], + publicApi: [], + relationships: [], + deployStatus: "pending", + review: null, + metadata: {}, + codeType: "A", + }, + ], + dependencies: [], + relationships: [], + deployOrder: [ARTIFACT_ID], + warnings: [], + metadata: {}, + }; +} + +const generatorFor = (bundle) => + ok({ + output: JSON.stringify(bundle), + usage_status: "success", + usage_count: 1, + usage_month: WEEK, + }); + +const reviewFor = (summary) => + ok({ + output: JSON.stringify({ + overallReview: { status: "pass", score: 99, summary }, + findings: [], + artifacts: [{ id: ARTIFACT_ID, review: { status: "pass", findings: [] } }], + }), + }); + +const OLD = makeBundle("string oldGreeting() { return 'old'; }"); +const NEW = makeBundle("string newGreeting() { return 'new'; }"); + +function stepOf(route) { + try { + return JSON.parse(route.request().postData() || "{}").step || ""; + } catch { + return ""; + } +} + +/** + * Intercept the shared pipeline endpoint. The Architect stage always answers + * with the spec fixture. The FIRST generator/review responses establish the + * initial successful result; subsequent generator/review responses drive the + * replacement run. + * + * @param {object} c + * @param {object} c.generate - how a replacement generator answers: a bundle, + * or `providerError` to force a generator failure. + * @param {object} c.review - how a replacement review answers: a summary + * object, or `providerError` to force a reviewer failure. + * @returns {{generatorCalls:()=>number, reviewCalls:()=>number}} + */ +async function routePipeline(page, { generate, review }) { + let gens = 0; + let revs = 0; + await page.route(ENDPOINTS.pipeline, async (route) => { + const step = stepOf(route); + if (step === "architect") { + await route.fulfill(oneArtifact()); + return; + } + if (step === "generator") { + gens += 1; + if (gens === 1) { + await route.fulfill(generatorFor(OLD)); + return; + } + await route.fulfill(typeof generate === "function" ? generate(gens) : generate); + return; + } + if (step === "review") { + revs += 1; + if (revs === 1) { + await route.fulfill(reviewPassed()); + return; + } + await route.fulfill(typeof review === "function" ? review(revs) : review); + return; + } + await route.fulfill(oneArtifact()); + }); + return { + generatorCalls: () => gens, + reviewCalls: () => revs, + }; +} + +async function openHome(page, overrides = {}) { + await page.addInitScript(() => { + localStorage.setItem("hasSeenWalkthrough", "true"); + }); + await applyDefaultRoutes(page, { + [ENDPOINTS.getSubscription]: freeSubscription(), + ...overrides, + }); + await page.goto("/"); + await expect(page.locator("#pipeline-input")).toBeVisible(); +} + +async function establishInitialResult(page, router) { + await page.locator("#pipeline-input").fill("A greeting action"); + await page.locator("#hero-send").click(); + await expect(page.locator("#results-view")).toHaveClass(/visible/); + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + return router; +} + +const refineButton = (page) => page.locator("#btn-refine-header"); +const banner = (page) => page.locator("#results-replacement-error"); +const retryButton = (page) => page.locator("#results-replacement-error-retry"); + +test.describe("Refinement preserves the previous successful result", () => { + for (const viewport of [ + { name: "desktop", width: 1440, height: 900 }, + { name: "mobile", width: 390, height: 844 }, + ]) { + test(`successful refinement swaps in the new result without stale tabs (${viewport.name})`, async ({ page }) => { + await page.setViewportSize(viewport); + await openHome(page); + const router = await routePipeline(page, { + generate: generatorFor(NEW), + review: reviewFor("new review"), + }); + await establishInitialResult(page, router); + + await refineButton(page).click(); + + // The replacement generation + review both succeeded, so the new code + // and review own the Results surface. + await expect(page.locator("#results-code-output")).toContainText("newGreeting"); + await expect(page.locator("#results-code-output")).not.toContainText("oldGreeting"); + await expect(page.locator("#results-summary-detail")).toContainText("new review"); + // No stale tabs: the new bundle defines exactly one artifact, and + // selecting it shows the re-generated code with the selection reconciled. + await expect(page.locator("#artifact-tabs .artifact-tab")).toHaveCount(1); + await page.getByRole("tab", { name: /GreetUser/ }).click(); + await expect(page.locator("#results-code-output")).toContainText("newGreeting"); + await expect( + page.locator(`#artifact-tab-${ARTIFACT_ID}`), + ).toHaveAttribute("aria-selected", "true"); + await expect(banner(page)).toBeHidden(); + + // Usage accounting unchanged: one generator call per replacement, no + // double charge from the single click. + expect(router.generatorCalls()).toBe(2); + }); + } + + test("a generator failure restores the old code, copyable, with a persistent retry (desktop)", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await openHome(page); + const router = await routePipeline(page, { + generate: () => providerError(), + review: reviewFor("unused"), + }); + await establishInitialResult(page, router); + + await refineButton(page).click(); + + // Old code stays inspectable and copyable behind the persistent error. + await expect(page.locator("#results-view")).toHaveClass(/visible/); + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + await expect(banner(page)).not.toBeHidden(); + await expect(retryButton(page)).toBeVisible(); + // Persistent, unlike a toast: still present a moment later. + await page.waitForTimeout(900); + await expect(banner(page)).not.toBeHidden(); + // Keyboard: a failed replacement lands focus on its retry control. + await expect(retryButton(page)).toBeFocused(); + + expect(router.generatorCalls()).toBe(2); + }); + + test("a reviewer failure restores the old result, not a half-swapped new code (mobile)", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openHome(page); + const router = await routePipeline(page, { + generate: generatorFor(NEW), + review: () => providerError(), + }); + await establishInitialResult(page, router); + + await refineButton(page).click(); + + // The generator returned new code, but the review then failed: the old + // result must be restored — new code must NOT be visible. + await expect(page.locator("#results-view")).toHaveClass(/visible/); + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + await expect(page.locator("#results-code-output")).not.toContainText("newGreeting"); + await expect(retryButton(page)).toBeVisible(); + }); + + test("retrying a failed replacement succeeds and swaps in the new result (desktop)", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await openHome(page); + const router = await routePipeline(page, { + generate: (call) => (call === 2 ? providerError() : generatorFor(NEW)), + review: reviewFor("new review"), + }); + await establishInitialResult(page, router); + + await refineButton(page).click(); + await expect(retryButton(page)).toBeVisible(); + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + + await retryButton(page).click(); + await expect(page.locator("#results-code-output")).toContainText("newGreeting"); + await expect(page.locator("#results-code-output")).not.toContainText("oldGreeting"); + await expect(banner(page)).toBeHidden(); + }); +}); + +test.describe("Pasted build-error regeneration", () => { + test("pasting build errors and fixing them swaps in the new bundle (desktop)", async ({ page }) => { + await page.setViewportSize({ width: 1440, height: 900 }); + await openHome(page); + const router = await routePipeline(page, { + generate: generatorFor(NEW), + review: reviewFor("new review"), + }); + await establishInitialResult(page, router); + + await page.locator("#btn-error-regen-header").click(); + const textarea = page.locator("#ff-error-paste-input"); + await expect(textarea).toBeVisible(); + await expect(textarea).toBeFocused(); + await textarea.fill("Error: 'oldGreeting' is undefined"); + + await page.locator("#btn-fix-from-errors").click(); + + await expect(page.locator("#results-code-output")).toContainText("newGreeting"); + await expect(page.locator("#results-code-output")).not.toContainText("oldGreeting"); + await expect(banner(page)).toBeHidden(); + expect(router.generatorCalls()).toBe(2); + }); + + test("empty build-error input is rejected without charging and never leaves the previous result (mobile)", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openHome(page); + const router = await routePipeline(page, { + generate: generatorFor(NEW), + review: reviewFor("new review"), + }); + await establishInitialResult(page, router); + + await page.locator("#btn-error-regen-header").click(); + const textarea = page.locator("#ff-error-paste-input"); + await textarea.fill(" "); + await page.locator("#btn-fix-from-errors").click(); + + // No generation ran, no charge, no crash — the previous result is intact. + expect(router.generatorCalls()).toBe(1); + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + await expect(banner(page)).toBeHidden(); + }); + + test("cancelling the build-error input preserves the typed context and the result (mobile)", async ({ page }) => { + await page.setViewportSize({ width: 390, height: 844 }); + await openHome(page); + const router = await routePipeline(page, { + generate: generatorFor(NEW), + review: reviewFor("new review"), + }); + await establishInitialResult(page, router); + + await page.locator("#btn-error-regen-header").click(); + const textarea = page.locator("#ff-error-paste-input"); + await textarea.fill("Error: build failed for reason X"); + + await page.locator("#btn-error-input-cancel").click(); + + await expect(page.locator("#error-input-panel")).toBeHidden(); + // The typed errors are preserved so the user does not retype on reopen. + await expect(textarea).toHaveValue("Error: build failed for reason X"); + // The previous result stays intact. + await expect(page.locator("#results-code-output")).toContainText("oldGreeting"); + expect(router.generatorCalls()).toBe(1); + }); +}); diff --git a/index.html b/index.html index 8b189ef..5c58e15 100644 --- a/index.html +++ b/index.html @@ -919,6 +919,93 @@ .results-action-bar .btn-error-regen { flex-grow: 1.45; } + .results-action-scope { + flex: 0 0 auto; + display: block; + margin: 10px 24px 4px; + font-size: 11px; + color: var(--muted, #64748b); + text-align: center; + } + .results-replacement-error { + border: 1px solid #fbbf24; + background: #fffbeb; + border-radius: var(--radius-lg, 16px); + margin: 0 24px 12px; + padding: 12px 14px; + } + .results-replacement-error-body { + display: flex; + align-items: center; + gap: 10px; + flex-wrap: wrap; + } + .results-replacement-error-icon { + width: 20px; + height: 20px; + flex: 0 0 20px; + color: #d97706; + } + .results-replacement-error-text { + display: flex; + flex-direction: column; + gap: 2px; + flex: 1 1 200px; + min-width: 180px; + } + .results-replacement-error-text strong { + color: #b45309; + font-size: 12.5px; + } + .results-replacement-error-text span { + color: #92400e; + font-size: 12px; + } + .results-replacement-error .btn-header-action { + min-height: 36px; + padding: 6px 14px; + } + + /* Shared-control styling for the pasted-build-error input overlay. */ + .error-input-card { + background: var(--surface, #fff); + border: 1px solid var(--border, #e5e7eb); + border-radius: var(--radius-lg, 16px); + box-shadow: 0 16px 48px -12px rgba(0, 0, 0, 0.18); + padding: 20px; + display: flex; + flex-direction: column; + gap: 12px; + } + .error-input-header { + display: flex; + align-items: center; + justify-content: space-between; + gap: 10px; + } + .error-input-textarea { + width: 100%; + min-height: 160px; + resize: vertical; + background: #fffbf0; + border-color: #fcd34d; + color: #78350f; + font-family: "DM Mono", "SFMono-Regular", ui-monospace, monospace; + font-size: 12.5px; + line-height: 1.6; + } + .error-input-textarea:focus-visible { + border-color: #f59e0b; + box-shadow: 0 0 0 3px color-mix(in srgb, #f59e0b 35%, transparent); + } + .btn-error-input-cancel { + background: transparent; + color: var(--muted, #64748b); + border: 1px solid var(--border, #e5e7eb); + } + .btn-error-input-cancel:hover:not(:disabled) { + background: var(--surface-sunken, #f1f5f9); + } .btn-deploy-results { background: linear-gradient(135deg, #6366f1 0%, #4f46e5 100%); color: #fff; @@ -3518,6 +3605,23 @@

Generation Results

0 files + + +
- -
+

Refine & Regenerate acts on the selected artifact · build-error fixes regenerate the whole bundle.