Skip to content

write-ai-prompt-better

项目概述

write-ai-prompt-better 是一个 VS Code 扩展,核心功能是帮开发者在编辑器里快速收集上下文(文件路径、代码片段、终端输出),组装成结构化的 Markdown Prompt,一键复制给 AI 编程助手(Claude Code、GitHub Copilot、Cursor、Cline、Windsurf 等)。

技术栈:

层次技术
语言TypeScript(strict 模式)
运行时VS Code Extension Host (Node.js)
UI原生 HTML + CSS + Vanilla JS(内联嵌入到 Provider 单文件)
构建tsc 直接编译为 CommonJS,无打包器
存储vscode.ExtensionContext.globalState(4 个持久化键)
安全CSP nonce 保护 Webview 脚本
通信postMessage 双向消息协议(16 种 Webview 消息 + 6 种 Extension 消息)
i18n自定义模块,EN / zh-CN 双语完整覆盖(130+ 翻译键)

结构化 Prompt 格式

扩展的核心输出是一个四段式 Markdown Prompt,每一段有明确的语义边界。

构建函数位于 WriteBetterPromptProvider.ts:700-740

ts
function buildPrompt(){
  var parts = [];
  // 1. Skills — 引用的 AI Skill 文件路径
  if (state.selectedSkills.length > 0){
    var lines = [];
    for (var i=0;i<state.selectedSkills.length;i++){
      lines.push('- ' + BT + state.selectedSkills[i].path + BT);
    }
    parts.push(MSG.promptSkillHeader + NL + NL + lines.join(NL));
  }
  // 2. Background — 上下文(文件 / 终端 / 笔记)
  if (state.contextItems.length > 0){
    var body = [MSG.promptBackgroundIntro];
    for (var j=0;j<state.contextItems.length;j++){
      var item = state.contextItems[j];
      if (item.type === 'file'){
        body.push('📄 ' + BT + item.filePath + BT + (item.lineRange ? '#' + item.lineRange : ''));
      } else if (item.type === 'terminal'){
        body.push('💻 Terminal output:\n' + FENCE + '\n' + item.content + '\n' + FENCE);
      } else if (item.type === 'manual'){
        body.push('📝 ' + item.content);
      }
    }
    parts.push(MSG.promptBackgroundHeader + NL + NL + body.join(NL));
  }
  // 3. Requirements — 需求描述
  var req = $('req-textarea').value.trim();
  if (req) parts.push(MSG.promptRequirementHeader + NL + NL + req);
  // 4. Validation — 验证方法
  var val = $('val-textarea').value.trim();
  if (val) parts.push(MSG.promptValidationHeader + NL + NL + val);
  return parts.join(NL + NL);
}

关键设计:文件类型上下文在侧边栏卡片中显示完整内容供人工审查,但生成的 Prompt 只输出文件路径引用——AI 工具自行读取文件内容。这避免了把整个代码库塞进 prompt 造成的 token 浪费。只有终端输出的文本内容会原样嵌入 prompt。


架构设计

整体架构

核心模式:集中状态 + 多视图同步

Provider 在 Extension Host 进程中维护一份 _contextItems: ContextItem[] 作为唯一状态源。每当命令处理器添加新的上下文条目,Provider 通过 _broadcast() 将最新状态推送给所有打开的 Webview(侧边栏 + 独立编辑器窗口)。多视图之间实时同步,无需手动刷新。

消息协议

types.ts:56-81 定义了 16 种 Webview→Extension 消息和 6 种 Extension→Webview 消息:

方向主要消息类型
Webview → Extensionready / copyToClipboard / saveHistory / deleteHistoryItem / savePresets / getSkills / removeContextItem / addManualItem / changeLanguage
Extension → WebviewsyncContextItems / historyData / presetsData / skillsData / clearAll

上下文收集机制

扩展注册了 4 条右键菜单命令,覆盖了开发者日常与 AI 交互时最需要的上下文来源。

编辑器选中内容 — addFileContent

extension.ts:75-101 — 读取 editor.selection 的文本内容,附带文件绝对路径和行号范围(如 L12-20)。同时将代码片段复制到剪贴板(用于即时粘贴):

ts
const text = doc.getText(selection);
const lineRange = startLine === endLine ? 'L' + startLine : 'L' + startLine + '-' + endLine;
const item: ContextItem = {
  id: genId(),
  type: 'file',
  content: text,
  filePath,
  lineRange,
  timestamp: Date.now(),
};
void vscode.env.clipboard.writeText(text);  // 顺便写入剪贴板
provider.addContextItem(item);

完整文件 — addFileToContext

extension.ts:103-124 — 通过 vscode.workspace.fs.readFile() 读取文件全部内容,适用于在标签页右键添加整个文件。

终端输出 — addTerminalContent(巧妙的 workaround)

extension.ts:126-144 — VS Code API 无法直接读取终端选中文本,所以扩展采用了一个变通方案:

ts
// 1. 执行系统命令:复制终端选中内容到剪贴板
await vscode.commands.executeCommand('workbench.action.terminal.copySelection');
// 2. 等待 150ms 让系统完成剪贴板写入
await delay(150);
// 3. 从剪贴板读回
const text = await vscode.env.clipboard.readText();

这是一个经典的「API 限制下的迂回策略」——借道剪贴板实现了 VS Code API 不直接暴露的能力。

资源管理器文件/目录引用 — addPathToContext

extension.ts:146-167 — 通过 vscode.workspace.fs.stat() 判断是文件还是目录,分别创建 fileReffolder 类型的上下文条目。这两类条目不携带内容,只提供路径引用供 AI 工具自行读取。

上下文类型总览

type来源内容
file编辑器选中 / 标签页右键完整代码文本 + 行号范围
terminal终端右键选中终端输出文本(剪贴板 workaround)
manualWebview 内手动输入自由文本
folderExplorer 目录右键仅路径引用
fileRefExplorer 文件右键仅路径引用

Webview 内联 UI 的工程技巧

这个项目最特殊的工程决策是:整套 UI(HTML + CSS + JS)作为模板字符串内联在一个 TypeScript 文件的三个方法中。

为什么内联

VSCode Webview 扩展通常可以将 HTML/CSS/JS 拆分为独立文件,但本项目选择了单文件嵌入。好处是:

  • 零外部依赖、零构建步骤(只靠 tsc 编译)
  • 所有字符串可以直接访问 i18n 模块的翻译
  • 部署和分发极其轻量(单个 .vsix 只有几个 JS 文件)

转义约定

由于整个 JavaScript 代码嵌入在模板字符串中,反引号(backtick)和换行符会引发解析冲突。项目定义了全局简写来规避:

ts
// WriteBetterPromptProvider.ts 内部
const NL = String.fromCharCode(10);     // 换行符
const BT = String.fromCharCode(96);     // 反引号
const FENCE = BT + BT + BT;             // 三反引号代码块

对应地在 webview 中:

js
// 注入到 webview 的 JS
var NL = String.fromCharCode(10);
var BT = String.fromCharCode(96);
var FENCE = BT + BT + BT;

因此 buildPrompt() 中的代码 '📄 ' + BT + item.filePath + BT 实际生成的是 📄 `filePath`

国际化注入

i18n.ts:376-378getWebviewMessages() 将当前语言的完整翻译对象(72 个键)序列化注入到 webview 的 MSG 全局变量中:

ts
// Provider 生成 HTML 时注入
const messages = getWebviewMessages();
const MSG_JSON = JSON.stringify(messages);  // {"promptSkillHeader":"## Reference...", ...}

Webview 中的 JS 通过 MSG.promptSkillHeader 等直接访问翻译字符串,切换语言时 Extension Host 重新发送完整翻译对象。

语言切换

用户在 Webview UI 中切换 EN / zh-CN,发送 changeLanguage 消息到 Extension Host → Provider 调用 setLang(lang) 写入 globalState → 重新注入翻译对象并发送到所有视图。


AI Skill 自动发现

扩展能自动扫描工作区中的 AI 技能文件,并将其引用到 Prompt 的 Skills 段落中。

扫描策略

WriteBetterPromptProvider.ts:303-334 遍历 11 条路径,覆盖 5 种 AI 工具

ts
private async _scanSkills(force = false): Promise<SkillItem[]> {
  if (!force && this._skills.length > 0) return this._skills;
  const skills: SkillItem[] = [];
  const home = os.homedir();
  const workspaceRoot = vscode.workspace.workspaceFolders?.[0]?.uri.fsPath ?? '';

  // --- 全局级(用户主目录)---
  await this._scanSkillDirs(path.join(home, '.claude', 'skills'), L.claudeGlobal, skills);
  await this._readSingle(path.join(home, '.claude', 'CLAUDE.md'), L.claudeGlobal, 'CLAUDE.md', skills);
  await this._scanDir(path.join(home, '.cursor', 'rules'), '.mdc', L.cursorGlobal, skills);
  await this._readSingle(path.join(home, '.cursorrules'), L.cursorGlobal, '.cursorrules', skills);
  this._readCopilotInstructions(L.copilotGlobal, skills);

  // --- 项目级(工作区根目录)---
  if (workspaceRoot) {
    await this._scanSkillDirs(path.join(workspaceRoot, '.claude', 'skills'), L.claudeProject, skills);
    await this._scanDir(path.join(workspaceRoot, '.cursor', 'rules'), '.mdc', L.cursorProject, skills);
    await this._readSingle(path.join(workspaceRoot, '.cursorrules'), L.cursorProject, '.cursorrules', skills);
    await this._readSingle(path.join(workspaceRoot, '.clinerules'), L.clineProject, '.clinerules', skills);
    await this._readSingle(path.join(workspaceRoot, '.windsurfrules'), L.windsurfProject, '.windsurfrules', skills);
    await this._readSingle(path.join(workspaceRoot, '.github', 'copilot-instructions.md'), L.copilotProject, 'copilot-instructions.md', skills);
  }

  this._skills = skills;
  return skills;
}

三类扫描器

方法用途示例
_scanSkillDirs()读取子目录中的 SKILL.mdClaude Code ~/.claude/skills/<name>/SKILL.md
_scanDir()读取指定扩展名的文件Cursor ~/.cursor/rules/*.mdc
_readSingle()读取单个固定路径文件.cursorrules.clinerules.windsurfrules

YAML Frontmatter 解析

每个 skill 文件可能包含 YAML frontmatter(--- 分隔)。Provider 的 _parseFrontmatter() 方法提取 namedescription 字段用于 UI 展示,同时保留完整 content 供 prompt 引用。

工具分组

扫描结果按工具和层级分组,通过 i18n.ts:400-413getSkillAgentLabels() 生成中文标签:

Agent 标签示例路径
Claude Code · 全局~/.claude/skills/*/SKILL.md
Cursor · 项目.cursor/rules/*.mdc
Cline · 项目.clinerules
Windsurf · 项目.windsurfrules
Copilot · 项目.github/copilot-instructions.md

模糊搜索

Webview 中的 skill 下拉框实现了字符级别的模糊匹配(fuzzyMatch),用户输入时不需要完整拼写即可快速筛选技能文件。


预设系统

快速填充 Requirement 和 Validation 文本框的短语模板。

三级优先级

README.md:259-261 定义了三层预设优先级:

  1. UI 管理预设globalStatewbp.presets / wbp.validationPresets)——最高优先级,用户通过 ⚙ 面板自行添加/编辑
  2. settings.json 预设(VS Code 配置 writeBetterPrompt.presets)——仅作为首次安装时的种子数据
  3. 内置默认预设(由 i18n.ts:381-397 按当前语言生成)——兜底默认值

内置预设列表

i18n.ts:381-391 定义了 6 个内置需求预设:

ts
export function getDefaultPresets() {
  const m = getWebviewMessages();
  return [
    { label: m.presetAdjustStyleLabel, value: m.presetAdjustStyleValue },       // 调整样式/UI
    { label: m.presetTroubleshootLabel, value: m.presetTroubleshootValue },     // 排查 bug
    { label: m.presetImplementFeatureLabel, value: m.presetImplementFeatureValue }, // 实现功能
    { label: m.presetRefineFeatureLabel, value: m.presetRefineFeatureValue },   // 优化功能
    { label: m.presetCodeReviewLabel, value: m.presetCodeReviewValue },         // 代码审查
    { label: m.presetAddCommentsLabel, value: m.presetAddCommentsValue },       // 添加注释
  ];
}

验证方法有一个内置预设:「运行 npm run build 无错误通过」

预设管理面板

用户点击 ⚙ 按钮打开预设管理弹窗——可以添加、编辑、删除需求预设和验证预设。所有修改通过 savePresets / saveValidationPresets 消息回写 Extension Host,持久化到 globalState


历史记录与状态恢复

每次生成 Prompt 时自动保存一份快照,支持无限历史、预览、恢复、删除。

数据结构

types.ts:37-47 定义 HistoryItem

ts
export interface HistoryItem {
  id: string;
  prompt: string;              // 完整 Markdown Prompt
  preview: string;             // 前 ~100 字符(去除 Markdown 标题)
  timestamp: number;
  contextItems: ContextItem[]; // 生成此 prompt 时的上下文快照
}

核心设计:Context Snapshot

每条历史记录不仅保存最终 Prompt 文本,还保存当时所有 ContextItem 的快照。这使得「恢复」功能可以精确重建当时的所有上下文卡片——包括文件路径、代码内容、行号、终端输出。

跨语言恢复

恢复时不仅解析当前语言的 Requirements / Validation 段落,还会尝试用另一种语言的段落标题做 fallback(promptRequirementHeaderAlt / promptValidationHeaderAlt),确保切换语言后的历史记录也能被正确还原。

预览

makePreview() 方法(WriteBetterPromptProvider.ts:742-749)去除 Markdown 标题行和空行,截取前约 100 个字符作为列表预览文本——保持历史列表的简洁可读。


总结

write-ai-prompt-better 是一个「用最朴素的技术栈解决最高频的开发痛点」的典型案例:不依赖 React/Vue/任何打包器,4 个源文件,单一 Provider 类承载 1209 行的完整 UI 逻辑,却能高效地把「跟 AI 描述当前开发任务」这件事从手工低效操作变成右键点击的机械动作。

几个值得借鉴的设计点:

  • 集中状态 + 多视图广播:一份 _contextItems 数组驱动侧边栏和独立窗口的实时同步,干净利落
  • 终端剪贴板 workaround:借道剪贴板实现 VS Code API 不暴露的能力,150ms 延迟换取功能完整性
  • 模板字符串内联 UI:反向工程中的约束美学——为了零依赖和 i18n 直接访问,接受了 var / NL / BT 的编码约定
  • YAML Frontmatter 解析:统一了对 Claude Code / Cursor / Cline / Windsurf 技能文件的发现和元数据提取
  • Context Snapshot 历史:不只是保存文本,而是保存完整的可恢复上下文状态
  • 三级预设优先级:UI 管理 > settings.json > 内置默认,兼顾可配置性和开箱即用

站点:https://write-ai-prompt-better.tangzixuan.cc/
源码:github.com/tangzixuan/write-ai-prompt-better