# Pindrop.js
> Zero-dependency TypeScript library that adds Figma-style comment pins to any web page. Agents can read the page, drop pinned comments on specific elements, and resolve them — no backend required.
## Install
```html
```
Or via npm: `npm install pindrop.js`
## Agent API
### Discover elements on the page
`getCommentableElements(): CommentableElement[]`
Returns semantic, visible elements with their selectors, labels, and bounding rects. Call this first to orient the agent before deciding where to comment.
```js
const elements = pindrop.getCommentableElements()
// [{ selector: '#hero', label: 'Ship feedback faster.', rect: { x, y, width, height } }, ...]
```
### Leave a comment by selector
`addComment({ selector, text, author?, meta? }): Comment | null`
Pins a comment to the element matching `selector`. Returns the created `Comment` (including its `id` for follow-up actions), or `null` if the element is not found.
```js
const comment = pindrop.addComment({
selector: '#submit-btn',
text: 'Button label is too vague — consider "Submit order" instead.',
author: 'Claude',
meta: { source: 'agent', model: 'claude-sonnet-4-6' },
})
```
### Leave a comment by viewport coordinates
`addComment({ x, y, text, author?, meta? }): Comment | null`
Alternative to selector-based targeting. `x` and `y` are viewport fractions (0–1). The library hits the element at that point and anchors to it; falls back to a viewport-only pin if no element is hit. Useful when reasoning from a screenshot.
```js
pindrop.addComment({ x: 0.7, y: 0.85, text: 'This chart looks misaligned', author: 'Agent' })
```
### Reply to a comment
`addReply({ commentId, text, author? }): Reply | null`
```js
const reply = pindrop.addReply({
commentId: comment.id,
text: 'Specifically the bar at index 3.',
author: 'Claude',
})
```
### Resolve a comment
`resolveComment(commentId, author?): Comment | null`
Returns the updated comment with `resolved: true`, `resolvedBy`, and `resolvedAt` set.
```js
pindrop.resolveComment(comment.id, 'Claude')
```
### Read all comments
`getComments(): Comment[]`
```js
const all = pindrop.getComments()
const open = all.filter(c => !c.resolved)
```
### Listen for events
`on(event, callback): () => void`
```js
pindrop.on('comment:add', (comment) => sendToSlack(comment))
pindrop.on('comment:resolve', (comment) => closeTicket(comment.id))
```
Available events: `comment:add`, `comment:delete`, `comment:resolve`, `comment:reopen`, `reply:add`, `anchor:lost`, `mode:change`, `import:complete`, `export:complete`.
## Comment shape
```ts
interface Comment {
id: string
anchor: { selector: string; offsetX: number; offsetY: number; viewportX: number; viewportY: number }
author: string
text: string
createdAt: string // ISO 8601
updatedAt: string
resolved: boolean
resolvedBy?: string
resolvedAt?: string
replies: Reply[]
meta?: { source?: 'agent' | 'human'; model?: string; [key: string]: unknown }
}
```
## Function calling tool definitions
Drop these into your agent's tool list to give it native Pindrop support:
```json
[
{
"name": "get_commentable_elements",
"description": "Returns a list of semantic, visible elements on the current page with their CSS selectors, labels, and positions. Call this first to understand the page layout before leaving comments.",
"parameters": { "type": "object", "properties": {}, "required": [] }
},
{
"name": "leave_comment",
"description": "Pin a review comment on a specific element on the page. Use selector from get_commentable_elements or provide viewport coordinates from a screenshot.",
"parameters": {
"type": "object",
"properties": {
"selector": { "type": "string", "description": "CSS selector of the target element. Omit if using x/y." },
"x": { "type": "number", "description": "Viewport X fraction (0–1). Use with y when targeting by coordinates." },
"y": { "type": "number", "description": "Viewport Y fraction (0–1). Use with x when targeting by coordinates." },
"text": { "type": "string", "description": "The comment text." },
"author": { "type": "string", "description": "Agent name shown on the comment pin." }
},
"required": ["text"]
}
},
{
"name": "resolve_comment",
"description": "Mark a comment as resolved after the issue has been addressed.",
"parameters": {
"type": "object",
"properties": {
"commentId": { "type": "string", "description": "The id from the Comment object returned by leave_comment." }
},
"required": ["commentId"]
}
}
]
```
## Typical agent workflow
```js
// 1. Discover what's on the page
const elements = pindrop.getCommentableElements()
// 2. Send elements + screenshot to LLM, get back structured feedback
const feedback = await llm.call({
prompt: 'Review this UI and return JSON comments: [{ selector, text }]',
context: elements,
screenshot: await screenshot(),
})
// 3. Pin each comment
for (const item of feedback) {
pindrop.addComment({ selector: item.selector, text: item.text, author: 'Agent', meta: { source: 'agent' } })
}
```
## Docs
Full documentation: https://pindropjs.com/docs
Configuration options: https://pindropjs.com/docs/configuration
Custom backend (sync to your API): https://pindropjs.com/docs/custom-backend