Skip to content
Closed
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
2 changes: 1 addition & 1 deletion packages/chrome/package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@agentblame/chrome",
"version": "0.2.7",
"version": "0.2.8",
"description": "Agent Blame Chrome Extension - See AI attribution on GitHub PRs",
"private": true,
"scripts": {
Expand Down
98 changes: 83 additions & 15 deletions packages/chrome/src/content/analytics-overlay.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,21 +10,22 @@ import {
type AnalyticsData,
type AnalyticsHistoryEntry,
getAnalytics,
clearAnalyticsCache,
} from "../lib/mock-analytics";

const PAGE_CONTAINER_ID = "agentblame-page-container";
const ORIGINAL_CONTENT_ATTR = "data-agentblame-hidden";

// Tool color palette - High contrast colors that work in light/dark themes
// Tool color palette - Soft, pleasant colors that work in light/dark themes
const TOOL_COLOR_PALETTE = [
"#0969da", // Blue
"#cf222e", // Red
"#1a7f37", // Green
"#8250df", // Purple
"#bf8700", // Gold/Yellow
"#0550ae", // Dark Blue
"#bf3989", // Magenta
"#1b7c83", // Teal
"#5B8DEE", // Soft blue (Cursor)
"#E07B53", // Soft coral/orange (Claude Code)
"#4DBBAA", // Soft teal (OpenCode)
"#9B7ED9", // Soft purple
"#6ABF69", // Soft green
"#E5A84B", // Soft amber
"#D97BA2", // Soft rose
"#5AADCF", // Soft cyan
];

/**
Expand All @@ -34,6 +35,7 @@ function formatProviderName(provider: string): string {
const names: Record<string, string> = {
cursor: "Cursor",
claudeCode: "Claude Code",
opencode: "OpenCode",
copilot: "Copilot",
windsurf: "Windsurf",
aider: "Aider",
Expand Down Expand Up @@ -325,7 +327,7 @@ function handlePeriodChange(period: PeriodOption): void {
}

/**
* Attach event listeners to period dropdown
* Attach event listeners to period dropdown and refresh button
*/
function attachPeriodListeners(): void {
const dropdown = document.getElementById("agentblame-period-select");
Expand All @@ -335,6 +337,38 @@ function attachPeriodListeners(): void {
handlePeriodChange(select.value as PeriodOption);
});
}

const refreshBtn = document.getElementById("agentblame-refresh-btn");
if (refreshBtn) {
refreshBtn.addEventListener("click", handleRefresh);
}
}

/**
* Handle refresh button click - clear cache and refetch data
*/
async function handleRefresh(): Promise<void> {
if (!currentOwner || !currentRepo) return;

const container = document.getElementById(PAGE_CONTAINER_ID);
if (!container) return;

// Show loading state
container.innerHTML = renderLoadingState();

// Clear cache and refetch
await clearAnalyticsCache(currentOwner, currentRepo);
const analytics = await getAnalytics(currentOwner, currentRepo);

if (!analytics) {
container.innerHTML = renderEmptyState();
return;
}

currentAnalytics = analytics;
const filtered = filterAnalyticsByPeriod(analytics, currentPeriod);
container.innerHTML = renderAnalyticsPage(currentOwner, currentRepo, filtered, analytics);
attachPeriodListeners();
}

/**
Expand Down Expand Up @@ -366,7 +400,7 @@ function renderAnalyticsPage(
AI code attribution analytics
</div>
</div>
<div>
<div class="d-flex gap-2 flex-items-center">
<select id="agentblame-period-select" class="form-select">
${(Object.keys(PERIOD_LABELS) as PeriodOption[])
.map(
Expand All @@ -375,6 +409,13 @@ function renderAnalyticsPage(
)
.join("")}
</select>
<button id="agentblame-refresh-btn" class="btn btn-sm" title="Refresh analytics data">
<svg class="octicon" viewBox="0 0 16 16" width="16" height="16">
<path fill-rule="evenodd" d="M8 3a5 5 0 1 0 4.546 2.914.75.75 0 0 1 1.363-.613A6.5 6.5 0 1 1 8 1.5v2A.75.75 0 0 1 6.75 4v-.75A.75.75 0 0 1 8 3z"></path>
<path fill-rule="evenodd" d="M8 0a.75.75 0 0 1 .75.75v3.5a.75.75 0 0 1-1.5 0V.75A.75.75 0 0 1 8 0z"></path>
<path fill-rule="evenodd" d="M7.47 3.22a.75.75 0 0 1 1.06 0l1.5 1.5a.75.75 0 0 1-1.06 1.06l-1.5-1.5a.75.75 0 0 1 0-1.06z"></path>
</svg>
</button>
</div>
</div>

Expand Down Expand Up @@ -440,7 +481,7 @@ function renderRepositorySection(
);

// Colors
const aiColor = "var(--color-severe-fg, #f78166)"; // GitHub's coral orange
const aiColor = "#b86540"; // Mesa Orange
const humanColor = "var(--color-success-fg, #238636)"; // GitHub's addition green

return `
Expand Down Expand Up @@ -552,7 +593,7 @@ function renderContributorsSection(analytics: AnalyticsData): string {
<div class="flex-1 d-flex flex-items-center gap-2">
<span class="f6 color-fg-muted" style="width: 55px;">${aiPercent}% AI</span>
<div class="d-flex flex-1 rounded-2 overflow-hidden" style="height: 8px; max-width: 200px;">
<div style="width: ${aiPercent}%; background: var(--color-severe-fg);"></div>
<div style="width: ${aiPercent}%; background: #b86540;"></div>
<div style="width: ${humanPercent}%; background: var(--color-success-fg);"></div>
</div>
</div>
Expand Down Expand Up @@ -608,7 +649,7 @@ function renderPullRequestsSection(
const aiPercent = pr.added > 0 ? Math.round((pr.aiLines / pr.added) * 100) : 0;
const badgeStyle =
aiPercent > 50
? "background: var(--color-severe-fg); color: white;"
? "background: #b86540; color: white;"
: aiPercent > 0
? "background: var(--color-attention-emphasis); color: white;"
: "background: var(--color-success-emphasis); color: white;";
Expand Down Expand Up @@ -648,9 +689,36 @@ function renderPullRequestsSection(

/**
* Format model name for display
* Handles model IDs like "claude-opus-4-5-20251101" -> "Claude Opus 4.5"
*/
function formatModelName(model: string): string {
return model
// Known model name mappings for cleaner display
const knownModels: Record<string, string> = {
"claude": "Claude",
"claude-3-opus": "Claude 3 Opus",
"claude-3-sonnet": "Claude 3 Sonnet",
"claude-3.5-sonnet": "Claude 3.5 Sonnet",
"claude-3-haiku": "Claude 3 Haiku",
"gpt-4": "GPT-4",
"gpt-4o": "GPT-4o",
"gpt-4-turbo": "GPT-4 Turbo",
};

// Check for exact match first
if (knownModels[model]) {
return knownModels[model];
}

// Handle versioned model names like "claude-opus-4-5-20251101"
// Extract the model family and version, strip the date suffix
const datePattern = /-\d{8}$/;
let cleaned = model.replace(datePattern, "");

// Convert patterns like "4-5" to "4.5" for version numbers
cleaned = cleaned.replace(/-(\d+)-(\d+)$/, "-$1.$2");

// Title case and replace dashes with spaces
return cleaned
.replace(/-/g, " ")
.replace(/\b\w/g, (c) => c.toUpperCase());
}
Expand Down
6 changes: 3 additions & 3 deletions packages/chrome/src/content/content.css
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

/* Attribution gutter colors */
:root {
--ab-ai-color: var(--color-severe-fg, #f78166); /* GitHub's coral orange */
--ab-ai-color: #b86540; /* Mesa Orange */
}

/* Attribution gutter - box-shadow on the new line number cell */
Expand All @@ -24,7 +24,7 @@
padding: 8px 16px;
margin: 12px 0;
background: var(--bgColor-default, var(--color-canvas-default, #ffffff));
border: 1px solid rgba(247, 129, 102, 0.25);
border: 1px solid rgba(184, 101, 64, 0.25);
border-left: 3px solid var(--ab-ai-color);
border-radius: 6px;
font-size: 13px;
Expand Down Expand Up @@ -196,7 +196,7 @@
@media (prefers-color-scheme: dark) {
.ab-pr-summary {
background: var(--bgColor-default, #0d1117);
border-color: rgba(247, 129, 102, 0.4);
border-color: rgba(184, 101, 64, 0.4);
border-left-color: var(--ab-ai-color);
}

Expand Down
13 changes: 12 additions & 1 deletion packages/chrome/src/content/content.ts
Original file line number Diff line number Diff line change
Expand Up @@ -286,7 +286,6 @@ function findAttribution(
filePath,
filePath.replace(/^\//, ""),
`/${filePath}`,
filePath.split("/").slice(-1)[0], // Just filename
];

for (const variant of variants) {
Expand All @@ -297,6 +296,18 @@ function findAttribution(
}
}

// Last resort: match by filename only (for React UI which may only show filename)
// Search through the map for any key that ends with the same filename:lineNumber
const filename = filePath.split("/").pop() || filePath;
if (filename && filename !== filePath) {
for (const [mapKey, value] of map.entries()) {
// mapKey format is "path/to/file.ext:lineNumber"
if (mapKey.endsWith(`/${filename}:${lineNumber}`) || mapKey === `${filename}:${lineNumber}`) {
return value;
}
}
}

return null;
}

Expand Down
92 changes: 77 additions & 15 deletions packages/chrome/src/content/github-dom.ts
Original file line number Diff line number Diff line change
Expand Up @@ -46,12 +46,12 @@ export function isFilesChangedTab(): boolean {
*/
export function getDiffContainers(): HTMLElement[] {
// Try multiple selectors for GitHub's various diff layouts
// Order matters - try more specific selectors first
const selectors = [
".file", // Standard file container
'[data-details-container-group="file"]', // Alternative structure
".js-file", // JS-enhanced file container
"diff-layout", // New React-based diff component
"[data-hpc]", // High performance container
];

for (const selector of selectors) {
Expand All @@ -62,6 +62,38 @@ export function getDiffContainers(): HTMLElement[] {
}
}

// React UI: [data-hpc] is a parent container - find individual file sections within it
const hpcContainer = document.querySelector("[data-hpc]");
if (hpcContainer) {
// New React UI: Each file's diff is in a separate table
// Find tables that contain diff lines and use them as containers
const tables = hpcContainer.querySelectorAll("table");
if (tables.length > 0) {
const diffTables: HTMLElement[] = [];
for (const table of tables) {
// Only include tables that have diff content (diff-line-row or diff-text-cell)
if (table.querySelector("tr.diff-line-row, .diff-text-cell")) {
diffTables.push(table as HTMLElement);
}
}
if (diffTables.length > 0) {
log(`Found ${diffTables.length} diff tables as file containers`);
return diffTables;
}
}

// Try to find file containers using copilot-diff-entry (used in some new UI versions)
const copilotEntries = hpcContainer.querySelectorAll("copilot-diff-entry");
if (copilotEntries.length > 0) {
log(`Found ${copilotEntries.length} copilot-diff-entry elements`);
return Array.from(copilotEntries) as HTMLElement[];
}

// Fallback: Return the [data-hpc] container itself
log(`Fallback: returning [data-hpc] as single container`);
return [hpcContainer as HTMLElement];
}

// Fallback: find data-tagsearch-path and traverse up to find container
const pathElements = document.querySelectorAll("[data-tagsearch-path]");
log(`Fallback: found ${pathElements.length} path elements`);
Expand Down Expand Up @@ -123,20 +155,50 @@ export function getFilePath(container: HTMLElement): string {
return fileLink.getAttribute("title") || fileLink.textContent?.trim() || "";
}

// React UI: Look for file name in header with CSS module class
// Classes look like: DiffFileHeader-module__file-name--xxxxx
const allElements = container.querySelectorAll("*");
for (const el of allElements) {
const className = el.className;
if (typeof className === "string" && className.includes("file-name")) {
const text = el.textContent?.trim();
// Filter out navigation characters and ensure we have a valid file name
if (text && text.length > 0 && !text.includes("…") && text.includes(".")) {
// Clean up any special unicode characters GitHub uses for RTL/LTR marks
const cleanPath = text.replace(/[\u200E\u200F\u202A-\u202E]/g, "").trim();
if (cleanPath) {
log(`Found file path via React UI: ${cleanPath}`);
return cleanPath;
// React UI: For table containers, look for the file path in parent/sibling structure
// The file header with the path is often a sibling or in an ancestor's child
let current: HTMLElement | null = container;
for (let depth = 0; depth < 10 && current; depth++) {
const parent = current.parentElement;
if (!parent) break;

// Check if parent or any sibling has the path
const pathInParent = parent.querySelector("[data-tagsearch-path]");
if (pathInParent) {
// Make sure this path element is associated with our container
// by checking if the path element's container (going up) matches our container's parent
const path = pathInParent.getAttribute("data-tagsearch-path") || "";

// Find the closest common ancestor between pathInParent and container
// If pathInParent is within the same file block, use it
let pathAncestor: HTMLElement | null = pathInParent as HTMLElement;
for (let i = 0; i < 10 && pathAncestor; i++) {
if (pathAncestor === parent) {
return path;
}
pathAncestor = pathAncestor.parentElement;
}
}

current = parent;
}

// React UI: Look for file name in elements with "file-name" in class
// Search in container and parents
const searchContexts = [container, container.parentElement, container.parentElement?.parentElement].filter(Boolean) as HTMLElement[];
for (const ctx of searchContexts) {
const allElements = ctx.querySelectorAll("*");
for (const el of allElements) {
const className = el.className;
if (typeof className === "string" && className.includes("file-name")) {
const text = el.textContent?.trim();
// Filter out navigation characters and ensure we have a valid file name
if (text && text.length > 0 && !text.includes("…") && text.includes(".")) {
// Clean up any special unicode characters GitHub uses for RTL/LTR marks
const cleanPath = text.replace(/[\u200E\u200F\u202A-\u202E]/g, "").trim();
if (cleanPath) {
return cleanPath;
}
}
}
}
Expand Down
17 changes: 17 additions & 0 deletions packages/chrome/src/lib/mock-analytics.ts
Original file line number Diff line number Diff line change
Expand Up @@ -113,6 +113,23 @@ async function setCachedAnalytics(owner: string, repo: string, data: AnalyticsDa
}
}

/**
* Clear cached analytics for a repository
*/
export async function clearAnalyticsCache(owner: string, repo: string): Promise<void> {
const cacheKey = `analytics_${owner}_${repo}`;

// Clear memory cache
memoryCache.delete(cacheKey);

// Clear storage cache
try {
await chrome.storage.local.remove(cacheKey);
} catch {
// Storage access failed, memory cache still cleared
}
}

/**
* Mock analytics data for UI development
*/
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": "0.2.6",
"version": "0.2.7",
"description": "CLI to track AI-generated vs human-written code",
"license": "Apache-2.0",
"repository": {
Expand Down
Loading