How Pinpoint recreates VS Code’s element-selection workflow for Codex, Claude Code, and other coding agents using the debugger, workspace files, and the clipboard
Press enter or click to view image in full size
Pinpoint in action: select an element in VS Code’s Integrated Browser, then paste its generated @.pinpoint/... reference into your coding agent.
Before going further, VS Code already has an element-selection feature in its Integrated Browser.
You can click an element on the rendered page and add its context directly to Copilot Chat.
VS Code’s native element picker. Highlight the element-selection button and the option showing that the context is added to Copilot Chat.
Pinpoint did not invent that interaction.
The gap I wanted to solve was portability. I use coding agents other than Copilot, and VS Code’s built-in workflow does not give me an obvious way to take the selected element context and pass it to Codex, Claude Code, or another AI chat.
Pinpoint recreates the element-selection workflow, captures the selected element’s context into a local Markdown file, and copies an @.pinpoint/... reference that can be pasted into whichever coding agent you use.
Press enter or click to view image in full size
Pinpoint’s picker inside the Integrated Browser, showing the highlighted element, selector, and dimensions.
The visible interaction is intentionally familiar.
The difference is what happens after the click.
Instead of sending the context directly into one specific chat integration, Pinpoint writes it into the workspace as a normal file. Codex, Claude Code, or another coding agent can then read it like any other project file.
Almost nothing about building it was that simple.
Why this problem still exists
If you build frontends with an AI coding agent, you have probably had some version of this conversation:
Me: The header looks broken on mobile.
Agent: searches for “header,” finds four components with similar names, edits the wrong one, and confidently fixes a problem you were not talking about.
The agent can read the entire repository, but it does not automatically know which rendered element I am pointing at.
From the developer’s perspective, the page makes the problem obvious. You can see the exact element, its position, its dimensions, the CSS rules affecting it, and which variable is producing the wrong color.
The coding agent sees source files and has to reconstruct all of that from code.
VS Code’s built-in Copilot workflow solves this when Copilot Chat is your destination. I wanted the same basic interaction while working with other agents.
My manual workaround was to inspect the element in DevTools, copy its outer HTML, copy the relevant styles, and paste everything into the chat.
It worked, but it flooded the prompt with a wall of text. By the third element, I usually stopped bothering.
Pinpoint captures the element’s selector, URL, outer HTML, dimensions, matched and inherited CSS, resolved values, and CSS variables.
It writes everything into a neatly named Markdown report inside the workspace and puts an @-mention of that file on the clipboard.
The idea was simple.
The picker exists, but the extension API does not
VS Code’s native feature proves that the Integrated Browser can support element selection.
However, third-party extensions are not given a public API for reusing that picker or retrieving its result.
From an extension author’s perspective, the Integrated Browser API surface is effectively empty.
There is no supported method to:
- read the page DOM
- inject a script
- listen for element selections
- retrieve the context generated by the native picker
- redirect that context from Copilot into another agent
Pinpoint therefore could not simply call VS Code’s existing feature and ask it for the selected element.
It needed its own capture path.
There is, however, one useful back door: the Integrated Browser tab is debuggable.
VS Code’s built-in JavaScript debugger can attach to it much like it attaches to Chrome. Once attached, a debug session can evaluate arbitrary JavaScript inside the page.
const started = await vscode.debug.startDebugging(undefined, {
type: 'editor-browser',
request: 'attach',
name: 'Pinpoint (Integrated Browser)',
urlFilter: '*',
timeout: 4000
});// Later, using the attached session:await session.customRequest('evaluate', {
expression: pickerSource,
context: 'repl'
});
Pinpoint’s “arm the picker” flow therefore looks like this:
- Attach the debugger.
- Evaluate the picker script inside the page.
- Confirm that the picker has started.
- Disconnect the debugger immediately.
The picker continues running inside the page after the debugger disconnects.
Keeping the debug session alive would leave VS Code’s floating debug toolbar hovering above the editor like a confused ghost, so Pinpoint attaches, injects, and detaches within a second or two.
There is plenty of fiddly detail hidden inside that sequence.
Pinpoint suppresses the debug toolbar and status bar, races stopDebugging against a timeout because it sometimes never settles on a blank tab, and retries evaluation until the page confirms that PINPOINT_ACTIVE exists.
Experimental features keep you humble.
Rebuilding the interaction with an isolated picker
Because VS Code’s native picker is not exposed to extensions, Pinpoint injects its own element-selection overlay into the page.
It behaves like a classic browser element picker:
- the cursor becomes a crosshair
- a highlight box follows the pointer
- a label displays the selector and dimensions
- clicking selects the element
- pressing Escape cancels the operation
Two implementation choices do most of the defensive work.
Closed shadow DOM
The overlay is mounted inside a div with:
all: initial;
pointer-events: none;
z-index: 2147483647;It also uses a closed shadow root.
That prevents the page’s own CSS from leaking into the picker interface and stops page scripts from easily modifying its internal elements.
Capture-phase event listeners
The picker listens for clicks at the window level using the capture phase:
window.addEventListener('click', onClick, true);When an element is selected, it also calls:
event.preventDefault();
event.stopPropagation();
event.stopImmediatePropagation();That means selecting a button does not actually press the button.
You can inspect a delete button without deleting anything, select a navigation link without leaving the page, and choose a form submit control without submitting the form.
Capturing CSS the way DevTools presents it
Capturing the HTML is straightforward:
element.outerHTMLPinpoint truncates extremely large elements at 60 KB so a single selection cannot produce an absurd report.
The CSS is where most of the useful context lives, but the goal is not to dump everything — only what helps an agent understand the element.
Pinpoint captures four layers.
Matched rules
Pinpoint walks through document.styleSheets, recurses into active media queries, and tests selectors using:
element.matches(rule.selectorText)Cross-origin stylesheets can throw when accessed, so each is wrapped in try/catch. If blocked, Pinpoint skips it.
Crucially, Pinpoint preserves authored declarations:
rule.style.cssTextThis keeps important context like:
color: var(--brand);instead of only showing computed values.
Inherited rules
Some visible styles come from ancestors.
Pinpoint walks up the DOM and collects inherited properties such as:
colorfont-familyfont-sizefont-weightline-height
This mirrors DevTools’ “Inherited from…” sections and helps agents trace where styles originate.
Resolved values
getComputedStyle() returns hundreds of properties, most of them noise.
Pinpoint filters aggressively by comparing the element against a pristine version in a hidden iframe. Only differences from browser defaults are kept.
It also recombines longhand properties into readable shorthands:
margin-top: 8px;
margin-right: 16px;
margin-bottom: 8px;
margin-left: 16px;becomes:
margin: 8px 16px;The result is far more readable and useful for agents.
CSS variables
Pinpoint detects var(...) usage and resolves those variables:
background: var(--surface-2);may produce:
--surface-2: #f6f8fa;This gives both the design token and its current value.
The final report resembles the DevTools Styles panel, but formatted as Markdown and accessible to the agent.
Getting the data out of the browser
At this point, Pinpoint had a new problem.
By the time the user clicks an element, the debugger has already disconnected.
The picker is now running independently inside the page, with no documented communication channel back to the extension host.
The Integrated Browser page cannot call extension APIs.
There is no exposed postMessage bridge.
There is no supported event channel.
There is, however, one resource both the page and the extension can access:
The clipboard.
So the clipboard became an IPC channel.
When the user selects an element, the injected picker serializes its payload and writes it to the clipboard with a marker prefix:
await navigator.clipboard.writeText(
`PINPOINT_CONTEXT:${JSON.stringify(payload)}`
);While the picker is armed, the extension polls the clipboard approximately every 300 milliseconds:
const text = await vscode.env.clipboard.readText();if (text.startsWith('PINPOINT_CONTEXT:')) {
const payload = JSON.parse(text.slice(MARKER.length));
const { mention } = writeContextFile(payload); await vscode.env.clipboard.writeText(`@${mention}`);
}
The extension detects the marker, parses the payload, writes the Markdown report, and replaces the raw clipboard data with a usable file mention.
The user never normally sees the serialized JSON. Within a fraction of a second, it becomes something like:
@.pinpoint/weather-summary.mdIs polling the clipboard as an IPC channel elegant?
No.
Is it surprisingly reliable, permissionless, and compatible with the otherwise isolated browser surface?
Yes.
It also degrades reasonably well. If the extension does not process the payload immediately, the data still exists on the clipboard rather than disappearing into an inaccessible internal channel.
Pinpoint also falls back to document.execCommand('copy') if the modern Clipboard API fails. If even that fails, it attempts to preserve at least the selected element’s selector.
The ugly channel that exists beats the clean channel that does not.
Why the result is a file
This is where Pinpoint differs most clearly from VS Code’s native Copilot workflow.
Instead of attaching the selected context directly to one specific chat, Pinpoint converts it into an ordinary workspace artifact.
Early versions copied the entire report into the chat input.
That was terrible.
A single element can generate several thousand tokens of HTML and CSS. Pasting all of it directly into the conversation buries the user’s actual request and consumes context before the agent has even started working.
The better approach is to write the report to a Markdown file and reference it.
Coding agents already understand file mentions such as:
@.pinpoint/header.mdThey can open the report when needed without forcing the entire contents into the visible prompt.
This also makes the workflow agent-neutral.
Pinpoint does not need a private integration with Codex, Claude Code, Copilot, or any other chat interface.
It only needs to create a file that the agent can read.
Why the file must live inside the workspace
My first implementation wrote reports to the operating system’s temporary directory.
The generated mentions looked correct, but agents frequently could not read them.
Coding agents are usually sandboxed to the active workspace. A file in the system temp directory may exist on the machine while remaining inaccessible to the agent.
Pinpoint therefore stores reports in:
.pinpoint/inside the workspace.
The folder receives its own .gitignore, preventing generated reports from polluting the repository.
Pinpoint also removes reports older than 24 hours so the folder does not accumulate indefinitely.
The reports are temporary context, not project documentation.
File naming matters
The original filenames were technically descriptive and practically unreadable.
An early capture might produce something like:
element-span.weather__summary.weather__summary__bottom-1784388238159.mdThat is unique, but unpleasant to read inside a chat input.
Pinpoint now takes the first useful ID or class from the selector and converts it into a compact filename:
weather-summary.mdCapturing the same element again creates:
weather-summary-2.mdOne oddly specific rule survived from testing: filenames cannot contain #.
Some agents interpret # inside a file mention as the beginning of a line-range reference:
@file.md#L10-L20A selector-derived filename containing # could therefore be parsed as a truncated path.
Every naming rule eventually becomes a scar with a story.
The feature I deleted: automatic chat attachment
For a while, Pinpoint attempted to do more.
It detected open coding-agent chats and pushed the generated report directly into them.
The extension had:
- commands for specific agents
- logic for detecting active chat tabs
- a menu for choosing the destination agent
- workarounds involving temporary preview editors
- bespoke integrations discovered by inspecting extension manifests
The demo looked great.
The maintenance story did not.
Chats docked in sidebars were often invisible to the normal tab API, so much of the code existed to guess what the editor could not reliably observe.
Each integration was also one upstream extension update away from silently breaking.
Meanwhile, the fallback path worked everywhere:
- Pinpoint copies the file mention.
- The user presses paste.
It worked with every agent, including agents Pinpoint had never heard of.
So I deleted the direct integrations, the agent-selection setting, and the gear menu.
Pinpoint now works with every AI chat precisely because it integrates directly with none of them.
The price of that portability is one keystroke.
What I took away from building it
Experimental surfaces reward reading, not searching
Almost none of the Integrated Browser behavior used by Pinpoint is documented for extension authors.
The editor-browser debug type came from reading VS Code’s manifests and bundled source.
When you build on undocumented surfaces, the manifest sometimes becomes your API reference.
Design every failure into something useful
If debugger attachment fails, Pinpoint shows a clear error and avoids leaving behind a zombie debug toolbar.
If the clipboard write fails, it attempts an older copy mechanism.
If the full payload cannot be copied, it tries to preserve at least the selector.
Users should rarely notice the fallback machinery.
That is the point.
A paste you can trust beats an integration you cannot
The automatic chat integration was the flashiest feature in the extension.
It was also the first feature worth deleting.
A universal workflow that requires one paste is better than a zero-paste workflow that breaks whenever another extension changes an internal command.
The existing feature helped clarify the product
VS Code’s native picker already demonstrates that selecting rendered elements is useful for coding agents.
Pinpoint’s contribution is not inventing the concept.
Its purpose is separating that workflow from a single chat destination.
The selected context becomes a portable file rather than an attachment controlled by one integration.
The result
VS Code’s native element picker is already a good solution when Copilot Chat is your destination.
Pinpoint exists for the cases where it is not.
It recreates the useful part of that workflow — the ability to point at the exact rendered element — but exports the result as a normal workspace file instead of sending it into a specific proprietary chat integration.
That portability costs one extra keystroke.
Select the element, paste the copied @.pinpoint/... reference into your agent, and ask it to make the change.
In exchange, the same workflow works with Codex, Claude Code, and any other tool capable of reading files from the workspace.
Pinpoint is free, open source, and available on the VS Code Marketplace.
Select an element, paste its context into your agent, and avoid another conversation about “the other header.”