diff --git a/.github/plugin/marketplace.json b/.github/plugin/marketplace.json index 8677354b..6c0efd27 100644 --- a/.github/plugin/marketplace.json +++ b/.github/plugin/marketplace.json @@ -1515,7 +1515,7 @@ "name": "sentry-triage", "source": "plugins/sentry-triage", "description": "Scan live Sentry issues in a Copilot canvas, group them by urgency, and hand issues off for tracking or a fix PR.", - "version": "1.1.0" + "version": "1.2.0" }, { "name": "signals-dashboard", diff --git a/extensions/sentry-triage/components/card.mjs b/extensions/sentry-triage/components/card.mjs index 4ad20d12..b8c9259f 100644 --- a/extensions/sentry-triage/components/card.mjs +++ b/extensions/sentry-triage/components/card.mjs @@ -2,7 +2,9 @@ import { escapeHtml, safeHref } from '../escape.mjs' function statusLabel(workStatus) { if (!workStatus || typeof workStatus !== 'object') return '' - if (workStatus.phase === 'working' || workStatus.phase === 'queued') return '⏳ working…' + if (workStatus.phase === 'working' || workStatus.phase === 'queued') { + return workStatus.copilotFix ? '⏳ starting Copilot fix session…' : '⏳ filing tracking issue…' + } if (workStatus.phase === 'done') { // Numbers move into clickable links (statusLinks); keep the label a plain badge. return 'created ✓' diff --git a/extensions/sentry-triage/components/page.mjs b/extensions/sentry-triage/components/page.mjs index 46460a66..0f358904 100644 --- a/extensions/sentry-triage/components/page.mjs +++ b/extensions/sentry-triage/components/page.mjs @@ -48,6 +48,7 @@ export function Page({ prTargets, prSettingsOpen, plainEnglishView = false, + plainEnglishEnriching = false, projects = [], availableModels = [], issueTrackers, @@ -313,14 +314,14 @@ export function Page({
-
@@ -406,6 +407,10 @@ export function Page({ // is already running for a slug, later callers (e.g. the Scan/Fetch gate) // queue here and are flushed when it settles, instead of being dropped. const projectResolveWaiters = {}; + // Ceiling for a single exact-slug lookup before we give up and report a + // transient failure. Generous enough to absorb a normal queue wait behind + // an in-progress scan, short enough that the user is never stranded. + const RESOLVE_TIMEOUT_MS = 20000; function projectResolveKey(org, slug) { return String(org || "").trim().toLowerCase() + "/" + String(slug || "").trim().toLowerCase(); } @@ -431,6 +436,10 @@ export function Page({ // An empty project scans all projects and needs no lookup. verifyProjectForScan(org, project).then((v) => { if (!v.ok) { + // "stale" means the user changed the org/project while the lookup was + // in flight — they've already moved on, so don't scan the old scope + // and don't nag them about a slug they abandoned. + if (v.reason === "stale") return; showToast(v.reason === "missing" ? "No project \u201C" + project + "\u201D in " + org + " — check the slug." : "Couldn't verify that project with Sentry — try again."); @@ -699,6 +708,7 @@ export function Page({ let lastAutoFetchedOrgDefault = ""; let currentAvailableModels = ${jsonForScript(Array.isArray(availableModels) ? availableModels : [])}; let currentPlainEnglishView = ${jsonForScript(plainEnglishView)}; + let currentPlainEnglishEnriching = ${jsonForScript(plainEnglishEnriching)}; let currentScanError = ${jsonForScript(scanError || '')}; let currentScannedTotal = ${jsonForScript(scannedTotal)}; let currentScannedCapped = ${jsonForScript(scannedCapped)}; @@ -741,7 +751,7 @@ export function Page({ function statusText(status) { if (!status || typeof status !== "object") return ""; - if (status.phase === "queued" || status.phase === "working") return "⏳ working…"; + if (status.phase === "queued" || status.phase === "working") return status.copilotFix ? "⏳ starting Copilot fix session…" : "⏳ filing tracking issue…"; if (status.phase === "skipped") return "🔒 already being worked on"; if (status.phase === "tracked") return "👀 Tracked"; if (status.phase === "done") { @@ -1089,17 +1099,34 @@ export function Page({ if (psel && psel.value !== msg.period) psel.value = msg.period; } if (Array.isArray(msg.periods)) currentPeriods = msg.periods; + if (typeof msg.plainEnglishEnriching === "boolean" && msg.plainEnglishEnriching !== currentPlainEnglishEnriching) { + currentPlainEnglishEnriching = msg.plainEnglishEnriching; + syncTitleSwitch(); + } if (Array.isArray(msg.projects)) { const forOrg = typeof msg.projectsOrg === "string" ? msg.projectsOrg.trim().toLowerCase() : ""; + const projectsComplete = msg.projectsComplete === true; + // Keep-longest ONLY for streamed/incomplete snapshots. The server + // streams partial pages (each broadcast is a growing snapshot), and a + // discovery run that fails or is cut short mid-traversal publishes a + // truncated snapshot for an org we already have fully loaded — the + // dropdown "sometimes comes back much shorter" symptom. But a COMPLETE + // traversal is authoritative and may legitimately be shorter (projects + // deleted upstream), so it must be allowed to replace the cache — + // otherwise deleted slugs would linger in autocomplete for the panel's + // whole life. Growth within a run still lands normally. + const known = forOrg ? projectsByOrg[forOrg] : null; + const regression = !projectsComplete && Array.isArray(known) && known.length > msg.projects.length; + const projects = regression ? known : msg.projects; // Always cache under the org this list belongs to so re-selecting it is // instant next time. - if (forOrg) projectsByOrg[forOrg] = msg.projects; + if (forOrg && !regression) projectsByOrg[forOrg] = msg.projects; // Only paint the on-screen field if this broadcast matches the org the // user currently has selected — a background refresh for a previous org // must not clobber the current list. const sel = selectedOrgSlug(); if (!forOrg || !sel || forOrg === sel) { - currentSentryProjects = msg.projects; + currentSentryProjects = projects; setProjectLoading(false); // Repaint whichever project field is on screen with the new options. if (currentOrg) renderProjectSwitcher(); @@ -1129,7 +1156,20 @@ export function Page({ currentSavedDefaultOrg = msg.savedDefaultOrg; refreshDefaultBtn(); } - if (Array.isArray(msg.availableModels)) currentAvailableModels = msg.availableModels; + if (Array.isArray(msg.availableModels)) { + currentAvailableModels = msg.availableModels; + // The host's model catalog can change under us (models ship or retire). + // Drop any per-card override whose id is no longer offered: otherwise the + // + - Plain-English titles + Plain-English messages - Showing ${plainEnglishView ? 'plain-English summaries' : 'raw errors'} + ${plainEnglishEnriching ? 'Preparing plain-English summaries — toggle available shortly…' : `Showing ${plainEnglishView ? 'plain-English summaries' : 'raw errors'}`}
@@ -406,6 +407,10 @@ export function Page({ // is already running for a slug, later callers (e.g. the Scan/Fetch gate) // queue here and are flushed when it settles, instead of being dropped. const projectResolveWaiters = {}; + // Ceiling for a single exact-slug lookup before we give up and report a + // transient failure. Generous enough to absorb a normal queue wait behind + // an in-progress scan, short enough that the user is never stranded. + const RESOLVE_TIMEOUT_MS = 20000; function projectResolveKey(org, slug) { return String(org || "").trim().toLowerCase() + "/" + String(slug || "").trim().toLowerCase(); } @@ -431,6 +436,10 @@ export function Page({ // An empty project scans all projects and needs no lookup. verifyProjectForScan(org, project).then((v) => { if (!v.ok) { + // "stale" means the user changed the org/project while the lookup was + // in flight — they've already moved on, so don't scan the old scope + // and don't nag them about a slug they abandoned. + if (v.reason === "stale") return; showToast(v.reason === "missing" ? "No project \u201C" + project + "\u201D in " + org + " — check the slug." : "Couldn't verify that project with Sentry — try again."); @@ -699,6 +708,7 @@ export function Page({ let lastAutoFetchedOrgDefault = ""; let currentAvailableModels = ${jsonForScript(Array.isArray(availableModels) ? availableModels : [])}; let currentPlainEnglishView = ${jsonForScript(plainEnglishView)}; + let currentPlainEnglishEnriching = ${jsonForScript(plainEnglishEnriching)}; let currentScanError = ${jsonForScript(scanError || '')}; let currentScannedTotal = ${jsonForScript(scannedTotal)}; let currentScannedCapped = ${jsonForScript(scannedCapped)}; @@ -741,7 +751,7 @@ export function Page({ function statusText(status) { if (!status || typeof status !== "object") return ""; - if (status.phase === "queued" || status.phase === "working") return "⏳ working…"; + if (status.phase === "queued" || status.phase === "working") return status.copilotFix ? "⏳ starting Copilot fix session…" : "⏳ filing tracking issue…"; if (status.phase === "skipped") return "🔒 already being worked on"; if (status.phase === "tracked") return "👀 Tracked"; if (status.phase === "done") { @@ -1089,17 +1099,34 @@ export function Page({ if (psel && psel.value !== msg.period) psel.value = msg.period; } if (Array.isArray(msg.periods)) currentPeriods = msg.periods; + if (typeof msg.plainEnglishEnriching === "boolean" && msg.plainEnglishEnriching !== currentPlainEnglishEnriching) { + currentPlainEnglishEnriching = msg.plainEnglishEnriching; + syncTitleSwitch(); + } if (Array.isArray(msg.projects)) { const forOrg = typeof msg.projectsOrg === "string" ? msg.projectsOrg.trim().toLowerCase() : ""; + const projectsComplete = msg.projectsComplete === true; + // Keep-longest ONLY for streamed/incomplete snapshots. The server + // streams partial pages (each broadcast is a growing snapshot), and a + // discovery run that fails or is cut short mid-traversal publishes a + // truncated snapshot for an org we already have fully loaded — the + // dropdown "sometimes comes back much shorter" symptom. But a COMPLETE + // traversal is authoritative and may legitimately be shorter (projects + // deleted upstream), so it must be allowed to replace the cache — + // otherwise deleted slugs would linger in autocomplete for the panel's + // whole life. Growth within a run still lands normally. + const known = forOrg ? projectsByOrg[forOrg] : null; + const regression = !projectsComplete && Array.isArray(known) && known.length > msg.projects.length; + const projects = regression ? known : msg.projects; // Always cache under the org this list belongs to so re-selecting it is // instant next time. - if (forOrg) projectsByOrg[forOrg] = msg.projects; + if (forOrg && !regression) projectsByOrg[forOrg] = msg.projects; // Only paint the on-screen field if this broadcast matches the org the // user currently has selected — a background refresh for a previous org // must not clobber the current list. const sel = selectedOrgSlug(); if (!forOrg || !sel || forOrg === sel) { - currentSentryProjects = msg.projects; + currentSentryProjects = projects; setProjectLoading(false); // Repaint whichever project field is on screen with the new options. if (currentOrg) renderProjectSwitcher(); @@ -1129,7 +1156,20 @@ export function Page({ currentSavedDefaultOrg = msg.savedDefaultOrg; refreshDefaultBtn(); } - if (Array.isArray(msg.availableModels)) currentAvailableModels = msg.availableModels; + if (Array.isArray(msg.availableModels)) { + currentAvailableModels = msg.availableModels; + // The host's model catalog can change under us (models ship or retire). + // Drop any per-card override whose id is no longer offered: otherwise the + //