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
Binary file modified docs/analytics-dashboard.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
2 changes: 1 addition & 1 deletion packages/chrome/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/cli/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
2 changes: 1 addition & 1 deletion packages/extension/package.json
Original file line number Diff line number Diff line change
@@ -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": {
Expand Down
480 changes: 250 additions & 230 deletions packages/extension/src/content/analyticsOverlay.ts

Large diffs are not rendered by default.

31 changes: 28 additions & 3 deletions packages/extension/src/content/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -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 */
Expand Down Expand Up @@ -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 {
Expand Down
139 changes: 82 additions & 57 deletions packages/extension/src/content/githubDom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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"]',
Expand Down Expand Up @@ -414,15 +421,15 @@ 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
}
if (lineNumCells.length === 1) {
return lineNumCells[0] as HTMLElement;
}

// React UI fallback
// Fallback
const diffLineNum = row.querySelector('[data-line-number]') as HTMLElement;
return diffLineNum;
}
Expand Down Expand Up @@ -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);
}
}
}

Expand All @@ -480,6 +490,7 @@ export function removeAllMarkers(): void {
delete el.dataset.agent;
delete el.dataset.model;
delete el.dataset.content;
delete el.dataset.promptNum;
}
});

Expand Down Expand Up @@ -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");
}

/**
Expand All @@ -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 <table> 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;
}
Expand Down Expand Up @@ -679,31 +704,13 @@ export function showLoading(): void {
</div>
`;

// 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");
}

/**
Expand Down Expand Up @@ -765,23 +772,41 @@ export function showError(message: string): void {
</div>
`;

// 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;
}

/**
Expand Down
12 changes: 10 additions & 2 deletions packages/extension/src/content/router.ts
Original file line number Diff line number Diff line change
Expand Up @@ -200,6 +200,7 @@ function buildAttributionMap(

// Build prompt index map: sessionId:promptId -> P1, P2, etc.
const promptIndexMap = new Map<string, string>();
const promptNumberMap = new Map<string, number>();
let promptCounter = 1;

for (const [_commitSha, note] of notes) {
Expand All @@ -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,
Expand All @@ -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,
Expand Down Expand Up @@ -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}`;
Expand All @@ -265,6 +272,7 @@ function buildAttributionMap(
model: session?.model || null,
sessionId: range.sessionId,
promptContent,
promptNumber,
});
}
}
Expand Down Expand Up @@ -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;
Expand Down
1 change: 1 addition & 0 deletions packages/extension/src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/firefox/manifest.json
Original file line number Diff line number Diff line change
@@ -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",
Expand Down