import { styles } from '../styles.mjs' import { Category } from './category.mjs' import { escapeHtml, jsonForScript } from '../escape.mjs' // Subtitle + empty-state copy shared by the server render below. The inline // client script keeps byte-identical wording (search "KEEP IN SYNC") so the // first paint and every SSE re-render read the same. `total` is how many open // issues the scan examined; `capped` means the scan hit its configured cap // so we render "N+" to signal there may be more. function scannedLabel(total, capped) { return capped ? String(total) + '+' : String(total) } function subtitleText(shown, total, capped) { if (total > shown) { return 'Top ' + shown + ' of ' + scannedLabel(total, capped) + ' open issues, prioritized by impact' } return shown + ' open issue' + (shown === 1 ? '' : 's') + ' to triage' } function emptyText(total, capped, project, older = 0, olderCapped = false) { const where = project ? ' for ' + project : '' if (total > 0) { return '✓ Scanned ' + scannedLabel(total, capped) + ' open issue' + (total === 1 ? '' : 's') + where + ' — none are urgent enough to surface right now.' } if (older > 0) { const one = older === 1 && !olderCapped return '✓ No open issues' + where + ' in this window — Sentry has ' + scannedLabel(older, olderCapped) + ' unresolved issue' + (one ? '' : 's') + ' in the last 90 days.' } return '✓ No open issues' + where + ' in this window.' } export function Page({ categories, scanError = '', scannedTotal = 0, scannedCapped = false, scannedOlder = 0, scannedOlderCapped = false, org, orgOptions = [], orgDefault = '', savedDefaultOrg = '', project, period = '24h', periods = [], connections, prTargets, prSettingsOpen, plainEnglishView = false, projectOptions = [], projects = [], availableModels = [], issueTrackers, selectedTracker, workByIssueKey, csrfToken = '', }) { const conn = connections && typeof connections === 'object' ? connections : { checked: false, sentry: { reachable: false, error: '' } } const checked = Boolean(conn.checked) // Optimistic before the first preflight so we never flash a gate spuriously. const sentryReady = !checked || Boolean(conn.sentry && conn.sentry.reachable) // The humanized preflight failure (unsupported host, dropped connection, sign-in // required, …). Shown in the gate so users see the real reason instead of a // generic "connect" message for problems reconnecting won't fix. const gateError = (conn.sentry && conn.sentry.error) || '' // Distinguish "not signed in" (needs `sentry auth login`) from a transient // connectivity failure where a credential likely exists. probeSentry() reports // configured:false only for auth failures, so a gated-but-configured state means // Sentry was simply unreachable — the gate then shows connectivity guidance // instead of wrongly telling an already-authenticated user to re-run login. const signedOut = !(conn.sentry && conn.sentry.configured) // Among configured-but-unreachable failures, only a transient network blip is // worth promising recovery for. An unknown, settled failure (classifySentryError // marks transient:false) gets neutral guidance instead of a false "it should // recover on the next check" — the humanized error carries the real reason. const transientConn = Boolean(conn.sentry && conn.sentry.transient) const categoriesWithStatus = categories.map((category) => ({ ...category, issues: category.issues.map((issue) => ({ ...issue, workStatus: workByIssueKey[issue.key], })), })) const totalIssues = categoriesWithStatus.reduce((sum, c) => sum + c.issues.length, 0) const hasOrg = Boolean(org) // Whether the currently-prefilled setup org is the one the user explicitly // saved as their default. Drives the ⭐ control's active state on first paint. const isSavedDefault = Boolean(savedDefaultOrg) && orgDefault === savedDefaultOrg // Setup-form org control. find_organizations reliably enumerates the orgs this // Sentry connection can access, so we prefill the slug input with the best match // (orgDefault) and, when there's more than one org, add a native ` + orgSlugs .map((slug) => ``) .join('') + `` : '' // Project autocomplete source. The Sentry CLI SDK returns the org's full // project list, so the project field is a combobox (a text input + a filtered // suggestion menu) rather than a typed slug box — scalable to orgs with many // projects. Empty value = all projects. Rendered client-side; the server just // seeds the input value and an empty menu container. const projectSlugs = Array.isArray(projects) ? projects.filter(Boolean) : [] // A text input + suggestion menu wrapper. Shared shape for the setup screen and // the header so the autocomplete JS can attach to either by id. const projectComboInput = (id, cls, extraAttrs = '') => `` + `` + `` // Header org control. Mirrors the project switcher so the org can be changed // without returning to the setup screen. Renders as a dropdown when the // connection exposes 2+ orgs (matching the setup screen), else a text input. const headerOrgSlugs = !org || orgSlugs.includes(org) ? orgSlugs : [org, ...orgSlugs] const orgSwitcherControl = headerOrgSlugs.length >= 2 ? `` : `` const orgSwitcher = hasOrg ? `` : '' // Header project switcher: an autocomplete combobox + an explicit Fetch button. // Picking a suggestion re-scans that project immediately; a typed value applies // on Enter or Fetch (empty = all projects). const projectSwitcher = hasOrg ? `` : '' // Time-window switcher. Defaults to the last 24h; changing it re-scans over the // wider window so the user can look further back from the issues list. const periodList = Array.isArray(periods) ? periods : [] const periodSwitcher = hasOrg ? `` : '' // Registered app projects for the Local hand-off target dropdown. Loaded // eagerly on open via the agent, so this may be empty on the very first render. const projectList = Array.isArray(projectOptions) ? projectOptions : [] const localProjectId = prTargets && prTargets.local ? (prTargets.local.projectId || '') : '' const projectOptionsHtml = projectList .map((p) => { const label = p.repo ? `${p.name} (${p.repo})` : p.name return `` }) .join('') return ` ${!hasOrg ? `

Triage your live Sentry errors

Surfaces the issues that need attention right now. Set your org, project, and repo, then scan.

  1. Org & project — scope the scan.
  2. Repo — lets us flag issues already being worked on (👀 Tracked).
  3. Scan — then open a tracking issue, or start a Copilot fix session with a draft PR.
Saved on this machine to ~/.copilot/sentry-triage/preferences.json
` : `

On-call error triage for ${escapeHtml(org)}${project ? ` / ${escapeHtml(project)}` : ''} — issues grouped by why they need your attention right now.

${escapeHtml(subtitleText(totalIssues, scannedTotal, scannedCapped))}${project ? '' : ' · all projects'}

`}
or
Summary of the actions above

Repo

Where we check whether an issue already has a tracking issue or draft PR (shown as 👀 Tracked), and the default repo for cloud draft PRs. Recommended whenever you set a project.

Open issue

Draft PR

Local target
Cloud target

Runs in the Repo above.

Showing ${plainEnglishView ? 'plain-English summaries' : 'raw errors'}
${categoriesWithStatus.map((cat) => Category({ ...cat, plainEnglishView, availableModels })).join('')}
` }