agent-source-locator
v0.3.2
Published
Alt+click any element in your browser and its source location is typed straight into the terminal running your AI agent.
Maintainers
Readme
agent-source-locator
English · 简体中文
在浏览器里 Alt+点击一个元素,它的源码位置会直接打进正在运行 AI Agent 的那个终端的输入行里。不用复制粘贴,不需要 MCP,没有工具栏。
❯ src/pages/Home.tsx:42:7 src/components/Header.tsx:10:3 ▊点三个元素,切到终端,接着敲「把这几处都换成新的间距 token」,回车。光标本来就停在那些位置后面。
为什么不用现成的工具
- LocatorJS 这类工具的目标是跳转到编辑器,不是喂给 Agent。而且它们的零接入模式依赖 React 的
_debugSource,React 19 已经把它移除了,官方至今没有替代方案。 - stagewise、Domscribe、Frontman 确实是面向 AI Agent 的,但它们清一色走 MCP 或 IDE 扩展,并且都要附带一个工具栏、一个聊天框或一个常驻进程。
这个工具只做一件事,而这件事恰好是上面那些都不做的:把位置放到你终端的输入行上。
支持范围
| 构建工具 | React | Vue | 接入成本 | |---|---|---|---| | Vite | ✅ | ✅ | 一行 | | webpack | ✅ | — | 一行 | | rspack | ✅ | — | 一行 | | Next.js (Turbopack) | ✅ | — | 三处,见下文 |
React 端的注入与构建工具无关(基于 oxc-parser,不需要 Babel —— @vitejs/plugin-react-swc 和 builtin:swc-loader 都能配合)。Vue 端仅支持 Vite:它的 transform 必须跑在 @vitejs/plugin-vue 之前,而其它构建工具里没有对应的东西。
安装
让 Agent 帮你装
接入方式随构建工具而异,最后还有一步必须在你自己的终端里执行——这恰好是那个 已经跑在这个终端里的 Agent 最适合干的活。把下面这段贴给 Claude Code、Codex 或任何 能执行命令的 Agent CLI:
在这个项目里接入 agent-source-locator —— 先执行 `npx agent-source-locator@latest prompt`,
然后按它打印出来的指南操作。它打印的是 prompts/install.md:识别技术栈、安装依赖、接入
构建工具、应用不在仓库根目录时设置 pathPrefix、绑定终端、验证。指南随包一起发布,
所以它永远和正在安装的版本对得上。
如果你的 Agent 不能执行命令但能联网,就直接把原文喂给它:
按照 https://raw.githubusercontent.com/Hexi1997/agent-source-locator/main/prompts/install.md
在这个项目里接入 agent-source-locator整个流程里唯一可能自己失败的是绑定终端这一步——Agent 继承的就是你的终端环境,所以
agent-source-locator attach 能成功的地方它就能成功,不能成功的地方它会把原因
告诉你(见支持的终端)。
或者手动装
npm i -D agent-source-locator
# pnpm add -D agent-source-locator
# yarn add -D agent-source-locator然后按你用的构建工具选一节。所有接入都只在开发模式生效,不影响生产构建。
// vite.config.ts
import { defineConfig } from 'vite'
import react from '@vitejs/plugin-react' // 或 @vitejs/plugin-react-swc
import { agentSourceLocator } from 'agent-source-locator'
export default defineConfig({
plugins: [agentSourceLocator(), react()],
})顺序无所谓——插件是 enforce: 'pre',永远先于 React 的编译器拿到源码。Babel 版和 SWC 版都能用,因为注入完全不碰你的 JSX 编译链路。
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { agentSourceLocator } from 'agent-source-locator'
export default defineConfig({
plugins: [agentSourceLocator(), vue()],
})需要 Vue 3 SFC。@vue/compiler-sfc 是懒加载的,纯 React 项目不会为它付出任何代价。
构建工具报出的路径是相对它自己的 root 的,而 monorepo 里那就是子包目录。所以 apps/web/src/pages/Home.vue 这个文件产出的是:
src/pages/Home.vue:116:13你的 Agent 从仓库根出发根本找不到它。声明一个前缀即可:
// vite.config.ts
agentSourceLocator({ pathPrefix: '@web' })// webpack.config.js / rspack.config.js
new AgentSourceLocatorPlugin({ pathPrefix: '@web' })// next.config.ts
withAgentSourceLocator(nextConfig, { pathPrefix: '@web' })@web/src/pages/Home.vue:116:13前缀是自由格式的——写成什么能让你的 Agent 找到文件,就写成什么。
只有当前缀以字母或数字结尾(也就是看起来像个目录名)时才会插入 / 分隔符。这样单独一个 @ 就能直接粘在路径前面——这一点很关键,因为 Agent CLI 会把 @some/path 当作文件引用来解析,而不是普通文本:
| pathPrefix | 结果 | 适用场景 |
|---|---|---|
| apps/web | apps/web/src/App.vue:12:3 | monorepo,纯路径 |
| @web | @web/src/App.vue:12:3 | monorepo,作为文件引用 |
| @ | @src/App.vue:12:3 | 单应用,作为文件引用 |
| @/ | @/src/App.vue:12:3 | 你就是想要那个分隔符 |
端点会拒绝任何含
..的路径,所以../web这样的前缀会让每次点击都静默地校验失败。配了这种前缀时插件会给出警告。
配置可以写在文件里而不是构建配置里,这样个人改自己的设置就不会动到团队共享的部分:
// asl.config.json —— 提交进仓库
{ "pathPrefix": "apps/web" }// asl.config.local.json —— 加进 gitignore
{ "terminal": false }terminal: false 会保留 overlay 和剪贴板,但永远不往终端写。此时 toast 只显示
copied to clipboard——因为那本来就是预期的全部结果,而不是什么东西失败了。
两个文件都是从项目根目录向上查找的,所以 monorepo 可以在仓库根放一份共享的
asl.config.json,而任意子包——或任意一个开发者——都能在更近的位置覆盖它。
优先级从高到低:
| 层级 | 例子 | 作用范围 |
|---|---|---|
| 环境变量 | ASL_TERMINAL=0 pnpm dev | 单次运行 |
| asl.config.local.json | { "terminal": false } | 你自己 |
| asl.config.json | { "pathPrefix": "apps/web" } | 整个项目 |
| 构建配置 | agentSourceLocator({ pathPrefix: 'apps/web' }) | 整个项目 |
| 内置默认值 | 终端写入开启 | — |
文件层高于构建配置是有意为之:如果写死在代码里的选项优先级更高,那么一个已提交的
vite.config.ts 就会让本地文件什么都覆盖不了——而那正是本地文件存在的全部意义。
可识别的键是 terminal、pathPrefix 和 port。写错的键会给出警告而不是被静默忽略,
文件格式损坏时会回退到默认值,而不是让 dev server 挂掉。
Next 那边,instrumentation.ts 通常只是一行 re-export,没地方传参数,所以请用配置文件
或环境变量——也可以直接调用:
import { register as start } from 'agent-source-locator/next/instrumentation'
export const register = () => start({ terminal: false })默认两个 transform 都开着,但各自只对认识的文件生效(.jsx/.tsx 与 .vue),所以单框架项目本来就没有额外开销。想显式指定也可以:
agentSourceLocator({ frameworks: ['react'] }) // 完全跳过 .vue// webpack.config.js(ESM)
import { AgentSourceLocatorPlugin } from 'agent-source-locator/webpack'
export default {
mode: 'development',
plugins: [new AgentSourceLocatorPlugin()],
devServer: { port: 3000 },
}// webpack.config.cjs(CommonJS)
const { AgentSourceLocatorPlugin } = require('agent-source-locator/webpack')
module.exports = {
mode: 'development',
plugins: [new AgentSourceLocatorPlugin()],
}插件会自己搞定全部三件事:把 loader 注册为 .jsx/.tsx 的 pre 规则、把 overlay 追加到每个 entry、把 /__asl/emit 挂到 devServer 上。
两个需要注意的点:
mode不能是production—— 是的话插件会直接返回。webpack serve默认是 development,但如果共用配置里写死了mode: 'production',插件会静默失效。- 插件会读
devServer.host。如果你把服务暴露到 localhost 之外,终端注入会自动关闭,只保留剪贴板。
与 webpack 完全一致——rspack 实现了同样的 plugin 和 dev-server 契约,只是引入路径不同:
// rspack.config.js
import { AgentSourceLocatorPlugin } from 'agent-source-locator/rspack'
export default {
mode: 'development',
plugins: [new AgentSourceLocatorPlugin()],
devServer: { port: 3000 },
}需要安装 @rspack/dev-server,端点才会被挂载。
Turbopack(Next 16 起的默认构建器)不支持 webpack plugin,但支持 webpack loader。插件本来一次做完的另外两件事,只能分别接到 Next 自己的钩子上——所以是三个文件而不是一个。
1. next.config.ts —— 通过 turbopack.rules 注册 loader,并把 /__asl/* 重写到 sidecar,使端点保持同源:
import type { NextConfig } from 'next'
import { withAgentSourceLocator } from 'agent-source-locator/next'
const nextConfig: NextConfig = {
// ...你的配置
}
export default withAgentSourceLocator(nextConfig)2. instrumentation.ts —— 放在项目根目录,与 next.config.ts 同级;如果项目用了 src 目录,则放在 src/ 下。不要放进 app/。register() 是唯一在 dev server 启动时执行 Node 代码的钩子,sidecar 就在这里启动:
export { register } from 'agent-source-locator/next/instrumentation'如果你已经有自己的 register,从里面调用我们的:
export async function register() {
await (await import('agent-source-locator/next/instrumentation')).register()
// ...你自己的初始化
}3. app/layout.tsx —— Turbopack 没有注入 script 标签的钩子,所以 overlay 要从组件树里引入。它不渲染任何内容:
import { AgentSourceLocator } from 'agent-source-locator/next/client'
export default function RootLayout({ children }: { children: React.ReactNode }) {
return (
<html lang="zh">
<body>
{children}
<AgentSourceLocator />
</body>
</html>
)
}补充说明:
- server component 和 client component 都能定位——loader 作用于所有
.tsx,所以服务端渲染出的标签同样带属性。 - sidecar 监听 127.0.0.1:7091。同时跑多个 Next 项目时,用
ASL_PORT指定一个空闲端口——next.config.ts和instrumentation.ts都会读它,一个变量就够。 - 这一节针对的是 Next 默认的 Turbopack。
next dev --webpack未经测试——那种情况下 loader 需要改从next.config的webpack()钩子注册,而不是turbopack.rules。
每个应用各自接入、各自声明前缀,不存在仓库级的统一配置:
// apps/web-vue/vite.config.ts
agentSourceLocator({ pathPrefix: 'apps/web-vue' })// apps/web-next/next.config.ts
withAgentSourceLocator(nextConfig, { pathPrefix: 'apps/web-next' })然后在仓库根目录 attach 一次就够:
cd <仓库根目录> && npx agent-source-locator attach绑定关系的查找会逐级向上冒泡,所以底下每个应用都会命中这一条记录,全部写进同一个终端。在两个应用之间来回点,结果会交替累积在同一行:
❯ apps/web-vue/src/App.vue:13:5 apps/web-next/app/Counter.tsx:10:7 apps/web-vue/src/App.vue:16:5各应用抵达终端的通道并不相同,这一点只在规划端口时才需要关心:
| 技术栈 | 通道 | 额外端口 | |---|---|---| | Vite | 挂在自己 dev server 上的 middleware | 无 | | webpack / rspack | 挂在自己 dev server 上的 middleware | 无 | | Next.js | sidecar + rewrite | 7091 |
所以任意多个 Vite/webpack 应用都能共存。但两个及以上的 Next 应用会争抢 7091。 后启动的那个仍会正常启动——它只是打印一条「端口被占用」的日志,然后在没有自己 sidecar 的情况下运行,也就是说它的点击会由第一个应用的 sidecar 处理。如果两个应用继承的是同一条仓库根 attach 记录,结果依然会落到正确的终端;但想让它们彼此独立,就给其中一个换个端口:
ASL_PORT=7092 next devnext.config.ts 和 instrumentation.ts 读的是同一个变量,设一次就同时管住 rewrite 目标和 sidecar。
同一个仓库的两份 checkout(git worktree,或者直接复制的目录)各跑一套 dev 时也会撞上同样的问题。各 worktree 的 dev 端口通常是按偏移整体平移的,但 7091 不会跟着走,于是第二份会静默借用第一份的 sidecar,点击落进另一份 checkout 对应的终端。现在抢不到端口时,插件会指名道姓:
[agent-source-locator] clicks from /repo-b will be typed into the terminal
attached to /repo-a. Set ASL_PORT to a free port to keep them separate.给每个 worktree 配一个跟它 dev 端口配套的 ASL_PORT 即可。
残留的 sidecar
sidecar 跑在 Next dev server 进程内部,所以 Ctrl+C 会连它一起带走。但关掉终端窗口、或者杀掉启动器进程时,next-server 会继续存活——Next 不会随父进程退出,而幸存下来的那个进程同时占着 dev 端口和 7091。
这种情况下 sidecar 会察觉自己已被 init 收养,并在约 30 秒内让出端口,好让下一个 dev server 起自己的:
[agent-source-locator] parent process is gone; releasing the sidecar port
so a new dev server can start its own.dev server 本身是故意不动的——要不要杀掉它不该由这个工具决定。如果某个残留实例挡了路,Next 会直接告诉你它的 pid(Another next dev server is already running… PID: 12345),或者:
lsof -ti:7091 # 看谁占着 sidecar 端口一个应用以 iframe 嵌入另一个
可以用,除了两个应用各自接入之外不需要任何额外配置:
- iframe 是独立的 document,有自己的 overlay,所以在它内部的悬浮和点击都由它自己处理。父页面的 overlay 看不到 iframe 里的元素,反之亦然——不会出现双重高亮。
- iframe 内发出的请求打到被嵌入应用自己的 origin(
localhost:5599/__asl/emit,而不是父页面的),所以同源校验和 Next 的 rewrite 都照常工作。 - 按住 Alt 时,无论焦点在哪个 document 上都能生效——overlay 是从鼠标事件里读这个修饰键的,而不是追踪键盘状态,正是因为没有焦点的 iframe 收不到任何键盘事件。
父页面这边需要加一件事,前提是你希望 iframe 内的剪贴板兜底也能用:
<iframe src="http://localhost:5599" allow="clipboard-write"></iframe>不加的话,浏览器会拒绝跨域 iframe 的剪贴板写入(端口不同即为不同 origin)。终端注入不受影响——那条链路根本不经过剪贴板。
绑定终端
在运行 Agent 的那个终端里,进入项目目录执行:
npx agent-source-locator attach然后在浏览器里按住 Alt/Option,点任意元素。
| 命令 | 作用 |
|---|---|
| agent-source-locator attach | 把当前项目指向你执行该命令的这个终端 session |
| agent-source-locator status | 查看已绑定的 session 以及它是否还活着 |
| agent-source-locator test | 写入一条示例位置,用来验证链路是否打通 |
| agent-source-locator detach | 解除当前项目的绑定 |
| agent-source-locator prompt | 打印一份给 Agent 照着做的接入指南 |
绑定关系按项目目录存放在 ~/.config/agent-source-locator/sessions.json,查找时会逐级向上冒泡——在仓库根目录 attach 一次,里面所有子应用都能用。重启 dev server 不会丢失绑定;关掉终端窗口会。
选中父级
左键点击命中的是最近的那个带位置的元素,多数时候这正是你要的——但不总是。 你能用鼠标指到的往往是叶子:卡片里的一行文字、按钮里的一个 span;而你想交给 Agent 的通常是包着它的那个组件。
Alt+右键会弹出一个候选列表,从上到下依次是该元素本身、以及所有包着它的 带位置元素:
┌────────────────────────────────────────────┐
│ <span> src/components/Card.tsx:21:9 click│
│ <div> src/components/Card.tsx:14:3 │
│ <li> src/pages/Home.tsx:48:7 │
│ <ul> src/pages/Home.tsx:44:5 │
└────────────────────────────────────────────┘鼠标划过某一行,页面上就会高亮对应的元素——当几个候选的路径看起来差不多时,
这是唯一可靠的分辨方式。点某一行即发送该位置。标了 click 的那行就是直接
Alt+左键会发出去的那个,所以这个菜单是简单手势的超集,而不是另一套模式。
这条链一直通到 <body>,所以菜单只显示最近的 5 层——再往上基本都是没人
想指的布局容器。
按 Esc、滚动页面、或在别处点一下都会关掉它。不按 Alt 的右键完全 不受影响,浏览器自己的菜单和页面自己的菜单都照常工作。
下面「已知限制」里那条也由此得到补救:组件吞掉 props 时,点击会落到它最近的 带标记的祖先上,而菜单里会列出它上面的其余各层。
支持的终端
绑定的含义是:另一个进程要能往某个特定的 session 里打字。具备这个能力的 终端非常少,下面这张表就是由此而来——所以在怀疑哪里坏了之前,先看看你的终端在哪一栏:
| 终端 | 能否绑定 | 方式 |
|---|---|---|
| tmux | ✅ 内置支持 | send-keys -l,任意平台,含 WSL |
| iTerm2 | ✅ 内置支持 | AppleScript write text … newline NO(macOS) |
| kitty | ⚙️ 手动配置 | 有 kitty @ send-text,配成 command sink 即可 |
| WezTerm | ⚙️ 手动配置 | 有 wezterm cli send-text,同理 |
| VS Code / Cursor | ❌ | 不对外部进程开放接口——见下文 |
| Terminal.app | ❌ | do script 是执行命令,无法只输入不回车 |
| Warp、Ghostty、Alacritty | ❌ | 没有远程控制接口 |
| Windows Terminal | ❌ | 无法定位到某个具体 session |
✅ 表示 agent-source-locator attach 会自动检测到;⚙️ 表示终端本身能做到,但需要你自己配;
❌ 表示从外部根本无法实现。
如果你的终端是 ❌
在里面跑一层 tmux,这是通用答案,只多一条命令:
tmux # 在 Cursor、Terminal.app、Windows Terminal…… 里都行
agent-source-locator attach # 在 tmux 里绑定之后所有文本都会打进那个 tmux pane——也就是你在编辑器里正看着的那个。
编辑器是这里最常见的情况。VS Code 和 Cursor 确实提供了往终端注入文本的能力,但只开放给自己的扩展,不对外部进程开放,也没有任何开关能改变这一点。
如果你的终端是 ⚙️
手动配一条 command sink,位置文本通过 stdin 传入,所以任何能触达你终端的命令都行:
// ~/.config/agent-source-locator/sessions.json
{
"version": 1,
"sessions": {
"/path/to/your/project": {
"type": "command",
"command": ["kitty", "@", "send-text", "--match", "title:my-agent", "--stdin"],
"attachedAt": "2026-01-01T00:00:00.000Z"
}
}
}这两个是根据它们各自文档里的 CLI 写的,并未在此实测,所以具体参数请当作起点而不是定论。
绑定失败时怎么查
agent-source-locator attach 会说明它为什么绑不上——是终端本身不支持,还是单纯没继承到环境变量
(子进程里 $TERM_PROGRAM 为空是另一回事,修法也完全不同)。如果 dev 脚本只报一句
Command failed,手动跑一次 agent-source-locator attach:脚本经常把 stderr 吞掉,而解释就在那里。
以上都不影响剪贴板——无论有没有绑定,位置始终会写入剪贴板。
Windows
agent-source-locator attach 在 Windows 上找不到可绑定的目标,但其余部分都是跨平台的:注入、overlay、
端点、剪贴板都照常工作,产出的路径也会统一规范化成正斜杠。有两条路能把终端写入拿回来:
- WSL + tmux —— tmux sink 与平台无关
- 自定义
commandsink —— 同上,位置文本通过 stdin 传入
一个注意点:残留 sidecar 的回收依赖「进程被 init 收养」这一 POSIX 语义,在 Windows 上 不生效,但也无害。
工作原理
开发模式下,你写的每个元素都会被注入 data-asl="path:line:column" 属性。React 端用 oxc-parser 拿 offset、用 magic-string 插入——只解析、不做 transform,所以与后续由谁来编译 JSX 无关。Vue 端用 @vue/compiler-sfc 做同样的事,并且跑在 @vitejs/plugin-vue 之前,这样后者缓存的 descriptor 已经是注入过的。
两边产出完全相同的属性,所以运行时不需要区分框架。
运行时 overlay 会把点击解析到最近一个带上述属性的祖先元素——所以即便你点的是第三方组件内部的 DOM,落点仍然是你自己写的那一行。Alt+右键则把这条祖先链整个列出来供你挑选。随后它把位置写入剪贴板,并 POST 给 dev server,由后者打进已绑定的终端 session。
所有逻辑都只在开发模式生效,生产构建产物里不含任何相关代码。
安全性
这个端点会往一个活着的终端里打字,所以它被当作真实攻击面对待:
- 文本内容不由浏览器提供。 浏览器只发送
{file, line, column},服务端校验之后自行拼接字符串。 - 所有 Unicode 控制字符一律拒绝。 payload 里的
\n会直接提交那一行输入,而 ESC 可以驱动终端转义序列。 - 非
POST拒绝,超过 4 KB 的请求体拒绝,含..或超过 512 字符的路径拒绝。 - Sink 全部走
execFile,不经过 shell。iTerm2 的脚本通过argv传值,绝不做字符串拼接。 - 如果 dev server 绑定到了 localhost 之外,终端注入会自动禁用,只保留剪贴板。
Origin 校验在不同接入方式下略有差异。 Vite/webpack/rspack 下浏览器直连 dev server,所以要求 Origin 与 Host 完全相等。Next 下请求经由 rewrite 代理,Host 会被替换成 sidecar 自己的地址,完全相等已不可能——因此 sidecar 改为要求 Origin 必须是 loopback 地址,并且只监听 127.0.0.1。真正的跨站页面依然会被拒绝;残留的缺口是另一个本地 dev server 上的页面有可能访问到它。
已知限制
- Vue 仅支持 Vite,因为 SFC 的编译发生在
@vitejs/plugin-vue里。 - 点击一个「吞掉 props」(没有把 props 透传到 DOM 节点)的组件时,高亮的会是它最近的带标记祖先。这是预期中的降级行为,不是 bug——而且 Alt+右键会把它上面的各层都列出来。
- Vue 模板里的
<template>和<slot>会被跳过——它们本身不渲染成 DOM 元素,属性放上去也会消失。 oxc-parser依赖原生二进制产物;没有预编译包的冷门平台无法使用。
许可
MIT
