Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
158 changes: 142 additions & 16 deletions app.js
Original file line number Diff line number Diff line change
Expand Up @@ -3688,6 +3688,109 @@ 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");
const upgradeEl = document.getElementById("results-replacement-error-upgrade");
if (titleEl) titleEl.textContent = `${PIPELINE_STAGE_LABELS[stage] || "Regeneration"}: ${failure.title}`;
if (messageEl) messageEl.textContent = failure.message;
banner.hidden = false;

// A quota/usage-limit rejection is the one failure a protected retry cannot
// fix — re-firing launches another generation into an already-exhausted
// allowance, a dead end. Surface the upgrade affordance as the primary
// action, exactly as the non-replacement path does. Every other failure
// keeps the retry.
let focusTarget = null;
if (upgradeEl) {
upgradeEl.hidden = !failure.canUpgrade;
if (failure.canUpgrade) {
upgradeEl.textContent = "View plans";
upgradeEl.onclick = () => openPricingModal();
focusTarget = upgradeEl;
}
}
if (retryEl) {
retryEl.hidden = failure.canUpgrade;
if (!failure.canUpgrade) {
retryEl.onclick = () => {
hideReplacementFailure();
if (typeof retry === "function") retry();
};
retryEl.textContent = "Retry";
focusTarget = retryEl;
}
}
// Keyboard accessible: a failed replacement lands focus on the action that
// can actually resolve it.
(focusTarget || banner).focus({ preventScroll: true });
}

function hideReplacementFailure() {
const banner = document.getElementById(RESULTS_REPLACEMENT_ERROR_ID);
if (banner) banner.hidden = true;
Comment on lines +3770 to +3772

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Stale failure survives new results

After a replacement fails, this banner is only hidden by Retry or by starting another replacement. If the user edits the prompt and completes a fresh full pipeline, the old failure remains over the new successful result. Its Retry button then runs the old replacement action against the new current result. Clear the replacement failure state when abandoning or resetting a result, or before starting every full pipeline.

Greptile automatically discovered a related ticket stating that persistent retry UI must preserve and act on the prior successful result, which informed this comment.

Knowledge Base Used: Review and results presentation

Source Used: Linear — Preserve previous results through refinement and build-error regeneration

Prompt To Fix With AI
This is a comment left during a code review.
Path: app.js
Line: 3749-3751

Comment:
**Stale failure survives new results**

After a replacement fails, this banner is only hidden by Retry or by starting another replacement. If the user edits the prompt and completes a fresh full pipeline, the old failure remains over the new successful result. Its Retry button then runs the old replacement action against the new current result. Clear the replacement failure state when abandoning or resetting a result, or before starting every full pipeline.

Greptile automatically discovered a related ticket stating that persistent retry UI must preserve and act on the prior successful result, which informed this comment.

**Knowledge Base Used:** [Review and results presentation](https://app.greptile.com/connect-i-o/-/custom-context/knowledge-base/sgardoll/customcodeconnectforflutterflow/-/docs/review-and-results-presentation.md)

**Source Used:** Linear — [Preserve previous results through refinement and build-error regeneration](https://linear.app/stuart-gardoll/issue/STU-379/preserve-previous-results-through-refinement-and-build-error)

---

For each issue above, determine whether it is valid and should be fixed. If so, fix it directly.

Fix in Claude Code Fix in Codex Fix in Devin

}

/**
* 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();

@devin-ai-integration devin-ai-integration Bot Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Failure restoration closes artifact view

When refinement fails from an artifact tab, showResultsView switches the restored result to Summary. The selected artifact panel closes despite restoring its ID.

Learn more

A result has two related selection fields: selectedArtifactId identifies the file, while resultsViewMode decides whether that file or Summary is visible. The snapshot saves only the ID. During failure restoration, showResultsView unconditionally sets resultsViewMode to "summary", so the restored ID no longer owns the visible tab panel.

Example: A user opens artifact B and clicks Refine. If generation fails, artifact B remains in selectedArtifactId, but Results opens on Summary instead of artifact B.

Recommended fix: Include resultsViewMode in snapshotGenerationResult, then repaint the restored state without calling a helper that overwrites that mode. Alternatively, extend showResultsView with an explicit mode-preservation option for restoration only.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

@devin-ai-integration devin-ai-integration Bot Sep 22, 2026 •

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Restoration clears prior feedback

After feedback was submitted, any replacement failure calls showResultsView and clears that vote. The restored generation can then receive duplicate feedback.

Learn more

The failure path restores the old generation rather than committing a new one. However, showResultsView always calls resetResultsFeedbackState, whose contract is to clear feedback for a new generation. This removes the old result's pressed state, pending lock, and status even though that result remains current.

Example: A user votes thumbs-up, starts refinement, and the review fails. The old code returns with both feedback buttons unpressed, so another thumbs-up sends feedback again for the same generation.

Recommended fix: Repaint a restored result without resetting feedback. Add a preserveFeedback option to showResultsView, or separate result rendering from new-generation initialization and use only the rendering portion here.

Devin Review


Was this helpful? React with 👍 or 👎 to provide feedback.

updateSelectedArtifactPanels();
showReplacementFailure(error, { stage, runId, retry });
updateDeployButtonVisibility();
}

async function runRefinement() {
console.log("runRefinement called");

Expand All @@ -3702,6 +3805,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 = `<svg class="w-4 h-4 animate-spin" fill="none" stroke="currentColor" viewBox="0 0 24 24">
Expand All @@ -3711,7 +3818,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),
Expand Down Expand Up @@ -3763,6 +3872,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;

Expand All @@ -3782,14 +3901,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.
Expand Down Expand Up @@ -3865,7 +3985,12 @@ async function regenerateFromPastedErrors() {
</svg> 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),
Expand Down Expand Up @@ -3921,14 +4046,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
Expand Down
30 changes: 14 additions & 16 deletions e2e/stu377-pipeline-progress.spec.js
Original file line number Diff line number Diff line change
Expand Up @@ -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"]);
});
Expand Down Expand Up @@ -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();
});
});

Expand Down
Loading
Loading