@trim21/personal-pi-extensions 0.1.642 → 0.1.644
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/package.json +1 -1
- package/src/claude-code/files.ts +22 -36
- package/src/lib/file-reads.ts +61 -20
- package/src/lib/lsp/client.ts +23 -11
- package/src/lib/lsp/lsp.ts +32 -39
- package/src/opencode/files.ts +24 -37
package/package.json
CHANGED
package/src/claude-code/files.ts
CHANGED
|
@@ -17,7 +17,8 @@ import {
|
|
|
17
17
|
createReadsState,
|
|
18
18
|
type FileSnapshot,
|
|
19
19
|
type ReadsState,
|
|
20
|
-
|
|
20
|
+
recordRead,
|
|
21
|
+
recordReads,
|
|
21
22
|
requireCurrentRead,
|
|
22
23
|
restoreReads,
|
|
23
24
|
snapshotOf,
|
|
@@ -247,13 +248,11 @@ export function registerFileTools(
|
|
|
247
248
|
{ type: "text", text: `Read image file [${imageMime}]` },
|
|
248
249
|
{ type: "image", data, mimeType: imageMime },
|
|
249
250
|
];
|
|
250
|
-
const
|
|
251
|
-
const key = await readStateKey(filePath);
|
|
252
|
-
state.reads.set(key, snapshot);
|
|
251
|
+
const reads = await recordRead(state, filePath, snapshotOf(image, false));
|
|
253
252
|
return {
|
|
254
253
|
content,
|
|
255
254
|
details: {
|
|
256
|
-
reads
|
|
255
|
+
reads,
|
|
257
256
|
pendant: {
|
|
258
257
|
subtitle: formatSubtitlePath(ctx.cwd, filePath),
|
|
259
258
|
title: "Read",
|
|
@@ -264,7 +263,6 @@ export function registerFileTools(
|
|
|
264
263
|
|
|
265
264
|
const offset = params.offset ?? 1;
|
|
266
265
|
const limit = params.limit;
|
|
267
|
-
const key = await readStateKey(filePath);
|
|
268
266
|
const buffer = await readFile(filePath);
|
|
269
267
|
|
|
270
268
|
if (isBinary(buffer.subarray(0, SAMPLE_BYTES))) {
|
|
@@ -285,8 +283,7 @@ export function registerFileTools(
|
|
|
285
283
|
`File content (${estimatedTokens} tokens) exceeds maximum allowed tokens (${MAX_READ_TOKENS}). Use offset and limit parameters to read specific portions of the file, or search for specific content instead of reading the whole file.`,
|
|
286
284
|
);
|
|
287
285
|
}
|
|
288
|
-
const
|
|
289
|
-
state.reads.set(key, snapshot);
|
|
286
|
+
const reads = await recordRead(state, filePath, snapshotOf(buffer));
|
|
290
287
|
// 与 Edit / Write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
291
288
|
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
292
289
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
@@ -300,7 +297,7 @@ export function registerFileTools(
|
|
|
300
297
|
},
|
|
301
298
|
],
|
|
302
299
|
details: {
|
|
303
|
-
reads
|
|
300
|
+
reads,
|
|
304
301
|
pendant: {
|
|
305
302
|
subtitle: formatSubtitlePath(
|
|
306
303
|
ctx.cwd,
|
|
@@ -384,9 +381,7 @@ export function registerFileTools(
|
|
|
384
381
|
await mkdir(dirname(filePath), { recursive: true });
|
|
385
382
|
}
|
|
386
383
|
await writeFile(filePath, newString, "utf8");
|
|
387
|
-
const
|
|
388
|
-
const key = await readStateKey(filePath);
|
|
389
|
-
state.reads.set(key, snapshot);
|
|
384
|
+
const reads = await recordRead(state, filePath, snapshotOf(newString));
|
|
390
385
|
const diff = generateDiffString("", convertLeadingTabsToSpaces(newString));
|
|
391
386
|
signal?.throwIfAborted();
|
|
392
387
|
const diagnostics = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
@@ -399,7 +394,7 @@ export function registerFileTools(
|
|
|
399
394
|
diff: diff.diff,
|
|
400
395
|
patch: generateUnifiedPatch(filePath, "", convertLeadingTabsToSpaces(newString)),
|
|
401
396
|
firstChangedLine: diff.firstChangedLine,
|
|
402
|
-
reads
|
|
397
|
+
reads,
|
|
403
398
|
},
|
|
404
399
|
diagnostics,
|
|
405
400
|
];
|
|
@@ -436,8 +431,7 @@ export function registerFileTools(
|
|
|
436
431
|
"File is a Jupyter Notebook. Use the NotebookEditTool to edit this file.",
|
|
437
432
|
);
|
|
438
433
|
}
|
|
439
|
-
|
|
440
|
-
requireCurrentRead(state, key, filePath, content);
|
|
434
|
+
await requireCurrentRead(state, filePath, content);
|
|
441
435
|
await access(filePath, constants.R_OK | constants.W_OK);
|
|
442
436
|
signal?.throwIfAborted();
|
|
443
437
|
const original = content.toString("utf8");
|
|
@@ -450,8 +444,7 @@ export function registerFileTools(
|
|
|
450
444
|
signal,
|
|
451
445
|
});
|
|
452
446
|
await writeFile(filePath, restored, "utf8");
|
|
453
|
-
const
|
|
454
|
-
state.reads.set(key, snapshot);
|
|
447
|
+
const reads = await recordRead(state, filePath, snapshotOf(restored));
|
|
455
448
|
// patch/diff 仅供显示:前导 tab 转空格,避免 UI 渲染错位(对齐 Claude Code)
|
|
456
449
|
const diff = generateDiffString(
|
|
457
450
|
convertLeadingTabsToSpaces(original),
|
|
@@ -475,7 +468,7 @@ export function registerFileTools(
|
|
|
475
468
|
convertLeadingTabsToSpaces(restored),
|
|
476
469
|
),
|
|
477
470
|
firstChangedLine: diff.firstChangedLine,
|
|
478
|
-
reads
|
|
471
|
+
reads,
|
|
479
472
|
},
|
|
480
473
|
diagnostics,
|
|
481
474
|
];
|
|
@@ -527,13 +520,11 @@ export function registerFileTools(
|
|
|
527
520
|
[string, FileToolDetails, DiagnosticReport]
|
|
528
521
|
>(filePath, async () => {
|
|
529
522
|
let original: string | undefined;
|
|
530
|
-
let key: string | undefined;
|
|
531
523
|
try {
|
|
532
524
|
const value = await stat(filePath);
|
|
533
525
|
if (value.isFile()) {
|
|
534
526
|
const content = await readFile(filePath);
|
|
535
|
-
|
|
536
|
-
requireCurrentRead(state, key, filePath, content);
|
|
527
|
+
await requireCurrentRead(state, filePath, content);
|
|
537
528
|
original = content.toString("utf8");
|
|
538
529
|
}
|
|
539
530
|
} catch (error) {
|
|
@@ -552,10 +543,7 @@ export function registerFileTools(
|
|
|
552
543
|
});
|
|
553
544
|
await mkdir(dirname(filePath), { recursive: true });
|
|
554
545
|
await writeFile(filePath, params.content, "utf8");
|
|
555
|
-
const
|
|
556
|
-
// 新建文件:writeFile 之后 realpath 才能解析;覆盖写则复用上面的 key
|
|
557
|
-
const resolvedKey = key ?? (await readStateKey(filePath));
|
|
558
|
-
state.reads.set(resolvedKey, snapshot);
|
|
546
|
+
const reads = await recordRead(state, filePath, snapshotOf(params.content));
|
|
559
547
|
const diff = generateDiffString(original ?? "", params.content);
|
|
560
548
|
const text =
|
|
561
549
|
original === undefined
|
|
@@ -572,7 +560,7 @@ export function registerFileTools(
|
|
|
572
560
|
diff: diff.diff,
|
|
573
561
|
patch: generateUnifiedPatch(filePath, original ?? "", params.content),
|
|
574
562
|
firstChangedLine: diff.firstChangedLine,
|
|
575
|
-
reads
|
|
563
|
+
reads,
|
|
576
564
|
},
|
|
577
565
|
diagnostics,
|
|
578
566
|
];
|
|
@@ -639,16 +627,14 @@ export default function claudeCodeFileTools(
|
|
|
639
627
|
onEnabled: (pi, service) => {
|
|
640
628
|
registerLspRenameTool(pi, service, {
|
|
641
629
|
policy,
|
|
642
|
-
recordReads:
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
return reads;
|
|
651
|
-
},
|
|
630
|
+
recordReads: (applied) =>
|
|
631
|
+
recordReads(
|
|
632
|
+
state,
|
|
633
|
+
applied.map((fileEdit) => ({
|
|
634
|
+
path: fileEdit.path,
|
|
635
|
+
snapshot: snapshotOf(fileEdit.newText),
|
|
636
|
+
})),
|
|
637
|
+
),
|
|
652
638
|
});
|
|
653
639
|
registerLspInspectTools(pi, service);
|
|
654
640
|
},
|
package/src/lib/file-reads.ts
CHANGED
|
@@ -6,11 +6,15 @@
|
|
|
6
6
|
* - 工具名集合:claude-code 是大写 Read/Edit/Write,opencode 是小写;
|
|
7
7
|
* - 指纹来源:claude-code 手里已有内容,直接 snapshotOf(content);opencode 的
|
|
8
8
|
* read 是流式分页读取,整文件指纹用 fileDigest(path)。
|
|
9
|
+
*
|
|
10
|
+
* 记账 key 的解析与读写由本模块独占(readStateKey / recordRead / require*),
|
|
11
|
+
* 调用点只给路径与快照,不自己拼 details 里的 map。
|
|
9
12
|
*/
|
|
10
13
|
|
|
11
14
|
import { createHash } from "node:crypto";
|
|
12
15
|
import { createReadStream } from "node:fs";
|
|
13
16
|
import { realpath } from "node:fs/promises";
|
|
17
|
+
import { basename, dirname, join } from "node:path";
|
|
14
18
|
|
|
15
19
|
import type { ExtensionContext } from "@earendil-works/pi-coding-agent";
|
|
16
20
|
import { type Static, Type } from "typebox";
|
|
@@ -72,32 +76,73 @@ export async function digestIfExists(filePath: string): Promise<string | undefin
|
|
|
72
76
|
}
|
|
73
77
|
|
|
74
78
|
/**
|
|
75
|
-
* 记账 key:解析 symlink
|
|
76
|
-
* old_string
|
|
79
|
+
* 记账 key:解析 symlink 后的真实路径。文件尚未落盘(Write 新建 / Edit 空
|
|
80
|
+
* old_string 创建)时向上找到最深的已存在祖先、解析它再把剩余路径段拼回去——
|
|
81
|
+
* 结果与文件落盘后 realpath(filePath) 相同,因此调用点不必关心算 key 的时机
|
|
82
|
+
* (写入路径尤其容易在创建分支上算早)。
|
|
77
83
|
*/
|
|
78
84
|
export async function readStateKey(filePath: string): Promise<string> {
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
85
|
+
const pending: string[] = [];
|
|
86
|
+
let current = filePath;
|
|
87
|
+
for (;;) {
|
|
88
|
+
try {
|
|
89
|
+
return join(await realpath(current), ...pending.toReversed());
|
|
90
|
+
} catch (error) {
|
|
91
|
+
if (!isMissingPath(error)) {
|
|
92
|
+
throw error;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
const parent = dirname(current);
|
|
96
|
+
if (parent === current) {
|
|
97
|
+
// 一路到文件系统根仍不存在:无法解析,回退调用方给出的路径
|
|
83
98
|
return filePath;
|
|
84
99
|
}
|
|
85
|
-
|
|
100
|
+
pending.push(basename(current));
|
|
101
|
+
current = parent;
|
|
86
102
|
}
|
|
87
103
|
}
|
|
88
104
|
|
|
105
|
+
/**
|
|
106
|
+
* 记账「这些文件的内容即各自的 snapshot」:解析 key 写入 state,返回可直接放进
|
|
107
|
+
* 工具 details 的 reads 片段。
|
|
108
|
+
*
|
|
109
|
+
* 这一侧是对象而不是 Map:details 是 JSON(宿主按 `JsonValue` 写进 session
|
|
110
|
+
* JSONL),Map 序列化过去会变成 `{}`,记账就丢了;读取方向与之相对(
|
|
111
|
+
* `deserializeReads` 从同一个对象形状还原成 Map)。
|
|
112
|
+
*/
|
|
113
|
+
export async function recordReads(
|
|
114
|
+
state: ReadsState,
|
|
115
|
+
entries: readonly { path: string; snapshot: FileSnapshot }[],
|
|
116
|
+
): Promise<Record<string, FileSnapshot>> {
|
|
117
|
+
const reads: Record<string, FileSnapshot> = {};
|
|
118
|
+
for (const entry of entries) {
|
|
119
|
+
const key = await readStateKey(entry.path);
|
|
120
|
+
state.reads.set(key, entry.snapshot);
|
|
121
|
+
reads[key] = entry.snapshot;
|
|
122
|
+
}
|
|
123
|
+
return reads;
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** 单文件记账:`recordReads` 的单项形式。 */
|
|
127
|
+
export async function recordRead(
|
|
128
|
+
state: ReadsState,
|
|
129
|
+
filePath: string,
|
|
130
|
+
snapshot: FileSnapshot,
|
|
131
|
+
): Promise<Record<string, FileSnapshot>> {
|
|
132
|
+
return await recordReads(state, [{ path: filePath, snapshot }]);
|
|
133
|
+
}
|
|
134
|
+
|
|
89
135
|
function snapshotsEqual(left: FileSnapshot, right: FileSnapshot): boolean {
|
|
90
136
|
return left.digest === right.digest;
|
|
91
137
|
}
|
|
92
138
|
|
|
93
139
|
/** 校验「已读且未变」:未读过、非文本、指纹不符都拒绝写入。 */
|
|
94
|
-
export function requireCurrentRead(
|
|
140
|
+
export async function requireCurrentRead(
|
|
95
141
|
state: ReadsState,
|
|
96
|
-
key: string,
|
|
97
142
|
filePath: string,
|
|
98
143
|
currentContent: Uint8Array | string,
|
|
99
|
-
): void {
|
|
100
|
-
const readSnapshot = state.reads.get(
|
|
144
|
+
): Promise<void> {
|
|
145
|
+
const readSnapshot = state.reads.get(await readStateKey(filePath));
|
|
101
146
|
if (!readSnapshot) {
|
|
102
147
|
throw new Error("File has not been read yet. Read it first before writing to it.");
|
|
103
148
|
}
|
|
@@ -113,21 +158,17 @@ export function requireCurrentRead(
|
|
|
113
158
|
|
|
114
159
|
/**
|
|
115
160
|
* 过期校验:该文件已有读取记录时,要求磁盘上的当前指纹与记录一致,否则拒绝
|
|
116
|
-
*
|
|
161
|
+
* 写入;从未读过(没有记录)时直接放行,且不做整文件指纹计算。
|
|
117
162
|
*
|
|
118
163
|
* opencode 的 write 用这个:没读过的文件允许直接写,读过之后再被外部改动就必须
|
|
119
|
-
* 重新 read
|
|
164
|
+
* 重新 read。文件已不存在(指纹 undefined)同样算过期。
|
|
120
165
|
*/
|
|
121
|
-
export function requireUnchangedRead(
|
|
122
|
-
state
|
|
123
|
-
key: string,
|
|
124
|
-
currentDigest: string | undefined,
|
|
125
|
-
): void {
|
|
126
|
-
const readSnapshot = state.reads.get(key);
|
|
166
|
+
export async function requireUnchangedRead(state: ReadsState, filePath: string): Promise<void> {
|
|
167
|
+
const readSnapshot = state.reads.get(await readStateKey(filePath));
|
|
127
168
|
if (!readSnapshot) {
|
|
128
169
|
return;
|
|
129
170
|
}
|
|
130
|
-
if (
|
|
171
|
+
if (readSnapshot.digest !== (await digestIfExists(filePath))) {
|
|
131
172
|
throw new Error(
|
|
132
173
|
"File has been modified since read, either by the user or by a linter. Read it again before attempting to write it.",
|
|
133
174
|
);
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -282,21 +282,33 @@ export const clientDefaults = {
|
|
|
282
282
|
maxOpenDocuments: 32,
|
|
283
283
|
} as const;
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
/**
|
|
286
|
+
* 客户端可调项的唯一定义:`create()` 的覆盖参数、`lsp.ts` 的 `ResolvedLspConfig`
|
|
287
|
+
* 与 `resolveConfig` 共用同一份字段集合,新增旋钮只需在这里声明一次即可被
|
|
288
|
+
* 配置解析与客户端构造同时要求。超时均为解析后的毫秒数,缺省值见 clientDefaults。
|
|
289
|
+
*/
|
|
290
|
+
export interface LspClientOptions {
|
|
291
|
+
/** push 诊断去抖(ms)。 */
|
|
292
|
+
diagnosticsDebounceMs: number;
|
|
293
|
+
/** document 模式诊断等待上限(ms)。 */
|
|
294
|
+
diagnosticsDocumentWaitTimeoutMs: number;
|
|
295
|
+
/** 上一份诊断为空的文档的等待上限(ms);缺省见 clientDefaults。 */
|
|
296
|
+
diagnosticsSilentWaitTimeoutMs: number;
|
|
297
|
+
/** full 模式诊断等待上限(ms)。 */
|
|
298
|
+
diagnosticsFullWaitTimeoutMs: number;
|
|
299
|
+
/** 单次 pull 诊断请求超时(ms)。 */
|
|
300
|
+
diagnosticsRequestTimeoutMs: number;
|
|
301
|
+
/** 服务器 initialize 握手超时(ms)。 */
|
|
302
|
+
initializeTimeoutMs: number;
|
|
303
|
+
/** 驻留文档上限(LRU 容量,缺省 32);超过时淘汰最久未使用并 didClose。 */
|
|
304
|
+
maxOpenDocuments: number;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
export interface CreateInput extends Partial<LspClientOptions> {
|
|
286
308
|
serverID: string;
|
|
287
309
|
server: LspServerHandle;
|
|
288
310
|
root: string;
|
|
289
311
|
directory: string;
|
|
290
|
-
/** 可覆盖的超时参数(缺省用 client 默认值,由全局/本地 lsp.json 配置注入)。 */
|
|
291
|
-
diagnosticsDebounceMs?: number;
|
|
292
|
-
diagnosticsDocumentWaitTimeoutMs?: number;
|
|
293
|
-
/** 上一份诊断为空的文档的等待上限;缺省见 clientDefaults。 */
|
|
294
|
-
diagnosticsSilentWaitTimeoutMs?: number;
|
|
295
|
-
diagnosticsFullWaitTimeoutMs?: number;
|
|
296
|
-
diagnosticsRequestTimeoutMs?: number;
|
|
297
|
-
initializeTimeoutMs?: number;
|
|
298
|
-
/** 驻留文档上限(LRU 容量,缺省 32);超过时淘汰最久未使用并 didClose。 */
|
|
299
|
-
maxOpenDocuments?: number;
|
|
300
312
|
/** 覆盖 rename 覆盖校验的轮询节奏(缺省见 DEFAULT_RENAME_VERIFICATION_TIMING)。 */
|
|
301
313
|
renameVerificationTiming?: Partial<RenameVerificationTiming>;
|
|
302
314
|
}
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -37,6 +37,7 @@ import {
|
|
|
37
37
|
type Diagnostic,
|
|
38
38
|
type Info as LspClient,
|
|
39
39
|
type InspectLocation,
|
|
40
|
+
type LspClientOptions,
|
|
40
41
|
LspMethodNotSupportedError,
|
|
41
42
|
RenameNotPossibleError,
|
|
42
43
|
WATCH_KIND_CHANGE,
|
|
@@ -68,7 +69,7 @@ const watchConfigSchema = Type.Object({
|
|
|
68
69
|
ignore: Type.Optional(Type.Array(Type.String())),
|
|
69
70
|
});
|
|
70
71
|
|
|
71
|
-
/** lsp.json 的配置项(全局与本地同构;只描述用户可写的原始形态,缺省见
|
|
72
|
+
/** lsp.json 的配置项(全局与本地同构;只描述用户可写的原始形态,缺省见 watchDefaults / clientDefaults)。 */
|
|
72
73
|
const lspConfigSchema = Type.Object({
|
|
73
74
|
/** 配置文件版本(当前 1);未知版本会被 typebox 严格校验拒绝。 */
|
|
74
75
|
version: Type.Optional(Type.Number()),
|
|
@@ -78,7 +79,7 @@ const lspConfigSchema = Type.Object({
|
|
|
78
79
|
enabled: Type.Optional(Type.Array(Type.String())),
|
|
79
80
|
/** 从启用集中排除的服务器 id(缺省 = 无)。 */
|
|
80
81
|
disabled: Type.Optional(Type.Array(Type.String())),
|
|
81
|
-
/** 工作区文件监听配置(缺省全部字段见
|
|
82
|
+
/** 工作区文件监听配置(缺省全部字段见 watchDefaults)。 */
|
|
82
83
|
watch: Type.Optional(watchConfigSchema),
|
|
83
84
|
/** 驻留文档上限(LRU 容量),超过时淘汰最久未使用并 didClose。 */
|
|
84
85
|
maxOpenDocuments: Type.Optional(Type.Number({ minimum: 1 })),
|
|
@@ -99,18 +100,12 @@ const lspConfigSchema = Type.Object({
|
|
|
99
100
|
/** 配置值(单文件解析结果);超时字段为原始写法(number 或字符串),换算在 resolveConfig。 */
|
|
100
101
|
export type LspConfig = Static<typeof lspConfigSchema>;
|
|
101
102
|
|
|
102
|
-
/**
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
enabled: true,
|
|
109
|
-
debounceMs: 300,
|
|
110
|
-
flushMs: 1_000,
|
|
111
|
-
maxBatch: 500,
|
|
112
|
-
},
|
|
113
|
-
maxOpenDocuments: clientDefaults.maxOpenDocuments,
|
|
103
|
+
/** 解析期缺省:watch 段(无 client 对应项,数值在此集中);客户端参数缺省见 clientDefaults。 */
|
|
104
|
+
const watchDefaults = {
|
|
105
|
+
enabled: true,
|
|
106
|
+
debounceMs: 300,
|
|
107
|
+
flushMs: 1_000,
|
|
108
|
+
maxBatch: 500,
|
|
114
109
|
} as const;
|
|
115
110
|
|
|
116
111
|
/** 服务器启动失败后自动重试的冷却(ms);冷却内跳过,之后下次触碰自动重试。 */
|
|
@@ -127,8 +122,12 @@ interface EffectiveWatchConfig {
|
|
|
127
122
|
ignore: string[];
|
|
128
123
|
}
|
|
129
124
|
|
|
130
|
-
/**
|
|
131
|
-
|
|
125
|
+
/**
|
|
126
|
+
* 全局 + 本地合并并解析后的生效配置:所有字段为确定值,无"未配置"歧义。
|
|
127
|
+
* 客户端参数的整体形状复用 `LspClientOptions`(定义在 client.ts,与
|
|
128
|
+
* `create()` 的覆盖参数同源),因此这里不再逐字段重声明。
|
|
129
|
+
*/
|
|
130
|
+
export interface ResolvedLspConfig extends LspClientOptions {
|
|
132
131
|
/** 合并后的服务器定义(未配置任何服务器时为空表)。 */
|
|
133
132
|
servers: Record<string, ServerConfig>;
|
|
134
133
|
/** enabled 白名单(undefined = 全部启用)。 */
|
|
@@ -137,15 +136,6 @@ export interface ResolvedLspConfig {
|
|
|
137
136
|
disabled: Set<string> | undefined;
|
|
138
137
|
/** 工作区监听配置(缺省值已应用)。 */
|
|
139
138
|
watch: EffectiveWatchConfig;
|
|
140
|
-
/** 驻留文档 LRU 容量。 */
|
|
141
|
-
maxOpenDocuments: number;
|
|
142
|
-
/** 以下超时均为换算后的毫秒数(缺省见 configDefaults / clientDefaults)。 */
|
|
143
|
-
diagnosticsDebounceMs: number;
|
|
144
|
-
diagnosticsDocumentWaitTimeoutMs: number;
|
|
145
|
-
diagnosticsSilentWaitTimeoutMs: number;
|
|
146
|
-
diagnosticsFullWaitTimeoutMs: number;
|
|
147
|
-
diagnosticsRequestTimeoutMs: number;
|
|
148
|
-
initializeTimeoutMs: number;
|
|
149
139
|
}
|
|
150
140
|
|
|
151
141
|
/** "500" → 500、"5s" → 5000、"1m" → 60000;无效字符串返回 NaN(由调用方兜底缺省)。 */
|
|
@@ -177,7 +167,7 @@ function toMs(value: number | string | undefined): number | undefined {
|
|
|
177
167
|
}
|
|
178
168
|
|
|
179
169
|
/**
|
|
180
|
-
* 把合并后的原始配置解析为生效配置:应用
|
|
170
|
+
* 把合并后的原始配置解析为生效配置:应用 watchDefaults / clientDefaults 缺省、字符串时长换算、
|
|
181
171
|
* 白名单转 Set;per-server 的 workingDir / rootMarkers 互斥校验在此完成
|
|
182
172
|
* (配置解析期抛错,与 enabled 白名单校验同层)。
|
|
183
173
|
*/
|
|
@@ -195,13 +185,13 @@ export function resolveConfig(raw: LspConfig): ResolvedLspConfig {
|
|
|
195
185
|
enabled: raw.enabled === undefined ? undefined : new Set(raw.enabled),
|
|
196
186
|
disabled: raw.disabled === undefined ? undefined : new Set(raw.disabled),
|
|
197
187
|
watch: {
|
|
198
|
-
enabled: raw.watch?.enabled ??
|
|
199
|
-
debounceMs: toMs(raw.watch?.debounceMs) ??
|
|
200
|
-
flushMs:
|
|
201
|
-
maxBatch: raw.watch?.maxBatch ??
|
|
188
|
+
enabled: raw.watch?.enabled ?? watchDefaults.enabled,
|
|
189
|
+
debounceMs: toMs(raw.watch?.debounceMs) ?? watchDefaults.debounceMs,
|
|
190
|
+
flushMs: watchDefaults.flushMs,
|
|
191
|
+
maxBatch: raw.watch?.maxBatch ?? watchDefaults.maxBatch,
|
|
202
192
|
ignore: raw.watch?.ignore ?? [],
|
|
203
193
|
},
|
|
204
|
-
maxOpenDocuments: raw.maxOpenDocuments ??
|
|
194
|
+
maxOpenDocuments: raw.maxOpenDocuments ?? clientDefaults.maxOpenDocuments,
|
|
205
195
|
diagnosticsDebounceMs: toMs(raw.diagnosticsDebounceMs) ?? clientDefaults.diagnosticsDebounceMs,
|
|
206
196
|
diagnosticsDocumentWaitTimeoutMs:
|
|
207
197
|
toMs(raw.diagnosticsDocumentWaitTimeoutMs) ?? clientDefaults.diagnosticsDocumentWaitTimeoutMs,
|
|
@@ -848,19 +838,22 @@ export function createLspService(
|
|
|
848
838
|
reportStartupFailure(key, adapter.id, root, "binary not found", notify);
|
|
849
839
|
return;
|
|
850
840
|
}
|
|
841
|
+
const overrides: Partial<LspClientOptions> = {};
|
|
842
|
+
if (adapter.diagnosticsWaitMs !== undefined) {
|
|
843
|
+
overrides.diagnosticsDocumentWaitTimeoutMs = adapter.diagnosticsWaitMs;
|
|
844
|
+
}
|
|
845
|
+
if (adapter.startupTimeoutMs !== undefined) {
|
|
846
|
+
overrides.initializeTimeoutMs = adapter.startupTimeoutMs;
|
|
847
|
+
}
|
|
851
848
|
const client = await create({
|
|
852
849
|
serverID: adapter.id,
|
|
853
850
|
server: handle,
|
|
854
851
|
root,
|
|
855
852
|
directory: cwd,
|
|
856
|
-
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
|
|
861
|
-
diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
|
|
862
|
-
initializeTimeoutMs: adapter.startupTimeoutMs ?? config.initializeTimeoutMs,
|
|
863
|
-
maxOpenDocuments: config.maxOpenDocuments,
|
|
853
|
+
// 客户端参数整体取自生效配置(ResolvedLspConfig extends LspClientOptions),
|
|
854
|
+
// 不做逐字段搬运:新增旋钮时漏接线会静默失效,这里靠展开消除该位置。
|
|
855
|
+
...config,
|
|
856
|
+
...overrides,
|
|
864
857
|
});
|
|
865
858
|
if (state.closing || state.disabled) {
|
|
866
859
|
await client.shutdown();
|
package/src/opencode/files.ts
CHANGED
|
@@ -38,11 +38,11 @@ import { Type } from "typebox";
|
|
|
38
38
|
|
|
39
39
|
import {
|
|
40
40
|
createReadsState,
|
|
41
|
-
digestIfExists,
|
|
42
41
|
fileDigest,
|
|
43
42
|
type FileSnapshot,
|
|
44
43
|
type ReadsState,
|
|
45
|
-
|
|
44
|
+
recordRead,
|
|
45
|
+
recordReads,
|
|
46
46
|
requireCurrentRead,
|
|
47
47
|
requireUnchangedRead,
|
|
48
48
|
restoreReads,
|
|
@@ -500,13 +500,11 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
|
|
|
500
500
|
{ type: "text", text: "Image read successfully" },
|
|
501
501
|
{ type: "image", data: base64, mimeType },
|
|
502
502
|
];
|
|
503
|
-
const
|
|
504
|
-
const snapshot = snapshotOf(buffer, false);
|
|
505
|
-
state.reads.set(key, snapshot);
|
|
503
|
+
const reads = await recordRead(state, absolutePath, snapshotOf(buffer, false));
|
|
506
504
|
return {
|
|
507
505
|
content,
|
|
508
506
|
details: {
|
|
509
|
-
reads
|
|
507
|
+
reads,
|
|
510
508
|
pendant: { subtitle: formatSubtitlePath(ctx.cwd, absolutePath) },
|
|
511
509
|
},
|
|
512
510
|
};
|
|
@@ -521,7 +519,6 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
|
|
|
521
519
|
|
|
522
520
|
// 记账指纹先于分页读取:文件若在读取期间被改动,指纹对应的是改动前的
|
|
523
521
|
// 内容,后续 edit 会要求重新 read(失败方向安全)。
|
|
524
|
-
const key = await readStateKey(absolutePath);
|
|
525
522
|
const snapshot: FileSnapshot = { digest: await fileDigest(absolutePath), textEditable: true };
|
|
526
523
|
|
|
527
524
|
const effectiveOffset = offset || 1;
|
|
@@ -555,7 +552,7 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
|
|
|
555
552
|
outputText = `${header}${numbered}\n\n(End of file - total ${page.count} lines)${footer}`;
|
|
556
553
|
}
|
|
557
554
|
|
|
558
|
-
|
|
555
|
+
const reads = await recordRead(state, absolutePath, snapshot);
|
|
559
556
|
|
|
560
557
|
// opencode: 与 edit / write 同一条驻留路径:didOpen 后等待该文件的诊断并报告
|
|
561
558
|
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
@@ -572,7 +569,7 @@ function registerReadTool(pi: ExtensionAPI, getService: () => LspService, state:
|
|
|
572
569
|
content,
|
|
573
570
|
details: {
|
|
574
571
|
...details,
|
|
575
|
-
reads
|
|
572
|
+
reads,
|
|
576
573
|
pendant: {
|
|
577
574
|
subtitle: formatSubtitlePath(
|
|
578
575
|
ctx.cwd,
|
|
@@ -667,16 +664,14 @@ function registerEditTool(
|
|
|
667
664
|
await mkdir(dirname(absolutePath), { recursive: true });
|
|
668
665
|
signal?.throwIfAborted();
|
|
669
666
|
await writeFile(absolutePath, newString, "utf8");
|
|
670
|
-
// 新建文件无需先 read
|
|
671
|
-
const
|
|
672
|
-
const snapshot = snapshotOf(newString);
|
|
673
|
-
state.reads.set(key, snapshot);
|
|
667
|
+
// 新建文件无需先 read(无处可读),写后记账
|
|
668
|
+
const reads = await recordRead(state, absolutePath, snapshotOf(newString));
|
|
674
669
|
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
675
670
|
signal,
|
|
676
671
|
});
|
|
677
672
|
return [
|
|
678
673
|
"Edit applied successfully.",
|
|
679
|
-
{ diff: "", patch: "", firstChangedLine: 0, reads
|
|
674
|
+
{ diff: "", patch: "", firstChangedLine: 0, reads },
|
|
680
675
|
diagnostics,
|
|
681
676
|
] as const;
|
|
682
677
|
}
|
|
@@ -702,8 +697,7 @@ function registerEditTool(
|
|
|
702
697
|
|
|
703
698
|
// 指纹校验必须发生在写入之前:未 read 或外部已改动都拒绝编辑
|
|
704
699
|
const rawContent = await readFile(absolutePath, "utf8");
|
|
705
|
-
|
|
706
|
-
requireCurrentRead(state, key, absolutePath, rawContent);
|
|
700
|
+
await requireCurrentRead(state, absolutePath, rawContent);
|
|
707
701
|
|
|
708
702
|
const applied = applyEdit(rawContent, oldString, newString, replaceAll);
|
|
709
703
|
signal?.throwIfAborted();
|
|
@@ -715,8 +709,7 @@ function registerEditTool(
|
|
|
715
709
|
signal,
|
|
716
710
|
});
|
|
717
711
|
await writeFile(absolutePath, applied.finalContent, "utf8");
|
|
718
|
-
const
|
|
719
|
-
state.reads.set(key, snapshot);
|
|
712
|
+
const reads = await recordRead(state, absolutePath, snapshotOf(applied.finalContent));
|
|
720
713
|
|
|
721
714
|
const diffOld = normalizeToLF(applied.contentOld);
|
|
722
715
|
const diffNew = normalizeToLF(applied.contentNew);
|
|
@@ -731,7 +724,7 @@ function registerEditTool(
|
|
|
731
724
|
diff: diffResult.diff,
|
|
732
725
|
patch,
|
|
733
726
|
firstChangedLine: diffResult.firstChangedLine,
|
|
734
|
-
reads
|
|
727
|
+
reads,
|
|
735
728
|
},
|
|
736
729
|
diagnostics,
|
|
737
730
|
] as const;
|
|
@@ -811,12 +804,9 @@ function registerWriteTool(
|
|
|
811
804
|
async () => {
|
|
812
805
|
signal?.throwIfAborted();
|
|
813
806
|
|
|
814
|
-
const key = await readStateKey(absolutePath);
|
|
815
807
|
// write 不要求先读过;但若已有读取记录,磁盘内容必须仍是读取时的样子,
|
|
816
808
|
// 否则先重新 read(外部改动过的文件不让盲写覆盖)
|
|
817
|
-
|
|
818
|
-
requireUnchangedRead(state, key, await digestIfExists(absolutePath));
|
|
819
|
-
}
|
|
809
|
+
await requireUnchangedRead(state, absolutePath);
|
|
820
810
|
|
|
821
811
|
// opencode: desiredBom = source.bom || next.bom —— 保留原文件 BOM,
|
|
822
812
|
// 否则用新内容自带的 BOM。旧内容整读一次,既是 BOM 的来源,也是审批预览
|
|
@@ -842,14 +832,13 @@ function registerWriteTool(
|
|
|
842
832
|
await mkdir(dir, { recursive: true });
|
|
843
833
|
signal?.throwIfAborted();
|
|
844
834
|
await writeFile(absolutePath, finalContent, "utf8");
|
|
845
|
-
//
|
|
846
|
-
const
|
|
847
|
-
state.reads.set(key, snapshot);
|
|
835
|
+
// 写后记账:紧接着的 edit 不该再要求重新 read
|
|
836
|
+
const reads = await recordRead(state, absolutePath, snapshotOf(finalContent));
|
|
848
837
|
const diagnostics = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, {
|
|
849
838
|
signal,
|
|
850
839
|
});
|
|
851
840
|
|
|
852
|
-
return ["Wrote file successfully.", { reads
|
|
841
|
+
return ["Wrote file successfully.", { reads }, diagnostics] as const;
|
|
853
842
|
},
|
|
854
843
|
);
|
|
855
844
|
|
|
@@ -906,16 +895,14 @@ export default function opencodeFileTools(
|
|
|
906
895
|
onEnabled: (pi, service) => {
|
|
907
896
|
registerLspRenameTool(pi, service, {
|
|
908
897
|
policy,
|
|
909
|
-
recordReads:
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
return reads;
|
|
918
|
-
},
|
|
898
|
+
recordReads: (applied) =>
|
|
899
|
+
recordReads(
|
|
900
|
+
state,
|
|
901
|
+
applied.map((fileEdit) => ({
|
|
902
|
+
path: fileEdit.path,
|
|
903
|
+
snapshot: snapshotOf(fileEdit.newText),
|
|
904
|
+
})),
|
|
905
|
+
),
|
|
919
906
|
});
|
|
920
907
|
registerLspInspectTools(pi, service);
|
|
921
908
|
},
|