Imported from Zenty-Me/PromptPilot (
AGENTS.md). Install upstream withnpx skills add Zenty-Me/PromptPilot. Copyright stays with the author.
AGENTS.md — PromptPilot
Chrome extension (Manifest V3), vanilla JS. One-way injects prompts into web-AI chat boxes.
Build & verification
- No build step and no bundler. The extension runs directly from source. Load it unpacked via
chrome://extensions(enable Developer mode) → "Load unpacked". - No linter or CI. Run
npm testbefore declaring work done — it runs nine suites plus the legacy drag check:tests/shortcut.test.js— shortcut parse/match/format/validate + URL normalization (pure logic,vm-loaded).tests/consistency.test.js— static cross-file contract check (DOM ids,data-*producers/consumers, message types, cross-module exports, manifest vsCONTENT_SCRIPT_FILES,hidden类的 CSS 兜底).tests/shortcuts-dom.test.js—content/shortcuts.jsdispatch behaviour driven by real jsdomKeyboardEvents.tests/inject-dom.test.js— custom-site matching,enabledfiltering, selector precedence, replace/append 注入模式.tests/detector-dom.test.js—shared/detector.jsselector generation / input & send detection, pluscontent/inject.jswriting detected selectors back to storage.tests/site-match.test.js—PromptUtils.matchSiteForUrl/patternMatchesUrl(最具体优先、enabled 过滤、端口忽略).tests/popup-dom.test.js— popup 交互:用真实 jsdom + 桩chromeAPI 加载整套popup/*.js,覆盖标签筛选、追加注入、注入失败自愈、未授权站点授权。要加新的 popup 交互测试就放这里,别再造一套 harness。tests/panel-ball.test.js—content/panel.js悬浮球定位:right/bottom ↔ left/top 换算、越界夹取、窗口 resize 后重新夹取。这是唯一真正加载content/panel.js的测试,也因此守住了panel-styles.js必须先于panel.js的加载顺序。dragtest.js— jsdom check of the Draggabilly drag stack.
node --check <file>still catches syntax errors, but is not sufficient on its own.- After editing any source file, reload the extension at
chrome://extensionsfor changes to take effect.
Dependencies are vendored, not from npm
- Third-party libs live as pre-minified files in
lib/(e.g.draggabilly.js,purify.min.js,lucide.min.js).node_modules/is gitignored and not used at runtime. package.jsonlists deps only for reference/dev convenience — do not assume annpm installwires them into the extension. To add a library, vendor the file intolib/and reference it inmanifest.json.- Manifest
content_scripts[].jslist is the load order and it matters:ev-emitter.js,get-size.js,unidragger.jsmust precededraggabilly.js;nanoid.js/purify.min.jsmust precedeshared/utils.js(which callsnanoidandDOMPurify).
Architecture (manifests are source of truth)
manifest.jsonis authoritative for supported sites and content-script wiring — the README's platform table andlib/file list are stale; trust the manifest.background.jsis a Service Worker with two jobs: forward the browser-levelopen-prompt-panelcommand to the active tab (toggle_panel), and keep dynamically registered content scripts in sync withcustomSites(see below). ItimportScripts("shared/utils.js", "shared/defaults.js").content/inject.jsholds the per-site input/send Selector maps (AI platform DOM hooks).content/panel.jsis the in-page floating picker.content/shortcuts.jsis the in-page configurable-shortcut dispatcher.popup/is the full management UI (also reused as the Side Panel viaside_panel.default_path).popup/storage.jsowns allchrome.storage.localaccess;shared/defaults.jsseeds preset data.shared/utils.jsprovides template parsing ({{var}}/{{var:default}}),escapeHtml/generateId, shortcut helpers, and match-pattern helpers.
Custom sites (adding a web AI) — how it actually works
manifest.jsoncontent_scripts[].matchescovers only the built-in sites. User-added sites get scripts viachrome.scripting.registerContentScriptsat runtime, keyed by IDpp-site-<siteId>(CONTENT_SCRIPT_ID_PREFIX).optional_host_permissionsishttp://*/*+https://*/*. The popup requests the per-site origin (it has the user gesture); the background never prompts —syncRegisteredScripts()silently skips unauthorized sites. Changing this split will break permission granting.- URL normalization lives in one place (
PromptUtils.normalizeMatchPattern/isValidMatchPattern/originOfPattern). background and popup must both call it, otherwise "popup granted permission but background thinks the pattern is invalid". - Newly registered scripts load on the next page load/refresh; there is no way to retro-inject into an already-open tab.
storage.onChangedoncustomSitestriggers a debounced sync, so plain storage writes from the popup are enough to register/unregister.
智能添加站点(只填网址)
- 用户只填 URL:
PromptUtils.deriveSitePattern推导匹配规则(域名 + 首段路径 +/*,深层会话 ID 会被丢掉),PromptUtils.deriveSiteName从域名推导名称。两者都在shared/utils.js,并有tests/shortcut.test.js覆盖。 shared/detector.js负责页面 DOM 探测(输入框打分挑优、发送按钮语义识别、生成稳定 CSS Selector)。它有两个调用方,因此不能依赖 PromptUtils / PromptInjector:popup 用chrome.scripting.executeScript({files:["shared/detector.js"]})单独注入它,内容脚本则通过 manifest /CONTENT_SCRIPT_FILES加载。改这个文件要同时兼顾两条路。- 保存时若网址正开着活动标签页,popup 当场在该页探测;否则先存
inputSelector: "",由content/inject.js在用户访问该站点时补全(SPA 输入框常晚于document_idle,所以按 0.8/2.5/5 秒重试几次)。 - 已经手写过
inputSelector的站点不会被自动识别覆盖,pendingDetectSite()是这条保护的唯一判据。
Configurable shortcuts — two independent layers
- In-page (fully configurable):
PromptDefaults.SHORTCUT_METAdefines actions; bindings live instorage.shortcutsas canonical lowercase strings (ctrl+shift+p).content/shortcuts.jsmatches them and dispatches toPromptPanel. Adding an action requires editing bothSHORTCUT_METAand therunActiondispatch (the consistency test enforces this). - Browser-level:
manifest.commandscannot be rewritten at runtime — the UI showschrome.commands.getAll()and links tochrome://extensions/shortcuts. background mirrors active global combos intostorage.globalShortcuts;content/shortcuts.jsignores any combo in that list so a shared combo isn't handled twice (which would toggle the panel open then straight back closed). - Combos are canonicalized through
PromptUtils.serializeShortcut(order: ctrl → alt → shift → meta → key). Never compare raw strings. - Key matching uses
event.code(KeyP,Digit1) rather thanevent.key, soShift+P/Shift+1don't degrade intoP/!.
Styling
- Two stylesheets, one palette.
popup/popup.cssuses:roottokens;content/panel-styles.jsre-declares the same values scoped under#prompt-injector-panel(it is injected into third-party pages and cannot use:root). They are copies, sotests/theme.test.jsasserts the pairs match. If you change a colour, change it in both. - Design language: neutral graphite greys with a single indigo accent (
--accent). The accent marks state (selected, focused, primary action) — never decoration. Deliberately avoided: gradients, glow/aura box-shadows, pulsing animations, and "hover turns everything accent-coloured". - Scale is fixed: radii
3/5/8/10px, font sizes10/11/12/13/14px(never below 10px), three shadow tiers (--sh-sm/md/lg). Use the tokens, not raw values. - Every rule in
panel-styles.jsneeds!important(host page CSS is unpredictable and may load later), and every selector must bepp-/pi-prefixed so the host page isn't polluted. content/panel-styles.jsmust load beforecontent/panel.js— panel.js is an IIFE that readsPP_BALL_SIZEat the top level. The jsdom tests stubPromptPaneland won't catch an ordering regression; the consistency test does.- Text symbols (
☰,⊟,▼,×) are gone — all iconography is Lucide, so new UI must use<i data-lucide="...">and callrefreshIcons()after injecting markup.
Conventions / gotchas
- Template variables use double-brace syntax
{{name:default}};fillTemplatefalls back to the default (or leaves the raw token) when no value is supplied. - Every failure path needs an exit, not just a toast. 注入失败 / 未授权站点这类终态必须给用户可点的下一步(见下方「失败自愈」)。新功能的验收标准包含"失败之后怎么走"。
- All persistence is
chrome.storage.local(survives browser restart), initialized fromPromptDefaultson first run. - Icons draw from local Lucide (no network/emoji) — keep new UI consistent.
- Custom sites let users override input/send Selectors. Resolution order is custom site → built-in hostname map → generic
contenteditable/textareadetection (custom sites win over built-ins). When several custom patterns match, the longestpatternwins;enabled === falsesites are skipped, and a missingenabledfield is treated as enabled. CONTENT_SCRIPT_FILESinshared/defaults.jsis the single source for content-script load order — manifestcontent_scripts, background dynamic registration, and the popup'sexecuteScriptfallback all reference it. Never hand-roll the list.injectPromptToPage(text, shouldSubmit, options)acceptsoptions.modeof"replace"(default) or"append"。append 走已有内容之后追加(textarea 用\n连接),入口有两个:popup 右键菜单「追加到现有内容」,以及面板里 Alt + 点击条目;模板填值注入会沿用发起时选定的 mode(pendingInjectMode/currentPanelTemplateMode)。- 悬浮球位置落盘的是
right/bottom(距视口右下角的距离),球体坐标只由它推导。改动位置时必须经applyBallPos(它把最终 left/top 夹进视口再反推 right/bottom),并且ballPos内存副本要在拖拽/吸附后同步(persistBallPos)。窗口 resize 会重新夹取 —— 少了这一步,用户把窗口拖窄后球会永久停在视口外点不到。 - Content-script UI strings are Chinese; keep new user-facing copy in the same voice.
失败自愈(failure → next step)
- popup:
doInject失败 →offerRepair()用PromptUtils.matchSiteForUrl找到当前页面命中的站点(没有就用deriveSitePattern推导一条),弹出行动条#action-bar(showActionBar(text, btnLabel, handler)/hideActionBar(),在列表上方,不止贴合一个场景,也能给"Site added, reload?"用)。点按钮 → 申请权限 →detectFromActiveTab→ 写回站点 → 自动重试刚才那次注入(lastInjectState)。 - 面板:
injectFromPanel统一收口。失败时先用页面里已经加载的PromptDetector就地重识别并写 storage,等 storage 事件回流(setTimeout80ms)再重试一次;仍失败才收尾提示。 - 未授权:站点列表每项都有默认隐藏的
[data-site-grant]按钮,markUnauthorizedSites()同时管未授权标记和这个按钮的显隐。授权成功后suggestTabReload()找到正在访问该站点的标签页问一句要不要刷新(内容脚本只在页面加载时注入)。 - popup 既作为 action popup 也作为侧边栏运行:
isPopupWindow()判断是否该window.close()—— 侧边栏里不关,避免同一份代码两种行为。
Styling 陷阱
.hidden是通用工具类(popup/popup.css),不是每个组件各写一条的历史遗留方式。组件级的.x.hidden曾经漏过:#import-file、站点"未授权"标记都因此藏不住。tests/consistency.test.js守着这条底线。- 拼接 HTML 字符串时,三元表达式后面还要继续拼接就必须加括号。render.js 曾因为漏括号把"未授权标记 + 编辑/删除按钮"整段算进了 else 分支 —— 配好选择器的站点会丢掉全部操作按钮。这种 bug 只能靠 jsdom 测试暴露,
node --check查不出来。