agent-source-locator

Alt+click any element in your browser and its source location is typed straight into the terminal running your AI agent.
2,667 1
Install
npm install agent-source-locator
Latest Version:0.3.2
License:MIT
Last Updated:Jul 29, 2026
Links: GitHub  ·  npm
Maintainer: hexi1997

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 — React
// 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 — Vue
// 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 项目不会为它付出任何代价。

Monorepo —— pathPrefix

构建工具报出的路径是相对它自己的 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 })
Vite — 限定框架

默认两个 transform 都开着,但各自只对认识的文件生效(.jsx/.tsx 与 .vue),所以单框架项目本来就没有额外开销。想显式指定也可以:

agentSourceLocator({ frameworks: ['react'] })  // 完全跳过 .vue
webpack
// 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 之外,终端注入会自动关闭,只保留剪贴板。
rspack

与 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,端点才会被挂载。

Next.js(App Router)

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。
同一仓库里的多个前端(含 iframe 场景)

每个应用各自接入、各自声明前缀,不存在仓库级的统一配置:

// 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 dev

next.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 与平台无关
  • 自定义 command sink —— 同上,位置文本通过 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

Related Packages

@inspect-devtools/vite-vue

Vite source inspector for Vue applications.

2,451 1
vite-plugin-vue-inspector

Jump to local IDE source code when clicking Vue elements in the browser.

53,106,520 809
vite-plugin-vue-template-inspector

Vite/Webpack plugin: Alt+click Vue template elements to copy the source path or...

778 0
ahemui

Point at anything in your running app and your coding agent gets the exact file...

532 0

Version History

Version Released License
0.3.2 MIT
0.3.0 MIT
0.2.12 MIT
0.2.11 MIT
0.2.10 MIT