@trim21/personal-pi-extensions 0.1.490 → 0.1.491
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/bwrap/approval-suggest.ts +1 -1
- package/src/bwrap/core.ts +6 -6
- package/src/bwrap/mihomo-config.ts +3 -1
- package/src/bwrap/network-stack.ts +2 -2
- package/src/claude-code/edit-utils.ts +4 -4
- package/src/claude-code/files.ts +40 -28
- package/src/claude-code/glob.ts +1 -1
- package/src/gh-readonly.ts +12 -12
- package/src/lib/cli.ts +3 -6
- package/src/lib/lsp/client.ts +12 -9
- package/src/lib/lsp/inspect-tool.ts +2 -2
- package/src/lib/lsp/language.ts +1 -1
- package/src/lib/lsp/lsp.ts +123 -33
- package/src/lib/lsp/rename-tool.ts +2 -2
- package/src/lib/ui.ts +2 -2
- package/src/lib/write-guard.ts +1 -1
- package/src/openai-cost/config.ts +1 -1
- package/src/openai-cost/cost.ts +1 -1
- package/src/opencode/edit-engine.ts +1 -1
- package/src/opencode/files.ts +33 -25
- package/src/talk/index.ts +2 -8
- package/src/talk/registry.ts +1 -1
- package/src/vision-agent.ts +2 -2
package/package.json
CHANGED
package/src/bwrap/core.ts
CHANGED
|
@@ -105,11 +105,11 @@ export function resolveBwrap(config: BwrapConfig): ResolvedBwrap {
|
|
|
105
105
|
const base = {
|
|
106
106
|
mode: config.mode,
|
|
107
107
|
bwrapPath: config.bwrapPath,
|
|
108
|
-
writablePaths: config.writablePaths
|
|
108
|
+
writablePaths: config.writablePaths,
|
|
109
109
|
extraWritablePaths: config.extraWritablePaths,
|
|
110
|
-
denyPaths: config.denyPaths
|
|
111
|
-
extraArgs: config.extraArgs
|
|
112
|
-
networkAllowlist: config.networkAllowlist
|
|
110
|
+
denyPaths: config.denyPaths,
|
|
111
|
+
extraArgs: config.extraArgs,
|
|
112
|
+
networkAllowlist: config.networkAllowlist,
|
|
113
113
|
mihomoPath: config.mihomoPath,
|
|
114
114
|
slirp4netnsPath: config.slirp4netnsPath,
|
|
115
115
|
approvalRules: config.approvalRules ?? [],
|
|
@@ -529,8 +529,8 @@ export function createBwrapBashOperations(
|
|
|
529
529
|
}, timeout * 1000)
|
|
530
530
|
: undefined;
|
|
531
531
|
const onAbort = () => killChild(child);
|
|
532
|
-
child.stdout
|
|
533
|
-
child.stderr
|
|
532
|
+
child.stdout.on("data", onData);
|
|
533
|
+
child.stderr.on("data", onData);
|
|
534
534
|
signal?.addEventListener("abort", onAbort, { once: true });
|
|
535
535
|
child.once("error", reject);
|
|
536
536
|
child.once("close", (exitCode) => {
|
|
@@ -81,7 +81,9 @@ function parseAllowlistEntry(entry: string): AllowlistEntry {
|
|
|
81
81
|
const match = /^\[(.+)\](?::(\d+))?$/.exec(entry);
|
|
82
82
|
if (match?.[1] === undefined) throw new Error(`Invalid allowlist entry "${entry}"`);
|
|
83
83
|
host = match[1];
|
|
84
|
-
|
|
84
|
+
// 端口组 (?::(\d+))? 可选:无端口时 match[2] 在运行时是 undefined
|
|
85
|
+
const portPart = match[2] as string | undefined;
|
|
86
|
+
port = portPart === undefined ? undefined : parsePort(portPart);
|
|
85
87
|
} else {
|
|
86
88
|
const colon = entry.lastIndexOf(":");
|
|
87
89
|
if (colon === -1) {
|
|
@@ -332,8 +332,8 @@ export async function startNetworkStack(options: NetworkStackOptions): Promise<N
|
|
|
332
332
|
killChild(child.pid);
|
|
333
333
|
};
|
|
334
334
|
|
|
335
|
-
child.stdout
|
|
336
|
-
child.stderr
|
|
335
|
+
child.stdout.on("data", execOptions.onData);
|
|
336
|
+
child.stderr.on("data", execOptions.onData);
|
|
337
337
|
execOptions.signal?.addEventListener("abort", onAbort, { once: true });
|
|
338
338
|
|
|
339
339
|
child.once("error", (error) => {
|
|
@@ -73,10 +73,10 @@ function applyCurlySingleQuotes(str: string): string {
|
|
|
73
73
|
for (let i = 0; i < chars.length; i++) {
|
|
74
74
|
if (chars[i] === "'") {
|
|
75
75
|
// 缩写中的撇号(如 don't)用右花引号,不做开/闭判断
|
|
76
|
-
const prev = chars[i - 1];
|
|
77
|
-
const next = chars[i + 1];
|
|
78
|
-
const prevIsLetter =
|
|
79
|
-
const nextIsLetter =
|
|
76
|
+
const prev = i > 0 ? chars[i - 1] : "";
|
|
77
|
+
const next = i + 1 < chars.length ? chars[i + 1] : "";
|
|
78
|
+
const prevIsLetter = /\p{L}/u.test(prev);
|
|
79
|
+
const nextIsLetter = /\p{L}/u.test(next);
|
|
80
80
|
if (prevIsLetter && nextIsLetter) {
|
|
81
81
|
result.push(RIGHT_SINGLE_CURLY_QUOTE);
|
|
82
82
|
} else {
|
package/src/claude-code/files.ts
CHANGED
|
@@ -16,7 +16,7 @@ import { Type } from "typebox";
|
|
|
16
16
|
|
|
17
17
|
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
18
18
|
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
19
|
-
import { type LspService,
|
|
19
|
+
import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
|
|
20
20
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
21
21
|
import { formatSubtitlePath } from "../lib/path.js";
|
|
22
22
|
import type { ToolPendant } from "../lib/pendant.ts";
|
|
@@ -229,7 +229,7 @@ function requireCurrentRead(
|
|
|
229
229
|
export function registerFileTools(
|
|
230
230
|
pi: ExtensionAPI,
|
|
231
231
|
state: ClaudeCodeState,
|
|
232
|
-
|
|
232
|
+
getService: () => LspService,
|
|
233
233
|
): void {
|
|
234
234
|
pi.registerTool({
|
|
235
235
|
name: "Read",
|
|
@@ -352,9 +352,11 @@ export function registerFileTools(
|
|
|
352
352
|
const snapshot = { ...snapshotOf(buffer), offset, limit };
|
|
353
353
|
state.reads.set(key, snapshot);
|
|
354
354
|
// LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
|
|
355
|
-
void
|
|
356
|
-
|
|
357
|
-
|
|
355
|
+
void getService()
|
|
356
|
+
.notifyFile(filePath, ctx.cwd)
|
|
357
|
+
.catch(() => {
|
|
358
|
+
// 后台通知失败不影响读取
|
|
359
|
+
});
|
|
358
360
|
return {
|
|
359
361
|
content: [{ type: "text", text: formatted.text }],
|
|
360
362
|
details: {
|
|
@@ -444,7 +446,7 @@ export function registerFileTools(
|
|
|
444
446
|
text: diagnosticText,
|
|
445
447
|
errorCount,
|
|
446
448
|
warningCount,
|
|
447
|
-
} = await
|
|
449
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
448
450
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
449
451
|
signal,
|
|
450
452
|
});
|
|
@@ -547,7 +549,7 @@ export function registerFileTools(
|
|
|
547
549
|
text: diagnosticText,
|
|
548
550
|
errorCount,
|
|
549
551
|
warningCount,
|
|
550
|
-
} = await
|
|
552
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
551
553
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
552
554
|
});
|
|
553
555
|
return [
|
|
@@ -646,7 +648,7 @@ export function registerFileTools(
|
|
|
646
648
|
text: diagnosticText,
|
|
647
649
|
errorCount,
|
|
648
650
|
warningCount,
|
|
649
|
-
} = await
|
|
651
|
+
} = await getService().lspDiagnosticsForFile(filePath, ctx.cwd, {
|
|
650
652
|
notify: (message, level) => ctx.ui.notify(message, level),
|
|
651
653
|
});
|
|
652
654
|
return [
|
|
@@ -675,23 +677,8 @@ export function registerFileTools(
|
|
|
675
677
|
},
|
|
676
678
|
});
|
|
677
679
|
|
|
678
|
-
//
|
|
679
|
-
//
|
|
680
|
-
// 持久化(FILE_TOOL_NAMES 的 restoreFileReads 依赖 details.reads)。
|
|
681
|
-
registerLspRenameTool(pi, service, {
|
|
682
|
-
recordReads: async (applied) => {
|
|
683
|
-
const reads: Record<string, FileSnapshot> = {};
|
|
684
|
-
for (const fileEdit of applied) {
|
|
685
|
-
const key = await readStateKey(fileEdit.path);
|
|
686
|
-
const snapshot = snapshotOf(fileEdit.newText);
|
|
687
|
-
state.reads.set(key, snapshot);
|
|
688
|
-
reads[key] = snapshot;
|
|
689
|
-
}
|
|
690
|
-
return reads;
|
|
691
|
-
},
|
|
692
|
-
});
|
|
693
|
-
// 只读符号查询工具(find-definition / find-reference / inspect)与 opencode 共享。
|
|
694
|
-
registerLspInspectTools(pi, service);
|
|
680
|
+
// 只读符号查询工具(find-definition / find-reference / inspect)与 opencode 共享;
|
|
681
|
+
// 它们由 manager 的 onEnabled 回调注册,不在这里注册。
|
|
695
682
|
}
|
|
696
683
|
|
|
697
684
|
/** 会更新 reads state 并随 details 持久化快照的工具名。 */
|
|
@@ -723,10 +710,34 @@ function restoreFileReads(
|
|
|
723
710
|
* state 归本文件所有:扩展实例内创建,并随 session 事件从历史分支恢复,
|
|
724
711
|
* 与主进程 index.ts 聚合加载时的行为一致。
|
|
725
712
|
*/
|
|
726
|
-
export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
727
|
-
const service = registerLsp(pi);
|
|
713
|
+
export default function claudeCodeFileTools(pi: ExtensionAPI, options?: LspServiceOptions): void {
|
|
728
714
|
const state = createClaudeCodeState();
|
|
729
715
|
|
|
716
|
+
// LSP 专属工具(lsp-rename / inspect 族)仅在 lsp.json 存在 enabled 服务器时
|
|
717
|
+
// 注册(session_start 校验后);本工具集跟踪 read-before-write 状态,rename
|
|
718
|
+
// 落盘的文件要标记为已读并随 details 持久化(restoreFileReads 依赖 details.reads)。
|
|
719
|
+
const manager = createLspManager(
|
|
720
|
+
pi,
|
|
721
|
+
{
|
|
722
|
+
onEnabled: (pi, service) => {
|
|
723
|
+
registerLspRenameTool(pi, service, {
|
|
724
|
+
recordReads: async (applied) => {
|
|
725
|
+
const reads: Record<string, FileSnapshot> = {};
|
|
726
|
+
for (const fileEdit of applied) {
|
|
727
|
+
const key = await readStateKey(fileEdit.path);
|
|
728
|
+
const snapshot = snapshotOf(fileEdit.newText);
|
|
729
|
+
state.reads.set(key, snapshot);
|
|
730
|
+
reads[key] = snapshot;
|
|
731
|
+
}
|
|
732
|
+
return reads;
|
|
733
|
+
},
|
|
734
|
+
});
|
|
735
|
+
registerLspInspectTools(pi, service);
|
|
736
|
+
},
|
|
737
|
+
},
|
|
738
|
+
options,
|
|
739
|
+
);
|
|
740
|
+
|
|
730
741
|
// 扩展实例在进程启动 / /reload / /new / /resume / /fork 时重建,内存里的
|
|
731
742
|
// 已读记账随之丢失。这里从当前分支的历史工具结果里恢复:digest 是当时的值,
|
|
732
743
|
// 若文件在此期间被外部修改,Edit/Write 时的指纹对比仍会要求重新 Read,
|
|
@@ -742,5 +753,6 @@ export default function claudeCodeFileTools(pi: ExtensionAPI): void {
|
|
|
742
753
|
restoreFileReads(state, ctx.sessionManager);
|
|
743
754
|
});
|
|
744
755
|
|
|
745
|
-
|
|
756
|
+
// 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
|
|
757
|
+
registerFileTools(pi, state, () => manager.mustLazyGetService());
|
|
746
758
|
}
|
package/src/claude-code/glob.ts
CHANGED
|
@@ -34,7 +34,7 @@ function extractGlobBaseDirectory(pattern: string): {
|
|
|
34
34
|
} {
|
|
35
35
|
const globChars = /[*?[{]/;
|
|
36
36
|
const match = globChars.exec(pattern);
|
|
37
|
-
if (!match
|
|
37
|
+
if (!match) {
|
|
38
38
|
// 无 glob 特殊字符:字面路径,目录部分作为 baseDir
|
|
39
39
|
return { baseDir: dirname(pattern), relativePattern: basename(pattern) };
|
|
40
40
|
}
|
package/src/gh-readonly.ts
CHANGED
|
@@ -122,12 +122,12 @@ export function runGh(
|
|
|
122
122
|
timeoutId = setTimeout(() => killProcess("timeout"), timeout);
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
-
proc.stdout
|
|
125
|
+
proc.stdout.on("data", (data: Buffer) => {
|
|
126
126
|
const text = data.toString();
|
|
127
127
|
stdout += text;
|
|
128
128
|
combined.push(text);
|
|
129
129
|
});
|
|
130
|
-
proc.stderr
|
|
130
|
+
proc.stderr.on("data", (data: Buffer) => {
|
|
131
131
|
const text = data.toString();
|
|
132
132
|
stderr += text;
|
|
133
133
|
combined.push(text);
|
|
@@ -793,7 +793,7 @@ export async function renderStepLog(
|
|
|
793
793
|
const totalLines = clean.split("\n").length;
|
|
794
794
|
let logToShow = clean;
|
|
795
795
|
let appliedOffset = false;
|
|
796
|
-
if (offset !== undefined && offset
|
|
796
|
+
if (offset !== undefined && offset > 1) {
|
|
797
797
|
if (offset > totalLines) {
|
|
798
798
|
return {
|
|
799
799
|
content: [
|
|
@@ -867,7 +867,7 @@ export async function renderJobLogs(
|
|
|
867
867
|
): Promise<CiLogsResult> {
|
|
868
868
|
const { job, offset, limit, full } = params;
|
|
869
869
|
|
|
870
|
-
if (
|
|
870
|
+
if (jobs.length === 0) {
|
|
871
871
|
return {
|
|
872
872
|
content: [{ type: "text", text: `No jobs found for run ${params.runId}` }],
|
|
873
873
|
details: {},
|
|
@@ -923,7 +923,7 @@ export async function renderJobLogs(
|
|
|
923
923
|
|
|
924
924
|
// Apply offset on the cleaned text, then truncate.
|
|
925
925
|
let logToShow = clean;
|
|
926
|
-
if (offset !== undefined && offset
|
|
926
|
+
if (offset !== undefined && offset > 1) {
|
|
927
927
|
if (offset > totalLines) {
|
|
928
928
|
steps.push({ name: s.name });
|
|
929
929
|
continue;
|
|
@@ -1037,9 +1037,12 @@ export async function writeLogFile(
|
|
|
1037
1037
|
|
|
1038
1038
|
let content: string;
|
|
1039
1039
|
let what: string;
|
|
1040
|
-
if (step
|
|
1040
|
+
if (step === undefined) {
|
|
1041
|
+
content = stripAnsi(rawLog);
|
|
1042
|
+
what = `job "${targetJob.name}" (id: ${targetJob.id})`;
|
|
1043
|
+
} else {
|
|
1041
1044
|
const found = targetJob.steps.find((s) => s.name.toLowerCase() === step.toLowerCase());
|
|
1042
|
-
if (
|
|
1045
|
+
if (found === undefined) {
|
|
1043
1046
|
return {
|
|
1044
1047
|
content: [
|
|
1045
1048
|
{
|
|
@@ -1064,9 +1067,6 @@ export async function writeLogFile(
|
|
|
1064
1067
|
}
|
|
1065
1068
|
content = cleanStepOutput(stepLog);
|
|
1066
1069
|
what = `step ${found.number} ("${found.name}") of job "${targetJob.name}"`;
|
|
1067
|
-
} else {
|
|
1068
|
-
content = stripAnsi(rawLog);
|
|
1069
|
-
what = `job "${targetJob.name}" (id: ${targetJob.id})`;
|
|
1070
1070
|
}
|
|
1071
1071
|
|
|
1072
1072
|
const target = resolve(cwd ?? process.cwd(), outputFile);
|
|
@@ -1480,7 +1480,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1480
1480
|
getJobLog(String(run_id), jobId, effectiveRepo, signal, ctx.cwd, params);
|
|
1481
1481
|
|
|
1482
1482
|
// ── Write the complete log to a file ───────────────────────────────
|
|
1483
|
-
if (output_file !== undefined && output_file !==
|
|
1483
|
+
if (output_file !== undefined && output_file !== "") {
|
|
1484
1484
|
const result = await writeLogFile(
|
|
1485
1485
|
{ runId: String(run_id), job, step, outputFile: output_file },
|
|
1486
1486
|
jobs,
|
|
@@ -1492,7 +1492,7 @@ export default function ghReadonlyTools(pi: ExtensionAPI) {
|
|
|
1492
1492
|
}
|
|
1493
1493
|
|
|
1494
1494
|
// ── Fetch a specific step's logs (requires `job`) ─────────────────
|
|
1495
|
-
if (step !== undefined
|
|
1495
|
+
if (step !== undefined) {
|
|
1496
1496
|
onUpdate?.({
|
|
1497
1497
|
content: [{ type: "text", text: `Fetching job list...` }],
|
|
1498
1498
|
details: {},
|
package/src/lib/cli.ts
CHANGED
|
@@ -238,7 +238,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
238
238
|
} else if (info.kind === "boolean") {
|
|
239
239
|
rawFlags[info.key] = true;
|
|
240
240
|
} else {
|
|
241
|
-
const next = tokens
|
|
241
|
+
const next = tokens.at(i + 1);
|
|
242
242
|
if (next !== undefined && !looksLikeFlag(next)) {
|
|
243
243
|
rawFlags[info.key] = next;
|
|
244
244
|
i++;
|
|
@@ -266,7 +266,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
266
266
|
} else if (j + 1 < rest.length) {
|
|
267
267
|
rawFlags[info.key] = rest.slice(j + 1);
|
|
268
268
|
} else {
|
|
269
|
-
const next = tokens
|
|
269
|
+
const next = tokens.at(i + 1);
|
|
270
270
|
if (next !== undefined && !looksLikeFlag(next)) {
|
|
271
271
|
rawFlags[info.key] = next;
|
|
272
272
|
i++;
|
|
@@ -327,10 +327,7 @@ export function parseCommand<TFlags extends TObject>(
|
|
|
327
327
|
parsed = Value.Convert(spec.flags, Value.Clone(parsed));
|
|
328
328
|
if (!Value.Check(spec.flags, parsed)) {
|
|
329
329
|
const [first] = [...Value.Errors(spec.flags, parsed)];
|
|
330
|
-
return errorResult(
|
|
331
|
-
spec,
|
|
332
|
-
`Invalid arguments: ${first?.message ?? "value does not match flags"}`,
|
|
333
|
-
);
|
|
330
|
+
return errorResult(spec, `Invalid arguments: ${first.message}`);
|
|
334
331
|
}
|
|
335
332
|
} catch {
|
|
336
333
|
return errorResult(spec, "Invalid arguments");
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -25,6 +25,7 @@ import type {
|
|
|
25
25
|
Hover,
|
|
26
26
|
Location as LspLocation,
|
|
27
27
|
LocationLink,
|
|
28
|
+
Range,
|
|
28
29
|
WorkspaceEdit,
|
|
29
30
|
} from "vscode-languageserver-types";
|
|
30
31
|
|
|
@@ -192,7 +193,9 @@ function toInspectLocations(result: DefinitionResult): InspectLocation[] {
|
|
|
192
193
|
for (const item of items) {
|
|
193
194
|
if ("targetUri" in item) {
|
|
194
195
|
if (!item.targetUri.startsWith("file:")) continue;
|
|
195
|
-
const range =
|
|
196
|
+
const range =
|
|
197
|
+
(item as Omit<LocationLink, "targetSelectionRange"> & { targetSelectionRange?: Range })
|
|
198
|
+
.targetSelectionRange ?? item.targetRange;
|
|
196
199
|
locations.push({
|
|
197
200
|
path: normalize(fileURLToPath(item.targetUri)),
|
|
198
201
|
line: range.start.line,
|
|
@@ -424,7 +427,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
424
427
|
new StreamMessageReader(input.server.process.stdout),
|
|
425
428
|
new StreamMessageWriter(input.server.process.stdin),
|
|
426
429
|
);
|
|
427
|
-
input.server.process.stderr
|
|
430
|
+
input.server.process.stderr.resume();
|
|
428
431
|
/** 连接或服务器进程已关闭;pull 重试循环以此终止,避免无界等待。 */
|
|
429
432
|
let connectionClosed = false;
|
|
430
433
|
input.server.process.once("exit", () => {
|
|
@@ -581,7 +584,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
581
584
|
await connection.sendNotification("workspace/didChangeConfiguration", { settings });
|
|
582
585
|
}
|
|
583
586
|
|
|
584
|
-
const files: Record<string, { version: number; text: string }> = {};
|
|
587
|
+
const files: Record<string, { version: number; text: string } | undefined> = {};
|
|
585
588
|
|
|
586
589
|
// ── 驻留 LRU ────────────────────────────────────────────────────────────────
|
|
587
590
|
|
|
@@ -823,16 +826,16 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
823
826
|
if (timeout <= 0) return Promise.resolve(false);
|
|
824
827
|
return new Promise<boolean>((resolve) => {
|
|
825
828
|
let finished = false;
|
|
829
|
+
const timer = setTimeout(() => finish(false), timeout);
|
|
826
830
|
const finish = (result: boolean) => {
|
|
827
831
|
if (finished) return;
|
|
828
832
|
finished = true;
|
|
829
|
-
|
|
833
|
+
clearTimeout(timer);
|
|
830
834
|
registrationListeners.delete(listener);
|
|
831
835
|
resolve(result);
|
|
832
836
|
};
|
|
833
837
|
const listener = () => finish(true);
|
|
834
838
|
registrationListeners.add(listener);
|
|
835
|
-
const timer = setTimeout(() => finish(false), timeout);
|
|
836
839
|
});
|
|
837
840
|
}
|
|
838
841
|
|
|
@@ -846,12 +849,14 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
846
849
|
return new Promise<boolean>((resolve) => {
|
|
847
850
|
let finished = false;
|
|
848
851
|
let debounceTimer: ReturnType<typeof setTimeout> | undefined;
|
|
852
|
+
const timeoutTimer = setTimeout(() => finish(false), request.timeout);
|
|
853
|
+
const unsub = () => diagnosticListeners.delete(listener);
|
|
849
854
|
const finish = (result: boolean) => {
|
|
850
855
|
if (finished) return;
|
|
851
856
|
finished = true;
|
|
852
857
|
if (debounceTimer) clearTimeout(debounceTimer);
|
|
853
|
-
|
|
854
|
-
unsub
|
|
858
|
+
clearTimeout(timeoutTimer);
|
|
859
|
+
unsub();
|
|
855
860
|
resolve(result);
|
|
856
861
|
};
|
|
857
862
|
const schedule = () => {
|
|
@@ -866,13 +871,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
866
871
|
);
|
|
867
872
|
};
|
|
868
873
|
|
|
869
|
-
const timeoutTimer = setTimeout(() => finish(false), request.timeout);
|
|
870
874
|
const listener = (event: { path: string; serverID: string }) => {
|
|
871
875
|
if (event.path !== request.path || event.serverID !== input.serverID) return;
|
|
872
876
|
schedule();
|
|
873
877
|
};
|
|
874
878
|
diagnosticListeners.add(listener);
|
|
875
|
-
const unsub = () => diagnosticListeners.delete(listener);
|
|
876
879
|
schedule();
|
|
877
880
|
});
|
|
878
881
|
}
|
|
@@ -216,7 +216,7 @@ async function probeSymbolCandidates(options: {
|
|
|
216
216
|
}
|
|
217
217
|
const groups = new Map<string, { output: InspectOutput; candidates: LspPosition[] }>();
|
|
218
218
|
for (const [index, candidate] of candidates.entries()) {
|
|
219
|
-
const output = outputs
|
|
219
|
+
const output = outputs.at(index);
|
|
220
220
|
if (!output) continue;
|
|
221
221
|
const group = groups.get(output.text);
|
|
222
222
|
if (group) group.candidates.push(candidate);
|
|
@@ -234,7 +234,7 @@ async function probeSymbolCandidates(options: {
|
|
|
234
234
|
`Ambiguous symbol on line ${options.line} of ${options.filePath}: several distinct symbols share the name "${options.symbol}". Retry with 'character' (1-based) to pick one:\n${listing}`,
|
|
235
235
|
);
|
|
236
236
|
}
|
|
237
|
-
const first = [...groups.values()]
|
|
237
|
+
const first = [...groups.values()].at(0);
|
|
238
238
|
if (!first) throw new Error("LSP inspect returned no result");
|
|
239
239
|
return first.output;
|
|
240
240
|
}
|
package/src/lib/lsp/language.ts
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* 文件扩展名 → LSP languageId 映射,用于 didOpen 通知。
|
|
3
3
|
* 移植自 opencode packages/opencode/src/lsp/language.ts。
|
|
4
4
|
*/
|
|
5
|
-
export const LANGUAGE_EXTENSIONS: Record<string, string> = {
|
|
5
|
+
export const LANGUAGE_EXTENSIONS: Record<string, string | undefined> = {
|
|
6
6
|
".abap": "abap",
|
|
7
7
|
".bat": "bat",
|
|
8
8
|
".bib": "bibtex",
|
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -18,7 +18,7 @@
|
|
|
18
18
|
|
|
19
19
|
import { readFileSync } from "node:fs";
|
|
20
20
|
import { readFile } from "node:fs/promises";
|
|
21
|
-
import { homedir } from "node:os";
|
|
21
|
+
import { homedir, tmpdir } from "node:os";
|
|
22
22
|
import { extname, join, normalize, relative, sep } from "node:path";
|
|
23
23
|
|
|
24
24
|
import type { ExtensionAPI, ExtensionUIContext } from "@earendil-works/pi-coding-agent";
|
|
@@ -90,12 +90,19 @@ export type LspConfig = Static<typeof lspConfigSchema>;
|
|
|
90
90
|
|
|
91
91
|
/** "500" → 500、"5s" → 5000、"1m" → 60000;无效字符串返回 NaN(由 toMs 过滤)。 */
|
|
92
92
|
function parseTimeoutString(value: string): number {
|
|
93
|
-
|
|
93
|
+
// 单位组永远参与匹配(缺省为空串),避免"可选捕获组在类型上不可空"的歧义
|
|
94
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|)\s*$/.exec(value.trim());
|
|
94
95
|
if (!match) return NaN;
|
|
95
96
|
const amount = Number(match[1]);
|
|
96
|
-
const
|
|
97
|
-
|
|
98
|
-
|
|
97
|
+
const factors: Record<string, number | undefined> = {
|
|
98
|
+
"": 1,
|
|
99
|
+
ms: 1,
|
|
100
|
+
s: 1_000,
|
|
101
|
+
m: 60_000,
|
|
102
|
+
h: 3_600_000,
|
|
103
|
+
};
|
|
104
|
+
const factor = factors[match[2]];
|
|
105
|
+
return amount * (factor ?? 1);
|
|
99
106
|
}
|
|
100
107
|
|
|
101
108
|
function toMs(value: number | string | undefined): number | undefined {
|
|
@@ -843,71 +850,154 @@ export interface LspServiceOptions {
|
|
|
843
850
|
globalConfigPath?: string;
|
|
844
851
|
}
|
|
845
852
|
|
|
846
|
-
|
|
847
|
-
|
|
848
|
-
|
|
849
|
-
|
|
850
|
-
|
|
851
|
-
pi
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
853
|
+
export interface LspManagerHooks {
|
|
854
|
+
/**
|
|
855
|
+
* session_start 校验通过且存在 enabled 服务器时调用(每实例至多一次);
|
|
856
|
+
* LSP 专属工具(rename / inspect 族)在此注册,未配置时保持不可见。
|
|
857
|
+
*/
|
|
858
|
+
onEnabled: (pi: ExtensionAPI, service: LspService) => void;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
export interface LspManager {
|
|
862
|
+
/**
|
|
863
|
+
* 文件工具的 service 访问器:永不抛错。disabled / session_start 未运行时
|
|
864
|
+
* 返回共享 no-op service(诊断与文件事件通知为空操作)。
|
|
865
|
+
*/
|
|
866
|
+
mustLazyGetService(): LspService;
|
|
867
|
+
/** 当前会话是否已创建 service(/lsp-stop 后仍为 true,内部 disabled 语义不变)。 */
|
|
868
|
+
haveEnabledLsp(): boolean;
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/**
|
|
872
|
+
* no-op service 单例:空 adapter 列表 → getClients 永远返回空,任何请求都是
|
|
873
|
+
* 空操作。globalConfigPath 指向必然不存在的文件——真实全局配置若声明了
|
|
874
|
+
* `enabled`,createLspService 的建时校验会因空 adapter 集合而抛错。
|
|
875
|
+
*/
|
|
876
|
+
const noopServiceHolder: { service?: LspService } = {};
|
|
877
|
+
function getNoopService(): LspService {
|
|
878
|
+
noopServiceHolder.service ??= createLspService(
|
|
879
|
+
[],
|
|
880
|
+
join(tmpdir(), ".pi-lsp-noop-global-does-not-exist.json"),
|
|
881
|
+
);
|
|
882
|
+
return noopServiceHolder.service;
|
|
883
|
+
}
|
|
884
|
+
|
|
885
|
+
/** 会话配置里生效(未被 enabled 白名单排除、未被 disabled)的服务器数量。 */
|
|
886
|
+
function enabledServerCount(config: LspConfig, adapters?: LspServerAdapter[]): number {
|
|
887
|
+
const ids = adapters ? adapters.map((adapter) => adapter.id) : Object.keys(config.servers ?? {});
|
|
888
|
+
return ids.filter((id) => {
|
|
889
|
+
if (config.enabled && !config.enabled.includes(id)) return false;
|
|
890
|
+
if (config.disabled?.includes(id)) return false;
|
|
891
|
+
return true;
|
|
892
|
+
}).length;
|
|
893
|
+
}
|
|
894
|
+
|
|
895
|
+
/**
|
|
896
|
+
* 创建 LSP manager 并注册 pi 会话生命周期:
|
|
897
|
+
* - session_start(被 pi await):加载并校验配置;存在 enabled 服务器才创建
|
|
898
|
+
* service(进程仍首次工具调用才 spawn)并调用 onEnabled 注册 LSP 工具;
|
|
899
|
+
* 配置缺失或错误则保持 disabled(错误 notify 后降级,不阻断会话启动)。
|
|
900
|
+
* - session_shutdown:关闭全部服务器进程。
|
|
901
|
+
* 文件工具经 mustLazyGetService 访问,在任何状态下都能安全工作。
|
|
902
|
+
*/
|
|
903
|
+
export function createLspManager(
|
|
904
|
+
pi: ExtensionAPI,
|
|
905
|
+
hooks: LspManagerHooks,
|
|
906
|
+
options?: LspServiceOptions,
|
|
907
|
+
): LspManager {
|
|
908
|
+
let service: LspService | undefined;
|
|
909
|
+
|
|
910
|
+
pi.on("session_start", async (_event, ctx) => {
|
|
911
|
+
// /reload 等路径可能对同一 runner 重发 session_start:先清掉旧实例再建
|
|
912
|
+
if (service !== undefined) {
|
|
913
|
+
// 旧实例关闭失败不阻断新会话构建
|
|
914
|
+
await service.shutdownAll().catch(() => {
|
|
915
|
+
/* noop */
|
|
856
916
|
});
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
917
|
+
service = undefined;
|
|
918
|
+
}
|
|
919
|
+
try {
|
|
920
|
+
const config = await loadLspConfig(ctx.cwd, options?.globalConfigPath);
|
|
921
|
+
validateConfig(config, options?.adapters);
|
|
922
|
+
if (enabledServerCount(config, options?.adapters) === 0) return;
|
|
923
|
+
const next = createLspService(options?.adapters, options?.globalConfigPath);
|
|
924
|
+
// footer status 显示当前所有 LSP server 状态(无 UI 时不显示)
|
|
925
|
+
next.attachStatus((text) =>
|
|
926
|
+
ctx.ui.setStatus("lsp", text ? ctx.ui.theme.fg("accent", text) : undefined),
|
|
927
|
+
);
|
|
928
|
+
service = next;
|
|
929
|
+
hooks.onEnabled(pi, next);
|
|
930
|
+
} catch (error) {
|
|
931
|
+
service = undefined;
|
|
932
|
+
if (error instanceof Error) ctx.ui.notify(error.message, "error");
|
|
933
|
+
}
|
|
934
|
+
});
|
|
935
|
+
|
|
936
|
+
pi.on("session_shutdown", () => {
|
|
937
|
+
void service?.shutdownAll();
|
|
863
938
|
});
|
|
939
|
+
|
|
864
940
|
// agent 生命周期边界显式刷新 status:agent 运行中 LSP server 才被惰性
|
|
865
941
|
// spawn(首次工具调用),start/end 时保证 footer 反映当前实际状态。
|
|
866
|
-
pi.on
|
|
867
|
-
pi.on
|
|
942
|
+
pi.on("agent_start", () => service?.refreshStatus());
|
|
943
|
+
pi.on("agent_end", () => service?.refreshStatus());
|
|
868
944
|
|
|
869
|
-
pi.registerCommand
|
|
945
|
+
pi.registerCommand("lsp-stop", {
|
|
870
946
|
description: "Stop all LSP servers and disable LSP until /lsp-start or /lsp-reload",
|
|
871
947
|
handler: async (_args, ctx) => {
|
|
948
|
+
if (service === undefined) {
|
|
949
|
+
ctx.ui.notify("LSP not configured: nothing to stop", "warning");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
872
952
|
await service.stop();
|
|
873
|
-
ctx.ui.notify
|
|
953
|
+
ctx.ui.notify("LSP disabled: all servers stopped", "info");
|
|
874
954
|
},
|
|
875
955
|
});
|
|
876
956
|
|
|
877
|
-
pi.registerCommand
|
|
957
|
+
pi.registerCommand("lsp-start", {
|
|
878
958
|
description: "Re-enable LSP; servers start on the next tool call",
|
|
879
959
|
handler: (_args, ctx) => {
|
|
960
|
+
if (service === undefined) {
|
|
961
|
+
ctx.ui.notify("LSP not configured: nothing to enable", "warning");
|
|
962
|
+
return Promise.resolve();
|
|
963
|
+
}
|
|
880
964
|
service.start();
|
|
881
|
-
ctx.ui.notify
|
|
965
|
+
ctx.ui.notify("LSP enabled: servers will start on the next tool call", "info");
|
|
882
966
|
return Promise.resolve();
|
|
883
967
|
},
|
|
884
968
|
});
|
|
885
969
|
|
|
886
|
-
pi.registerCommand
|
|
970
|
+
pi.registerCommand("lsp-reload", {
|
|
887
971
|
description: "Restart a specific LSP server: /lsp-reload <server-id>",
|
|
888
972
|
getArgumentCompletions: (prefix) =>
|
|
889
|
-
service
|
|
890
|
-
.serverIDs()
|
|
973
|
+
(service?.serverIDs() ?? [])
|
|
891
974
|
.toSorted()
|
|
892
975
|
.filter((id) => id.startsWith(prefix))
|
|
893
976
|
.map((id) => ({ value: id, label: id })),
|
|
894
977
|
handler: async (args, ctx) => {
|
|
978
|
+
if (service === undefined) {
|
|
979
|
+
ctx.ui.notify("LSP not configured: nothing to reload", "warning");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
895
982
|
const serverID = args.trim();
|
|
896
983
|
const known = service.serverIDs().toSorted();
|
|
897
984
|
if (!serverID) {
|
|
898
|
-
ctx.ui.notify
|
|
985
|
+
ctx.ui.notify(
|
|
899
986
|
`usage: /lsp-reload <server-id>${known.length > 0 ? ` (known: ${known.join(", ")})` : ""}`,
|
|
900
987
|
"warning",
|
|
901
988
|
);
|
|
902
989
|
return;
|
|
903
990
|
}
|
|
904
991
|
await service.reload(serverID);
|
|
905
|
-
ctx.ui.notify
|
|
992
|
+
ctx.ui.notify(
|
|
906
993
|
`LSP server "${serverID}" reloaded: will restart on the next tool call`,
|
|
907
994
|
"info",
|
|
908
995
|
);
|
|
909
996
|
},
|
|
910
997
|
});
|
|
911
998
|
|
|
912
|
-
return
|
|
999
|
+
return {
|
|
1000
|
+
mustLazyGetService: () => service ?? getNoopService(),
|
|
1001
|
+
haveEnabledLsp: () => service !== undefined,
|
|
1002
|
+
};
|
|
913
1003
|
}
|
|
@@ -150,7 +150,7 @@ export function registerLspRenameTool(
|
|
|
150
150
|
{ result: (typeof successes)[number]["result"]; candidates: typeof candidates }
|
|
151
151
|
>();
|
|
152
152
|
for (const [index, candidate] of candidates.entries()) {
|
|
153
|
-
const entry = successes
|
|
153
|
+
const entry = successes.at(index);
|
|
154
154
|
if (!entry) continue;
|
|
155
155
|
const key = canonicalizeEdit(entry.result.edit);
|
|
156
156
|
const group = groups.get(key);
|
|
@@ -172,7 +172,7 @@ export function registerLspRenameTool(
|
|
|
172
172
|
`Ambiguous rename target on line ${params.line} of ${filePath}: several distinct symbols share the name "${params.symbol}". Retry with 'character' (1-based) to pick one:\n${listing}`,
|
|
173
173
|
);
|
|
174
174
|
}
|
|
175
|
-
const firstGroup = [...groups.values()]
|
|
175
|
+
const firstGroup = [...groups.values()].at(0);
|
|
176
176
|
if (!firstGroup) throw new Error("LSP rename returned no target");
|
|
177
177
|
const { result } = firstGroup;
|
|
178
178
|
|
package/src/lib/ui.ts
CHANGED
|
@@ -76,7 +76,7 @@ export async function selectMultiple(
|
|
|
76
76
|
opts: { signal?: AbortSignal; doneLabel: string },
|
|
77
77
|
): Promise<string[]> {
|
|
78
78
|
const selected: string[] = [];
|
|
79
|
-
|
|
79
|
+
for (;;) {
|
|
80
80
|
const selectedSet = new Set(selected);
|
|
81
81
|
const displayToLabel = new Map<string, string>();
|
|
82
82
|
const round: SelectAction[] = [];
|
|
@@ -143,7 +143,7 @@ export async function selectCheckboxActions<T extends string>(
|
|
|
143
143
|
opts: { signal?: AbortSignal } = {},
|
|
144
144
|
): Promise<CheckboxActionResult<T> | undefined> {
|
|
145
145
|
const selected: string[] = [];
|
|
146
|
-
|
|
146
|
+
for (;;) {
|
|
147
147
|
const selectedSet = new Set(selected);
|
|
148
148
|
const displayToLabel = new Map<string, string>();
|
|
149
149
|
const round: SelectAction[] = [];
|
package/src/lib/write-guard.ts
CHANGED
|
@@ -143,7 +143,7 @@ export async function guardWriteAccess(
|
|
|
143
143
|
throw new Error(`Path "${absolutePath}" is outside workspace. No UI available for approval.`);
|
|
144
144
|
}
|
|
145
145
|
|
|
146
|
-
|
|
146
|
+
for (;;) {
|
|
147
147
|
const diffPreview = opts.change ? await buildDiffPreview(absolutePath, opts.change) : undefined;
|
|
148
148
|
const title =
|
|
149
149
|
`Model requests write access outside workspace:\n\n` +
|
|
@@ -66,7 +66,7 @@ function nonEmpty(value: string | undefined): string | undefined {
|
|
|
66
66
|
function normalizeModel(model: Static<typeof modelSchema>): OpenaiCostModelConfig | undefined {
|
|
67
67
|
const id = nonEmpty(model.id);
|
|
68
68
|
if (!id) return undefined;
|
|
69
|
-
const input = model.input
|
|
69
|
+
const input = model.input ?? [];
|
|
70
70
|
return {
|
|
71
71
|
id,
|
|
72
72
|
name: nonEmpty(model.name) ?? id,
|
package/src/openai-cost/cost.ts
CHANGED
|
@@ -90,7 +90,7 @@ export async function scanSseCost(body: ReadableStream<Uint8Array>): Promise<num
|
|
|
90
90
|
let buffer = "";
|
|
91
91
|
let last: number | undefined;
|
|
92
92
|
try {
|
|
93
|
-
|
|
93
|
+
for (;;) {
|
|
94
94
|
const { done, value } = await reader.read();
|
|
95
95
|
if (done) break;
|
|
96
96
|
buffer += decoder.decode(value, { stream: true });
|
|
@@ -372,7 +372,7 @@ const EscapeNormalizedReplacer: Replacer = function* (content, find) {
|
|
|
372
372
|
|
|
373
373
|
const MultiOccurrenceReplacer: Replacer = function* (content, find) {
|
|
374
374
|
let startIndex = 0;
|
|
375
|
-
|
|
375
|
+
for (;;) {
|
|
376
376
|
const index = content.indexOf(find, startIndex);
|
|
377
377
|
if (index === -1) break;
|
|
378
378
|
yield find;
|
package/src/opencode/files.ts
CHANGED
|
@@ -31,7 +31,7 @@ import { Type } from "typebox";
|
|
|
31
31
|
|
|
32
32
|
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
33
33
|
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
34
|
-
import { type LspService,
|
|
34
|
+
import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
|
|
35
35
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
36
36
|
import { formatSubtitlePath } from "../lib/path.js";
|
|
37
37
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
@@ -105,8 +105,8 @@ const IMAGE_SIGNATURES: {
|
|
|
105
105
|
return (
|
|
106
106
|
startsWithAscii(buf, 0, "BM") &&
|
|
107
107
|
buf.length >= 30 &&
|
|
108
|
-
|
|
109
|
-
[1, 4, 8, 16, 24, 32].includes(buf[28 + 1]
|
|
108
|
+
buf[28] === 1 &&
|
|
109
|
+
[1, 4, 8, 16, 24, 32].includes(buf[28 + 1])
|
|
110
110
|
);
|
|
111
111
|
},
|
|
112
112
|
mimeType: "image/bmp",
|
|
@@ -295,7 +295,7 @@ async function formatDirectoryEntries(dirPath: string): Promise<string[]> {
|
|
|
295
295
|
return results;
|
|
296
296
|
}
|
|
297
297
|
|
|
298
|
-
function registerReadTool(pi: ExtensionAPI,
|
|
298
|
+
function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
299
299
|
pi.registerTool({
|
|
300
300
|
name: "read",
|
|
301
301
|
label: "read",
|
|
@@ -434,9 +434,11 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
434
434
|
content = [{ type: "text", text: outputText }];
|
|
435
435
|
|
|
436
436
|
// opencode: LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
|
|
437
|
-
void
|
|
438
|
-
|
|
439
|
-
|
|
437
|
+
void getService()
|
|
438
|
+
.notifyFile(absolutePath, ctx.cwd)
|
|
439
|
+
.catch(() => {
|
|
440
|
+
// 后台通知失败不影响读取
|
|
441
|
+
});
|
|
440
442
|
|
|
441
443
|
return {
|
|
442
444
|
content,
|
|
@@ -462,7 +464,7 @@ const editSchema = Type.Object({
|
|
|
462
464
|
),
|
|
463
465
|
});
|
|
464
466
|
|
|
465
|
-
function registerEditTool(pi: ExtensionAPI,
|
|
467
|
+
function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
466
468
|
pi.registerTool({
|
|
467
469
|
name: "edit",
|
|
468
470
|
label: "edit",
|
|
@@ -525,7 +527,7 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
525
527
|
text: diagnosticText,
|
|
526
528
|
errorCount,
|
|
527
529
|
warningCount,
|
|
528
|
-
} = await
|
|
530
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
529
531
|
return [
|
|
530
532
|
"Edit applied successfully.",
|
|
531
533
|
{ diff: "", patch: "", firstChangedLine: 0 },
|
|
@@ -567,7 +569,7 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
567
569
|
text: diagnosticText,
|
|
568
570
|
errorCount,
|
|
569
571
|
warningCount,
|
|
570
|
-
} = await
|
|
572
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
571
573
|
return [
|
|
572
574
|
"Edit applied successfully.",
|
|
573
575
|
{ diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
@@ -615,7 +617,7 @@ export function resolveBom(
|
|
|
615
617
|
return { bom: sourceBom || nextBom, text };
|
|
616
618
|
}
|
|
617
619
|
|
|
618
|
-
function registerWriteTool(pi: ExtensionAPI,
|
|
620
|
+
function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
619
621
|
pi.registerTool({
|
|
620
622
|
name: "write",
|
|
621
623
|
label: "write",
|
|
@@ -667,7 +669,7 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
667
669
|
text: diagnosticText,
|
|
668
670
|
errorCount,
|
|
669
671
|
warningCount,
|
|
670
|
-
} = await
|
|
672
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
671
673
|
|
|
672
674
|
return [
|
|
673
675
|
"Wrote file successfully.",
|
|
@@ -694,19 +696,25 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
694
696
|
|
|
695
697
|
// ── 入口 ─────────────────────────────────────────────────────────────────────
|
|
696
698
|
|
|
697
|
-
export function registerFileTools(pi: ExtensionAPI,
|
|
698
|
-
registerReadTool(pi,
|
|
699
|
-
registerEditTool(pi,
|
|
700
|
-
registerWriteTool(pi,
|
|
701
|
-
//
|
|
702
|
-
// read-before-write 状态,不传 recordReads hook。
|
|
703
|
-
registerLspRenameTool(pi, service);
|
|
704
|
-
// 只读符号查询工具(find-definition / find-reference / inspect)与 claude-code 共享。
|
|
705
|
-
registerLspInspectTools(pi, service);
|
|
699
|
+
export function registerFileTools(pi: ExtensionAPI, getService: () => LspService): void {
|
|
700
|
+
registerReadTool(pi, getService);
|
|
701
|
+
registerEditTool(pi, getService);
|
|
702
|
+
registerWriteTool(pi, getService);
|
|
703
|
+
// lsp-rename / inspect 工具由 manager 的 onEnabled 回调注册,不在这里注册。
|
|
706
704
|
}
|
|
707
705
|
|
|
708
|
-
/** 独立入口:创建 LSP
|
|
709
|
-
export default function opencodeFileTools(pi: ExtensionAPI): void {
|
|
710
|
-
const
|
|
711
|
-
|
|
706
|
+
/** 独立入口:创建 LSP manager(session_start 时按配置启用)并注册文件工具。 */
|
|
707
|
+
export default function opencodeFileTools(pi: ExtensionAPI, options?: LspServiceOptions): void {
|
|
708
|
+
const manager = createLspManager(
|
|
709
|
+
pi,
|
|
710
|
+
{
|
|
711
|
+
onEnabled: (pi, service) => {
|
|
712
|
+
registerLspRenameTool(pi, service);
|
|
713
|
+
registerLspInspectTools(pi, service);
|
|
714
|
+
},
|
|
715
|
+
},
|
|
716
|
+
options,
|
|
717
|
+
);
|
|
718
|
+
// 文件工具无条件注册;service 惰性获取,disabled 时为 no-op。
|
|
719
|
+
registerFileTools(pi, () => manager.mustLazyGetService());
|
|
712
720
|
}
|
package/src/talk/index.ts
CHANGED
|
@@ -171,7 +171,7 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
171
171
|
// ── Lifecycle ──────────────────────────────────────────────────────────
|
|
172
172
|
|
|
173
173
|
pi.on("session_start", (_event, ctx: ExtensionContext) => {
|
|
174
|
-
const cwd = ctx.sessionManager.getCwd()
|
|
174
|
+
const cwd = ctx.sessionManager.getCwd();
|
|
175
175
|
const now = Date.now();
|
|
176
176
|
// A fork/branch/resume of a session that joined a group keeps that talk
|
|
177
177
|
// identity (agentId); a fresh session gets the new session id.
|
|
@@ -463,13 +463,7 @@ export default function talk(pi: ExtensionAPI) {
|
|
|
463
463
|
|
|
464
464
|
pi.registerMessageRenderer<DeliveryDetails>(DELIVERY_TYPE, (message, _options, theme) => {
|
|
465
465
|
const d = message.details;
|
|
466
|
-
if (
|
|
467
|
-
!d ||
|
|
468
|
-
typeof d.id !== "string" ||
|
|
469
|
-
typeof d.ts !== "number" ||
|
|
470
|
-
typeof d.body !== "string" ||
|
|
471
|
-
!d.from
|
|
472
|
-
) {
|
|
466
|
+
if (!d || typeof d.id !== "string" || typeof d.ts !== "number" || typeof d.body !== "string") {
|
|
473
467
|
return; // pre-renderer entries: keep pi's default custom-message box
|
|
474
468
|
}
|
|
475
469
|
const idTail = d.id.slice(-8);
|
package/src/talk/registry.ts
CHANGED
|
@@ -164,7 +164,7 @@ function pidAlive(pid: number, pidStart?: number): boolean {
|
|
|
164
164
|
process.kill(pid, 0);
|
|
165
165
|
} catch (error) {
|
|
166
166
|
// EPERM means the process exists but isn't ours — still alive
|
|
167
|
-
return (error as NodeJS.ErrnoException)
|
|
167
|
+
return (error as NodeJS.ErrnoException).code === "EPERM";
|
|
168
168
|
}
|
|
169
169
|
if (pidStart === undefined) return true;
|
|
170
170
|
const start = readStartTime(pid);
|
package/src/vision-agent.ts
CHANGED
|
@@ -276,7 +276,7 @@ export async function callVision(
|
|
|
276
276
|
messages: [{ role: "user", content, timestamp: Date.now() }],
|
|
277
277
|
},
|
|
278
278
|
{
|
|
279
|
-
maxTokens: model.maxTokens
|
|
279
|
+
maxTokens: model.maxTokens,
|
|
280
280
|
signal: withTimeout(signal, REQUEST_TIMEOUT_MS),
|
|
281
281
|
},
|
|
282
282
|
);
|
|
@@ -290,7 +290,7 @@ export async function callVision(
|
|
|
290
290
|
throw new Error("API 未返回内容");
|
|
291
291
|
}
|
|
292
292
|
const tokenStr =
|
|
293
|
-
typeof result.usage
|
|
293
|
+
typeof result.usage.totalTokens === "number" ? String(result.usage.totalTokens) : "?";
|
|
294
294
|
const labels = loaded.map((l) => l.label).join(", ");
|
|
295
295
|
return `[${labels}]\n${text}\n[模型: ${model.id}, tokens: ${tokenStr}]`;
|
|
296
296
|
}
|