@trim21/personal-pi-extensions 0.0.316 → 0.0.318
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +2 -2
- package/package.json +1 -1
- package/src/claude-code/files.ts +11 -8
- package/src/lib/lsp/lsp.ts +118 -34
- package/src/lib/lsp/server-config.ts +15 -3
- package/src/opencode/files.ts +3 -4
package/README.md
CHANGED
|
@@ -348,7 +348,7 @@ pi -e ./src/talk/index.ts
|
|
|
348
348
|
|
|
349
349
|
read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级诊断)。LSP protocol 是统一的,因此服务器不需要为每个语言写 adapter:用一份 JSON 配置声明如何启动即可。
|
|
350
350
|
|
|
351
|
-
|
|
351
|
+
配置文件(项目优先于全局):顶层字段(`enabled`/`disabled`、超时等)本地覆盖全局;`servers` 按服务器 id 合并——同名 id 本地整体覆盖、新增 id,全局其余服务器保留。
|
|
352
352
|
|
|
353
353
|
- `~/.pi/agent/lsp.json`(全局)
|
|
354
354
|
- `.pi/lsp.json`(项目)
|
|
@@ -382,7 +382,7 @@ read/edit/write 工具内置 LSP 诊断(写文件后等待并报告 ERROR 级
|
|
|
382
382
|
- `startupTimeoutMs` / `diagnosticsWaitMs`:per-server 超时,覆盖全局配置与默认值
|
|
383
383
|
- `initializationOptions` 与 `settings` 按 LSP 语义分离:前者进 initialize 请求,后者进 didChangeConfiguration / workspace/configuration 请求
|
|
384
384
|
|
|
385
|
-
内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key
|
|
385
|
+
内置默认服务器(typescript / pyright / ruff / clangd)始终存在;`servers` 以 key 为服务器 id 与默认合并——同 key 整体覆盖(整个配置替换默认)、新 key 新增、`"enabled": false` 移除(如 `"clangd": { "enabled": false }`)。executable 的发现逻辑(如 tsserver 路径、venv 里的 python)不内置,需要时用 `bin` / `args` / `settings` 自行表达。
|
|
386
386
|
|
|
387
387
|
旧的 `enabled`(白名单)/ `disabled`(排除)与全局超时字段(`initializeTimeoutMs` 等)继续可用。
|
|
388
388
|
|
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -14,7 +14,7 @@ import {
|
|
|
14
14
|
} from "@earendil-works/pi-coding-agent";
|
|
15
15
|
import { Type } from "typebox";
|
|
16
16
|
|
|
17
|
-
import {
|
|
17
|
+
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
18
18
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
19
19
|
import {
|
|
20
20
|
type ClaudeCodeState,
|
|
@@ -390,7 +390,9 @@ export function registerFileTools(
|
|
|
390
390
|
state.reads.set(key, snapshot);
|
|
391
391
|
const diff = generateDiffString("", newString);
|
|
392
392
|
throwIfAborted(signal);
|
|
393
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
393
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
394
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
395
|
+
});
|
|
394
396
|
return [
|
|
395
397
|
`The file ${filePath} has been updated successfully.`,
|
|
396
398
|
{
|
|
@@ -469,7 +471,9 @@ export function registerFileTools(
|
|
|
469
471
|
? `The file ${filePath} has been updated. All occurrences were successfully replaced.`
|
|
470
472
|
: `The file ${filePath} has been updated successfully.`;
|
|
471
473
|
throwIfAborted(signal);
|
|
472
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
474
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
475
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
476
|
+
});
|
|
473
477
|
return [
|
|
474
478
|
text,
|
|
475
479
|
{
|
|
@@ -547,7 +551,9 @@ export function registerFileTools(
|
|
|
547
551
|
? `File created successfully at: ${filePath}`
|
|
548
552
|
: `The file ${filePath} has been updated successfully.`;
|
|
549
553
|
throwIfAborted(signal);
|
|
550
|
-
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd
|
|
554
|
+
const diagnosticText = await service.lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
555
|
+
notify: (message, level) => ctx.ui.notify(message, level),
|
|
556
|
+
});
|
|
551
557
|
return [
|
|
552
558
|
text,
|
|
553
559
|
{
|
|
@@ -598,8 +604,7 @@ function restoreFileReads(
|
|
|
598
604
|
* 与主进程 index.ts 聚合加载时的行为一致。
|
|
599
605
|
*/
|
|
600
606
|
export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
601
|
-
const service =
|
|
602
|
-
initLsp(pi, service);
|
|
607
|
+
const service = registerLsp(pi);
|
|
603
608
|
const state = createClaudeCodeState();
|
|
604
609
|
|
|
605
610
|
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
|
@@ -607,7 +612,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
607
612
|
// 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
|
|
608
613
|
// 防呆语义不因重建而弱化。
|
|
609
614
|
pi.on("session_start", (_event, ctx) => {
|
|
610
|
-
service.setUi(ctx.ui);
|
|
611
615
|
restoreFileReads(state, ctx.sessionManager);
|
|
612
616
|
});
|
|
613
617
|
|
|
@@ -615,7 +619,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
615
619
|
// session_start,扩展实例也不重建。这里同样重放当前分支,丢弃被抛弃分支
|
|
616
620
|
// 的记账,避免 state 与当前分支脱节。
|
|
617
621
|
pi.on("session_tree", (_event, ctx) => {
|
|
618
|
-
service.setUi(ctx.ui);
|
|
619
622
|
restoreFileReads(state, ctx.sessionManager);
|
|
620
623
|
});
|
|
621
624
|
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -1,17 +1,20 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* LSP 管理器:所有语言服务器连接的注册表与统一入口。
|
|
3
3
|
*
|
|
4
|
-
* - state(client 缓存、broken 集合、spawning 去重)是
|
|
4
|
+
* - state(client 缓存、broken 集合、spawning 去重)是 service 工厂的
|
|
5
5
|
* 闭包变量,不做成模块级全局;
|
|
6
6
|
* - 配置来源:全局 `~/.pi/agent/lsp.json` + 本地 `<cwd>/.pi/lsp.json`
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
7
|
+
* (本地覆盖全局):`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局
|
|
8
|
+
* 其余服务器保留),`enabled`/`disabled` 白名单与各超时参数继续生效;
|
|
9
|
+
* 配置在每个工具的调用 cwd 下惰性读取;enabled/disabled 引用不存在的
|
|
10
|
+
* 服务器 id 是配置错误:全局配置在扩展加载(createLspService)时抛错,
|
|
11
|
+
* 本地配置在 session 开始预加载时通知,工具调用时校验抛错兜底;
|
|
10
12
|
* - client 按 (root, serverID) 缓存,并发 spawn 去重,启动失败记入 broken
|
|
11
13
|
* 集合(服务实例生命周期内不再重试);
|
|
12
|
-
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile
|
|
14
|
+
* - 工具只与 touchFile / diagnostics / lspDiagnosticsForFile 三个方法打交道;通知回调按请求传入。
|
|
13
15
|
*/
|
|
14
16
|
|
|
17
|
+
import { readFileSync } from "node:fs";
|
|
15
18
|
import { readFile } from "node:fs/promises";
|
|
16
19
|
import { homedir } from "node:os";
|
|
17
20
|
import { extname, join, normalize, sep } from "node:path";
|
|
@@ -23,7 +26,13 @@ import { Value } from "typebox/value";
|
|
|
23
26
|
import { type LspServerAdapter } from "./adapter.js";
|
|
24
27
|
import { create, type CreateInput, type Diagnostic, type Info as LspClient } from "./client.js";
|
|
25
28
|
import { report } from "./diagnostic.js";
|
|
26
|
-
import {
|
|
29
|
+
import {
|
|
30
|
+
createAdapters,
|
|
31
|
+
defaultServers,
|
|
32
|
+
mergeServerConfigs,
|
|
33
|
+
mergeServerRecords,
|
|
34
|
+
serverConfigSchema,
|
|
35
|
+
} from "./server-config.js";
|
|
27
36
|
|
|
28
37
|
/** 超时值:number(毫秒,>=1)或字符串("500"、"5s"、"1m"),Parse 后由 toMs 统一换算。 */
|
|
29
38
|
const timeoutValue = Type.Union([Type.Number({ minimum: 1 }), Type.String()]);
|
|
@@ -99,9 +108,42 @@ async function readConfigFile(filePath: string): Promise<LspConfig> {
|
|
|
99
108
|
}
|
|
100
109
|
}
|
|
101
110
|
|
|
111
|
+
/** 同步版 readConfigFile(扩展加载时校验全局配置用)。 */
|
|
112
|
+
function readConfigFileSync(filePath: string): LspConfig {
|
|
113
|
+
try {
|
|
114
|
+
const raw = readFileSync(filePath, "utf8");
|
|
115
|
+
return Value.Parse(lspConfigSchema, JSON.parse(raw) as unknown);
|
|
116
|
+
} catch {
|
|
117
|
+
return {};
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
/**
|
|
122
|
+
* enabled/disabled 引用的 id 必须存在于实际生效的服务器集合(注入的 adapters
|
|
123
|
+
* 或默认 + 配置 servers),否则抛配置错误(避免静默失效)。
|
|
124
|
+
*/
|
|
125
|
+
function validateConfig(config: LspConfig, adapters?: LspServerAdapter[]): void {
|
|
126
|
+
const available = new Set(
|
|
127
|
+
adapters
|
|
128
|
+
? adapters.map((adapter) => adapter.id)
|
|
129
|
+
: Object.keys(mergeServerConfigs(defaultServers, config.servers)),
|
|
130
|
+
);
|
|
131
|
+
const unknown = [...(config.enabled ?? []), ...(config.disabled ?? [])].filter(
|
|
132
|
+
(id) => !available.has(id),
|
|
133
|
+
);
|
|
134
|
+
if (unknown.length > 0) {
|
|
135
|
+
const list = [...available].toSorted().join(", ") || "none";
|
|
136
|
+
throw new Error(
|
|
137
|
+
`lsp.json: unknown server id in enabled/disabled: ${unknown.join(", ")} (available: ${list})`,
|
|
138
|
+
);
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
102
142
|
/**
|
|
103
143
|
* 合并后的生效配置:全局 `~/.pi/agent/lsp.json` 为基底,本地
|
|
104
|
-
* `<cwd>/.pi/lsp.json`
|
|
144
|
+
* `<cwd>/.pi/lsp.json` 覆盖——顶层标量字段(enabled/disabled、超时等)本地
|
|
145
|
+
* 直接替换;`servers` 按 id 合并(同名 id 整体覆盖、新增 id,全局其余服务器
|
|
146
|
+
* 保留)。
|
|
105
147
|
*/
|
|
106
148
|
export async function loadLspConfig(
|
|
107
149
|
cwd: string,
|
|
@@ -111,14 +153,23 @@ export async function loadLspConfig(
|
|
|
111
153
|
readConfigFile(globalConfigPath),
|
|
112
154
|
readConfigFile(join(cwd, ".pi", "lsp.json")),
|
|
113
155
|
]);
|
|
114
|
-
|
|
156
|
+
const servers = mergeServerRecords(globalConfig.servers, localConfig.servers);
|
|
157
|
+
return {
|
|
158
|
+
...globalConfig,
|
|
159
|
+
...localConfig,
|
|
160
|
+
...(servers && { servers }),
|
|
161
|
+
};
|
|
115
162
|
}
|
|
116
163
|
|
|
117
|
-
/**
|
|
164
|
+
/**
|
|
165
|
+
* 按配置过滤 adapter 列表。enabled/disabled 引用了实际生效集合中不存在的
|
|
166
|
+
* 服务器 id 时抛错(配置错误,避免静默失效)。
|
|
167
|
+
*/
|
|
118
168
|
export function filterAdapters(
|
|
119
169
|
adapters: LspServerAdapter[],
|
|
120
170
|
config: LspConfig,
|
|
121
171
|
): LspServerAdapter[] {
|
|
172
|
+
validateConfig(config, adapters);
|
|
122
173
|
return adapters.filter((adapter) => {
|
|
123
174
|
if (config.enabled && !config.enabled.includes(adapter.id)) return false;
|
|
124
175
|
if (config.disabled?.includes(adapter.id)) return false;
|
|
@@ -130,15 +181,23 @@ interface LspState {
|
|
|
130
181
|
clients: LspClient[];
|
|
131
182
|
broken: Set<string>;
|
|
132
183
|
spawning: Map<string, Promise<LspClient | undefined>>;
|
|
184
|
+
closing: boolean;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
export interface LspRequestOptions {
|
|
188
|
+
notify?: ExtensionUIContext["notify"];
|
|
133
189
|
}
|
|
134
190
|
|
|
135
191
|
export interface LspService {
|
|
136
|
-
touchFile(
|
|
192
|
+
touchFile(
|
|
193
|
+
file: string,
|
|
194
|
+
cwd: string,
|
|
195
|
+
diagnostics?: "document" | "full",
|
|
196
|
+
options?: LspRequestOptions,
|
|
197
|
+
): Promise<void>;
|
|
137
198
|
diagnostics(): Promise<Record<string, Diagnostic[]>>;
|
|
138
|
-
lspDiagnosticsForFile(file: string, cwd: string): Promise<string>;
|
|
199
|
+
lspDiagnosticsForFile(file: string, cwd: string, options?: LspRequestOptions): Promise<string>;
|
|
139
200
|
shutdownAll(): Promise<void>;
|
|
140
|
-
/** 绑定当前会话的 UI 上下文,LSP 服务器启动失败时用它发错误通知(传 undefined 解绑)。 */
|
|
141
|
-
setUi(ui?: Pick<ExtensionUIContext, "notify">): void;
|
|
142
201
|
}
|
|
143
202
|
|
|
144
203
|
/** 文件必须在工作目录内才启用 LSP(对齐 opencode 的 containsPath)。 */
|
|
@@ -158,14 +217,24 @@ export function createLspService(
|
|
|
158
217
|
adapters?: LspServerAdapter[],
|
|
159
218
|
globalConfigPath?: string,
|
|
160
219
|
): LspService {
|
|
220
|
+
// 扩展加载时校验全局配置(本地配置在 session_start 预加载时校验)
|
|
221
|
+
validateConfig(
|
|
222
|
+
readConfigFileSync(globalConfigPath ?? join(homedir(), ".pi", "agent", "lsp.json")),
|
|
223
|
+
adapters,
|
|
224
|
+
);
|
|
161
225
|
const state: LspState = {
|
|
162
226
|
clients: [],
|
|
163
227
|
broken: new Set(),
|
|
164
228
|
spawning: new Map(),
|
|
229
|
+
closing: false,
|
|
165
230
|
};
|
|
166
|
-
let ui: Pick<ExtensionUIContext, "notify"> | undefined;
|
|
167
231
|
|
|
168
|
-
async function getClients(
|
|
232
|
+
async function getClients(
|
|
233
|
+
file: string,
|
|
234
|
+
cwd: string,
|
|
235
|
+
notify?: ExtensionUIContext["notify"],
|
|
236
|
+
): Promise<LspClient[]> {
|
|
237
|
+
if (state.closing) return [];
|
|
169
238
|
if (!containsPath(file, cwd)) return [];
|
|
170
239
|
const config = await loadLspConfig(cwd, globalConfigPath);
|
|
171
240
|
const timeout = timeoutOptions(config);
|
|
@@ -198,7 +267,7 @@ export function createLspService(
|
|
|
198
267
|
const handle = await adapter.spawn(root, cwd);
|
|
199
268
|
if (!handle) {
|
|
200
269
|
state.broken.add(key);
|
|
201
|
-
|
|
270
|
+
notify?.(
|
|
202
271
|
`LSP server "${adapter.id}" is not available for ${root} (binary not found)`,
|
|
203
272
|
"error",
|
|
204
273
|
);
|
|
@@ -214,6 +283,10 @@ export function createLspService(
|
|
|
214
283
|
diagnosticsDocumentWaitTimeoutMs:
|
|
215
284
|
adapter.diagnosticsWaitMs ?? timeout.diagnosticsDocumentWaitTimeoutMs,
|
|
216
285
|
});
|
|
286
|
+
if (state.closing) {
|
|
287
|
+
await client.shutdown();
|
|
288
|
+
return;
|
|
289
|
+
}
|
|
217
290
|
const duplicate = state.clients.find((c) => c.root === root && c.serverID === adapter.id);
|
|
218
291
|
if (duplicate) {
|
|
219
292
|
await client.shutdown();
|
|
@@ -223,7 +296,7 @@ export function createLspService(
|
|
|
223
296
|
return client;
|
|
224
297
|
} catch (error) {
|
|
225
298
|
state.broken.add(key);
|
|
226
|
-
|
|
299
|
+
notify?.(
|
|
227
300
|
`LSP server "${adapter.id}" failed to start for ${root}: ${
|
|
228
301
|
error instanceof Error ? error.message : String(error)
|
|
229
302
|
}`,
|
|
@@ -252,8 +325,9 @@ export function createLspService(
|
|
|
252
325
|
file: string,
|
|
253
326
|
cwd: string,
|
|
254
327
|
diagnostics?: "document" | "full",
|
|
328
|
+
options?: LspRequestOptions,
|
|
255
329
|
): Promise<void> {
|
|
256
|
-
const clients = await getClients(file, cwd);
|
|
330
|
+
const clients = await getClients(file, cwd, options?.notify);
|
|
257
331
|
await Promise.all(
|
|
258
332
|
clients.map(async (client) => {
|
|
259
333
|
const after = Date.now();
|
|
@@ -281,8 +355,12 @@ export function createLspService(
|
|
|
281
355
|
* edit/write 用:等待文档诊断并返回该文件的 ERROR 报告(空串表示无错误)。
|
|
282
356
|
* 内部所有 LSP 失败都会被吞掉,不干扰写操作本身。
|
|
283
357
|
*/
|
|
284
|
-
async function lspDiagnosticsForFile(
|
|
285
|
-
|
|
358
|
+
async function lspDiagnosticsForFile(
|
|
359
|
+
file: string,
|
|
360
|
+
cwd: string,
|
|
361
|
+
options?: LspRequestOptions,
|
|
362
|
+
): Promise<string> {
|
|
363
|
+
await touchFile(file, cwd, "document", options);
|
|
286
364
|
const all = await diagnostics();
|
|
287
365
|
const normalized = normalize(file);
|
|
288
366
|
return report(normalized, all[normalized] ?? []);
|
|
@@ -290,6 +368,8 @@ export function createLspService(
|
|
|
290
368
|
|
|
291
369
|
/** 终止全部服务器进程(session_shutdown 时调用)。 */
|
|
292
370
|
async function shutdownAll(): Promise<void> {
|
|
371
|
+
if (state.closing) return;
|
|
372
|
+
state.closing = true;
|
|
293
373
|
await Promise.all(state.clients.map((client) => client.shutdown())).catch(() => {
|
|
294
374
|
// 个别进程退出失败不阻止清理流程
|
|
295
375
|
});
|
|
@@ -297,21 +377,25 @@ export function createLspService(
|
|
|
297
377
|
state.broken.clear();
|
|
298
378
|
}
|
|
299
379
|
|
|
300
|
-
return {
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
ui = nextUi;
|
|
307
|
-
},
|
|
308
|
-
};
|
|
380
|
+
return { touchFile, diagnostics, lspDiagnosticsForFile, shutdownAll };
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
export interface LspServiceOptions {
|
|
384
|
+
adapters?: LspServerAdapter[];
|
|
385
|
+
globalConfigPath?: string;
|
|
309
386
|
}
|
|
310
387
|
|
|
311
|
-
/**
|
|
312
|
-
export function
|
|
313
|
-
|
|
314
|
-
pi.on?.("session_shutdown", () =>
|
|
315
|
-
|
|
388
|
+
/** 创建 LSP service 并注册 pi 的进程级清理生命周期。 */
|
|
389
|
+
export function registerLsp(pi: ExtensionAPI, options?: LspServiceOptions): LspService {
|
|
390
|
+
const service = createLspService(options?.adapters, options?.globalConfigPath);
|
|
391
|
+
pi.on?.("session_shutdown", () => service.shutdownAll());
|
|
392
|
+
// session 开始是最早能拿到本地配置 cwd 的时机:预加载并校验,配置错误立即通知
|
|
393
|
+
pi.on?.("session_start", (_event, ctx) => {
|
|
394
|
+
void loadLspConfig(ctx.cwd, options?.globalConfigPath)
|
|
395
|
+
.then((config) => validateConfig(config, options?.adapters))
|
|
396
|
+
.catch((error: unknown) => {
|
|
397
|
+
if (error instanceof Error) ctx.ui.notify?.(error.message, "error");
|
|
398
|
+
});
|
|
316
399
|
});
|
|
400
|
+
return service;
|
|
317
401
|
}
|
|
@@ -4,8 +4,8 @@
|
|
|
4
4
|
*
|
|
5
5
|
* 配置文件沿用 lsp.json(全局 ~/.pi/agent/lsp.json + 本地 <cwd>/.pi/lsp.json):
|
|
6
6
|
* 顶层 `servers` 是 id → 配置的 record,按 id 与内置默认服务器合并
|
|
7
|
-
*
|
|
8
|
-
* 白名单过滤。
|
|
7
|
+
* (同名 id 整体覆盖、新增 id、enabled:false 禁用),之后仍受现有
|
|
8
|
+
* enabled/disabled 白名单过滤。
|
|
9
9
|
*
|
|
10
10
|
* executable 发现统一由用户配置:bin 支持绝对路径 / 项目工作区
|
|
11
11
|
* (node_modules/.bin、.venv/bin、venv/bin)/ PATH,不再内置各语言的
|
|
@@ -111,7 +111,19 @@ export const defaultServers: Record<string, ServerConfig> = {
|
|
|
111
111
|
},
|
|
112
112
|
};
|
|
113
113
|
|
|
114
|
-
/**
|
|
114
|
+
/** 按 id 合并 servers record:同名 id 整体覆盖(不做逐字段 merge),其余保留;返回 undefined 表示没有任何 servers 定义。 */
|
|
115
|
+
export function mergeServerRecords(
|
|
116
|
+
...records: (Readonly<Record<string, ServerConfig>> | undefined)[]
|
|
117
|
+
): Record<string, ServerConfig> | undefined {
|
|
118
|
+
const merged: Record<string, ServerConfig> = {};
|
|
119
|
+
for (const record of records) {
|
|
120
|
+
if (!record) continue;
|
|
121
|
+
Object.assign(merged, record);
|
|
122
|
+
}
|
|
123
|
+
return Object.keys(merged).length > 0 ? merged : undefined;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 用户 servers(id → 配置)与默认配置合并:同名 id 整体覆盖、新增 id、enabled:false 移除。 */
|
|
115
127
|
export function mergeServerConfigs(
|
|
116
128
|
defaults: Readonly<Record<string, ServerConfig>>,
|
|
117
129
|
user: Readonly<Record<string, ServerConfig>> | undefined,
|
package/src/opencode/files.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Opencode File Tools —— read / edit / write 统一构建点。
|
|
3
3
|
*
|
|
4
4
|
* 三个工具在同一个 registerFileTools(pi, service) 里注册,共享同一个 LSP
|
|
5
|
-
* service 实例(
|
|
5
|
+
* service 实例(registerLsp 创建的闭包变量),与 claude-code/files.ts 共享
|
|
6
6
|
* read-snapshot state 的方式一致;不再用模块级全局缓存。
|
|
7
7
|
*
|
|
8
8
|
* 各工具行为对齐 opencode commit 999be62662(v1.2.25-1672-g999be62662,
|
|
@@ -30,7 +30,7 @@ import {
|
|
|
30
30
|
} from "@earendil-works/pi-coding-agent";
|
|
31
31
|
import { Type } from "typebox";
|
|
32
32
|
|
|
33
|
-
import {
|
|
33
|
+
import { type LspService, registerLsp } from "../lib/lsp/lsp.js";
|
|
34
34
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
35
35
|
import {
|
|
36
36
|
detectLineEnding,
|
|
@@ -695,7 +695,6 @@ export function registerFileTools(pi: ExtensionAPI, service: LspService): void {
|
|
|
695
695
|
|
|
696
696
|
/** 独立入口:创建 LSP service(闭包共享给三个工具)并注册。 */
|
|
697
697
|
export default function opencodeFileTools(pi: ExtensionAPI): void {
|
|
698
|
-
const service =
|
|
699
|
-
initLsp(pi, service);
|
|
698
|
+
const service = registerLsp(pi);
|
|
700
699
|
registerFileTools(pi, service);
|
|
701
700
|
}
|