diff --git a/docs/analytics-dashboard.png b/docs/analytics-dashboard.png index 287ad06..8771ea2 100644 Binary files a/docs/analytics-dashboard.png and b/docs/analytics-dashboard.png differ diff --git a/packages/chrome/manifest.json b/packages/chrome/manifest.json index cfb8f83..2af861a 100644 --- a/packages/chrome/manifest.json +++ b/packages/chrome/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Agent Blame", - "version": "3.0.3", + "version": "3.0.7", "description": "See AI-generated vs human-written code on GitHub PRs", "icons": { "16": "icons/icon16.png", diff --git a/packages/cli/package.json b/packages/cli/package.json index d8d40ed..57ba88b 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -1,6 +1,6 @@ { "name": "@mesadev/agentblame", - "version": "3.0.6", + "version": "3.0.7", "description": "CLI to track AI-generated vs human-written code", "license": "Apache-2.0", "repository": { diff --git a/packages/extension/package.json b/packages/extension/package.json index 544d30e..765c768 100644 --- a/packages/extension/package.json +++ b/packages/extension/package.json @@ -1,6 +1,6 @@ { "name": "@agentblame/extension", - "version": "3.0.3", + "version": "3.0.7", "description": "Agent Blame Extension - Shared source for Chrome and Firefox", "private": true, "devDependencies": { diff --git a/packages/extension/src/content/analyticsOverlay.ts b/packages/extension/src/content/analyticsOverlay.ts index e599ca0..df2d60b 100644 --- a/packages/extension/src/content/analyticsOverlay.ts +++ b/packages/extension/src/content/analyticsOverlay.ts @@ -539,10 +539,11 @@ function renderStatsSection(analytics: AnalyticsData): string { ? Math.round((unknownLines / summary.totalLines) * 100) : 0; - // Commit to Prompt ratio - const ratio = summary.commits > 0 ? (summary.prompts / summary.commits).toFixed(1) : "0"; - // Use distinct colors: blue (good) -> amber (medium) -> red (poor) - const ratioColor = parseFloat(ratio) <= 1.5 ? "#0969da" : parseFloat(ratio) <= 2.5 ? "#9a6700" : "#cf222e"; + // Prompt efficiency: commits / prompts (higher = better, 1.0 = ideal) + const efficiency = summary.prompts > 0 ? (summary.commits / summary.prompts) : 0; + const efficiencyStr = efficiency > 0 ? efficiency.toFixed(2) : "0"; + // Use distinct colors: blue (good) -> amber (medium) -> muted (poor) + const ratioColor = efficiency >= 0.67 ? "#539BF5" : efficiency >= 0.4 ? "#C69026" : "#8b949e"; // Build tool breakdown const toolEntries = Object.entries(summary.tools || {}) @@ -554,124 +555,148 @@ function renderStatsSection(analytics: AnalyticsData): string { .sort(([, a], [, b]) => b - a) .slice(0, 5); - // Distinct colors for each card type - const aiColor = "#b86540"; // Coral/orange - const humanColor = "#238636"; // Green + // Distinct colors for each card type (mid-tone, works on light & dark) + const aiColor = "#E07B53"; // Coral/orange + const humanColor = "#57AB5A"; // Green const unknownColor = "#8b949e"; // Gray - const totalColor = "#8957e5"; // Purple + const totalColor = "#A371F7"; // Purple + + // Donut ring SVG helper (percentage as arc) + const donutRing = (percent: number, color: string, size: number = 56) => { + const r = (size - 6) / 2; + const circ = 2 * Math.PI * r; + const offset = circ - (percent / 100) * circ; + return ` + + + `; + }; + + // Stacked bar for AI vs Human split + const splitBar = ` +
+
+
+ ${unknownLines > 0 ? `
` : ''} +
+ `; return ` -
-
-

Summary

-
+ +
- -
- -
-
${aiPercent}%
-
AI-Generated
-
${summary.aiLines.toLocaleString()} lines
+
+ + +
+
+ ${donutRing(aiPercent, aiColor)} +
+ ${aiPercent}% +
+
+
AI-Generated
+
${summary.aiLines.toLocaleString()} lines
- -
-
${humanPercent}%
-
Human-Written
-
${summary.humanLines.toLocaleString()} lines
+ +
+
+ ${donutRing(humanPercent, humanColor)} +
+ ${humanPercent}% +
+
+
Human-Written
+
${summary.humanLines.toLocaleString()} lines
- - ${unknownLines > 0 ? ` -
-
${unknownPercent}%
-
Untracked
-
${unknownLines.toLocaleString()} lines
+ +
+
${summary.totalLines.toLocaleString()}
+
Total Lines
+
tracked in repository
- ` : ''} - -
-
${summary.totalLines.toLocaleString()}
-
Total Lines
-
tracked in repository
+ +
+
${efficiencyStr}
+
Prompt Efficiency
+
${summary.commits} commits · ${summary.prompts} prompts
- -
-
1:${ratio}
-
Commit:Prompt
-
${summary.commits} commits, ${summary.prompts} prompts
+
+ + + ${splitBar} +
+
+ AI ${aiPercent}%
+
+ Human ${humanPercent}% +
+ ${unknownLines > 0 ? ` +
+ Untracked ${unknownPercent}% +
+ ` : ''}
+
+
- -
- - ${toolEntries.length > 0 ? (() => { + + ${toolEntries.length > 0 || modelEntries.length > 0 ? ` +
+ ${toolEntries.length > 0 ? ` +
+
+

By Tool

+
+
+ ${toolEntries.map(([name, lines], i) => { + const percent = summary.aiLines > 0 ? Math.round((lines / summary.aiLines) * 100) : 0; + const color = TOOL_COLOR_PALETTE[i % TOOL_COLOR_PALETTE.length]; return ` -
-
-

By Tool

-
-
- ${toolEntries.map(([name, lines], i) => { - const percent = summary.aiLines > 0 ? Math.round((lines / summary.aiLines) * 100) : 0; - const color = TOOL_COLOR_PALETTE[i % TOOL_COLOR_PALETTE.length]; - return ` -
-
- - ${formatProviderName(name)} -
-
-
-
-
- ${lines.toLocaleString()} (${percent}%) -
-
- `; - }).join('')} +
+ + ${formatProviderName(name)} +
+
+ ${percent}% · ${lines.toLocaleString()}
`; - })() : ''} + }).join('')} +
+
+ ` : ''} - - ${modelEntries.length > 0 ? (() => { + ${modelEntries.length > 0 ? ` +
+
+

Top Models

+
+
+ ${modelEntries.map(([name, lines], i) => { + const percent = summary.aiLines > 0 ? Math.round((lines / summary.aiLines) * 100) : 0; + const color = TOOL_COLOR_PALETTE[(toolEntries.length + i) % TOOL_COLOR_PALETTE.length]; return ` -
-
-

Top Models

-
-
- ${modelEntries.map(([name, lines], i) => { - const percent = summary.aiLines > 0 ? Math.round((lines / summary.aiLines) * 100) : 0; - const color = TOOL_COLOR_PALETTE[(toolEntries.length + i) % TOOL_COLOR_PALETTE.length]; - return ` -
-
- - ${escapeHtml(name)} -
-
-
-
-
- ${lines.toLocaleString()} (${percent}%) -
-
- `; - }).join('')} +
+ + ${escapeHtml(name)} +
+
+ ${percent}% · ${lines.toLocaleString()}
`; - })() : ''} + }).join('')}
+ ` : ''}
+ ` : ''} `; } @@ -716,114 +741,107 @@ function renderTrendSection(filteredAnalytics: AnalyticsData, fullAnalytics: Ana label: m.displayName, })); - // Compute commit:prompt ratio trend + // Compute prompt efficiency trend (commits / prompts, higher = better) const ratioData = trendData.commits.map((c, i) => - c > 0 ? trendData.prompts[i] / c : 0 + trendData.prompts[i] > 0 ? c / trendData.prompts[i] : 0 ); - const aiColor = "#b86540"; - const ratioColor = "#9a6700"; + const aiColor = "#E07B53"; + const ratioColor = "#C69026"; return ` -
-
-

Trends

+ +
+
+

AI Code Percentage

+
+ + AI % +
-
- -
-
-

AI Code Percentage

-
- - AI % -
-
-
- ${renderTrendLine(trendData.aiPercent, aiColor, 700, 100, 0.15, "%")} -
- ${trendData.labels[0] || ''} - ${trendData.labels[trendData.labels.length - 1] || ''} -
-
+
+ ${renderTrendLine(trendData.aiPercent, aiColor, 700, 100, 0.15, "%")} +
+ ${trendData.labels[0] || ''} + ${trendData.labels[trendData.labels.length - 1] || ''}
+
+
- -
-
-

Prompt Efficiency

-
- - Prompts per Commit (1.0 = ideal) -
-
-
- ${ratioData.some(v => v > 0) - ? renderTrendLine(ratioData, ratioColor, 700, 100, 0.15, "") - : '
No ratio data for this period
' - } -
- ${trendData.labels[0] || ''} - ${trendData.labels[trendData.labels.length - 1] || ''} -
-
+ +
+
+

Prompt Efficiency

+
+ + Commits per Prompt (1.0 = ideal)
- - - ${toolColors.length > 0 ? ` -
-
-

Tool Usage

-
- ${toolColors.map(t => ` - - - ${t.displayName} - - `).join('')} -
-
-
- ${toolSeries.some(s => s.data.some(v => v > 0)) - ? renderMultiTrendLine(toolSeries, 700, 100, " lines") - : '
No tool data for this period
' - } -
- ${trendData.labels[0] || ''} - ${trendData.labels[trendData.labels.length - 1] || ''} -
-
+
+
+ ${ratioData.some(v => v > 0) + ? renderTrendLine(ratioData, ratioColor, 700, 100, 0.15, "") + : '
No ratio data for this period
' + } +
+ ${trendData.labels[0] || ''} + ${trendData.labels[trendData.labels.length - 1] || ''}
- ` : ''} +
+
- - ${modelColors.length > 0 ? ` -
-
-

Model Usage

-
- ${modelColors.map(m => ` - - - ${escapeHtml(m.displayName)} - - `).join('')} -
-
-
- ${modelSeries.some(s => s.data.some(v => v > 0)) - ? renderMultiTrendLine(modelSeries, 700, 100, " lines") - : '
No model data for this period
' - } -
- ${trendData.labels[0] || ''} - ${trendData.labels[trendData.labels.length - 1] || ''} -
-
+ + ${toolColors.length > 0 ? ` +
+
+

Tool Usage

+
+ ${toolColors.map(t => ` + + + ${t.displayName} + + `).join('')} +
+
+
+ ${toolSeries.some(s => s.data.some(v => v > 0)) + ? renderMultiTrendLine(toolSeries, 700, 100, " lines") + : '
No tool data for this period
' + } +
+ ${trendData.labels[0] || ''} + ${trendData.labels[trendData.labels.length - 1] || ''} +
+
+
+ ` : ''} + + + ${modelColors.length > 0 ? ` +
+
+

Model Usage

+
+ ${modelColors.map(m => ` + + + ${escapeHtml(m.displayName)} + + `).join('')} +
+
+
+ ${modelSeries.some(s => s.data.some(v => v > 0)) + ? renderMultiTrendLine(modelSeries, 700, 100, " lines") + : '
No model data for this period
' + } +
+ ${trendData.labels[0] || ''} + ${trendData.labels[trendData.labels.length - 1] || ''}
- ` : ''}
+ ` : ''} `; } @@ -883,12 +901,7 @@ function renderTrendLine( ${minLabel}
-
- -
-
-
- +
@@ -896,9 +909,14 @@ function renderTrendLine( + + + + + ${[1, 2, 3, 4].map(i => { const x = padding + (i / 5) * (chartWidth - padding * 2); - return ``; + return ``; }).join('\n ')} @@ -964,16 +982,16 @@ function renderMultiTrendLine( ${minLabel}
-
- -
-
-
- +
+ + + + + ${[1, 2, 3, 4].map(i => { const x = padding + (i / 5) * (chartWidth - padding * 2); - return ``; + return ``; }).join('\n ')} ${lines} @@ -1101,9 +1119,9 @@ function renderContributorsSection(analytics: AnalyticsData): string { if (contributors.length === 0) { return ` -
-
-

Contributors

+
+
+

Contributors

@@ -1114,8 +1132,8 @@ function renderContributorsSection(analytics: AnalyticsData): string { `; } - const aiColor = "#b86540"; - const humanColor = "#238636"; + const aiColor = "#E07B53"; + const humanColor = "#57AB5A"; const rows = contributors .slice(0, 10) @@ -1124,9 +1142,10 @@ function renderContributorsSection(analytics: AnalyticsData): string { c.totalLines > 0 ? Math.round((c.aiLines / c.totalLines) * 100) : 0; const humanPercent = 100 - aiPercent; - // Commit:Prompt ratio - const ratio = c.commits > 0 ? (c.prompts / c.commits).toFixed(1) : "0"; - const ratioColor = parseFloat(ratio) <= 1.5 ? "#238636" : parseFloat(ratio) <= 2.5 ? "#9a6700" : "#b86540"; + // Prompt efficiency: commits / prompts (higher = better) + const efficiency = c.prompts > 0 ? (c.commits / c.prompts) : 0; + const efficiencyStr = efficiency > 0 ? efficiency.toFixed(2) : "0"; + const ratioColor = efficiency >= 0.67 ? "#57AB5A" : efficiency >= 0.4 ? "#C69026" : "#8b949e"; // GitHub avatar URL - works for any username (not emails) const isEmail = c.username.includes('@'); @@ -1134,7 +1153,7 @@ function renderContributorsSection(analytics: AnalyticsData): string { const initial = c.username.charAt(0).toUpperCase(); return ` -
+
${!isEmail ? ` ${aiPercent}% AI
- 1:${ratio} + ${efficiencyStr}
${c.totalLines.toLocaleString()} @@ -1172,9 +1191,9 @@ function renderContributorsSection(analytics: AnalyticsData): string { .join(""); return ` -
-
-

Contributors

+
+
+

Contributors

${contributors.length}
@@ -1201,9 +1220,9 @@ function renderPullRequestsSection( if (recentPRs.length === 0) { return ` -
-
-

Recent Activity

+
+
+

Recent Activity

@@ -1214,17 +1233,18 @@ function renderPullRequestsSection( `; } - const aiColor = "#b86540"; - const humanColor = "#238636"; + const aiColor = "#E07B53"; + const humanColor = "#57AB5A"; const rows = recentPRs .map((pr, index) => { const aiPercent = pr.added > 0 ? Math.round((pr.aiLines / pr.added) * 100) : 0; const humanPercent = 100 - aiPercent; - // Commit:Prompt ratio - const ratio = pr.commits > 0 ? (pr.prompts / pr.commits).toFixed(1) : "0"; - const ratioColor = parseFloat(ratio) <= 1.5 ? "#238636" : parseFloat(ratio) <= 2.5 ? "#9a6700" : "#b86540"; + // Prompt efficiency: commits / prompts (higher = better) + const efficiency = pr.prompts > 0 ? (pr.commits / pr.prompts) : 0; + const efficiencyStr = efficiency > 0 ? efficiency.toFixed(2) : "0"; + const ratioColor = efficiency >= 0.67 ? "#57AB5A" : efficiency >= 0.4 ? "#C69026" : "#8b949e"; // Badge color based on AI percentage let badgeColor = humanColor; @@ -1233,7 +1253,7 @@ function renderPullRequestsSection( badgeColor = aiColor; badgeText = `${aiPercent}% AI`; } else if (aiPercent > 25) { - badgeColor = "#9a6700"; // Amber for mixed + badgeColor = "#C69026"; // Amber for mixed badgeText = `${aiPercent}% AI`; } else if (aiPercent > 0) { badgeColor = humanColor; @@ -1243,7 +1263,7 @@ function renderPullRequestsSection( const dateStr = pr.date ? formatDate(pr.date).split(',')[0] : ''; return ` -
+
@@ -1266,7 +1286,7 @@ function renderPullRequestsSection(
- 1:${ratio} + ${efficiencyStr}
+${pr.added} @@ -1279,9 +1299,9 @@ function renderPullRequestsSection( .join(""); return ` -
-
-

Recent Activity

+
+
+

Recent Activity

${recentPRs.length}
diff --git a/packages/extension/src/content/content.css b/packages/extension/src/content/content.css index 5c24ce3..8a1ebdd 100644 --- a/packages/extension/src/content/content.css +++ b/packages/extension/src/content/content.css @@ -8,18 +8,40 @@ /* Attribution gutter - orange left bar on AI-generated lines */ /* Only AI-generated lines are marked - human lines have no marker (cleaner UI) */ -/* AI-generated lines marker - orange left border */ +/* AI-generated lines marker - thin orange bar with floating prompt badge */ .ab-gutter-ai { position: relative; - box-shadow: inset 3px 0 0 0 var(--ab-ai-color) !important; + box-shadow: inset 4px 0 0 0 var(--ab-ai-color) !important; cursor: pointer; } +/* Rounded-rect badge with prompt number, hugging the gutter bar */ +.ab-gutter-ai[data-prompt-num]::after { + content: attr(data-prompt-num); + position: absolute; + left: 0; + top: 50%; + transform: translateY(-50%); + height: 14px; + line-height: 14px; + text-align: center; + padding: 0 2px 0 1px; + box-sizing: border-box; + font-size: 8px; + font-weight: 700; + font-family: ui-monospace, SFMono-Regular, "SF Mono", Menlo, Consolas, monospace; + color: #fff; + background: var(--ab-ai-color); + border-radius: 2px; + pointer-events: none; + z-index: 1; +} + /* New GitHub UI (React) - ensure styling works */ td.diff-line-number.ab-gutter-ai, [data-grid-cell-id].ab-gutter-ai { position: relative !important; - box-shadow: inset 3px 0 0 0 var(--ab-ai-color) !important; + box-shadow: inset 4px 0 0 0 var(--ab-ai-color) !important; } /* Prompt tooltip - appears on hover over orange gutter bar */ @@ -268,6 +290,9 @@ td.diff-line-number.ab-gutter-ai, font-size: 12px; font-weight: 500; border-radius: 12px; + order: 999; + flex-shrink: 0; + align-self: center; } .ab-file-badge.high-ai { diff --git a/packages/extension/src/content/githubDom.ts b/packages/extension/src/content/githubDom.ts index 8a20d4f..ff1a043 100644 --- a/packages/extension/src/content/githubDom.ts +++ b/packages/extension/src/content/githubDom.ts @@ -53,6 +53,7 @@ export function getDiffContainers(): HTMLElement[] { '[data-details-container-group="file"]', // Alternative structure ".js-file", // JS-enhanced file container "diff-layout", // New React-based diff component + '[role="region"][id^="diff-"]', // New GitHub UI (2026+) ]; for (const selector of selectors) { @@ -148,6 +149,12 @@ export function getFilePath(container: HTMLElement): string { return pathElement.getAttribute("data-tagsearch-path") || ""; } + // New UI: data-file-path attribute on expand button + const filePathEl = container.querySelector("[data-file-path]"); + if (filePathEl) { + return filePathEl.getAttribute("data-file-path") || ""; + } + // Try to find path in file header link const fileLink = container.querySelector( '.file-header a[title], .file-info a[href*="blob"]', @@ -414,7 +421,7 @@ function findNewLineNumberCell(element: HTMLElement): HTMLElement | null { if (!row) return null; // In unified view: two blob-num cells, we want the second one (new line number) - const lineNumCells = row.querySelectorAll('.blob-num, .diff-line-number'); + const lineNumCells = row.querySelectorAll('.blob-num, .diff-line-number, .new-diff-line-number'); if (lineNumCells.length >= 2) { return lineNumCells[1] as HTMLElement; // Second column = new line numbers } @@ -422,7 +429,7 @@ function findNewLineNumberCell(element: HTMLElement): HTMLElement | null { return lineNumCells[0] as HTMLElement; } - // React UI fallback + // Fallback const diffLineNum = row.querySelector('[data-line-number]') as HTMLElement; return diffLineNum; } @@ -460,6 +467,9 @@ export function injectMarker( if (attribution.promptContent) { lineNumCell.dataset.content = attribution.promptContent; } + if (attribution.promptNumber != null) { + lineNumCell.dataset.promptNum = String(attribution.promptNumber); + } } } @@ -480,6 +490,7 @@ export function removeAllMarkers(): void { delete el.dataset.agent; delete el.dataset.model; delete el.dataset.content; + delete el.dataset.promptNum; } }); @@ -596,31 +607,13 @@ export function injectPRSummary(stats: { summary.className = "ab-pr-summary"; summary.innerHTML = statsHtml; - // Strategy 1: Legacy UI - inject before the first .file container - const firstFileContainer = document.querySelector(".file"); - if (firstFileContainer?.parentElement) { - firstFileContainer.parentElement.insertBefore(summary, firstFileContainer); - log("Injected PR summary banner (legacy UI - before .file)"); - return; - } - - // Strategy 2: React UI - inject before [data-hpc] container - const hpc = document.querySelector("[data-hpc]"); - if (hpc?.parentElement) { - hpc.parentElement.insertBefore(summary, hpc); - log("Injected PR summary banner (React UI - before [data-hpc])"); - return; + const injectionPoint = findBannerInjectionPoint(); + if (injectionPoint) { + injectionPoint.parent.insertBefore(summary, injectionPoint.before); + log("Injected PR summary banner"); + } else { + log("Could not find injection point for PR summary banner"); } - - // Strategy 3: Fallback - try #files_bucket or .pr-toolbar - const fallbackArea = document.querySelector("#files_bucket, .pr-toolbar, .pull-request-tab-content"); - if (fallbackArea) { - fallbackArea.insertBefore(summary, fallbackArea.firstChild); - log("Injected PR summary banner (fallback)"); - return; - } - - log("Could not find injection point for PR summary banner"); } /** @@ -631,10 +624,42 @@ export function injectFileBadge( aiLines: number, totalLines: number, ): void { - const header = container.querySelector( + // Try to find header within the container (classic UI) + let header = container.querySelector( ".file-header, .file-info, [data-tagsearch-path]", ); + // New UI: use the main header flex container (badge uses CSS order to appear at end) + if (!header) { + const filePathButton = container.querySelector("[data-file-path]"); + if (filePathButton) { + header = filePathButton.closest('[class*="diff-file-header"]'); + } + } + + // Fallback: container is a and the file header is a previous sibling + if (!header) { + let current: HTMLElement | null = container; + for (let depth = 0; depth < 5 && current && !header; depth++) { + let sibling = current.previousElementSibling; + while (sibling) { + if (sibling instanceof HTMLElement) { + const nested = sibling.querySelector( + "[data-tagsearch-path], .file-header, .file-info, [data-file-path]", + ); + if (nested) { + // For data-file-path, use its parent section as the header + const fp = nested.closest("[data-file-path]"); + header = fp ? fp.parentElement : nested; + break; + } + } + sibling = sibling.previousElementSibling; + } + current = current.parentElement; + } + } + if (!header || header.querySelector(".ab-file-badge")) { return; } @@ -679,31 +704,13 @@ export function showLoading(): void { `; - // Strategy 1: Legacy UI - inject before the first .file container - const firstFileContainer = document.querySelector(".file"); - if (firstFileContainer?.parentElement) { - firstFileContainer.parentElement.insertBefore(summary, firstFileContainer); - log("Injected PR summary loading banner (legacy UI - before .file)"); - return; - } - - // Strategy 2: React UI - inject before [data-hpc] container - const hpc = document.querySelector("[data-hpc]"); - if (hpc?.parentElement) { - hpc.parentElement.insertBefore(summary, hpc); - log("Injected PR summary loading banner (React UI - before [data-hpc])"); - return; + const injectionPoint = findBannerInjectionPoint(); + if (injectionPoint) { + injectionPoint.parent.insertBefore(summary, injectionPoint.before); + log("Injected PR summary loading banner"); + } else { + log("Could not find injection point for PR summary loading banner"); } - - // Strategy 3: Fallback - try #files_bucket or .pr-toolbar - const fallbackArea = document.querySelector("#files_bucket, .pr-toolbar, .pull-request-tab-content"); - if (fallbackArea) { - fallbackArea.insertBefore(summary, fallbackArea.firstChild); - log("Injected PR summary loading banner (fallback)"); - return; - } - - log("Could not find injection point for PR summary loading banner"); } /** @@ -765,23 +772,41 @@ export function showError(message: string): void { `; - // Try to inject at the same locations as showLoading + const injectionPoint = findBannerInjectionPoint(); + if (injectionPoint) { + injectionPoint.parent.insertBefore(summary, injectionPoint.before); + } +} + +/** + * Find an injection point for summary/loading banners + */ +function findBannerInjectionPoint(): { parent: Element; before: Element | null } | null { + // Strategy 1: Legacy UI - inject before the first .file container const firstFileContainer = document.querySelector(".file"); if (firstFileContainer?.parentElement) { - firstFileContainer.parentElement.insertBefore(summary, firstFileContainer); - return; + return { parent: firstFileContainer.parentElement, before: firstFileContainer }; + } + + // Strategy 2: New UI - inject before first file region + const firstNewUIContainer = document.querySelector('[role="region"][id^="diff-"]'); + if (firstNewUIContainer?.parentElement) { + return { parent: firstNewUIContainer.parentElement, before: firstNewUIContainer }; } + // Strategy 3: React UI - inject before [data-hpc] container const hpc = document.querySelector("[data-hpc]"); if (hpc?.parentElement) { - hpc.parentElement.insertBefore(summary, hpc); - return; + return { parent: hpc.parentElement, before: hpc }; } + // Strategy 4: Fallback const fallbackArea = document.querySelector("#files_bucket, .pr-toolbar, .pull-request-tab-content"); if (fallbackArea) { - fallbackArea.insertBefore(summary, fallbackArea.firstChild); + return { parent: fallbackArea, before: fallbackArea.firstChild as Element | null }; } + + return null; } /** diff --git a/packages/extension/src/content/router.ts b/packages/extension/src/content/router.ts index 75a5f61..6fb0190 100644 --- a/packages/extension/src/content/router.ts +++ b/packages/extension/src/content/router.ts @@ -200,6 +200,7 @@ function buildAttributionMap( // Build prompt index map: sessionId:promptId -> P1, P2, etc. const promptIndexMap = new Map(); + const promptNumberMap = new Map(); let promptCounter = 1; for (const [_commitSha, note] of notes) { @@ -213,6 +214,7 @@ function buildAttributionMap( if (!promptIndexMap.has(promptKey)) { const promptIdx = `P${promptCounter}`; promptIndexMap.set(promptKey, promptIdx); + promptNumberMap.set(promptKey, promptCounter); prompts.push({ index: promptIdx, agent: session.agent, @@ -229,6 +231,7 @@ function buildAttributionMap( if (!promptIndexMap.has(promptKey)) { const promptIdx = `P${promptCounter}`; promptIndexMap.set(promptKey, promptIdx); + promptNumberMap.set(promptKey, promptCounter); prompts.push({ index: promptIdx, agent: session.agent, @@ -256,6 +259,10 @@ function buildAttributionMap( promptContent = session.prompts; } + // Look up prompt number for this range + const promptKey = `${range.sessionId}:${range.promptId ?? "null"}`; + const promptNumber = promptNumberMap.get(promptKey); + // Add entry for each line in the range for (let line = range.startLine; line <= range.endLine; line++) { const key = `${filePath}:${line}`; @@ -265,6 +272,7 @@ function buildAttributionMap( model: session?.model || null, sessionId: range.sessionId, promptContent, + promptNumber, }); } } @@ -342,10 +350,10 @@ function setupPRObserver(): void { if (dominated > 10) return true; if ( node.matches?.( - "[data-tagsearch-path], .file, .diff-table, [data-hpc], .js-diff-load-container, tr.diff-line-row", + '[data-tagsearch-path], .file, .diff-table, [data-hpc], .js-diff-load-container, tr.diff-line-row, [role="region"][id^="diff-"], [data-file-path]', ) || node.querySelector?.( - "[data-tagsearch-path], .file, .diff-table, .blob-code-addition, tr.diff-line-row", + '[data-tagsearch-path], .file, .diff-table, .blob-code-addition, tr.diff-line-row, [role="region"][id^="diff-"], [data-file-path]', ) ) { return true; diff --git a/packages/extension/src/types.ts b/packages/extension/src/types.ts index db8530a..5952332 100644 --- a/packages/extension/src/types.ts +++ b/packages/extension/src/types.ts @@ -92,6 +92,7 @@ export interface LineAttribution { model: string | null; sessionId?: string; // Session ID for lookup promptContent?: string; // Prompt text for tooltip display + promptNumber?: number; // Prompt number (1, 2, 3...) for gutter display } // Prompt info for PR summary display diff --git a/packages/firefox/manifest.json b/packages/firefox/manifest.json index 040f523..da176f1 100644 --- a/packages/firefox/manifest.json +++ b/packages/firefox/manifest.json @@ -1,7 +1,7 @@ { "manifest_version": 3, "name": "Agent Blame", - "version": "3.0.3", + "version": "3.0.7", "description": "See AI-generated vs human-written code on GitHub PRs", "icons": { "16": "icons/icon16.png",