Skip to content
Open
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
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
node_modules/.claude/settings.local.json
node_modules
mcp/dist
extension/dist
.DS_Store
4 changes: 4 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,10 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an

Agentation captures class names, selectors, and element positions so AI agents can `grep` for the exact code you're referring to. Instead of describing "the blue button in the sidebar," you give the agent `.sidebar > button.primary` and your feedback.

## Chrome Extension

Use Agentation on any localhost page without adding it to your project. See [extension/README.md](./extension/README.md) for install instructions.

## Requirements

- React 18+
Expand Down
2 changes: 2 additions & 0 deletions extension/.gitignore
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
node_modules/
dist/
16 changes: 16 additions & 0 deletions extension/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
# Chrome Extension

Use Agentation on any localhost page without adding it to your project.

## Install from release

Download `agentation-extension.zip` from [Releases](https://github.com/benjitaylor/agentation/releases), unzip, then:
`chrome://extensions` → Developer mode → Load unpacked → select the folder.

## Build from source

```bash
pnpm extension:build
```

Then load `extension/` as an unpacked extension.
111 changes: 111 additions & 0 deletions extension/build.mjs
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
import * as esbuild from "esbuild";
import * as sass from "sass";
import postcss from "postcss";
import postcssModules from "postcss-modules";
import * as path from "path";
import * as fs from "fs";
import { fileURLToPath } from "url";

const __dirname = path.dirname(fileURLToPath(import.meta.url));
const watch = process.argv.includes("--watch");

const pkg = JSON.parse(
fs.readFileSync(path.resolve(__dirname, "../package/package.json"), "utf-8")
);
const VERSION = pkg.version;

// SCSS CSS Modules plugin — mirrors package/tsup.config.ts exactly
function scssModulesPlugin() {
return {
name: "scss-modules",
setup(build) {
build.onLoad({ filter: /\.scss$/ }, async (args) => {
const isModule = args.path.includes(".module.");
const parentDir = path.basename(path.dirname(args.path));
const baseName = path.basename(
args.path,
isModule ? ".module.scss" : ".scss"
);
const styleId = `${parentDir}-${baseName}`;

const result = sass.compile(args.path);
let css = result.css;

if (isModule) {
let classNames = {};
const postcssResult = await postcss([
postcssModules({
getJSON(cssFileName, json) {
classNames = json;
},
generateScopedName: "[name]__[local]___[hash:base64:5]",
}),
]).process(css, { from: args.path });
css = postcssResult.css;

return {
contents: `
const css = ${JSON.stringify(css)};
const classNames = ${JSON.stringify(classNames)};
if (typeof document !== 'undefined') {
let style = document.getElementById('feedback-tool-styles-${styleId}');
if (!style) {
style = document.createElement('style');
style.id = 'feedback-tool-styles-${styleId}';
style.textContent = css;
document.head.appendChild(style);
}
}
export default classNames;
`,
loader: "js",
};
} else {
return {
contents: `
const css = ${JSON.stringify(css)};
if (typeof document !== 'undefined') {
let style = document.getElementById('feedback-tool-styles-${styleId}');
if (!style) {
style = document.createElement('style');
style.id = 'feedback-tool-styles-${styleId}';
style.textContent = css;
document.head.appendChild(style);
}
}
export default {};
`,
loader: "js",
};
}
});
},
};
}

const ctx = await esbuild.context({
entryPoints: [path.resolve(__dirname, "src/content.tsx")],
bundle: true,
outfile: path.resolve(__dirname, "dist/content.js"),
format: "iife",
target: "chrome120",
jsx: "automatic",
jsxImportSource: "react",
plugins: [scssModulesPlugin()],
alias: {
agentation: path.resolve(__dirname, "../package/src/index.ts"),
},
define: {
"process.env.NODE_ENV": '"production"',
__VERSION__: JSON.stringify(VERSION),
},
});

if (watch) {
await ctx.watch();
console.log("Watching for changes...");
} else {
await ctx.rebuild();
await ctx.dispose();
console.log("Built extension/dist/content.js");
}
Binary file added extension/icons/icon-128.png
Copy link
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It'd be nice to have a sticker version of this (where the white background is the shape of the bunny) and the rest is transparent, instead of the white rounded rectangle

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/icons/icon-16.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added extension/icons/icon-48.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
31 changes: 31 additions & 0 deletions extension/manifest.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
{
"manifest_version": 3,
"name": "Agentation",
"description": "Visual feedback tool for AI coding agents. Annotate elements, add notes, copy structured output.",
"version": "0.1.0",
"permissions": ["storage", "activeTab", "tabs"],
"content_scripts": [
{
"matches": [
"http://localhost/*",
"http://127.0.0.1/*",
"https://localhost/*"
],
"js": ["dist/content.js"],
"run_at": "document_idle"
}
],
"action": {
"default_popup": "popup.html",
"default_icon": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
},
"icons": {
"16": "icons/icon-16.png",
"48": "icons/icon-48.png",
"128": "icons/icon-128.png"
}
}
24 changes: 24 additions & 0 deletions extension/package.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
{
"name": "agentation-extension",
"version": "0.1.0",
"private": true,
"type": "module",
"scripts": {
"build": "node build.mjs",
"watch": "node build.mjs --watch",
"zip": "zip -r agentation-extension.zip manifest.json popup.html popup.js icons/ dist/content.js"
},
"dependencies": {
"agentation": "workspace:*"
},
"devDependencies": {
"@types/react": "^18.2.0",
"@types/react-dom": "^18.2.0",
"esbuild": "^0.27.0",
"postcss": "^8.5.6",
"postcss-modules": "^6.0.1",
"react": "^18.2.0",
"react-dom": "^18.2.0",
"sass": "^1.97.2"
}
}
67 changes: 67 additions & 0 deletions extension/popup.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,67 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8" />
<style>
* { margin: 0; padding: 0; box-sizing: border-box; }
body {
width: 260px;
padding: 16px;
font-family: system-ui, -apple-system, sans-serif;
background: #fff;
color: #111;
/* outer_radius = inner_radius (8px) + padding (16px) = 24px */
border-radius: 24px;
}
h1 {
font-size: 14px;
font-weight: 600;
margin-bottom: 4px;
}
p {
font-size: 12px;
color: rgba(0,0,0,0.5);
line-height: 1.5;
margin-bottom: 12px;
}
.status {
display: flex;
align-items: center;
gap: 6px;
font-size: 12px;
padding: 8px 10px;
background: rgba(0,0,0,0.03);
border-radius: 8px;
margin-bottom: 6px;
}
.status:last-of-type { margin-bottom: 0; }
.dot {
width: 8px;
height: 8px;
border-radius: 50%;
flex-shrink: 0;
}
.dot.active { background: #34c759; }
.dot.inactive { background: rgba(0,0,0,0.15); }
.label { color: rgba(0,0,0,0.5); }
.value { color: #111; margin-left: auto; }
</style>
</head>
<body>
<h1>Agentation</h1>
<p>Visual feedback for AI coding agents</p>

<div class="status" id="toolbar-status">
<span class="dot inactive" id="toolbar-dot"></span>
<span class="label">Toolbar</span>
<span class="value" id="toolbar-value">Checking...</span>
</div>
<div class="status" id="mcp-status">
<span class="dot inactive" id="mcp-dot"></span>
<span class="label">MCP Server</span>
<span class="value" id="mcp-value">Checking...</span>
</div>

<script src="popup.js"></script>
</body>
</html>
45 changes: 45 additions & 0 deletions extension/popup.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
const toolbarDot = document.getElementById("toolbar-dot");
const toolbarValue = document.getElementById("toolbar-value");
const mcpStatus = document.getElementById("mcp-status");
const mcpDot = document.getElementById("mcp-dot");
const mcpValue = document.getElementById("mcp-value");

// Check if the toolbar is active on the current tab by matching
// against the content script patterns from manifest.json
const CONTENT_SCRIPT_PATTERNS = [
/^http:\/\/localhost(:\d+)?\//,
/^http:\/\/127\.0\.0\.1(:\d+)?\//,
/^https:\/\/localhost(:\d+)?\//,
];

chrome.tabs.query({ active: true, currentWindow: true }, (tabs) => {
const url = tabs[0]?.url || "";
const isActive = CONTENT_SCRIPT_PATTERNS.some((p) => p.test(url));

if (isActive) {
toolbarDot.className = "dot active";
toolbarValue.textContent = "Active";
checkMcpHealth();
} else {
toolbarDot.className = "dot inactive";
toolbarValue.textContent = "Inactive";
mcpStatus.style.display = "none";
}
});

function checkMcpHealth() {
fetch("http://localhost:4747/health")
.then((res) => {
if (res.ok) {
mcpDot.className = "dot active";
mcpValue.textContent = "Connected";
} else {
mcpDot.className = "dot inactive";
mcpValue.textContent = "Not responding";
}
})
.catch(() => {
mcpDot.className = "dot inactive";
mcpValue.textContent = "Not running";
});
}
42 changes: 42 additions & 0 deletions extension/src/content.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import React from "react";
import ReactDOM from "react-dom/client";
import { Agentation } from "agentation";

const MCP_DEFAULT_ENDPOINT = "http://localhost:4747";

function AgentationExtension() {
const [endpoint, setEndpoint] = React.useState<string | undefined>(undefined);

React.useEffect(() => {
fetch(`${MCP_DEFAULT_ENDPOINT}/health`)
.then((res) => {
if (res.ok) {
setEndpoint(MCP_DEFAULT_ENDPOINT);
}
})
.catch(() => {
// MCP server not available — run in local-only mode
});
}, []);

return <Agentation endpoint={endpoint} />;
}

function mount() {
const container = document.createElement("div");
container.id = "agentation-extension-root";
document.body.appendChild(container);

const root = ReactDOM.createRoot(container);
root.render(
<React.StrictMode>
<AgentationExtension />
</React.StrictMode>
);
}

if (document.body) {
mount();
} else {
document.addEventListener("DOMContentLoaded", mount);
}
5 changes: 4 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,10 @@
"mcp": "pnpm --filter agentation-mcp start",
"publish:agentation": "pnpm --filter agentation publish --access public",
"publish:mcp": "pnpm --filter agentation-mcp publish --access public",
"publish:all": "pnpm publish:agentation && pnpm publish:mcp"
"publish:all": "pnpm publish:agentation && pnpm publish:mcp",
"extension:build": "pnpm --filter agentation-extension build",
"extension:watch": "pnpm --filter agentation-extension watch",
"extension:zip": "pnpm --filter agentation-extension zip"
},
"pnpm": {
"onlyBuiltDependencies": [
Expand Down
Loading