mirror of
https://github.com/github/awesome-copilot.git
synced 2026-08-26 18:45:12 +00:00
Add Sentry Triage canvas extension (#2802)
An on-call error-triage canvas for GitHub Copilot: scans live Sentry issues, groups them by urgency, and hands issues off for tracking or a fix PR. Registers a reusable canvas extension under extensions/ with a matching plugins/sentry-triage manifest. The extension runs a loopback HTTP/SSE server per canvas instance (host-allow-listed, per-instance capability token never broadcast over SSE), renders the triage board server- and client-side, and drives model turns over a single shared Copilot session serialized so scan and work turns can't interleave. Work state is prototype-pollution-guarded against agent-controlled tracking keys, and sendAndWait timeouts are handled without duplicating external side effects. Co-authored-by: Copilot App <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
@@ -0,0 +1,115 @@
|
||||
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 === 'done') {
|
||||
// Numbers move into clickable links (statusLinks); keep the label a plain badge.
|
||||
return 'created ✓'
|
||||
}
|
||||
if (workStatus.phase === 'handed-off') {
|
||||
const sessionPart = workStatus.sessionName ? `🧵 ${workStatus.sessionName}` : '🧵 session started'
|
||||
return `${sessionPart} ↗`
|
||||
}
|
||||
if (workStatus.phase === 'skipped') return '🔒 already being worked on'
|
||||
if (workStatus.phase === 'tracked') return '👀 Tracked'
|
||||
if (workStatus.phase === 'error') return `⚠️ ${workStatus.error || 'work failed'}`
|
||||
return ''
|
||||
}
|
||||
|
||||
function stateSuffix(stateStr) {
|
||||
const s = typeof stateStr === 'string' ? stateStr.trim().toLowerCase() : ''
|
||||
return s ? ` (${s})` : ''
|
||||
}
|
||||
|
||||
function linkOrText(url, label) {
|
||||
return url
|
||||
? `<a href="${safeHref(url)}" target="_blank" rel="noopener noreferrer">${escapeHtml(label)}</a>`
|
||||
: `<span>${escapeHtml(label)}</span>`
|
||||
}
|
||||
|
||||
function statusLinks(workStatus) {
|
||||
if (!workStatus || typeof workStatus !== 'object') return ''
|
||||
const links = []
|
||||
const phase = workStatus.phase
|
||||
// Freshly created artifacts (done / handed-off): link the new issue and PR the
|
||||
// same way tracked/skipped link their existing ones, so the numbers are clickable.
|
||||
if (phase === 'done' || phase === 'handed-off') {
|
||||
if (workStatus.issueUrl || workStatus.issueNumber) {
|
||||
const base = workStatus.issueNumber ? `issue #${workStatus.issueNumber}` : 'issue created'
|
||||
links.push(linkOrText(workStatus.issueUrl, base))
|
||||
}
|
||||
if ((phase === 'done' || phase === 'handed-off') && (workStatus.prUrl || workStatus.prNumber)) {
|
||||
const base = workStatus.prNumber ? `PR #${workStatus.prNumber}` : 'PR opened'
|
||||
const label = `${base}${stateSuffix(workStatus.prState)}`
|
||||
links.push(linkOrText(workStatus.prUrl, label))
|
||||
}
|
||||
}
|
||||
if (workStatus.existingIssueUrl) {
|
||||
const base = workStatus.existingIssueNumber ? `issue #${workStatus.existingIssueNumber}` : 'existing issue'
|
||||
const label = `${base}${stateSuffix(workStatus.existingIssueState)}`
|
||||
links.push(linkOrText(workStatus.existingIssueUrl, label))
|
||||
}
|
||||
if (workStatus.existingPrUrl) {
|
||||
const base = workStatus.existingPrNumber ? `PR #${workStatus.existingPrNumber}` : 'existing PR'
|
||||
const label = `${base}${stateSuffix(workStatus.existingPrState)}`
|
||||
links.push(linkOrText(workStatus.existingPrUrl, label))
|
||||
}
|
||||
return links.join(' · ')
|
||||
}
|
||||
|
||||
export function Card({ key, summary, plainEnglish, reason, events, users, url, workStatus, plainEnglishView, availableModels = [] }) {
|
||||
const href = safeHref(url)
|
||||
const safeKey = escapeHtml(key)
|
||||
const title = plainEnglishView ? (plainEnglish || summary) : summary
|
||||
const status = statusLabel(workStatus)
|
||||
const links = statusLinks(workStatus)
|
||||
const statusClass = workStatus?.phase === 'done'
|
||||
? 'done'
|
||||
: workStatus?.phase === 'handed-off'
|
||||
? 'handed-off'
|
||||
: workStatus?.phase === 'skipped'
|
||||
? 'skipped'
|
||||
: workStatus?.phase === 'tracked'
|
||||
? 'tracked'
|
||||
: workStatus?.phase === 'error'
|
||||
? 'error'
|
||||
: workStatus?.phase === 'working' || workStatus?.phase === 'queued'
|
||||
? 'working'
|
||||
: 'idle'
|
||||
const meta = [
|
||||
events != null ? `${Number(events).toLocaleString()} events` : null,
|
||||
users != null ? `${Number(users).toLocaleString()} users` : null
|
||||
].filter(Boolean).join(' · ')
|
||||
|
||||
// Per-card model override. Empty value means "use the toolbar/batch default".
|
||||
const cardModelOptions = ['<option value="">Default</option>']
|
||||
.concat(
|
||||
(Array.isArray(availableModels) ? availableModels : [])
|
||||
.filter((model) => model && model.id)
|
||||
.map((model) => `<option value="${escapeHtml(model.id)}">${escapeHtml(model.label)}</option>`)
|
||||
)
|
||||
.join('')
|
||||
|
||||
return `<div class="card" data-key="${safeKey}">
|
||||
<label class="card-checkbox" title="Select this issue">
|
||||
<input type="checkbox" class="issue-check" data-key="${safeKey}" aria-label="Select issue ${safeKey}" />
|
||||
</label>
|
||||
<div class="card-link">
|
||||
<div class="card-header">
|
||||
<a href="${href}" target="_blank" rel="noopener noreferrer" class="card-key">${safeKey}</a>
|
||||
<span class="card-summary">${escapeHtml(title)}</span>
|
||||
</div>
|
||||
<span class="card-reason">${escapeHtml(reason)}</span>
|
||||
${meta ? `<span class="card-meta">${meta}</span>` : ''}
|
||||
<span class="card-work-status ${statusClass}" data-key="${safeKey}" ${status ? '' : 'style="display:none;"'}>
|
||||
${escapeHtml(status)}
|
||||
${links ? `<span class="card-work-links">${links}</span>` : ''}
|
||||
</span>
|
||||
</div>
|
||||
<label class="card-model-wrap" data-key="${safeKey}" style="display:none;" title="Model for this issue's fix session — Default uses the model chosen above the list">
|
||||
<span class="card-model-label">Model</span>
|
||||
<select class="card-model" data-key="${safeKey}">${cardModelOptions}</select>
|
||||
</label>
|
||||
</div>`
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
import { Card } from './card.mjs'
|
||||
import { escapeHtml } from '../escape.mjs'
|
||||
|
||||
const CATEGORY_DESCRIPTIONS = {
|
||||
'regressions': 'Issues that previously existed, were resolved, and have resurfaced — likely tied to a recent release.',
|
||||
'escalating': 'Issues Sentry flagged as escalating, plus older high-impact issues affecting many users or generating many events.',
|
||||
'new-critical': 'Brand new issues that have already hit multiple users within hours of first appearing.'
|
||||
}
|
||||
|
||||
export function Category({ id, name, issues, plainEnglishView, availableModels = [] }) {
|
||||
if (!issues.length) return ''
|
||||
|
||||
const description = CATEGORY_DESCRIPTIONS[id] || ''
|
||||
const safeId = escapeHtml(id)
|
||||
|
||||
return `<section class="category" data-category="${safeId}">
|
||||
<div class="category-header">
|
||||
<h2>${escapeHtml(name)} <span class="badge">${issues.length}</span></h2>
|
||||
<label class="category-select-all">
|
||||
<input type="checkbox" class="category-check" data-category="${safeId}" />
|
||||
Select all
|
||||
</label>
|
||||
${description ? `<p class="category-description">${description}</p>` : ''}
|
||||
</div>
|
||||
<div class="card-list">
|
||||
${issues.map((issue) => Card({ ...issue, plainEnglishView, availableModels })).join('')}
|
||||
</div>
|
||||
</section>`
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user