@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.624",
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-jsonrpc": "^9.0.1",
110
- "vscode-languageserver-types": "^3.18.0",
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": {
@@ -15,58 +15,79 @@ import { fileURLToPath, pathToFileURL } from "node:url";
15
15
 
16
16
  import {
17
17
  CancellationTokenSource,
18
- createMessageConnection,
19
- type MessageConnection,
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
- } from "vscode-jsonrpc/node";
24
- import type {
25
- Diagnostic as VSCodeDiagnostic,
26
- Hover,
27
- Location as LspLocation,
28
- LocationLink,
29
- Range,
30
- WorkspaceEdit,
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 = 1;
46
- export const WATCH_KIND_CHANGE = 2;
47
- export const WATCH_KIND_DELETE = 4;
48
- const WATCH_KIND_ALL = WATCH_KIND_CREATE | WATCH_KIND_CHANGE | WATCH_KIND_DELETE;
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, number> = {
57
- created: FILE_CHANGE_CREATED,
58
- changed: FILE_CHANGE_CHANGED,
59
- deleted: FILE_CHANGE_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 === LSP_CONTENT_MODIFIED)) {
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: MessageConnection;
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 = createMessageConnection(
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, CapabilityRegistration>();
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
- "textDocument/publishDiagnostics",
650
- (params: { uri: string; version?: number; diagnostics: Diagnostic[] }) => {
651
- const filePath = getFilePath(params.uri);
652
- if (!filePath) {
653
- return;
654
- }
655
- // 服务器版本滞后于已发送版本时,该 push 对应的是旧内容(异步重算未完成
656
- // 时的迟到结果)。忽略,避免与当前版本结果混淆。
657
- const currentVersion = documentVersions.get(filePath);
658
- const isStalePush =
659
- typeof params.version === "number" &&
660
- currentVersion !== undefined &&
661
- params.version !== currentVersion;
662
- if (isStalePush) {
663
- return;
664
- }
665
- published.set(filePath, {
666
- at: Date.now(),
667
- version: typeof params.version === "number" ? params.version : undefined,
668
- });
669
- const document = files[filePath];
670
- if (document !== undefined) {
671
- document.lastPushEmpty = params.diagnostics.length === 0;
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("client/registerCapability", (params) => {
684
- const registrations =
685
- (params as { registrations?: CapabilityRegistration[] }).registrations ?? [];
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 === "workspace/didChangeWatchedFiles") {
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
- registration.registerOptions?.watchers
691
- ?.map((watcher) => ({
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 === "textDocument/diagnostic") {
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("client/unregisterCapability", (params) => {
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 registrations) {
711
- if (registration.method === "workspace/didChangeWatchedFiles") {
691
+ for (const registration of params.unregisterations) {
692
+ if (registration.method === DidChangeWatchedFilesNotification.method) {
712
693
  watcherRegistrations.delete(registration.id);
713
- } else if (registration.method === "textDocument/diagnostic") {
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("workspace/workspaceFolders", () => [
703
+ connection.onRequest(WorkspaceFoldersRequest.type, () => [
723
704
  { name: "workspace", uri: pathToFileURL(input.root).href },
724
705
  ]);
725
- connection.onRequest("workspace/diagnostic/refresh", () => null);
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<{ capabilities?: ServerCapabilities }>("initialize", {
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?.diagnosticProvider);
748
+ const hasStaticPullDiagnostics = Boolean(initialized.capabilities.diagnosticProvider);
767
749
  // prepareProvider 只在静态声明为对象且显式开启时使用;其余情况跳过 prepare
768
750
  // 直接 rename(能力可能经动态注册,静态声明缺失不代表服务器不支持)。
769
- const renameProvider = initialized.capabilities?.renameProvider;
751
+ const renameProvider = initialized.capabilities.renameProvider;
770
752
  const hasPrepareProvider =
771
753
  typeof renameProvider === "object" && renameProvider.prepareProvider === true;
772
754
 
773
- await connection.sendNotification("initialized", {});
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("workspace/didChangeConfiguration", { settings });
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("textDocument/didClose", {
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<DocumentDiagnosticReport | null>("textDocument/diagnostic", {
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 (Array.isArray(report.items)) {
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 || !Array.isArray(related.items)) {
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<WorkspaceDiagnosticReport | null>("workspace/diagnostic", {
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 = item.uri ? getFilePath(item.uri) : undefined;
933
- if (!relatedPath || !Array.isArray(item.items)) {
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.registerOptions?.workspaceDiagnostics !== true,
935
+ (registration) => diagnosticRegistrationOptions(registration)?.workspaceDiagnostics !== true,
947
936
  );
948
937
  return {
949
938
  documentIdentifiers: [
950
- ...new Set(documentRegistrations.flatMap((r) => r.registerOptions?.identifier ?? [])),
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.registerOptions?.workspaceDiagnostics === true,
949
+ (registration) => diagnosticRegistrationOptions(registration)?.workspaceDiagnostics === true,
959
950
  );
960
951
  return {
961
952
  workspaceIdentifiers: [
962
- ...new Set(workspaceRegistrations.flatMap((r) => r.registerOptions?.identifier ?? [])),
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("textDocument/didChange", {
1259
+ await connection.sendNotification(DidChangeTextDocumentNotification.type, {
1267
1260
  textDocument: { uri, version: next },
1268
1261
  contentChanges:
1269
- syncKind === TEXT_DOCUMENT_SYNC_INCREMENTAL
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("textDocument/didOpen", {
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<T>(
1315
- method: string,
1316
- params: object,
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<T> {
1311
+ ): Promise<R> {
1319
1312
  if (!signal) {
1320
- return await connection.sendRequest<T>(method, params);
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<T>(method, params, source.token),
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 <T>(
1350
- method: string,
1351
- message: object,
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<T> => {
1346
+ ): Promise<R> => {
1354
1347
  try {
1355
- return await retryOnContentModified(() => sendAbortableRequest<T>(method, message, signal));
1348
+ return await retryOnContentModified(() => sendAbortableRequest(type, message, signal));
1356
1349
  } catch (error) {
1357
- if (error instanceof ResponseError && error.code === LSP_METHOD_NOT_FOUND) {
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: { uri: string; type: number }[] = [];
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("textDocument/didClose", {
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("workspace/didChangeWatchedFiles", {
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<{ uri: string }[] | null>(
1473
- "textDocument/references",
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<WorkspaceEdit | null>(
1494
- "textDocument/rename",
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 === LSP_METHOD_NOT_FOUND) {
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: PrepareRenameResponse | null;
1506
+ let prepared: PrepareRenameResult | null;
1514
1507
  try {
1515
1508
  prepared = await retryOnContentModified(() =>
1516
- sendAbortableRequest<PrepareRenameResponse | null>(
1517
- "textDocument/prepareRename",
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 === LSP_METHOD_NOT_FOUND) {
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 === LSP_METHOD_NOT_FOUND)) {
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<DefinitionResult>(
1612
- "textDocument/definition",
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<LspLocation[] | null>(
1621
- "textDocument/references",
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<Hover | null>(
1634
- "textDocument/hover",
1626
+ return await sendInspectRequest(
1627
+ HoverRequest.type,
1635
1628
  { textDocument: { uri }, position },
1636
1629
  request.signal,
1637
1630
  );
@@ -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; any text block starts a
20
- * new line. Line content is sanitized first: markdown marker characters are
21
- * stripped and whitespace (including newlines) is collapsed to single spaces,
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
- /** Progress log keeps only the most recent lines (rolling window). */
95
- const MAX_PROGRESS_LINES = 5;
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
- toolLine = undefined;
546
+ // thinking 不产生日志行,因此不打断工具行合并:跨轮次的连续工具调用
547
+ // 仍累加到同一 `tool:` 行。
542
548
  thinkingChars = 0;
543
549
  emitUpdate();
544
550
  break;