---
url: /wave-agent/specs/editor/code-completion.md
description: >-
  VS Code 扩展与 JetBrains 插件的行内代码补全（ghost text）：编辑停顿自动触发、Tab
  接受、补全专用模型与独立请求通道；含跨文件定义片段（两端各自采集）
---

# 功能规格说明：代码补全

**创建日期**：2026-10-15

## 概述

代码补全在编辑器光标处以行内灰字（ghost text）给出接下来的代码建议，用户按 Tab 接受。首期同时覆盖 VS Code 扩展与 JetBrains 插件两端，两端共享同一套由 CLI 侧（`wave --stdio` 子进程）提供的补全请求通道与模型配置；插件端只负责采集光标上下文、发起/取消请求，并把返回文本交给宿主平台的补全 API 渲染。

首期范围包含**行内补全**与**跨文件定义片段**，也不做本地模型推理。跨步骤的「下一步编辑」建议（next-edit）是同族的另一项能力，行为另成一份规格（见 [下一步编辑建议](next-edit-suggestion.md)）；两边共用的请求通道与宿主侧跳过条件由本规格定义。跨文件定义片段在 VS Code 与 JetBrains **两端都生效**，但采集路径不同：VS Code 侧用解析栈认出「该问哪些符号」、找定义交给宿主语言服务；JetBrains 侧不引入解析栈，改用平台自带的声明解析（见边界情况「跨文件定义片段的范围与预算」）。

补全与对话是两条**互不影响**的请求通道：补全不经过 agent 的完整对话轮次（无工具、无 system prompt、无 hooks），也不与对话共享既有的全局请求间隔限制（`aiService` 的 1 次/秒串行间隔）。补全请求的节流、取消与缓存全部由补全自身负责。

## 用户场景与测试 *（必填）*

### 用户故事：编辑停顿后自动出现行内建议（优先级：P1）

作为编码中的用户，我希望在编辑器里敲字停顿一下，光标后就自动出现灰色的续写建议，按 Tab 就能采纳，以便不必离开编辑器、也不必在对话框里描述意图。

**为什么是这个优先级**：这是补全功能的本体——没有「停顿即建议、Tab 即采纳」这条主链路，其余能力（配置、开关、自适应）都无处依附。插件端此前完全没有补全能力，因此本故事也是该能力的第一个可交付切片。

**独立测试**：在 VS Code 中打开一个 TypeScript 文件，在一行函数体中间敲入 `const user = ` 后停顿，验证光标后出现灰色建议文本；按 Tab 验证文本写入文档且光标落到建议末尾。在 JetBrains 中用同一个文件重复同样的操作，验证两端行为一致。

**验收场景**：

1. **假设** 已配置可用的模型且补全开关为开启，**当** 用户在编辑器中敲入字符后停顿（停顿时长由自适应防抖决定，基准 200ms、区间 100~1000ms），**则** 插件向 CLI 发起一次补全请求，并把返回的文本以行内灰字渲染在光标之后。
2. **假设** 光标后已渲染出建议，**当** 用户按 Tab 时，**则** 建议文本被插入文档、幽灵文本消失，且该次插入**不**进入撤销栈中与用户上一次输入合并的同一步（一次撤销即可撤销这次采纳）。
3. **假设** 光标后已渲染出建议，**当** 用户按 Esc 时，**则** 建议被忽略且本次不被再次请求，直到用户下一次输入或光标移动才可能重新请求。
4. **假设** 建议为多行，**当** 用户触发宿主原生的「接受下一个词」操作（VS Code 的 `Accept Next Word` 快捷键）时，**则** 只接受建议的第一个词或词片段，剩余部分仍以幽灵文本保留。JetBrains 平台是否支持按词部分接受**待验证**（见边界情况），不支持时该场景在 JetBrains 侧降级为「只有全量接受」。
5. **假设** 用户的建议已被接受，**当** 光标位于插入文本末尾且用户继续停顿，**则** 允许基于新上下文发起下一次补全请求。
6. **假设** 补全请求已发出但尚未返回，**当** 用户在文档中继续输入、移动光标、切换编辑器或切换标签页时，**则** 在途请求被取消（abort），其响应即使迟到也不得渲染。
7. **假设** 补全请求失败（网络错误、模型不可用、鉴权失效、超时），**当** 请求返回错误时，**则** 编辑器不显示任何建议、不弹出错误弹窗、不写入任何状态栏文案，仅静默跳过本次补全；连续失败不得使插件崩溃或阻塞用户编辑。
8. **假设** 模型返回空文本或纯空白，**当** 响应到达时，**则** 不渲染任何幽灵文本。
9. **假设** 模型返回的文本超过 10 行或超过 1000 字符，**当** 响应到达时，**则** 按上限截断后再渲染。

***

### 用户故事：跨文件定义片段进入补全上下文（优先级：P2）

作为在业务代码里写调用点的用户，我希望补全能看到我所调用符号在别的文件里的定义、本文件 import 进来的符号，以及光标所在的外层作用域，以便模型不必靠猜来续写参数列表与返回值。

**为什么是这个优先级**：这是本档改造的全部收益所在——只给文件内上下文时，评测中的补全准确率明显偏低（CrossCodeEval：StarCoder-15B 在 Python 上 code EM 8.82% → 检索到跨文件定义后 15.72%）。但缺失时补全本身仍然可用，按本规格既有的分级（P1 = 可用性前提、P2 = 质量与体验），它属于 P2。它同时是风险点：片段给得越多反而越差（同一评测里 top-5 到 top-20 的准确率单调衰减），所以必须有硬上限，不是「尽量多塞」。

**独立测试**：在一个含 `utils/format.ts`（导出 `formatDate`）的项目里，于另一文件的函数体中敲 `formatD` 并停顿，验证建议使用了 `formatDate` 的签名；再把 `formatDate` 改名后保存并重复，验证片段跟随更新（不回落到磁盘旧内容）。

**验收场景**：

1. **假设** 用户在 TS/TSX/JS/Python/Java 文件中，光标前是从其他文件 import 进来的符号，**当** 补全触发时，**则** 该符号的定义体被采集进上下文，整体位于光标前缀之前。
2. **假设** 光标位于某个方法或函数体内，**当** 补全触发时，**则** 外层作用域（所在函数的签名、所在类的声明）同在候选片段中。
3. **假设** 可采集到的定义片段超过上限（默认 3 段或超出字符预算），**当** 组装上下文时，**则** 按相关性保留靠前的若干段，且相关性最高的一段排在**最后**（离光标最近）。
4. **假设** 片段采集耗时超过 100ms，**当** 超时发生时，**则** 放弃片段并以「无片段」的上下文照常发起补全请求，不因采集失败而丢弃本次补全。
5. **假设** VS Code 宿主中光标所在语言当前没有语法解析支持（仅 TS/TSX/JS/Python/Java 有），**当** 补全触发时，**则** 降级为「只取光标处符号的定义体」，不报错、不提示；JetBrains 侧不依赖 grammar，不触发本降级。
6. **假设** 定义所在文件的扩展未激活，或定义位置不是 `file` scheme，**当** 采集片段时，**则** 视为该次无片段，不抛错、不影响本次补全。
7. **假设** 定义所在文件已在编辑器中打开且有未保存修改，**当** 采集片段时，**则** 取编辑器内存中的当前内容，不读磁盘旧版本。
8. **假设** 用户在 JetBrains 宿主中触发补全，**当** 请求发出时，**则** 上下文含光标前后文本，以及经 `TargetElementUtil` 解析到的跨文件声明（同样受 3 段上限与 100ms 超时约束）；语言插件不提供引用解析时退化为只有光标前后文本，不报错。

***

### 用户故事：手动触发补全（优先级：P2）

作为用户，我希望在自动建议没有出现时用一个快捷键主动要一次建议，以便在自动触发被防抖或上下文判定跳过时仍能拿到补全。

**为什么是这个优先级**：自动触发能覆盖绝大多数场景，手动触发是兜底入口（对应同一份请求链路），不是主链路，故为 P2。

**独立测试**：在编辑器中把光标停在一行代码中间，按下手动触发快捷键（VS Code / JetBrains 默认均为 `Alt+\`，可在 IDE 快捷键设置中改绑），验证立即出现建议；连按两次验证不会产生两条并发请求。

**验收场景**：

1. **假设** 补全开关为开启，**当** 用户按下手动触发快捷键时，**则** 立即发起一次补全请求，不受防抖等待影响。
2. **假设** 已有一次补全请求在途，**当** 用户再次按下手动触发快捷键时，**则** 取消在途请求并发起新的请求（不排队、不并发）。
3. **假设** 光标后已有建议，**当** 用户按下手动触发快捷键时，**则** 不重复请求，保持现有建议不变。
4. **假设** 补全开关为关闭，**当** 用户按下手动触发快捷键时，**则** 不发起任何请求、不显示建议。

***

### 用户故事：补全不挤占对话的模型调用（优先级：P1）

作为用户，我希望补全的高频请求不要拖慢、也不要被对话的请求节流卡住，以便我在正常对话的同时也能顺畅敲代码。

**为什么是这个优先级**：这是补全能否可用的前提——CLI 现有的轻量模型入口共用一个进程级 1 次/秒的串行请求间隔（`aiService` 的 `MIN_INTERVAL = 1000`），若补全复用该通道，行内补全的请求速率会被强制降到 1 次/秒并与对话轮次互相排队，两端都无法接受。这条约束决定补全必须是一条独立通道。

**独立测试**：在对话正在流式输出的同时连续敲入多段代码并停顿，验证补全仍按自身节奏返回、对话流式输出不出现额外停顿；反向验证补全请求不会被对话的请求节奏压到 1 次/秒。

**验收场景**：

1. **假设** 对话正在执行一轮（含工具调用），**当** 用户触发补全时，**则** 补全请求独立发起并返回，不与对话轮次互相排队。
2. **假设** 短时间内连续产生多次补全请求，**当** 请求发出时，**则** 同一时刻只允许一个补全请求在途，新请求取消旧请求（在途取消）+ 防抖合并，请求速率由补全自身的防抖与缓存决定，不套用对话的 1 次/秒间隔。
3. **假设** 补全请求在途且用户继续输入，**当** 用户敲击下一个字符时，**则** 立即取消在途请求，不等待响应、不产生第二条并发请求。
4. **假设** 一次补全请求被取消，**当** 其响应仍随后到达时，**则** 该响应被丢弃，不影响当前编辑状态。
5. **假设** 补全模型调用失败，**当** 错误发生时，**则** 错误只影响该次补全，不得中断、挂起或改变对话侧的任何状态。

***

### 用户故事：为补全单独配置模型（优先级：P1）

作为用户，我希望能为代码补全单独指定一个模型，以便用低延迟的补全专用模型（而不是对话用的大模型）换取更快的建议。

**为什么是这个优先级**：模型选择直接决定补全延迟与质量；思考型或大模型会让建议慢到不可用，因此「能选一个专用补全模型」是可用性的组成部分，而非可选优化。

**独立测试**：在设置中把补全模型指定为模型 A、对话模型指定为模型 B，触发补全并抓取请求，验证补全请求打到模型 A、对话请求打到模型 B；再把补全模型清空，验证补全回退到快速模型（fastModel）且仍能返回建议。

**验收场景**：

1. **假设** 用户在设置中指定了补全模型，**当** 触发补全时，**则** 请求使用该模型；未指定补全模型时回退到已配置的快速模型（fastModel）。
2. **假设** 补全模型候选列表已存在（与对话模型候选同源，即 CLI 已配置的模型集合），**当** 用户在设置页「全局设置 → 基础设置」打开「补全模型」下拉时，**则** 展示与对话模型一致的候选清单与当前值；该键未设置（settings.json 无此键）时下拉首项为显式项「未设置（跟随快速模型）」并选中（下拉没有 placeholder 语义），语义与语言下拉的「未设置（默认：中文）」一致（该行只在提供补全的宿主上渲染，见边界情况「补全设置行的宿主范围」）。
3. **假设** 用户修改了补全模型或补全开关，**当** 用户在设置页「全局设置 → 基础设置」改动它（开关拨动即写、下拉选中即写，设置页没有「保存」按钮，见 [agent-config.md](../core/agent-config.md)「设置页控件的保存时机」故事），**则** 该键写入 CLI 侧用户级 `~/.wave/settings.json` 的顶层键（与对话模型等现有设置项同一份真源，经 SDK 实时重载在下一轮生效），插件端不持有第二份模型配置；下一次补全请求即按新值生效，无需重启 IDE。
4. **假设** 补全模型名在配置中已失效（模型被移除或改名），**当** 触发补全时，**则** 该次补全静默失败（不弹错、不影响对话），设置页对失效值给出可读提示：下拉仍保留并显示该值（不静默回落到别的模型名），并在行内说明「当前值不在已配置的模型列表中」。
5. **假设** 用户尚未登录或尚未配置任何可用模型，**当** 用户在编辑器中敲字停顿，**则** 不发起补全请求、不显示任何建议（不产生鉴权失败噪音）。

***

### 用户故事：补全开关与生效范围（优先级：P2）

作为用户，我希望能够整体关闭补全、或只在某些语言里启用，并且在大文件、只读文件等不该打扰的场景里自动静默，以便补全不侵入不适合它的场合。

**为什么是这个优先级**：默认行为已可用，开关与范围控制属于让不同偏好的用户都能接受该能力的配套项，故为 P2。

**独立测试**：在设置中关闭补全开关，验证任何编辑停顿都不产生请求、不显示建议；重新开启后恢复；把某语言加入禁用列表，验证该语言文件中不再出现建议。

**验收场景**：

1. **假设** 补全开关为关闭（设置页「全局设置 → 基础设置」的「代码补全」开关，缺省为开），**当** 用户在编辑器中任意输入与停顿，**则** 不发起任何补全请求、不显示建议（开关在 SDK 侧判定，关闭时连一次模型请求都不发出）；该开关所在行的宿主范围见边界情况「补全设置行的宿主范围」。
2. **假设** 补全开关为开启（默认开启），**当** 用户在多语言混排的项目中编辑，**则** 默认对所有文本类语言生效。
3. **假设** 用户把某语言加入了补全禁用列表，**当** 用户在该语言文件中编辑时，**则** 不发起补全请求；从列表移除后恢复。
4. **假设** 当前文档是只读文件、未保存的新建文件（untitled）、设置页等虚拟文档、diff 视图或笔记本单元格，**当** 用户在其中暂停输入时，**则** 不发起补全请求、不显示建议。
5. **假设** 当前文档体积超过阈值（默认 1MB 或 20000 行），**当** 用户暂停输入时，**则** 跳过补全，不发起请求。
6. **假设** 编辑器中有非空选区或多光标，**当** 用户暂停输入时，**则** 不在选区处渲染建议（仅光标位于单个空选区处才触发）。
7. **假设** 用户正在使用输入法组合输入（如拼音未上屏），**当** 组合过程进行中时，**则** 不发起补全请求，待组合提交后再按停顿判断。

***

### 用户故事：建议的响应迟滞自适应与缓存（优先级：P2）

作为用户，我希望补全在模型响应慢时不要频繁打扰、在响应快时尽快给出建议，并且重复的上下文不要重复请求，以便补全体感跟得上打字节奏。

**为什么是这个优先级**：这是补全的体验调优层——缺失时功能仍可用（只是体感差），故为 P2；但它决定了补全在真实网络条件下的可用程度。

**独立测试**：在人为放慢模型响应的环境下连续输入，验证等待时间随最近响应耗时延长（上限 1000ms）；在响应正常时验证等待时间回落到基准附近。在同一位置反复输入删除相同的代码（回到相同前后缀），验证第二次不产生新请求而直接复用缓存结果。

**验收场景**：

1. **假设** 最近的补全响应耗时明显高于基准（如接近 1000ms），**当** 用户继续输入并停顿，**则** 防抖等待时间相应延长（上限 1000ms），减少无效请求。
2. **假设** 最近的补全响应很快，**当** 用户继续输入并停顿，**则** 防抖等待回落至基准附近（下限 100ms）。
3. **假设** 用户回到此前请求过的相同上下文（同一文档、同一文档版本、相同前缀与后缀），**当** 用户再次停顿，**则** 直接复用缓存中的建议而不发起新请求。
4. **假设** 缓存条目超过容量（默认 100 条）或超过存活时间（默认 5 分钟），**当** 新条目写入时，**则** 按最近最少使用与过期策略淘汰旧条目。
5. **假设** 补全请求正在等待响应，**当** 用户敲入新字符时，**则** 请求立即取消而非等待完成（避免用旧上下文的结果覆盖新位置）。
6. **假设** 补全请求在途，**当** 请求进行中，**则** 编辑器不显示任何加载指示（不闪烁、不显示进度），用户输入不被打断。

***

## 边界情况

* **补全请求与对话共享同一 CLI 子进程，但不得共享队列。** 补全不经过 `aiManager` 的对话轮次（无工具、无 system prompt、无 hooks），也不走 `aiService.callAgent` 的全局 1 次/秒请求间隔；补全在 CLI 侧有独立的调用路径与自己的节流策略。
* **模型配置真源在 CLI 侧。** 插件端不得自建第二份模型配置（插件端既有配置服务明确不负责模型选择）；补全模型与开关走**用户偏好链路**落盘到 CLI 侧用户级 `~/.wave/settings.json` 的**顶层键** `completionModel` / `completionEnabled`——设置页「全局设置 → 基础设置」的两行都经既有 webview→host 命令 `updateConfiguration` 提交——**「代码补全」开关拨动即写盘并即时生效**（载荷只含 `completionEnabled`，不依赖底部「保存」，失败回滚 + toast，见 [agent-config.md](../core/agent-config.md)「设置页控件的保存时机」故事），「补全模型」下拉选中即写盘（同属 `updateConfiguration` 单键载荷，设置页已无「保存」按钮），宿主转成 stdio RPC `updateUserSettings`（省略键 = 不改该键的 diff 语义照旧），插件端只做读写转发、不持有第二份模型配置；候选清单与对话模型同源，经 `getConfiguredModels` 拉取（设置页外壳在插件端是独立 webview，该命令必须同时注册在设置页路由，`scripts/audit-webview-commands.mjs` 会拦漏注册）。补全请求本身走新增的 stdio RPC 方法 `getCompletion`（参数：`prefix` / `suffix` / `language` / `filePath`；响应：`{ text, model }`，`text` 为空串表示无建议），会话级调用，同一会话同一时刻只保留一个在途请求——新请求到达时旧请求被中止，因此迟到的响应不会渲染。
* **补全设置行的宿主范围：只由提供补全的宿主渲染。** 「代码补全」开关与「补全模型」下拉只出现在提供行内补全的宿主（VS Code / JetBrains）的设置页里；桌面端设置页**不展示**这两行——桌面端没有代码编辑器，补全在那里从不触发，把开关摆在那里等于承诺一个不会发生的功能（远端网页等无编辑器宿主同理），见 [desktop-settings.md](../desktop/desktop-settings.md) 场景 3。设置页在插件端与桌面端**共用同一套实现**，因此该门禁必须按**宿主能力位**判定（有没有编辑器，而不是「宿主叫什么名字」），不得依赖「桌面端不渲染设置页」这类隐含前提。CLI 侧的键（顶层 `completionModel` / `completionEnabled`）、解析链与读写 RPC 不受影响：宿主只是不渲染这两行，键与语义照旧（命令行/手改 settings.json 仍然可配）。
* **补全开关在 SDK 侧生效。** 开关关闭时 `getCompletion` 在发起模型调用前直接返回空文本，不消耗一次模型请求。开关与补全模型读 settings.json 的顶层键 `completionModel` / `completionEnabled`，解析优先级为 `AgentOptions` > settings.json > 环境变量 `WAVE_COMPLETION_MODEL` > `fastModel`；`completionEnabled` 缺省为开，设置页的开关**不做占位态**（它的真实默认就是「开」，口径与自动记忆开关一致），未设置且未被拨动时不写该键。补全模型未设置时回退快速模型，**不**回退对话主模型；快速模型自身的取值链与 `model` 对称（`AgentOptions` > settings.json `fastModel` > `env.WAVE_FAST_MODEL`）。
* **3 秒请求超时。** 宿主侧对单个补全请求设 3 秒上限，超时即按“无建议”处理并取消在途请求——超过这个时间的建议已无价值，且持续占用在途通道。跨文件片段的采集（100ms 上限）发生在这 3 秒之内，不额外延长总超时。
* **补全通道不得把 agent 运行时拖进宿主包。** 宿主只能从 `wave-agent-sdk/host` 导入，且该入口的依赖闭包必须零第三方依赖、体积受限（`scripts/check-sdk-surface.mjs`）；补全的请求实现只能放在 CLI/SDK 侧，插件端拿不到也不得拿到底层模型客户端。跨文件片段所需的语法解析栈（tree-sitter 运行时与各语言 grammar）属于第三方依赖，只能存在于**采集侧宿主包**（VS Code 扩展）内，不得进入 `wave-agent-sdk` 的 `/host` 入口闭包。
* **上下文范围：** 请求内容默认包含光标前缀最多 150 行与光标后缀最多 30 行，并对前缀+后缀设总字符上限（默认 12000 字符），超出时从离光标最远处开始截断；跨文件定义片段另计一份预算，不占用这 12000 字符。不引入代码库向量索引，也不做仓库级全文检索。
* **跨文件定义片段的范围与预算：** 只采集三类，且都不做检索：① 光标前符号的定义体；② 本文件 import 语句导入的符号的定义体；③ 光标所在的外层作用域（函数签名、类声明）。定义落在**当前文件**的候选一律跳过——它的正文通常已在光标前缀窗口内，重复只会挤占预算。不采集「最近打开的文件」——片段数量超过最优区间后准确率反而下降，因此上限是**硬上限**（默认 3 段）。
  * **采集在插件侧完成，两端机制不同：**
    * **VS Code**：用 tree-sitter 解析当前文件，得到光标处的 AST 作用域链与 import 语句里的符号名；再逐个经宿主语言服务（VS Code `vscode.executeDefinitionProvider`）换成定义位置并读取定义体。**tree-sitter 只负责「该问哪些符号」，找定义永远走宿主语言服务**——宿主语言服务没有「给我这个文件、还我 import 了哪些名字」的接口，而正则会被注释里的 import、字符串中的同形文本、跨行 import、`import type` 与 `as` 重命名骗到，这是引入解析栈的唯一理由。
    * **JetBrains**：不引入解析栈，改用平台语言无关 API `TargetElementUtil.findTargetElement`（`ELEMENT_NAME_ACCEPTED | REFERENCED_ELEMENT_ACCEPTED`）——它由各语言插件实现，对 IDE 认得的所有语言都成立。候选来源是「光标处 ±1 字符」加「以 `import`/`from`/`using`/`use`/`require` 开头的行里的标识符」（正则粗筛负责认出 import 行，符号本身不靠正则判定）：落在引用上的候选解析出声明，落在关键字或字符串里的候选自然返回 `null`，因此不需要每语言写 import 语法，也不需要区分语言。该调用不可取消，整批放进一次 `ReadAction` 并按 100ms 硬超时兜底。
  * **语言覆盖**：VS Code 侧带 TS/TSX/JS/Python/Java 五种 grammar（Python 与 Java 各增一个 grammar 文件，包体约 +120 KB）；其他语言没有解析支持时降级为「只取光标处符号的定义体」。作用域节点类型与 import 语法按语言分别登记，新增语言需要同时补 grammar 文件、作用域节点表和 import 查询——三者缺一都会静默退化成「找不到符号」。JetBrains 侧不依赖 grammar，覆盖 IDE 本身支持的全部语言，但它采到的片段质量取决于该语言的插件是否实现了引用解析。
  * **预算与超时**：采集整体 100ms 硬超时，超时即按无片段处理、请求照常发出；插件侧只做「取、读、截、限流 ≤3 段」并只按**字符**预算截断，token 预算、去重、路径注释头与最终拼装都在 CLI 侧。
  * **新鲜度**：片段原文取编辑器内存中的当前文档内容（含未保存修改），不读磁盘缓存——陈旧片段不只是噪音，会把模型引向过时的 API。
  * **顺序**：额外片段整体前置到光标前缀之前，其中相关性最高的一段放最后（离光标最近）。
  * **协议**：插件向 CLI 传可选字段 `snippets`；`undefined` 表示该宿主没有这项能力，`[]` 表示尝试采集但没拿到，两者对 CLI 都是「无片段」。
* **隐私与数据外发：** 前缀/后缀内容、以及被采集到的那几段跨文件定义体（含文件路径注释头）会随补全请求发送到用户已配置的模型服务（与对话同一网关与凭据）；不额外上传整个文件或仓库，也不做代码库级上传；首期不支持本地推理。
* **JetBrains 平台差异（已对照 IC-2024.2.6 平台源码验证）：** JetBrains 侧使用平台行内补全扩展点（`<inline.completion.provider>` / `InlineCompletionProvider` + `InlineCompletionSingleSuggestion.build { emit(InlineCompletionGrayTextElement(text)) }`），而非自绘 Inlay；防抖、取消与多光标过滤由平台负责，插件侧不再叠加自己的防抖，只做 3 秒超时与返回前的光标位置校验。跨文件定义片段**在 JetBrains 同样生效**，但走的是另一条实现路径：JVM 加载不了 wasm grammar，而复制一份解析栈进插件或 CLI 又会让所有宿主为单一能力增重（wasm grammar 合计约 6.2 MB），因此改用平台自带的 `TargetElementUtil`——它把「这个位置指向哪个声明」这件事交给各语言的插件，插件侧只负责挑候选位置。这条是开源同行的既有做法（Tabby 的 `DefaultLanguageSupportProvider` 用同一个 API 提供声明位置），Continue 则因为在该钩子上留了 `NotImplementedError` 而在 JetBrains 侧没有跨文件能力。代价是精度依赖语言插件：引用解析做得差的文件类型，片段会退化成「采不到」。
* **只读与不可写文档按 scheme 判定：** VS Code 侧用 `workspace.fs.isWritableFileSystem(scheme)`（仅明确返回 false 时跳过）判断可写性，JetBrains 侧用 `Document.isWritable`；未命名文档、虚拟文件（非 `file` scheme）、内联 diff / 多差异编辑器中的探针文档一律跳过。
* **手动触发复用平台内置动作：** 两端均绑定平台自带的行内建议触发命令（VS Code `editor.action.inlineSuggest.trigger`，默认快捷键 `Alt+\`），不新增插件命令；手动触发跳过防抖等待，直接进入请求。
* **建议与实际文本的漂移：** 响应返回时若文档版本已变化（用户已继续编辑），该响应必须丢弃，不得按旧位置插入。
* **多标签页与多窗口：** 同一 IDE 内多个编辑器同时打开时，只有获得聚焦的编辑器可持有在途补全请求；切换编辑器取消在途请求。
* **与内联差异/冲突标记共存：** 文档处于合并冲突、内联 diff（如 blame/commit 视图）等特殊渲染状态时跳过补全。
* **建议不得触发其他副作用：** 渲染与取消建议都不写入文档、不入撤销栈、不改变光标位置；只有接受（Tab）才写入文档。
* **补全依赖会话已建立：** 补全模型与网关凭据都由 CLI 侧的会话解析，插件端在首个会话（侧边栏/标签页/窗口任一）建立前无法回答补全请求，此时按“无建议”静默处理；VS Code 侧因此不声明补全相关的激活事件——扩展未激活（未打开过 Wave 面板）时不会显示补全，避免为补全在每次 VS Code 启动时都付一次 CLI 进程启动成本。

## 假设

* 用户在 IDE 中已登录 wave，且 CLI 侧已配置至少一个可用模型（或已配置快速模型作为回退）。
* 插件与 CLI 的 stdio 连接已建立（见 [Stdio 传输层](../ui/stdio-transport.md)）；补全复用该连接，不额外 spawn 进程。
* 补全的请求链路与参数默认值两端共享，差异仅限宿主平台的渲染、快捷键接入与上下文采集（见 [插件端](plugin-host.md)）；跨文件片段由两端各自采集（路径不同，见边界情况「跨文件定义片段的范围与预算」），CLI/SDK 侧只消费可选字段 `snippets`。
* 补全的响应延迟以模型服务实际能力为上限；规格中的数值（防抖基准/区间、上下文行数、缓存容量与存活时间、文件体积阈值、建议行数上限）均为**默认值**，可在实现与调优阶段调整，调整后需同步更新本规格。
