@trim21/personal-pi-extensions 0.1.624 → 0.1.627
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 +3 -3
- package/src/lib/lsp/client.ts +174 -181
- package/src/spawn-agent.ts +15 -9
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@trim21/personal-pi-extensions",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.627",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
|
|
6
6
|
"keywords": [
|
|
@@ -106,8 +106,8 @@
|
|
|
106
106
|
"octokit": "^5.0.5",
|
|
107
107
|
"turndown": "^7.2.0",
|
|
108
108
|
"undici": "^8.0.0",
|
|
109
|
-
"vscode-
|
|
110
|
-
"vscode-languageserver-types": "^3.18.
|
|
109
|
+
"vscode-languageserver-protocol": "^3.18.4",
|
|
110
|
+
"vscode-languageserver-types": "^3.18.4",
|
|
111
111
|
"web-tree-sitter": "^0.27.0"
|
|
112
112
|
},
|
|
113
113
|
"optionalDependencies": {
|
package/src/lib/lsp/client.ts
CHANGED
|
@@ -15,58 +15,79 @@ import { fileURLToPath, pathToFileURL } from "node:url";
|
|
|
15
15
|
|
|
16
16
|
import {
|
|
17
17
|
CancellationTokenSource,
|
|
18
|
-
|
|
19
|
-
|
|
18
|
+
ConfigurationRequest,
|
|
19
|
+
createProtocolConnection,
|
|
20
|
+
DefinitionRequest,
|
|
21
|
+
type Diagnostic as VSCodeDiagnostic,
|
|
22
|
+
DiagnosticRefreshRequest,
|
|
23
|
+
type DiagnosticRegistrationOptions,
|
|
24
|
+
DidChangeConfigurationNotification,
|
|
25
|
+
DidChangeTextDocumentNotification,
|
|
26
|
+
DidChangeWatchedFilesNotification,
|
|
27
|
+
type DidChangeWatchedFilesRegistrationOptions,
|
|
28
|
+
DidCloseTextDocumentNotification,
|
|
29
|
+
DidOpenTextDocumentNotification,
|
|
30
|
+
DocumentDiagnosticReportKind,
|
|
31
|
+
DocumentDiagnosticRequest,
|
|
32
|
+
ErrorCodes,
|
|
33
|
+
FileChangeType as LspFileChangeType,
|
|
34
|
+
type FileEvent,
|
|
35
|
+
type Hover,
|
|
36
|
+
HoverRequest,
|
|
37
|
+
InitializedNotification,
|
|
38
|
+
InitializeRequest,
|
|
39
|
+
type Location as LspLocation,
|
|
40
|
+
type LocationLink,
|
|
41
|
+
LSPErrorCodes,
|
|
42
|
+
PrepareRenameRequest,
|
|
43
|
+
type PrepareRenameResult,
|
|
44
|
+
type ProtocolConnection,
|
|
45
|
+
type ProtocolRequestType,
|
|
46
|
+
PublishDiagnosticsNotification,
|
|
47
|
+
type Range,
|
|
48
|
+
ReferencesRequest,
|
|
49
|
+
type Registration,
|
|
50
|
+
RegistrationRequest,
|
|
51
|
+
RenameRequest,
|
|
52
|
+
type RequestParam,
|
|
20
53
|
ResponseError,
|
|
54
|
+
type ServerCapabilities,
|
|
21
55
|
StreamMessageReader,
|
|
22
56
|
StreamMessageWriter,
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
} from "vscode-languageserver-types";
|
|
57
|
+
TextDocumentSyncKind,
|
|
58
|
+
UnregistrationRequest,
|
|
59
|
+
WatchKind,
|
|
60
|
+
WorkDoneProgressCreateRequest,
|
|
61
|
+
WorkspaceDiagnosticRequest,
|
|
62
|
+
type WorkspaceEdit,
|
|
63
|
+
WorkspaceFoldersRequest,
|
|
64
|
+
} from "vscode-languageserver-protocol/node";
|
|
32
65
|
|
|
33
66
|
import type { LspServerHandle } from "./adapter.js";
|
|
34
67
|
import { LANGUAGE_EXTENSIONS } from "./language.js";
|
|
35
68
|
import { editFilePaths, stabilityAcceptable, trackStability } from "./rename.js";
|
|
36
69
|
import type { FileChange, FileChangeType } from "./watcher.js";
|
|
37
70
|
|
|
38
|
-
// LSP spec 常量
|
|
39
|
-
const FILE_CHANGE_CREATED = 1;
|
|
40
|
-
const FILE_CHANGE_CHANGED = 2;
|
|
41
|
-
const FILE_CHANGE_DELETED = 3;
|
|
42
|
-
const TEXT_DOCUMENT_SYNC_INCREMENTAL = 2;
|
|
43
|
-
|
|
44
71
|
/** LSP WatchKind 位掩码(FileSystemWatcher.kind,缺省 create|change|delete)。 */
|
|
45
|
-
export const WATCH_KIND_CREATE =
|
|
46
|
-
export const WATCH_KIND_CHANGE =
|
|
47
|
-
export const WATCH_KIND_DELETE =
|
|
48
|
-
const WATCH_KIND_ALL =
|
|
72
|
+
export const WATCH_KIND_CREATE = WatchKind.Create;
|
|
73
|
+
export const WATCH_KIND_CHANGE = WatchKind.Change;
|
|
74
|
+
export const WATCH_KIND_DELETE = WatchKind.Delete;
|
|
75
|
+
const WATCH_KIND_ALL = WatchKind.Create | WatchKind.Change | WatchKind.Delete;
|
|
49
76
|
|
|
50
77
|
/** 服务器通过 client/registerCapability 注册的单个 watcher。 */
|
|
51
|
-
interface WatcherGlob {
|
|
78
|
+
export interface WatcherGlob {
|
|
52
79
|
pattern: string;
|
|
53
80
|
kind: number;
|
|
54
81
|
}
|
|
55
82
|
|
|
56
|
-
const FILE_CHANGE_TYPE: Record<FileChangeType,
|
|
57
|
-
created:
|
|
58
|
-
changed:
|
|
59
|
-
deleted:
|
|
83
|
+
const FILE_CHANGE_TYPE: Record<FileChangeType, LspFileChangeType> = {
|
|
84
|
+
created: LspFileChangeType.Created,
|
|
85
|
+
changed: LspFileChangeType.Changed,
|
|
86
|
+
deleted: LspFileChangeType.Deleted,
|
|
60
87
|
};
|
|
61
88
|
|
|
62
89
|
export type Diagnostic = VSCodeDiagnostic;
|
|
63
90
|
|
|
64
|
-
/** LSP MethodNotFound(-32601):服务器未实现 prepareRename / rename 请求。 */
|
|
65
|
-
const LSP_METHOD_NOT_FOUND = -32601;
|
|
66
|
-
|
|
67
|
-
/** LSP ContentModified(-32801):服务器处理请求期间内容被修改,重发请求即可。 */
|
|
68
|
-
const LSP_CONTENT_MODIFIED = -32801;
|
|
69
|
-
|
|
70
91
|
/**
|
|
71
92
|
* 位置不可 rename 或服务器不具备 rename 能力;与传输失败等意外错误区分,
|
|
72
93
|
* 供调用方在多候选探测时跳过该 client 而不是中断整个操作。
|
|
@@ -151,7 +172,7 @@ async function retryOnContentModified<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
151
172
|
try {
|
|
152
173
|
return await fn();
|
|
153
174
|
} catch (error) {
|
|
154
|
-
if (!(error instanceof ResponseError && error.code ===
|
|
175
|
+
if (!(error instanceof ResponseError && error.code === LSPErrorCodes.ContentModified)) {
|
|
155
176
|
throw error;
|
|
156
177
|
}
|
|
157
178
|
if (attempt >= renameVerificationTiming.contentModifiedRetries) {
|
|
@@ -161,14 +182,8 @@ async function retryOnContentModified<T>(fn: () => Promise<T>): Promise<T> {
|
|
|
161
182
|
}
|
|
162
183
|
}
|
|
163
184
|
|
|
164
|
-
interface PrepareRenameResponse {
|
|
165
|
-
range?: unknown;
|
|
166
|
-
placeholder?: string;
|
|
167
|
-
defaultBehavior?: boolean;
|
|
168
|
-
}
|
|
169
|
-
|
|
170
185
|
/** renameSymbol 的请求与结果(line / character 为 0-based LSP position)。 */
|
|
171
|
-
interface RenameSymbolRequest {
|
|
186
|
+
export interface RenameSymbolRequest {
|
|
172
187
|
path: string;
|
|
173
188
|
line: number;
|
|
174
189
|
character: number;
|
|
@@ -177,14 +192,14 @@ interface RenameSymbolRequest {
|
|
|
177
192
|
signal?: AbortSignal;
|
|
178
193
|
}
|
|
179
194
|
|
|
180
|
-
interface RenameSymbolResult {
|
|
195
|
+
export interface RenameSymbolResult {
|
|
181
196
|
edit: WorkspaceEdit;
|
|
182
197
|
/** prepareRename 返回的符号当前名;服务器未提供 prepare 时缺省。 */
|
|
183
198
|
placeholder?: string;
|
|
184
199
|
}
|
|
185
200
|
|
|
186
201
|
/** definition / references / hover 请求的输入(line / character 为 0-based LSP position)。 */
|
|
187
|
-
interface InspectPositionRequest {
|
|
202
|
+
export interface InspectPositionRequest {
|
|
188
203
|
path: string;
|
|
189
204
|
line: number;
|
|
190
205
|
character: number;
|
|
@@ -272,7 +287,7 @@ function describeStartupFailure(
|
|
|
272
287
|
return parts.join("; ");
|
|
273
288
|
}
|
|
274
289
|
|
|
275
|
-
class InitializeError extends Error {
|
|
290
|
+
export class InitializeError extends Error {
|
|
276
291
|
readonly serverID: string;
|
|
277
292
|
constructor(serverID: string, cause: unknown, detail?: string) {
|
|
278
293
|
super(`Failed to initialize LSP server ${serverID}${detail ? `: ${detail}` : ""}`, { cause });
|
|
@@ -280,15 +295,6 @@ class InitializeError extends Error {
|
|
|
280
295
|
}
|
|
281
296
|
}
|
|
282
297
|
|
|
283
|
-
interface DocumentDiagnosticReport {
|
|
284
|
-
items?: Diagnostic[];
|
|
285
|
-
relatedDocuments?: Record<string, DocumentDiagnosticReport>;
|
|
286
|
-
}
|
|
287
|
-
|
|
288
|
-
interface WorkspaceDiagnosticReport {
|
|
289
|
-
items?: { uri?: string; items?: Diagnostic[] }[];
|
|
290
|
-
}
|
|
291
|
-
|
|
292
298
|
interface DiagnosticRequestResult {
|
|
293
299
|
handled: boolean;
|
|
294
300
|
matched: boolean;
|
|
@@ -304,27 +310,6 @@ interface PullResult {
|
|
|
304
310
|
timedOut: boolean;
|
|
305
311
|
}
|
|
306
312
|
|
|
307
|
-
interface CapabilityRegistration {
|
|
308
|
-
id: string;
|
|
309
|
-
method: string;
|
|
310
|
-
registerOptions?: {
|
|
311
|
-
identifier?: string;
|
|
312
|
-
workspaceDiagnostics?: boolean;
|
|
313
|
-
watchers?: { globPattern?: string; kind?: number }[];
|
|
314
|
-
};
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
interface ServerCapabilities {
|
|
318
|
-
textDocumentSync?:
|
|
319
|
-
| number
|
|
320
|
-
| {
|
|
321
|
-
change?: number;
|
|
322
|
-
};
|
|
323
|
-
diagnosticProvider?: unknown;
|
|
324
|
-
renameProvider?: boolean | { prepareProvider?: boolean };
|
|
325
|
-
[key: string]: unknown;
|
|
326
|
-
}
|
|
327
|
-
|
|
328
313
|
/**
|
|
329
314
|
* create 的直连缺省(单一来源):lsp.ts 的 resolveConfig 解析超时/LRU 时引用
|
|
330
315
|
* 同一组数值,保证配置层缺省与直连调用方取值一致。
|
|
@@ -368,7 +353,7 @@ export interface CreateInput {
|
|
|
368
353
|
export interface LspClient {
|
|
369
354
|
readonly root: string;
|
|
370
355
|
readonly serverID: string;
|
|
371
|
-
readonly connection:
|
|
356
|
+
readonly connection: ProtocolConnection;
|
|
372
357
|
readonly notify: {
|
|
373
358
|
open(request: { path: string }): Promise<number>;
|
|
374
359
|
/** 把工作区文件事件批量通知服务器;驻留文档不在此通道(走 didOpen/didChange/退场)。 */
|
|
@@ -589,7 +574,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
589
574
|
const initializeTimeoutMs = input.initializeTimeoutMs ?? clientDefaults.initializeTimeoutMs;
|
|
590
575
|
const maxOpenDocuments = input.maxOpenDocuments ?? clientDefaults.maxOpenDocuments;
|
|
591
576
|
|
|
592
|
-
const connection =
|
|
577
|
+
const connection = createProtocolConnection(
|
|
593
578
|
new StreamMessageReader(input.server.process.stdout),
|
|
594
579
|
new StreamMessageWriter(input.server.process.stdin),
|
|
595
580
|
);
|
|
@@ -616,7 +601,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
616
601
|
const pushDiagnostics = new Map<string, Diagnostic[]>();
|
|
617
602
|
const pullDiagnostics = new Map<string, Diagnostic[]>();
|
|
618
603
|
const published = new Map<string, { at: number; version?: number }>();
|
|
619
|
-
const diagnosticRegistrations = new Map<string,
|
|
604
|
+
const diagnosticRegistrations = new Map<string, Registration>();
|
|
620
605
|
/** registration id → workspace/didChangeWatchedFiles watchers(pattern + WatchKind 位)。 */
|
|
621
606
|
const watcherRegistrations = new Map<string, WatcherGlob[]>();
|
|
622
607
|
const registrationListeners = new Set<() => void>();
|
|
@@ -645,56 +630,54 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
645
630
|
|
|
646
631
|
// ── LSP 连接处理器 ─────────────────────────────────────────────────────────
|
|
647
632
|
|
|
648
|
-
connection.onNotification(
|
|
649
|
-
|
|
650
|
-
(
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
672
|
-
}
|
|
673
|
-
updatePushDiagnostics(filePath, params.diagnostics);
|
|
674
|
-
},
|
|
675
|
-
);
|
|
676
|
-
connection.onRequest("window/workDoneProgress/create", () => null);
|
|
677
|
-
connection.onRequest("workspace/configuration", (params) => {
|
|
678
|
-
const items = (params as { items?: { section?: string }[] }).items ?? [];
|
|
679
|
-
return items.map((item) =>
|
|
680
|
-
configurationValue(input.server.settings ?? input.server.initialization, item.section),
|
|
681
|
-
);
|
|
633
|
+
connection.onNotification(PublishDiagnosticsNotification.type, (params) => {
|
|
634
|
+
const filePath = getFilePath(params.uri);
|
|
635
|
+
if (!filePath) {
|
|
636
|
+
return;
|
|
637
|
+
}
|
|
638
|
+
// 服务器版本滞后于已发送版本时,该 push 对应的是旧内容(异步重算未完成
|
|
639
|
+
// 时的迟到结果)。忽略,避免与当前版本结果混淆。
|
|
640
|
+
const currentVersion = documentVersions.get(filePath);
|
|
641
|
+
const isStalePush =
|
|
642
|
+
typeof params.version === "number" &&
|
|
643
|
+
currentVersion !== undefined &&
|
|
644
|
+
params.version !== currentVersion;
|
|
645
|
+
if (isStalePush) {
|
|
646
|
+
return;
|
|
647
|
+
}
|
|
648
|
+
published.set(filePath, {
|
|
649
|
+
at: Date.now(),
|
|
650
|
+
version: typeof params.version === "number" ? params.version : undefined,
|
|
651
|
+
});
|
|
652
|
+
const document = files[filePath];
|
|
653
|
+
if (document !== undefined) {
|
|
654
|
+
document.lastPushEmpty = params.diagnostics.length === 0;
|
|
655
|
+
}
|
|
656
|
+
updatePushDiagnostics(filePath, params.diagnostics);
|
|
682
657
|
});
|
|
683
|
-
connection.onRequest(
|
|
684
|
-
|
|
685
|
-
|
|
658
|
+
connection.onRequest(WorkDoneProgressCreateRequest.type, () => {
|
|
659
|
+
/* 不展示服务器进度:仍按协议接受进度条创建 */
|
|
660
|
+
});
|
|
661
|
+
connection.onRequest(ConfigurationRequest.type, (params) =>
|
|
662
|
+
params.items.map((item) =>
|
|
663
|
+
configurationValue(input.server.settings ?? input.server.initialization, item.section),
|
|
664
|
+
),
|
|
665
|
+
);
|
|
666
|
+
connection.onRequest(RegistrationRequest.type, (params) => {
|
|
686
667
|
let changed = false;
|
|
687
|
-
for (const registration of registrations) {
|
|
688
|
-
if (registration.method ===
|
|
668
|
+
for (const registration of params.registrations) {
|
|
669
|
+
if (registration.method === DidChangeWatchedFilesNotification.method) {
|
|
670
|
+
const options = registration.registerOptions as
|
|
671
|
+
DidChangeWatchedFilesRegistrationOptions | undefined;
|
|
689
672
|
const watchers =
|
|
690
|
-
|
|
691
|
-
|
|
673
|
+
options?.watchers
|
|
674
|
+
.map((watcher) => ({
|
|
692
675
|
pattern: watcher.globPattern,
|
|
693
676
|
kind: watcher.kind ?? WATCH_KIND_ALL,
|
|
694
677
|
}))
|
|
695
678
|
.filter((watcher): watcher is WatcherGlob => typeof watcher.pattern === "string") ?? [];
|
|
696
679
|
watcherRegistrations.set(registration.id, watchers);
|
|
697
|
-
} else if (registration.method ===
|
|
680
|
+
} else if (registration.method === DocumentDiagnosticRequest.method) {
|
|
698
681
|
diagnosticRegistrations.set(registration.id, registration);
|
|
699
682
|
changed = true;
|
|
700
683
|
}
|
|
@@ -703,14 +686,12 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
703
686
|
emitRegistrationChange();
|
|
704
687
|
}
|
|
705
688
|
});
|
|
706
|
-
connection.onRequest(
|
|
707
|
-
const registrations =
|
|
708
|
-
(params as { unregisterations?: { id: string; method: string }[] }).unregisterations ?? [];
|
|
689
|
+
connection.onRequest(UnregistrationRequest.type, (params) => {
|
|
709
690
|
let changed = false;
|
|
710
|
-
for (const registration of
|
|
711
|
-
if (registration.method ===
|
|
691
|
+
for (const registration of params.unregisterations) {
|
|
692
|
+
if (registration.method === DidChangeWatchedFilesNotification.method) {
|
|
712
693
|
watcherRegistrations.delete(registration.id);
|
|
713
|
-
} else if (registration.method ===
|
|
694
|
+
} else if (registration.method === DocumentDiagnosticRequest.method) {
|
|
714
695
|
diagnosticRegistrations.delete(registration.id);
|
|
715
696
|
changed = true;
|
|
716
697
|
}
|
|
@@ -719,18 +700,20 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
719
700
|
emitRegistrationChange();
|
|
720
701
|
}
|
|
721
702
|
});
|
|
722
|
-
connection.onRequest(
|
|
703
|
+
connection.onRequest(WorkspaceFoldersRequest.type, () => [
|
|
723
704
|
{ name: "workspace", uri: pathToFileURL(input.root).href },
|
|
724
705
|
]);
|
|
725
|
-
connection.onRequest(
|
|
706
|
+
connection.onRequest(DiagnosticRefreshRequest.type, () => {
|
|
707
|
+
/* 诊断由本客户端主动拉取,刷新请求无需额外动作 */
|
|
708
|
+
});
|
|
726
709
|
connection.listen();
|
|
727
710
|
|
|
728
711
|
// ── initialize 握手 ─────────────────────────────────────────────────────────
|
|
729
712
|
|
|
730
713
|
const initialized = await withTimeout(
|
|
731
|
-
connection.sendRequest
|
|
714
|
+
connection.sendRequest(InitializeRequest.type, {
|
|
732
715
|
rootUri: pathToFileURL(input.root).href,
|
|
733
|
-
processId: input.server.process.pid,
|
|
716
|
+
processId: input.server.process.pid ?? null,
|
|
734
717
|
workspaceFolders: [{ name: "workspace", uri: pathToFileURL(input.root).href }],
|
|
735
718
|
initializationOptions: {
|
|
736
719
|
...input.server.initialization,
|
|
@@ -743,7 +726,6 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
743
726
|
diagnostics: { refreshSupport: false },
|
|
744
727
|
},
|
|
745
728
|
textDocument: {
|
|
746
|
-
synchronization: { didOpen: true, didChange: true },
|
|
747
729
|
diagnostic: { dynamicRegistration: true, relatedDocumentSupport: true },
|
|
748
730
|
publishDiagnostics: { versionSupport: false },
|
|
749
731
|
},
|
|
@@ -763,18 +745,18 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
763
745
|
});
|
|
764
746
|
|
|
765
747
|
const syncKind = getSyncKind(initialized.capabilities);
|
|
766
|
-
const hasStaticPullDiagnostics = Boolean(initialized.capabilities
|
|
748
|
+
const hasStaticPullDiagnostics = Boolean(initialized.capabilities.diagnosticProvider);
|
|
767
749
|
// prepareProvider 只在静态声明为对象且显式开启时使用;其余情况跳过 prepare
|
|
768
750
|
// 直接 rename(能力可能经动态注册,静态声明缺失不代表服务器不支持)。
|
|
769
|
-
const renameProvider = initialized.capabilities
|
|
751
|
+
const renameProvider = initialized.capabilities.renameProvider;
|
|
770
752
|
const hasPrepareProvider =
|
|
771
753
|
typeof renameProvider === "object" && renameProvider.prepareProvider === true;
|
|
772
754
|
|
|
773
|
-
await connection.sendNotification(
|
|
755
|
+
await connection.sendNotification(InitializedNotification.type, {});
|
|
774
756
|
|
|
775
757
|
const settings = input.server.settings ?? input.server.initialization;
|
|
776
758
|
if (settings) {
|
|
777
|
-
await connection.sendNotification(
|
|
759
|
+
await connection.sendNotification(DidChangeConfigurationNotification.type, { settings });
|
|
778
760
|
}
|
|
779
761
|
|
|
780
762
|
/**
|
|
@@ -819,7 +801,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
819
801
|
}
|
|
820
802
|
for (const path of plan.evict) {
|
|
821
803
|
lruOrder.delete(path);
|
|
822
|
-
await connection.sendNotification(
|
|
804
|
+
await connection.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
823
805
|
textDocument: { uri: pathToFileURL(path).href },
|
|
824
806
|
});
|
|
825
807
|
delete files[path];
|
|
@@ -864,7 +846,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
864
846
|
): Promise<DiagnosticRequestResult> {
|
|
865
847
|
let timedOut = false;
|
|
866
848
|
const report = await withTimeout(
|
|
867
|
-
connection.sendRequest
|
|
849
|
+
connection.sendRequest(DocumentDiagnosticRequest.type, {
|
|
868
850
|
...(identifier && { identifier }),
|
|
869
851
|
textDocument: { uri: pathToFileURL(filePath).href },
|
|
870
852
|
}),
|
|
@@ -887,14 +869,14 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
887
869
|
|
|
888
870
|
let handled = false;
|
|
889
871
|
let matched = false;
|
|
890
|
-
if (
|
|
872
|
+
if (report.kind === DocumentDiagnosticReportKind.Full) {
|
|
891
873
|
push(filePath, report.items);
|
|
892
874
|
handled = true;
|
|
893
875
|
matched = true;
|
|
894
876
|
}
|
|
895
877
|
for (const [uri, related] of Object.entries(report.relatedDocuments ?? {})) {
|
|
896
878
|
const relatedPath = getFilePath(uri);
|
|
897
|
-
if (!relatedPath ||
|
|
879
|
+
if (!relatedPath || related.kind !== DocumentDiagnosticReportKind.Full) {
|
|
898
880
|
continue;
|
|
899
881
|
}
|
|
900
882
|
push(relatedPath, related.items);
|
|
@@ -911,7 +893,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
911
893
|
): Promise<DiagnosticRequestResult> {
|
|
912
894
|
let timedOut = false;
|
|
913
895
|
const report = await withTimeout(
|
|
914
|
-
connection.sendRequest
|
|
896
|
+
connection.sendRequest(WorkspaceDiagnosticRequest.type, {
|
|
915
897
|
...(identifier && { identifier }),
|
|
916
898
|
previousResultIds: [],
|
|
917
899
|
}),
|
|
@@ -928,9 +910,9 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
928
910
|
|
|
929
911
|
const byFile = new Map<string, Diagnostic[]>();
|
|
930
912
|
let matched = false;
|
|
931
|
-
for (const item of report.items
|
|
932
|
-
const relatedPath =
|
|
933
|
-
if (!relatedPath ||
|
|
913
|
+
for (const item of report.items) {
|
|
914
|
+
const relatedPath = getFilePath(item.uri);
|
|
915
|
+
if (!relatedPath || item.kind !== DocumentDiagnosticReportKind.Full) {
|
|
934
916
|
continue;
|
|
935
917
|
}
|
|
936
918
|
const existing = byFile.get(relatedPath) ?? [];
|
|
@@ -941,13 +923,22 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
941
923
|
return { handled: true, matched, byFile, timedOut };
|
|
942
924
|
}
|
|
943
925
|
|
|
926
|
+
/** 动态注册的 pull 诊断选项:Registration.registerOptions 是 any,按注册方法收窄。 */
|
|
927
|
+
function diagnosticRegistrationOptions(
|
|
928
|
+
registration: Registration,
|
|
929
|
+
): DiagnosticRegistrationOptions | undefined {
|
|
930
|
+
return registration.registerOptions as DiagnosticRegistrationOptions | undefined;
|
|
931
|
+
}
|
|
932
|
+
|
|
944
933
|
function documentPullState() {
|
|
945
934
|
const documentRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
946
|
-
(registration) => registration
|
|
935
|
+
(registration) => diagnosticRegistrationOptions(registration)?.workspaceDiagnostics !== true,
|
|
947
936
|
);
|
|
948
937
|
return {
|
|
949
938
|
documentIdentifiers: [
|
|
950
|
-
...new Set(
|
|
939
|
+
...new Set(
|
|
940
|
+
documentRegistrations.flatMap((r) => diagnosticRegistrationOptions(r)?.identifier ?? []),
|
|
941
|
+
),
|
|
951
942
|
],
|
|
952
943
|
supported: hasStaticPullDiagnostics || documentRegistrations.length > 0,
|
|
953
944
|
};
|
|
@@ -955,11 +946,13 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
955
946
|
|
|
956
947
|
function workspacePullState() {
|
|
957
948
|
const workspaceRegistrations = [...diagnosticRegistrations.values()].filter(
|
|
958
|
-
(registration) => registration
|
|
949
|
+
(registration) => diagnosticRegistrationOptions(registration)?.workspaceDiagnostics === true,
|
|
959
950
|
);
|
|
960
951
|
return {
|
|
961
952
|
workspaceIdentifiers: [
|
|
962
|
-
...new Set(
|
|
953
|
+
...new Set(
|
|
954
|
+
workspaceRegistrations.flatMap((r) => diagnosticRegistrationOptions(r)?.identifier ?? []),
|
|
955
|
+
),
|
|
963
956
|
],
|
|
964
957
|
supported: workspaceRegistrations.length > 0,
|
|
965
958
|
};
|
|
@@ -1263,10 +1256,10 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1263
1256
|
// 保留 lastPushEmpty:安静期判据看的是「变更前服务器最后一份结论是否为空」
|
|
1264
1257
|
files[resolvedPath] = { ...document, version: next, text, syncedAt: Date.now() };
|
|
1265
1258
|
documentVersions.set(resolvedPath, next);
|
|
1266
|
-
await connection.sendNotification(
|
|
1259
|
+
await connection.sendNotification(DidChangeTextDocumentNotification.type, {
|
|
1267
1260
|
textDocument: { uri, version: next },
|
|
1268
1261
|
contentChanges:
|
|
1269
|
-
syncKind ===
|
|
1262
|
+
syncKind === TextDocumentSyncKind.Incremental
|
|
1270
1263
|
? [
|
|
1271
1264
|
{
|
|
1272
1265
|
range: { start: { line: 0, character: 0 }, end: endPosition(document.text) },
|
|
@@ -1282,7 +1275,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1282
1275
|
|
|
1283
1276
|
pushDiagnostics.delete(resolvedPath);
|
|
1284
1277
|
pullDiagnostics.delete(resolvedPath);
|
|
1285
|
-
await connection.sendNotification(
|
|
1278
|
+
await connection.sendNotification(DidOpenTextDocumentNotification.type, {
|
|
1286
1279
|
textDocument: { uri, languageId, version: 0, text },
|
|
1287
1280
|
});
|
|
1288
1281
|
files[resolvedPath] = { version: 0, text, syncedAt: Date.now() };
|
|
@@ -1311,13 +1304,13 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1311
1304
|
* ($/cancelRequest),也立刻以 signal.reason(默认 AbortError)拒绝本地
|
|
1312
1305
|
* promise——服务器可能永远不回应,工具调用不能就这么挂着。
|
|
1313
1306
|
*/
|
|
1314
|
-
async function sendAbortableRequest<
|
|
1315
|
-
|
|
1316
|
-
params:
|
|
1307
|
+
async function sendAbortableRequest<P, R, PR, E, RO>(
|
|
1308
|
+
type: ProtocolRequestType<P, R, PR, E, RO>,
|
|
1309
|
+
params: RequestParam<P>,
|
|
1317
1310
|
signal: AbortSignal | undefined,
|
|
1318
|
-
): Promise<
|
|
1311
|
+
): Promise<R> {
|
|
1319
1312
|
if (!signal) {
|
|
1320
|
-
return await connection.sendRequest
|
|
1313
|
+
return await connection.sendRequest(type, params);
|
|
1321
1314
|
}
|
|
1322
1315
|
if (signal.aborted) {
|
|
1323
1316
|
throw abortReason(signal);
|
|
@@ -1337,7 +1330,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1337
1330
|
signal.addEventListener("abort", onAbort, { once: true });
|
|
1338
1331
|
try {
|
|
1339
1332
|
return await Promise.race([
|
|
1340
|
-
connection.sendRequest
|
|
1333
|
+
connection.sendRequest(type, params, source.token),
|
|
1341
1334
|
aborted.promise,
|
|
1342
1335
|
]);
|
|
1343
1336
|
} finally {
|
|
@@ -1346,16 +1339,16 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1346
1339
|
}
|
|
1347
1340
|
}
|
|
1348
1341
|
|
|
1349
|
-
const sendInspectRequest = async <
|
|
1350
|
-
|
|
1351
|
-
message:
|
|
1342
|
+
const sendInspectRequest = async <P, R, PR, E, RO>(
|
|
1343
|
+
type: ProtocolRequestType<P, R, PR, E, RO>,
|
|
1344
|
+
message: RequestParam<P>,
|
|
1352
1345
|
signal?: AbortSignal,
|
|
1353
|
-
): Promise<
|
|
1346
|
+
): Promise<R> => {
|
|
1354
1347
|
try {
|
|
1355
|
-
return await retryOnContentModified(() => sendAbortableRequest
|
|
1348
|
+
return await retryOnContentModified(() => sendAbortableRequest(type, message, signal));
|
|
1356
1349
|
} catch (error) {
|
|
1357
|
-
if (error instanceof ResponseError && error.code ===
|
|
1358
|
-
throw new LspMethodNotSupportedError(input.serverID, method);
|
|
1350
|
+
if (error instanceof ResponseError && error.code === ErrorCodes.MethodNotFound) {
|
|
1351
|
+
throw new LspMethodNotSupportedError(input.serverID, type.method);
|
|
1359
1352
|
}
|
|
1360
1353
|
throw error;
|
|
1361
1354
|
}
|
|
@@ -1382,7 +1375,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1382
1375
|
notify: {
|
|
1383
1376
|
open: openDocument,
|
|
1384
1377
|
async watchedFiles(changes: FileChange[]): Promise<void> {
|
|
1385
|
-
const notified:
|
|
1378
|
+
const notified: FileEvent[] = [];
|
|
1386
1379
|
for (const change of changes) {
|
|
1387
1380
|
const resolvedPath = normalize(
|
|
1388
1381
|
isAbsolute(change.path) ? change.path : resolve(input.directory, change.path),
|
|
@@ -1406,7 +1399,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1406
1399
|
continue;
|
|
1407
1400
|
}
|
|
1408
1401
|
}
|
|
1409
|
-
await connection.sendNotification(
|
|
1402
|
+
await connection.sendNotification(DidCloseTextDocumentNotification.type, {
|
|
1410
1403
|
textDocument: { uri: pathToFileURL(resolvedPath).href },
|
|
1411
1404
|
});
|
|
1412
1405
|
delete files[resolvedPath];
|
|
@@ -1420,7 +1413,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1420
1413
|
if (notified.length === 0) {
|
|
1421
1414
|
return;
|
|
1422
1415
|
}
|
|
1423
|
-
await connection.sendNotification(
|
|
1416
|
+
await connection.sendNotification(DidChangeWatchedFilesNotification.type, {
|
|
1424
1417
|
changes: notified,
|
|
1425
1418
|
});
|
|
1426
1419
|
},
|
|
@@ -1469,8 +1462,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1469
1462
|
// 与编辑器行为一致。
|
|
1470
1463
|
const referencesRequest = () =>
|
|
1471
1464
|
retryOnContentModified(() =>
|
|
1472
|
-
sendAbortableRequest
|
|
1473
|
-
|
|
1465
|
+
sendAbortableRequest(
|
|
1466
|
+
ReferencesRequest.type,
|
|
1474
1467
|
{
|
|
1475
1468
|
textDocument: { uri },
|
|
1476
1469
|
position,
|
|
@@ -1490,8 +1483,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1490
1483
|
const sendRename = async (): Promise<WorkspaceEdit | null> => {
|
|
1491
1484
|
try {
|
|
1492
1485
|
return await retryOnContentModified(() =>
|
|
1493
|
-
sendAbortableRequest
|
|
1494
|
-
|
|
1486
|
+
sendAbortableRequest(
|
|
1487
|
+
RenameRequest.type,
|
|
1495
1488
|
{
|
|
1496
1489
|
textDocument: { uri },
|
|
1497
1490
|
position,
|
|
@@ -1501,7 +1494,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1501
1494
|
),
|
|
1502
1495
|
);
|
|
1503
1496
|
} catch (error) {
|
|
1504
|
-
if (error instanceof ResponseError && error.code ===
|
|
1497
|
+
if (error instanceof ResponseError && error.code === ErrorCodes.MethodNotFound) {
|
|
1505
1498
|
throw notRenameable();
|
|
1506
1499
|
}
|
|
1507
1500
|
throw error;
|
|
@@ -1510,17 +1503,17 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1510
1503
|
|
|
1511
1504
|
let placeholder: string | undefined;
|
|
1512
1505
|
if (hasPrepareProvider) {
|
|
1513
|
-
let prepared:
|
|
1506
|
+
let prepared: PrepareRenameResult | null;
|
|
1514
1507
|
try {
|
|
1515
1508
|
prepared = await retryOnContentModified(() =>
|
|
1516
|
-
sendAbortableRequest
|
|
1517
|
-
|
|
1509
|
+
sendAbortableRequest(
|
|
1510
|
+
PrepareRenameRequest.type,
|
|
1518
1511
|
{ textDocument: { uri }, position },
|
|
1519
1512
|
request.signal,
|
|
1520
1513
|
),
|
|
1521
1514
|
);
|
|
1522
1515
|
} catch (error) {
|
|
1523
|
-
if (error instanceof ResponseError && error.code ===
|
|
1516
|
+
if (error instanceof ResponseError && error.code === ErrorCodes.MethodNotFound) {
|
|
1524
1517
|
throw notRenameable();
|
|
1525
1518
|
}
|
|
1526
1519
|
throw error;
|
|
@@ -1528,7 +1521,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1528
1521
|
if (!prepared) {
|
|
1529
1522
|
throw notRenameable();
|
|
1530
1523
|
}
|
|
1531
|
-
if (typeof prepared.placeholder === "string") {
|
|
1524
|
+
if ("placeholder" in prepared && typeof prepared.placeholder === "string") {
|
|
1532
1525
|
placeholder = prepared.placeholder;
|
|
1533
1526
|
}
|
|
1534
1527
|
}
|
|
@@ -1537,7 +1530,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1537
1530
|
try {
|
|
1538
1531
|
locations = await referencesRequest();
|
|
1539
1532
|
} catch (error) {
|
|
1540
|
-
if (!(error instanceof ResponseError && error.code ===
|
|
1533
|
+
if (!(error instanceof ResponseError && error.code === ErrorCodes.MethodNotFound)) {
|
|
1541
1534
|
throw error;
|
|
1542
1535
|
}
|
|
1543
1536
|
const edit = await sendRename();
|
|
@@ -1608,8 +1601,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1608
1601
|
async definition(request: InspectPositionRequest): Promise<InspectLocation[]> {
|
|
1609
1602
|
const { uri, position } = await preparePositionRequest(request);
|
|
1610
1603
|
return toInspectLocations(
|
|
1611
|
-
await sendInspectRequest
|
|
1612
|
-
|
|
1604
|
+
await sendInspectRequest(
|
|
1605
|
+
DefinitionRequest.type,
|
|
1613
1606
|
{ textDocument: { uri }, position },
|
|
1614
1607
|
request.signal,
|
|
1615
1608
|
),
|
|
@@ -1617,8 +1610,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1617
1610
|
},
|
|
1618
1611
|
async references(request: InspectPositionRequest): Promise<InspectLocation[]> {
|
|
1619
1612
|
const { uri, position } = await preparePositionRequest(request);
|
|
1620
|
-
const locations = await sendInspectRequest
|
|
1621
|
-
|
|
1613
|
+
const locations = await sendInspectRequest(
|
|
1614
|
+
ReferencesRequest.type,
|
|
1622
1615
|
{
|
|
1623
1616
|
textDocument: { uri },
|
|
1624
1617
|
position,
|
|
@@ -1630,8 +1623,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
|
|
|
1630
1623
|
},
|
|
1631
1624
|
async hover(request: InspectPositionRequest): Promise<Hover | null> {
|
|
1632
1625
|
const { uri, position } = await preparePositionRequest(request);
|
|
1633
|
-
return await sendInspectRequest
|
|
1634
|
-
|
|
1626
|
+
return await sendInspectRequest(
|
|
1627
|
+
HoverRequest.type,
|
|
1635
1628
|
{ textDocument: { uri }, position },
|
|
1636
1629
|
request.signal,
|
|
1637
1630
|
);
|
package/src/spawn-agent.ts
CHANGED
|
@@ -16,9 +16,11 @@
|
|
|
16
16
|
* `MAX_PROGRESS_LINES` lines. Consecutive tool calls are merged into a
|
|
17
17
|
* single `tool:` line (`read x 2, glob`) and over-long line content is
|
|
18
18
|
* folded to the first/last 9 chars joined by `…`, so a burst of tool calls
|
|
19
|
-
* or a long text block does not flood the window;
|
|
20
|
-
* new line
|
|
21
|
-
*
|
|
19
|
+
* or a long text block does not flood the window; only a text block starts
|
|
20
|
+
* a new line — thinking is a transient status line, so it does not break the
|
|
21
|
+
* merge across turn boundaries. Line content is sanitized first: markdown
|
|
22
|
+
* marker characters are stripped and whitespace (including newlines) is
|
|
23
|
+
* collapsed to single spaces,
|
|
22
24
|
* so one log entry is always exactly one rendered line. The final line is
|
|
23
25
|
* always the subagent name as a code span (`` `scout` ``), followed by the
|
|
24
26
|
* live usage stats when there are any; it rides outside the rolling window so
|
|
@@ -91,8 +93,11 @@ export const SUBAGENT_DEFAULT_SANDBOX: BwrapConfig = completeBwrapConfig({
|
|
|
91
93
|
fs: { mode: "readonly" },
|
|
92
94
|
network: { mode: "block" },
|
|
93
95
|
});
|
|
94
|
-
/**
|
|
95
|
-
|
|
96
|
+
/**
|
|
97
|
+
* Progress log keeps only the most recent lines (rolling window).
|
|
98
|
+
* 展开后面板最多 6 行内容:4 行日志 + 可能的一行瞬态 thinking + 固定的 metadata 行。
|
|
99
|
+
*/
|
|
100
|
+
const MAX_PROGRESS_LINES = 4;
|
|
96
101
|
/** Progress line content (without the `tool:` / `text:` prefix) is capped at 21 chars; longer text is folded to the first/last 9 chars joined by ` … `. */
|
|
97
102
|
const MAX_PROGRESS_CHARS_PER_LINE = 21;
|
|
98
103
|
/**
|
|
@@ -464,7 +469,8 @@ export async function runAgent(
|
|
|
464
469
|
let logLines: string[] = [];
|
|
465
470
|
// 工具调用行合并:连续的 tool_execution_start 事件合并在同一 `tool:` 行
|
|
466
471
|
// (如 `tool: read x 2, glob`),相同工具名连续出现时计为 `name x N`,
|
|
467
|
-
//
|
|
472
|
+
// 不同名按调用顺序罗列;只有写入日志行的事件(text 块)打断合并,thinking
|
|
473
|
+
// 不写日志行、也不打断合并。
|
|
468
474
|
let toolLineSegments: string[] = [];
|
|
469
475
|
let toolLine: { name: string; count: number } | undefined;
|
|
470
476
|
// 思考中状态:thinking_start 打开、thinking_delta 累计字符数、thinking_end
|
|
@@ -476,7 +482,7 @@ export async function runAgent(
|
|
|
476
482
|
if (logLines.length > MAX_PROGRESS_LINES) {
|
|
477
483
|
logLines = logLines.slice(-MAX_PROGRESS_LINES);
|
|
478
484
|
}
|
|
479
|
-
//
|
|
485
|
+
// 写进日志的新行都会打断工具调用合并,下一批调用另起一行。
|
|
480
486
|
toolLine = undefined;
|
|
481
487
|
};
|
|
482
488
|
|
|
@@ -537,8 +543,8 @@ export async function runAgent(
|
|
|
537
543
|
break;
|
|
538
544
|
}
|
|
539
545
|
case "thinking_start": {
|
|
540
|
-
//
|
|
541
|
-
|
|
546
|
+
// thinking 不产生日志行,因此不打断工具行合并:跨轮次的连续工具调用
|
|
547
|
+
// 仍累加到同一 `tool:` 行。
|
|
542
548
|
thinkingChars = 0;
|
|
543
549
|
emitUpdate();
|
|
544
550
|
break;
|