@trim21/personal-pi-extensions 0.1.489 → 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 +41 -26
- 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 +139 -9
- package/src/lib/lsp/inspect-tool.ts +376 -0
- package/src/lib/lsp/language.ts +1 -1
- package/src/lib/lsp/lsp-find-definition.md +8 -0
- package/src/lib/lsp/lsp-find-reference.md +8 -0
- package/src/lib/lsp/lsp-inspect.md +8 -0
- package/src/lib/lsp/lsp.ts +200 -34
- 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 +34 -23
- package/src/spawn-agent.ts +2 -2
- package/src/talk/index.ts +2 -8
- package/src/talk/registry.ts +1 -1
- package/src/vision-agent.ts +2 -2
package/src/lib/lsp/lsp.ts
CHANGED
|
@@ -18,14 +18,14 @@
|
|
|
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";
|
|
25
25
|
import { minimatch } from "minimatch";
|
|
26
26
|
import { type Static, Type } from "typebox";
|
|
27
27
|
import { Value } from "typebox/value";
|
|
28
|
-
import type { WorkspaceEdit } from "vscode-languageserver-types";
|
|
28
|
+
import type { Hover, WorkspaceEdit } from "vscode-languageserver-types";
|
|
29
29
|
|
|
30
30
|
import { type LspServerAdapter } from "./adapter.js";
|
|
31
31
|
import {
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
type CreateInput,
|
|
34
34
|
type Diagnostic,
|
|
35
35
|
type Info as LspClient,
|
|
36
|
+
type InspectLocation,
|
|
37
|
+
LspMethodNotSupportedError,
|
|
36
38
|
RenameNotPossibleError,
|
|
37
39
|
WATCH_KIND_CHANGE,
|
|
38
40
|
WATCH_KIND_CREATE,
|
|
@@ -88,12 +90,19 @@ export type LspConfig = Static<typeof lspConfigSchema>;
|
|
|
88
90
|
|
|
89
91
|
/** "500" → 500、"5s" → 5000、"1m" → 60000;无效字符串返回 NaN(由 toMs 过滤)。 */
|
|
90
92
|
function parseTimeoutString(value: string): number {
|
|
91
|
-
|
|
93
|
+
// 单位组永远参与匹配(缺省为空串),避免"可选捕获组在类型上不可空"的歧义
|
|
94
|
+
const match = /^(\d+(?:\.\d+)?)(ms|s|m|h|)\s*$/.exec(value.trim());
|
|
92
95
|
if (!match) return NaN;
|
|
93
96
|
const amount = Number(match[1]);
|
|
94
|
-
const
|
|
95
|
-
|
|
96
|
-
|
|
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);
|
|
97
106
|
}
|
|
98
107
|
|
|
99
108
|
function toMs(value: number | string | undefined): number | undefined {
|
|
@@ -242,6 +251,16 @@ interface LspState {
|
|
|
242
251
|
/** 渲染 LSP status 文本的回调(传入 undefined 表示清除)。 */
|
|
243
252
|
export type StatusRenderer = (text: string | undefined) => void;
|
|
244
253
|
|
|
254
|
+
export type LspInspectQuery = "definition" | "references" | "hover";
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* 返回类型与 query 泛型关联:query 为 "hover" 时返回 hover 内容,否则返回
|
|
258
|
+
* 位置列表。实现内部用 cast 建立关联(TS 无法验证分支与泛型的对应关系)。
|
|
259
|
+
*/
|
|
260
|
+
export type LspInspectResult<Q extends LspInspectQuery = LspInspectQuery> = Q extends "hover"
|
|
261
|
+
? { serverID: string; query: "hover"; hover: Hover | null }
|
|
262
|
+
: { serverID: string; query: "definition" | "references"; locations: InspectLocation[] };
|
|
263
|
+
|
|
245
264
|
export interface LspRequestOptions {
|
|
246
265
|
notify?: ExtensionUIContext["notify"];
|
|
247
266
|
/** 中止时提前结束诊断等待(已中止时直接跳过诊断)。 */
|
|
@@ -288,6 +307,19 @@ export interface LspService {
|
|
|
288
307
|
newName: string;
|
|
289
308
|
options?: LspRequestOptions;
|
|
290
309
|
}): Promise<{ serverID: string; edit: WorkspaceEdit; placeholder?: string }>;
|
|
310
|
+
/**
|
|
311
|
+
* 只读符号查询(definition / references / hover):只面向 kind 为 "language"
|
|
312
|
+
* 的服务器,按配置顺序取第一个成功结果;服务器不支持该方法(MethodNotFound)
|
|
313
|
+
* 时跳过并尝试下一个,全部不支持时抛聚合错误。line / character 为 0-based。
|
|
314
|
+
*/
|
|
315
|
+
inspect<Q extends LspInspectQuery>(request: {
|
|
316
|
+
file: string;
|
|
317
|
+
cwd: string;
|
|
318
|
+
line: number;
|
|
319
|
+
character: number;
|
|
320
|
+
query: Q;
|
|
321
|
+
options?: LspRequestOptions;
|
|
322
|
+
}): Promise<LspInspectResult<Q>>;
|
|
291
323
|
shutdownAll(): Promise<void>;
|
|
292
324
|
/** 停止全部服务器并禁用 LSP:之后工具调用不再 spawn,直到 start/reload。 */
|
|
293
325
|
stop(): Promise<void>;
|
|
@@ -690,6 +722,56 @@ export function createLspService(
|
|
|
690
722
|
: new Error(`LSP rename failed on all servers — ${detail}`);
|
|
691
723
|
}
|
|
692
724
|
|
|
725
|
+
/** 只读符号查询:与 rename 同款多服务器策略,但 MethodNotFound 是"跳过"而非失败。 */
|
|
726
|
+
async function inspect<Q extends LspInspectQuery>(request: {
|
|
727
|
+
file: string;
|
|
728
|
+
cwd: string;
|
|
729
|
+
line: number;
|
|
730
|
+
character: number;
|
|
731
|
+
query: Q;
|
|
732
|
+
options?: LspRequestOptions;
|
|
733
|
+
}): Promise<LspInspectResult<Q>> {
|
|
734
|
+
const clients = await getClients(
|
|
735
|
+
request.file,
|
|
736
|
+
request.cwd,
|
|
737
|
+
request.options?.notify,
|
|
738
|
+
(adapter) => adapter.kind !== "linter",
|
|
739
|
+
);
|
|
740
|
+
if (clients.length === 0) {
|
|
741
|
+
throw new Error(
|
|
742
|
+
`no LSP language server available for ${request.file} (check lsp.json servers and kind)`,
|
|
743
|
+
);
|
|
744
|
+
}
|
|
745
|
+
const failures: { serverID: string; error: unknown }[] = [];
|
|
746
|
+
for (const client of clients) {
|
|
747
|
+
const position = { path: request.file, line: request.line, character: request.character };
|
|
748
|
+
try {
|
|
749
|
+
if (request.query === "hover") {
|
|
750
|
+
const hover = await client.hover(position);
|
|
751
|
+
return { serverID: client.serverID, query: "hover", hover } as LspInspectResult<Q>;
|
|
752
|
+
}
|
|
753
|
+
const locations =
|
|
754
|
+
request.query === "definition"
|
|
755
|
+
? await client.definition(position)
|
|
756
|
+
: await client.references(position);
|
|
757
|
+
return {
|
|
758
|
+
serverID: client.serverID,
|
|
759
|
+
query: request.query,
|
|
760
|
+
locations,
|
|
761
|
+
} as LspInspectResult<Q>;
|
|
762
|
+
} catch (error) {
|
|
763
|
+
failures.push({ serverID: client.serverID, error });
|
|
764
|
+
}
|
|
765
|
+
}
|
|
766
|
+
const allNotSupported = failures.every((f) => f.error instanceof LspMethodNotSupportedError);
|
|
767
|
+
const detail = failures
|
|
768
|
+
.map((f) => `${f.serverID}: ${f.error instanceof Error ? f.error.message : String(f.error)}`)
|
|
769
|
+
.join("; ");
|
|
770
|
+
throw allNotSupported
|
|
771
|
+
? new LspMethodNotSupportedError("all configured servers", `textDocument/${request.query}`)
|
|
772
|
+
: new Error(`LSP inspect failed on all servers — ${detail}`);
|
|
773
|
+
}
|
|
774
|
+
|
|
693
775
|
/** 关闭全部 client 并清空缓存;closing 置 true 让 in-flight spawn 自行退出。 */
|
|
694
776
|
async function closeAll(): Promise<void> {
|
|
695
777
|
state.closing = true;
|
|
@@ -752,6 +834,7 @@ export function createLspService(
|
|
|
752
834
|
diagnostics,
|
|
753
835
|
lspDiagnosticsForFile,
|
|
754
836
|
rename,
|
|
837
|
+
inspect,
|
|
755
838
|
shutdownAll,
|
|
756
839
|
stop,
|
|
757
840
|
start,
|
|
@@ -767,71 +850,154 @@ export interface LspServiceOptions {
|
|
|
767
850
|
globalConfigPath?: string;
|
|
768
851
|
}
|
|
769
852
|
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
pi
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
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 */
|
|
780
916
|
});
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
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
|
+
}
|
|
787
934
|
});
|
|
935
|
+
|
|
936
|
+
pi.on("session_shutdown", () => {
|
|
937
|
+
void service?.shutdownAll();
|
|
938
|
+
});
|
|
939
|
+
|
|
788
940
|
// agent 生命周期边界显式刷新 status:agent 运行中 LSP server 才被惰性
|
|
789
941
|
// spawn(首次工具调用),start/end 时保证 footer 反映当前实际状态。
|
|
790
|
-
pi.on
|
|
791
|
-
pi.on
|
|
942
|
+
pi.on("agent_start", () => service?.refreshStatus());
|
|
943
|
+
pi.on("agent_end", () => service?.refreshStatus());
|
|
792
944
|
|
|
793
|
-
pi.registerCommand
|
|
945
|
+
pi.registerCommand("lsp-stop", {
|
|
794
946
|
description: "Stop all LSP servers and disable LSP until /lsp-start or /lsp-reload",
|
|
795
947
|
handler: async (_args, ctx) => {
|
|
948
|
+
if (service === undefined) {
|
|
949
|
+
ctx.ui.notify("LSP not configured: nothing to stop", "warning");
|
|
950
|
+
return;
|
|
951
|
+
}
|
|
796
952
|
await service.stop();
|
|
797
|
-
ctx.ui.notify
|
|
953
|
+
ctx.ui.notify("LSP disabled: all servers stopped", "info");
|
|
798
954
|
},
|
|
799
955
|
});
|
|
800
956
|
|
|
801
|
-
pi.registerCommand
|
|
957
|
+
pi.registerCommand("lsp-start", {
|
|
802
958
|
description: "Re-enable LSP; servers start on the next tool call",
|
|
803
959
|
handler: (_args, ctx) => {
|
|
960
|
+
if (service === undefined) {
|
|
961
|
+
ctx.ui.notify("LSP not configured: nothing to enable", "warning");
|
|
962
|
+
return Promise.resolve();
|
|
963
|
+
}
|
|
804
964
|
service.start();
|
|
805
|
-
ctx.ui.notify
|
|
965
|
+
ctx.ui.notify("LSP enabled: servers will start on the next tool call", "info");
|
|
806
966
|
return Promise.resolve();
|
|
807
967
|
},
|
|
808
968
|
});
|
|
809
969
|
|
|
810
|
-
pi.registerCommand
|
|
970
|
+
pi.registerCommand("lsp-reload", {
|
|
811
971
|
description: "Restart a specific LSP server: /lsp-reload <server-id>",
|
|
812
972
|
getArgumentCompletions: (prefix) =>
|
|
813
|
-
service
|
|
814
|
-
.serverIDs()
|
|
973
|
+
(service?.serverIDs() ?? [])
|
|
815
974
|
.toSorted()
|
|
816
975
|
.filter((id) => id.startsWith(prefix))
|
|
817
976
|
.map((id) => ({ value: id, label: id })),
|
|
818
977
|
handler: async (args, ctx) => {
|
|
978
|
+
if (service === undefined) {
|
|
979
|
+
ctx.ui.notify("LSP not configured: nothing to reload", "warning");
|
|
980
|
+
return;
|
|
981
|
+
}
|
|
819
982
|
const serverID = args.trim();
|
|
820
983
|
const known = service.serverIDs().toSorted();
|
|
821
984
|
if (!serverID) {
|
|
822
|
-
ctx.ui.notify
|
|
985
|
+
ctx.ui.notify(
|
|
823
986
|
`usage: /lsp-reload <server-id>${known.length > 0 ? ` (known: ${known.join(", ")})` : ""}`,
|
|
824
987
|
"warning",
|
|
825
988
|
);
|
|
826
989
|
return;
|
|
827
990
|
}
|
|
828
991
|
await service.reload(serverID);
|
|
829
|
-
ctx.ui.notify
|
|
992
|
+
ctx.ui.notify(
|
|
830
993
|
`LSP server "${serverID}" reloaded: will restart on the next tool call`,
|
|
831
994
|
"info",
|
|
832
995
|
);
|
|
833
996
|
},
|
|
834
997
|
});
|
|
835
998
|
|
|
836
|
-
return
|
|
999
|
+
return {
|
|
1000
|
+
mustLazyGetService: () => service ?? getNoopService(),
|
|
1001
|
+
haveEnabledLsp: () => service !== undefined,
|
|
1002
|
+
};
|
|
837
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
|
@@ -30,7 +30,8 @@ import {
|
|
|
30
30
|
import { Type } from "typebox";
|
|
31
31
|
|
|
32
32
|
import { appendLspDiagnosticText } from "../lib/lsp/diagnostic.js";
|
|
33
|
-
import {
|
|
33
|
+
import { registerLspInspectTools } from "../lib/lsp/inspect-tool.js";
|
|
34
|
+
import { createLspManager, type LspService, type LspServiceOptions } from "../lib/lsp/lsp.js";
|
|
34
35
|
import { registerLspRenameTool } from "../lib/lsp/rename-tool.js";
|
|
35
36
|
import { formatSubtitlePath } from "../lib/path.js";
|
|
36
37
|
import { guardWriteAccess } from "../lib/write-guard.js";
|
|
@@ -104,8 +105,8 @@ const IMAGE_SIGNATURES: {
|
|
|
104
105
|
return (
|
|
105
106
|
startsWithAscii(buf, 0, "BM") &&
|
|
106
107
|
buf.length >= 30 &&
|
|
107
|
-
|
|
108
|
-
[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])
|
|
109
110
|
);
|
|
110
111
|
},
|
|
111
112
|
mimeType: "image/bmp",
|
|
@@ -294,7 +295,7 @@ async function formatDirectoryEntries(dirPath: string): Promise<string[]> {
|
|
|
294
295
|
return results;
|
|
295
296
|
}
|
|
296
297
|
|
|
297
|
-
function registerReadTool(pi: ExtensionAPI,
|
|
298
|
+
function registerReadTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
298
299
|
pi.registerTool({
|
|
299
300
|
name: "read",
|
|
300
301
|
label: "read",
|
|
@@ -433,9 +434,11 @@ function registerReadTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
433
434
|
content = [{ type: "text", text: outputText }];
|
|
434
435
|
|
|
435
436
|
// opencode: LSP 文件事件通知是后台任务,失败不影响读取(read 不驻留文档)
|
|
436
|
-
void
|
|
437
|
-
|
|
438
|
-
|
|
437
|
+
void getService()
|
|
438
|
+
.notifyFile(absolutePath, ctx.cwd)
|
|
439
|
+
.catch(() => {
|
|
440
|
+
// 后台通知失败不影响读取
|
|
441
|
+
});
|
|
439
442
|
|
|
440
443
|
return {
|
|
441
444
|
content,
|
|
@@ -461,7 +464,7 @@ const editSchema = Type.Object({
|
|
|
461
464
|
),
|
|
462
465
|
});
|
|
463
466
|
|
|
464
|
-
function registerEditTool(pi: ExtensionAPI,
|
|
467
|
+
function registerEditTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
465
468
|
pi.registerTool({
|
|
466
469
|
name: "edit",
|
|
467
470
|
label: "edit",
|
|
@@ -524,7 +527,7 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
524
527
|
text: diagnosticText,
|
|
525
528
|
errorCount,
|
|
526
529
|
warningCount,
|
|
527
|
-
} = await
|
|
530
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
528
531
|
return [
|
|
529
532
|
"Edit applied successfully.",
|
|
530
533
|
{ diff: "", patch: "", firstChangedLine: 0 },
|
|
@@ -566,7 +569,7 @@ function registerEditTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
566
569
|
text: diagnosticText,
|
|
567
570
|
errorCount,
|
|
568
571
|
warningCount,
|
|
569
|
-
} = await
|
|
572
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
570
573
|
return [
|
|
571
574
|
"Edit applied successfully.",
|
|
572
575
|
{ diff: diffResult.diff, patch, firstChangedLine: diffResult.firstChangedLine },
|
|
@@ -614,7 +617,7 @@ export function resolveBom(
|
|
|
614
617
|
return { bom: sourceBom || nextBom, text };
|
|
615
618
|
}
|
|
616
619
|
|
|
617
|
-
function registerWriteTool(pi: ExtensionAPI,
|
|
620
|
+
function registerWriteTool(pi: ExtensionAPI, getService: () => LspService): void {
|
|
618
621
|
pi.registerTool({
|
|
619
622
|
name: "write",
|
|
620
623
|
label: "write",
|
|
@@ -666,7 +669,7 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
666
669
|
text: diagnosticText,
|
|
667
670
|
errorCount,
|
|
668
671
|
warningCount,
|
|
669
|
-
} = await
|
|
672
|
+
} = await getService().lspDiagnosticsForFile(absolutePath, ctx.cwd, { signal });
|
|
670
673
|
|
|
671
674
|
return [
|
|
672
675
|
"Wrote file successfully.",
|
|
@@ -693,17 +696,25 @@ function registerWriteTool(pi: ExtensionAPI, service: LspService): void {
|
|
|
693
696
|
|
|
694
697
|
// ── 入口 ─────────────────────────────────────────────────────────────────────
|
|
695
698
|
|
|
696
|
-
export function registerFileTools(pi: ExtensionAPI,
|
|
697
|
-
registerReadTool(pi,
|
|
698
|
-
registerEditTool(pi,
|
|
699
|
-
registerWriteTool(pi,
|
|
700
|
-
//
|
|
701
|
-
// read-before-write 状态,不传 recordReads hook。
|
|
702
|
-
registerLspRenameTool(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 回调注册,不在这里注册。
|
|
703
704
|
}
|
|
704
705
|
|
|
705
|
-
/** 独立入口:创建 LSP
|
|
706
|
-
export default function opencodeFileTools(pi: ExtensionAPI): void {
|
|
707
|
-
const
|
|
708
|
-
|
|
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());
|
|
709
720
|
}
|
package/src/spawn-agent.ts
CHANGED
|
@@ -89,8 +89,8 @@ const SETTINGS_PATH = join(getAgentDir(), "settings.json");
|
|
|
89
89
|
* implementation file (they share a read-snapshot state); the `tools`
|
|
90
90
|
* allowlist still exposes only the declared subset. The opencode file tools
|
|
91
91
|
* (read/edit/write) likewise share opencode/files.ts (they share the LSP
|
|
92
|
-
* service instance); that file also registers the shared `lsp-rename`
|
|
93
|
-
* which
|
|
92
|
+
* service instance); that file also registers the shared `lsp-rename` and
|
|
93
|
+
* LSP inspect tools, which stay hidden unless a subagent declares them.
|
|
94
94
|
*/
|
|
95
95
|
const TOOL_EXTENSION_OVERRIDES: Record<string, string> = {
|
|
96
96
|
read: "opencode/files.ts",
|
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
|
}
|