fix(lsp): preserve execution-world URI semantics

This commit is contained in:
Tianyi Cui
2026-07-29 04:19:18 +08:00
parent 3537665806
commit 4997fb219e
24 changed files with 162 additions and 107 deletions

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-15-lsp-capability-seam.md
2026-07-15-lsp-capability-seam.md: a9dab45ebf034f6d5218b25a0868a7e86803719b
2026-07-15-lsp-capability-seam.zh.md: b411a2456ea2a3838e56422f58954473da51ae23
2026-07-15-lsp-capability-seam.md: 6e46fac0611c1b7282766cf57965a07e3ee6bb9b
2026-07-15-lsp-capability-seam.zh.md: b535b60df70e8a6132bb1596581be3ab1daba932

View File

@@ -62,7 +62,7 @@ interface LspProviderQuery extends LspQueryRequest {
}
type LspQueryResult =
| { readonly kind: 'locations'; readonly locations: readonly { readonly uri: string; readonly range: LspRange }[]; readonly resolvedWorkspaceRoot: string }
| { readonly kind: 'locations'; readonly locations: readonly { readonly uri: string; readonly range: LspRange }[]; readonly resolvedWorkspaceUri: string }
| { readonly kind: 'hover'; readonly hover: { readonly contents: string; readonly range?: LspRange } | null }
interface LspProvider {
@@ -77,7 +77,7 @@ interface LspService {
}
```
Mapping keys normalize to lowercase, leading-dot extensions selected from `filePath`'s final extension; language ids only synchronize documents. Seam positions and ranges are zero-based UTF-16. `findReferences` always includes declarations: providers enforce this internally, the local mapping sets `context.includeDeclaration: true`, and callers get no flag. Closed result unions normalize navigation to locations and hover to content or `null`; navigation results carry the provider's resolved workspace root so consumers relativize file URIs in the same canonical namespace. The seam exposes no protocol types, process or document controls, or generic request escape hatch.
Mapping keys normalize to lowercase, leading-dot extensions selected from `filePath`'s final extension; language ids only synchronize documents. Seam positions and ranges are zero-based UTF-16. `findReferences` always includes declarations: providers enforce this internally, the local mapping sets `context.includeDeclaration: true`, and callers get no flag. Closed result unions normalize navigation to locations and hover to content or `null`; navigation results carry the provider's canonical workspace URI so consumers relativize file URIs in the execution world's namespace. The seam exposes no protocol types, process or document controls, or generic request escape hatch.
`dsh-lsp-local` owns server configuration, JSON-RPC, process and transient-document state, and protocol translation. It reads through `ctx.fs` and launches through `ctx.subprocess`, depending on their interface packages rather than concrete providers; the [portable execution-world decision](2026-07-28-portable-execution-world-consumers.md) owns that pairing. The server-table key is its provider id. The plugin resolves every server-local setting before registration, rolls back earlier registrations if a later mapping is invalid or conflicts, and retains an independent process pool per provider. `dsh-tool-lsp` runtime-injects only `tools`, `lsp`, and `systemPrompt`, obtains the workspace from `exec.agent?.session.header.cwd` through a package-local `sessionCwd(exec)` helper matching the filesystem tools' lookup, and imports no provider.
@@ -98,7 +98,7 @@ interface LspToolInput {
The tool requires `workspaceRoot` from session `header.cwd`, with no fallback; absence fails as `LSP_WORKSPACE_REQUIRED` before querying or startup. The local provider resolves relative paths against that root and accepts absolute paths directly; both forms are canonicalized and rejected before startup when the target is outside the canonical workspace.
Locations render as stable, file-grouped `path:line:character` entries. A `file:` URI accepted by Node `fileURLToPath()` becomes a relative path inside the workspace or an absolute path outside it; other URIs remain verbatim. `maxLocations` defaults to `100` and reports omitted items; `maxResultChars` defaults to `16_000` and bounds every complete rendered result, including its truncation metadata. Empty locations and `null` hover are successful no-result responses; missing or malformed server payloads fail with structured `LSP_MALFORMED_RESPONSE` errors.
Locations render as stable, file-grouped `path:line:character` entries without applying harness-host path rules. A valid `file:` URI becomes a relative path inside the provider's canonical workspace URI or a URI-derived absolute path outside it; malformed and non-`file:` URIs remain verbatim. `maxLocations` defaults to `100` and reports omitted items; `maxResultChars` defaults to `16_000` and bounds every complete rendered result, including its truncation metadata. Empty locations and `null` hover are successful no-result responses; missing or malformed server payloads fail with structured `LSP_MALFORMED_RESPONSE` errors.
The transport-neutral presenter uses `{ card: 'generic', kind: 'search', title, locations: [{ path: file_path, line }] }` with an args-derived operation/cursor `title`. Because `FileLocation` has no character, follow-along focuses the input line while the title preserves the cursor; presentation remains pure.

View File

@@ -62,7 +62,7 @@ interface LspProviderQuery extends LspQueryRequest {
}
type LspQueryResult =
| { readonly kind: 'locations'; readonly locations: readonly { readonly uri: string; readonly range: LspRange }[]; readonly resolvedWorkspaceRoot: string }
| { readonly kind: 'locations'; readonly locations: readonly { readonly uri: string; readonly range: LspRange }[]; readonly resolvedWorkspaceUri: string }
| { readonly kind: 'hover'; readonly hover: { readonly contents: string; readonly range?: LspRange } | null }
interface LspProvider {
@@ -77,7 +77,7 @@ interface LspService {
}
```
映射键规范化为带前导点的小写扩展名,并按 `filePath` 的最后一个扩展名选择;语言 id 仅用于文档同步。服务边界中的位置和范围从零开始按 UTF-16 计数。`findReferences` 始终包含声明:提供方在内部执行该约束,本地映射设置 `context.includeDeclaration: true`,调用方不能配置。封闭结果联合将导航统一为位置,将 `hover` 统一为内容或 `null`;导航结果携带提供方解析后的工作区根目录,使消费方依据同一规范化根目录相对化文件 URI。服务边界不公开协议类型、进程或文档控制,也不提供通用请求逃生口。
映射键规范化为带前导点的小写扩展名,并按 `filePath` 的最后一个扩展名选择;语言 id 仅用于文档同步。服务边界中的位置和范围从零开始按 UTF-16 计数。`findReferences` 始终包含声明:提供方在内部执行该约束,本地映射设置 `context.includeDeclaration: true`,调用方不能配置。封闭结果联合将导航统一为位置,将 `hover` 统一为内容或 `null`;导航结果携带提供方的规范工作区 URI,使消费方在执行世界的命名空间内相对化文件 URI。服务边界不公开协议类型、进程或文档控制,也不提供通用请求逃生口。
`dsh-lsp-local` 负责服务器配置、JSON-RPC、进程与临时文档状态和协议转换。它通过 `ctx.fs` 读取,通过 `ctx.subprocess` 启动,只依赖二者的接口包而非具体提供方;[可移植执行环境决策](2026-07-28-portable-execution-world-consumers.md)负责定义这种配对。服务器表的键是提供方 id。插件在注册前解析每个服务器的本地设置;如果后续映射无效或发生冲突,插件会撤销此前的注册,并为每个提供方保留独立进程池。`dsh-tool-lsp` 在运行时只注入 `tools`、`lsp` 和 `systemPrompt`,通过包内的 `sessionCwd(exec)` 辅助函数从 `exec.agent?.session.header.cwd` 取得工作区,其取值方式与文件系统工具一致,也不导入提供方。
@@ -98,7 +98,7 @@ interface LspToolInput {
工具必须从会话 `header.cwd` 取得 `workspaceRoot`,没有后备值;缺失时在查询或启动前以 `LSP_WORKSPACE_REQUIRED` 失败。本地提供方基于根目录解析相对路径并直接接受绝对路径;两种路径都会进行规范化,如果目标位于规范工作区外,则在启动前拒绝。
位置按文件稳定分组并渲染为 `path:line:character`。Node `fileURLToPath()` 可接受的 `file:` URI 在工作区内转换为相对路径,在工作区外转换为绝对路径;其他 URI 保持原样。`maxLocations` 默认值为 `100`,并报告省略的条目;`maxResultChars` 默认值为 `16_000`,并限制每个完整渲染结果,其中包括截断元数据。空位置与 `null` hover 是成功的无结果响应;服务器载荷缺失或格式错误时,以结构化 `LSP_MALFORMED_RESPONSE` 错误失败。
位置在不应用 harness 宿主路径规则的情况下按文件稳定分组并渲染为 `path:line:character`。有效的 `file:` URI 落在提供方的规范工作区 URI 内时转换为相对路径,位于其外时转换为从 URI 派生的绝对路径;格式错误的 URI 与非 `file:` URI 保持原样。`maxLocations` 默认值为 `100`,并报告省略的条目;`maxResultChars` 默认值为 `16_000`,并限制每个完整渲染结果,其中包括截断元数据。空位置与 `null` hover 是成功的无结果响应;服务器载荷缺失或格式错误时,以结构化 `LSP_MALFORMED_RESPONSE` 错误失败。
与传输方式无关的展示器使用 `{ card: 'generic', kind: 'search', title, locations: [{ path: file_path, line }] }`,`title` 由参数推导并标明操作与光标。由于 `FileLocation` 没有 character,跟随位置聚焦输入行,标题保留完整光标;展示保持纯函数。

View File

@@ -2,5 +2,5 @@
# side as of the last confirmed-consistent state. Both languages carry equal authority;
# after editing either side, bring the other along and re-record with:
# pnpm run verify-translation-pairing --write .agents/notes/implemented/architecture/2026-07-28-portable-execution-world-consumers.md
2026-07-28-portable-execution-world-consumers.md: a5df393f31a25b7d16e63af240882e1d94093e34
2026-07-28-portable-execution-world-consumers.zh.md: 87f11fa53b5b7579969dcb15dc3aaad978018773
2026-07-28-portable-execution-world-consumers.md: 6f75778398e8f204c85b64fe096838a3b62d398e
2026-07-28-portable-execution-world-consumers.zh.md: 22edc591f7716a92fd2a68d74c84168189743fbd

View File

@@ -21,7 +21,7 @@ The subprocess interface owns the process coordinates and primitives: canonical
Generic consumers use that execution world:
- `dsh-bash-local` continues to map Bash semantics onto ordinary `ctx.subprocess.spawn()`.
- `dsh-lsp-local` reads and contains source through `ctx.fs`, resolves and launches language servers through `ctx.subprocess`, and sends provider-owned file URIs. Its JSON-RPC, pooling, synchronization, cancellation, and normalization stay unchanged.
- `dsh-lsp-local` reads and contains source through `ctx.fs`, resolves and launches language servers through `ctx.subprocess`, and carries provider-owned file URIs through initialization and result rendering. Its JSON-RPC, pooling, synchronization, cancellation, and normalization stay unchanged.
- `dsh-pty-local` maps persistent-shell semantics onto `ctx.subprocess.spawnTerminal()`. The local `node-pty` and process-inspection implementation moves into `dsh-subprocess-local`; another subprocess provider supplies the same primitive. The allocation signal is detached before publication, while readiness initialization retains setup cancellation. A timed-out asynchronous write, or a cancellation whose foreground signal fails during that write, retains the send reservation until the provider settles it; completion of a stale inspection resumes polling for the current send.
- `dsh-code-runtime-subprocess` materializes a dependency-free runner through `ctx.fs` and launches it through `ctx.subprocess`, preserving the Code Runtime binding and output contract across local or remote worlds. It shares host-side worker mechanics through the non-plugin `dsh-code-runtime-worker/runtime-host` subpath instead of copying them. Preparation carries one lifecycle signal through filesystem resolution, materialization, and executable lookup so disposal can abort a stalled provider. The heap-bounded worker rejects oversized binding frames before transfer, each outer hop enforces the same bound before serialization, and the launcher publishes an accepted terminal frame before reaping its controller so a descendant that inherits controller pipes cannot suppress completion; the host still awaits process-group quiescence.

View File

@@ -21,7 +21,7 @@ Status: implemented
通用消费方使用该执行世界:
- `dsh-bash-local` 继续把 Bash 语义映射到普通的 `ctx.subprocess.spawn()`。
- `dsh-lsp-local` 通过 `ctx.fs` 读取源文件并验证包含关系,通过 `ctx.subprocess` 解析和启动语言服务器,并发送由提供方负责的文件 URI。其 JSON-RPC、池化、同步、取消和规范化保持不变。
- `dsh-lsp-local` 通过 `ctx.fs` 读取源文件并验证包含关系,通过 `ctx.subprocess` 解析和启动语言服务器,并让由提供方负责的文件 URI 贯穿初始化与结果渲染。其 JSON-RPC、池化、同步、取消和规范化保持不变。
- `dsh-pty-local` 把持久 shell 语义映射到 `ctx.subprocess.spawnTerminal()`。本地 `node-pty` 与进程检查实现移入 `dsh-subprocess-local`;其他进程管理提供方则提供相同原语。分配信号会在发布前解除关联,而就绪初始化仍保留设置阶段的取消。异步写入超时,或在该写入期间取消时前台信号发送失败,都会保留发送预留,直至提供方将写入结算;陈旧检查完成后,会针对当前发送恢复轮询。
- `dsh-code-runtime-subprocess` 通过 `ctx.fs` 物化无依赖 runner,并通过 `ctx.subprocess` 启动它,从而在本地或远程执行世界中保留代码运行时的绑定与输出契约。它通过非插件子路径 `dsh-code-runtime-worker/runtime-host` 共享宿主侧 worker 机制,而不是复制这些机制。准备阶段让同一个生命周期信号贯穿文件系统解析、物化和可执行文件查找,使资源释放能够中止停滞的提供方操作。受堆上限约束的 worker 会在传输前拒绝过大的绑定帧;每个外层转发环节都会在序列化前执行相同的上限检查;launcher 会在回收 controller 前发布已接纳的终态帧,使继承 controller 管道的后代进程无法阻止完成;宿主仍会等待进程组完全停稳。