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
3 changes: 2 additions & 1 deletion .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
.DS_Store
.DS_Store
.pnpm-store
1 change: 1 addition & 0 deletions package/.gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -32,3 +32,4 @@ tsup.config.bundled_*.mjs
# npm pack output
*.tgz
BRANCH_NOTES.md
.pnpm-store
33 changes: 31 additions & 2 deletions package/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,20 @@
npm install agentation -D
```

## Script Tag / Browser

Load the self-contained browser bundle directly from jsDelivr:

```html
<script src="https://cdn.jsdelivr.net/npm/agentation@2.2.1/dist/agentation.browser.min.js"></script>
<script>
window.Agentation.mount();
</script>
```

This bundle includes its own React runtime, so there is no separate `react` or
`react-dom` dependency for script-tag usage.

## Usage

```tsx
Expand All @@ -28,6 +42,20 @@ function App() {

The toolbar appears in the bottom-right corner. Click to activate, then click any element to annotate it.

### Browser Module

If you want an imperative browser API without using a script tag:

```ts
import { mountAgentation } from "agentation/browser";

const agentation = mountAgentation();

agentation.update({ copyToClipboard: false });
// ...
agentation.destroy();
```

## Features

- **Click to annotate** – Click any element with automatic selector identification
Expand All @@ -38,7 +66,7 @@ The toolbar appears in the bottom-right corner. Click to activate, then click an
- **Structured output** – Copy markdown with selectors, positions, and context
- **Programmatic access** – Callback prop for direct integration with tools
- **Dark/light mode** – Toggle in settings, persists to localStorage
- **Zero dependencies** – Pure CSS animations, no runtime libraries
- **Self-contained browser bundle** – Script-tag usage includes the runtime it needs

## Props

Expand Down Expand Up @@ -121,7 +149,8 @@ Agentation captures class names, selectors, and element positions so AI agents c

## Requirements

- React 18+
- React 18+ for the React component entrypoint
- No React install required for the self-contained browser bundle
- Desktop browser (mobile not supported)

## Docs
Expand Down
4 changes: 2 additions & 2 deletions package/example/src/app/faq/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ const faqCategories: FAQCategory[] = [
},
{
question: "How do I install it?",
answer: "Install via npm with <code>npm install agentation -D</code>, then import and add the <code>&lt;Agentation /&gt;</code> component to your app. Works with React 18 and Next.js."
answer: "For React apps, install via npm with <code>npm install agentation -D</code>, then import and add the <code>&lt;Agentation /&gt;</code> component to your app. If you just want a browser toolbar on a plain HTML page, load <code>https://cdn.jsdelivr.net/npm/agentation@2.2.1/dist/agentation.browser.min.js</code> and call <code>window.Agentation.mount()</code>."
},
{
question: "Is there a Claude Code integration?",
Expand Down Expand Up @@ -94,7 +94,7 @@ const faqCategories: FAQCategory[] = [
items: [
{
question: "Is there a React dependency?",
answer: "Yes, Agentation requires React 18+ as a peer dependency. It's built as a React component to integrate seamlessly with modern React applications."
answer: "The default React component entrypoint requires React 18+ as a peer dependency. The script-tag browser bundle is self-contained, so if you load <code>dist/agentation.browser.min.js</code> from jsDelivr you do not need to install <code>react</code> or <code>react-dom</code> separately."
},
{
question: "Does it work with TypeScript?",
Expand Down
51 changes: 48 additions & 3 deletions package/example/src/app/install/page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -196,6 +196,9 @@ function CopyablePackageManager({ name, command }: { name: string; command: stri
}

export default function InstallPage() {
const browserBundleUrl =
"https://cdn.jsdelivr.net/npm/agentation@2.2.1/dist/agentation.browser.min.js";

return (
<>
<article className="article">
Expand Down Expand Up @@ -255,6 +258,45 @@ function App() {
/>
</section>

<section>
<h2>Use without React</h2>
<p>
If you just want the toolbar on a plain HTML page, CMS, or non-React app,
load the self-contained browser bundle from jsDelivr and call{" "}
<code>window.Agentation.mount()</code>.
</p>
<CodeBlock
code={`<script src="${browserBundleUrl}"></script>
<script>
window.Agentation.mount();
</script>`}
language="html"
/>
<p
style={{
fontSize: "0.875rem",
color: "rgba(0,0,0,0.5)",
marginTop: "0.5rem",
}}
>
The script bundle includes its own React runtime. No separate{" "}
<code>react</code> or <code>react-dom</code> install is required.
</p>
<p style={{ marginTop: "1rem" }}>
If you want the same browser-style lifecycle from code, import the
browser entrypoint:
</p>
<CodeBlock
code={`import { mountAgentation } from "agentation/browser";

const agentation = mountAgentation();

agentation.update({ copyToClipboard: false });
agentation.destroy();`}
language="tsx"
/>
</section>

<section>
<h2>Claude Code</h2>
<p>
Expand Down Expand Up @@ -375,7 +417,10 @@ function App() {
<h2>Requirements</h2>
<ul>
<li>
<strong>React 18+</strong> &mdash; Uses modern React features
<strong>React 18+</strong> &mdash; Required for the React component entrypoint
</li>
<li>
<strong>Script tag option</strong> &mdash; The browser bundle is self-contained and does not need a React install
</li>
<li>
<strong>Client-side only</strong> &mdash; Requires DOM access
Expand All @@ -385,8 +430,8 @@ function App() {
devices
</li>
<li>
<strong>Zero dependencies</strong> &mdash; No runtime deps beyond
React
<strong>Self-contained bundle</strong> &mdash; CDN usage ships with
its own runtime
</li>
</ul>
</section>
Expand Down
11 changes: 11 additions & 0 deletions package/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,17 @@
"types": "./dist/index.d.ts",
"default": "./dist/index.js"
}
},
"./browser": {
"types": "./dist/browser.d.ts",
"import": {
"types": "./dist/browser.d.mts",
"default": "./dist/browser.mjs"
},
"require": {
"types": "./dist/browser.d.ts",
"default": "./dist/browser.js"
}
}
},
"files": [
Expand Down
101 changes: 101 additions & 0 deletions package/src/browser.test.tsx
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
import { beforeEach, describe, expect, it, vi } from "vitest";

type RootLike = {
render: ReturnType<typeof vi.fn>;
unmount: ReturnType<typeof vi.fn>;
};

const rootMocks: RootLike[] = [];

const createRootMock = vi.fn((container: Element) => {
const root = {
container,
render: vi.fn(),
unmount: vi.fn(),
};
rootMocks.push(root);
return root;
});

vi.mock("react-dom/client", () => ({
createRoot: createRootMock,
}));

vi.mock("./components/page-toolbar-css", () => ({
PageFeedbackToolbarCSS: (props: Record<string, unknown>) => (
<div data-testid="mock-toolbar">{JSON.stringify(props)}</div>
),
}));

beforeEach(() => {
createRootMock.mockClear();
rootMocks.length = 0;
document.getElementById("agentation-browser-root")?.remove();
delete window.Agentation;
vi.resetModules();
});

describe("browser entrypoint", () => {
it("registers the global API with the current version", async () => {
await import("./browser");

expect(window.Agentation).toBeDefined();
expect(window.Agentation?.mount).toBeTypeOf("function");
expect(window.Agentation?.version).toBe("test");
});

it("mounts a single root, reuses it across updates, and tears it down cleanly", async () => {
const { mountAgentation } = await import("./browser");

const handle = mountAgentation({ copyToClipboard: false });

expect(createRootMock).toHaveBeenCalledTimes(1);
expect(document.querySelectorAll("#agentation-browser-root")).toHaveLength(1);
expect(rootMocks[0]?.render).toHaveBeenCalledTimes(1);

const secondHandle = mountAgentation({ copyToClipboard: true });
expect(secondHandle).toBe(handle);
expect(createRootMock).toHaveBeenCalledTimes(1);
expect(rootMocks[0]?.render).toHaveBeenCalledTimes(2);

handle.update({ webhookUrl: "https://example.test/hook" });
expect(rootMocks[0]?.render).toHaveBeenCalledTimes(3);

handle.destroy();
expect(rootMocks[0]?.unmount).toHaveBeenCalledTimes(1);
expect(document.getElementById("agentation-browser-root")).toBeNull();

const remountedHandle = mountAgentation();
expect(remountedHandle).not.toBe(handle);
expect(createRootMock).toHaveBeenCalledTimes(2);
});

it("throws a clear error outside the browser", async () => {
const { mountAgentation } = await import("./browser");

const documentDescriptor = Object.getOwnPropertyDescriptor(globalThis, "document");
const windowDescriptor = Object.getOwnPropertyDescriptor(globalThis, "window");

Object.defineProperty(globalThis, "document", {
configurable: true,
value: undefined,
});
Object.defineProperty(globalThis, "window", {
configurable: true,
value: undefined,
});

try {
expect(() => mountAgentation()).toThrow(
"Agentation browser entrypoint requires a DOM environment.",
);
} finally {
if (documentDescriptor) {
Object.defineProperty(globalThis, "document", documentDescriptor);
}
if (windowDescriptor) {
Object.defineProperty(globalThis, "window", windowDescriptor);
}
}
});
});
Loading