@trim21/personal-pi-extensions 0.1.576 → 0.1.578

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.576",
3
+ "version": "0.1.578",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
@@ -292,11 +292,12 @@ const UNSANDBOXED_DENIED = "User denied unsandboxed execution.";
292
292
 
293
293
  /** 命令没经沙箱时沿用 prompt 里的说法,给出在沙盒外重跑的手段。 */
294
294
  const SANDBOX_ESCAPE_HATCH =
295
- "[Sandbox] If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
295
+ "If the command needs more than that, use the `dangerouslyDisableSandbox` parameter to request unsandboxed execution; the user must approve this request.";
296
296
 
297
297
  /**
298
298
  * 命令失败时作为独立信息块附上的沙箱状态:默认写边界 + 网络层级,以及在沙盒外重跑的手段。
299
299
  * 写边界只说沙箱布局(不展开用户配置的可写路径),网络只说层级(不列白名单域名)。
300
+ * 整块包在 `<system-reminder>` 里,与其它系统注入的提示同一形态,模型不会把它当成命令输出。
300
301
  * 命令没经沙箱(allow-all、审批通过的全权限、Windows)时返回 undefined:
301
302
  * 没有沙箱就没什么可提示的。
302
303
  */
@@ -304,8 +305,10 @@ export function describeSandbox(resolved: ResolvedBwrap, unsandboxed: boolean):
304
305
  if (unsandboxed) return undefined;
305
306
  const writes = resolved.mode === "readonly" ? "the filesystem is read-only" : SANDBOX_WRITE_RULES;
306
307
  return [
307
- `[Sandbox] This command ran in a sandbox: ${writes}; ${describeNetwork(resolved)}.`,
308
+ "<system-reminder>",
309
+ `This command ran in a sandbox: ${writes}; ${describeNetwork(resolved)}.`,
308
310
  SANDBOX_ESCAPE_HATCH,
311
+ "</system-reminder>",
309
312
  ].join("\n");
310
313
  }
311
314
 
@@ -307,6 +307,16 @@ interface ServerCapabilities {
307
307
  export const clientDefaults = {
308
308
  diagnosticsDebounceMs: 150,
309
309
  diagnosticsDocumentWaitTimeoutMs: 5_000,
310
+ /**
311
+ * document 模式等待上限的补充:该文档上一份诊断为空(服务器手里是干净文档)时,
312
+ * 内容变化后只用这段安静期等新 push,不再等满 diagnosticsDocumentWaitTimeoutMs。
313
+ *
314
+ * typescript-language-server 的 FileDiagnostics.update 在「该类诊断上一轮为空、
315
+ * 这一轮仍为空」时直接 return 不推送,且它不实现 pull 诊断;这类文档等满整个
316
+ * 窗口也只会在窗口末尾返回同样的「无诊断」,白等一次编辑。诊断集合变成非空时
317
+ * 服务器必定推送,所以短安静期不会漏掉这次变更引入的错误。
318
+ */
319
+ diagnosticsSilentWaitTimeoutMs: 1_500,
310
320
  diagnosticsFullWaitTimeoutMs: 10_000,
311
321
  diagnosticsRequestTimeoutMs: 3_000,
312
322
  initializeTimeoutMs: 45_000,
@@ -321,6 +331,8 @@ export interface CreateInput {
321
331
  /** 可覆盖的超时参数(缺省用 client 默认值,由全局/本地 lsp.json 配置注入)。 */
322
332
  diagnosticsDebounceMs?: number;
323
333
  diagnosticsDocumentWaitTimeoutMs?: number;
334
+ /** 上一份诊断为空的文档的等待上限;缺省见 clientDefaults。 */
335
+ diagnosticsSilentWaitTimeoutMs?: number;
324
336
  diagnosticsFullWaitTimeoutMs?: number;
325
337
  diagnosticsRequestTimeoutMs?: number;
326
338
  initializeTimeoutMs?: number;
@@ -522,6 +534,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
522
534
  const diagnosticsDebounceMs = input.diagnosticsDebounceMs ?? clientDefaults.diagnosticsDebounceMs;
523
535
  const diagnosticsDocumentWaitTimeoutMs =
524
536
  input.diagnosticsDocumentWaitTimeoutMs ?? clientDefaults.diagnosticsDocumentWaitTimeoutMs;
537
+ const diagnosticsSilentWaitTimeoutMs =
538
+ input.diagnosticsSilentWaitTimeoutMs ?? clientDefaults.diagnosticsSilentWaitTimeoutMs;
525
539
  const diagnosticsFullWaitTimeoutMs =
526
540
  input.diagnosticsFullWaitTimeoutMs ?? clientDefaults.diagnosticsFullWaitTimeoutMs;
527
541
  const diagnosticsRequestTimeoutMs =
@@ -599,6 +613,8 @@ export async function create(input: CreateInput): Promise<LspClient> {
599
613
  at: Date.now(),
600
614
  version: typeof params.version === "number" ? params.version : undefined,
601
615
  });
616
+ const document = files[filePath];
617
+ if (document !== undefined) document.lastPushEmpty = params.diagnostics.length === 0;
602
618
  updatePushDiagnostics(filePath, params.diagnostics);
603
619
  },
604
620
  );
@@ -702,7 +718,15 @@ export async function create(input: CreateInput): Promise<LspClient> {
702
718
  await connection.sendNotification("workspace/didChangeConfiguration", { settings });
703
719
  }
704
720
 
705
- const files: Record<string, { version: number; text: string } | undefined> = {};
721
+ /**
722
+ * syncedAt:最后一次把该文档内容同步给服务器的时刻(didOpen / didChange)。
723
+ * lastPushEmpty:服务器对该文档最近一次 push 是否为空(无诊断),随驻留记录
724
+ * 一起在 didClose 时清除——只在当前驻留周期内成立。
725
+ */
726
+ const files: Record<
727
+ string,
728
+ { version: number; text: string; syncedAt: number; lastPushEmpty?: boolean } | undefined
729
+ > = {};
706
730
 
707
731
  // ── 驻留 LRU ────────────────────────────────────────────────────────────────
708
732
 
@@ -1009,7 +1033,20 @@ export async function create(input: CreateInput): Promise<LspClient> {
1009
1033
  after?: number;
1010
1034
  signal?: AbortSignal;
1011
1035
  }): Promise<void> {
1036
+ // 服务器对当前内容的最新结论已在手(最后一次 push 晚于最后一次内容同步,
1037
+ // 即此后没有再通知过内容变化):直接返回已有结果。部分服务器在诊断集合
1038
+ // 不变时不再推送(typescript-language-server 空→空不发布),等新 push 只会
1039
+ // 耗满整个窗口后得到同样的「无诊断」。
1040
+ const known = published.get(request.path);
1041
+ const document = files[request.path];
1042
+ if (known !== undefined && document !== undefined && known.at >= document.syncedAt) return;
1043
+
1012
1044
  const startedAt = request.after ?? Date.now();
1045
+ // 「上一份 push 为空」的文档用短安静期(见 clientDefaults.diagnosticsSilentWaitTimeoutMs)。
1046
+ const budget =
1047
+ files[request.path]?.lastPushEmpty === true
1048
+ ? Math.min(diagnosticsDocumentWaitTimeoutMs, diagnosticsSilentWaitTimeoutMs)
1049
+ : diagnosticsDocumentWaitTimeoutMs;
1013
1050
  // pull 与 push 语义相同:都是等「当前文档版本」的诊断结果,统一一个循环。
1014
1051
  // 先 pull(拿到即返回);pull 超时说明服务器未响应,不再重试 pull,只等
1015
1052
  // 版本匹配的 push 兜底;版本不匹配的 push 一律忽略(防迟到旧结果)。
@@ -1017,11 +1054,11 @@ export async function create(input: CreateInput): Promise<LspClient> {
1017
1054
  path: request.path,
1018
1055
  version: request.version,
1019
1056
  after: startedAt,
1020
- timeout: diagnosticsDocumentWaitTimeoutMs,
1057
+ timeout: budget,
1021
1058
  });
1022
1059
 
1023
1060
  while (!connectionClosed && !request.signal?.aborted) {
1024
- const remaining = diagnosticsDocumentWaitTimeoutMs - (Date.now() - startedAt);
1061
+ const remaining = budget - (Date.now() - startedAt);
1025
1062
  if (remaining <= 0) return;
1026
1063
  const result = await requestDocumentDiagnostics(request.path);
1027
1064
  if (result.matched) return;
@@ -1088,13 +1125,21 @@ export async function create(input: CreateInput): Promise<LspClient> {
1088
1125
 
1089
1126
  const document = files[resolvedPath];
1090
1127
  if (document !== undefined) {
1128
+ // 内容与服务器已知文本一致:不重复通知(didChange 会把已有诊断判为过期,
1129
+ // 而部分服务器对内容未产生新结论的文档不再推送,见 diagnosticsSilentWaitTimeoutMs)。
1130
+ if (document.text === text) {
1131
+ touch(resolvedPath);
1132
+ return document.version;
1133
+ }
1134
+
1091
1135
  // didChange:内容已变,旧诊断立即失效。清空缓存避免等待窗口内服务器
1092
1136
  // 重算未完成时(大项目可远超窗口)聚合到过期诊断;新 push 到达即填充。
1093
1137
  pushDiagnostics.delete(resolvedPath);
1094
1138
  pullDiagnostics.delete(resolvedPath);
1095
1139
 
1096
1140
  const next = document.version + 1;
1097
- files[resolvedPath] = { version: next, text };
1141
+ // 保留 lastPushEmpty:安静期判据看的是「变更前服务器最后一份结论是否为空」
1142
+ files[resolvedPath] = { ...document, version: next, text, syncedAt: Date.now() };
1098
1143
  documentVersions.set(resolvedPath, next);
1099
1144
  await connection.sendNotification("textDocument/didChange", {
1100
1145
  textDocument: { uri, version: next },
@@ -1118,7 +1163,7 @@ export async function create(input: CreateInput): Promise<LspClient> {
1118
1163
  await connection.sendNotification("textDocument/didOpen", {
1119
1164
  textDocument: { uri, languageId, version: 0, text },
1120
1165
  });
1121
- files[resolvedPath] = { version: 0, text };
1166
+ files[resolvedPath] = { version: 0, text, syncedAt: Date.now() };
1122
1167
  documentVersions.set(resolvedPath, 0);
1123
1168
  touch(resolvedPath);
1124
1169
  await evictExcess();
@@ -86,6 +86,8 @@ const lspConfigSchema = Type.Object({
86
86
  diagnosticsDebounceMs: Type.Optional(timeoutValue),
87
87
  /** document 模式诊断等待上限(ms)。 */
88
88
  diagnosticsDocumentWaitTimeoutMs: Type.Optional(timeoutValue),
89
+ /** 上一份诊断为空的文档的等待上限(ms);见 clientDefaults.diagnosticsSilentWaitTimeoutMs。 */
90
+ diagnosticsSilentWaitTimeoutMs: Type.Optional(timeoutValue),
89
91
  /** full 模式诊断等待上限(ms)。 */
90
92
  diagnosticsFullWaitTimeoutMs: Type.Optional(timeoutValue),
91
93
  /** 单次 pull 诊断请求超时(ms)。 */
@@ -140,6 +142,7 @@ export interface ResolvedLspConfig {
140
142
  /** 以下超时均为换算后的毫秒数(缺省见 configDefaults / clientDefaults)。 */
141
143
  diagnosticsDebounceMs: number;
142
144
  diagnosticsDocumentWaitTimeoutMs: number;
145
+ diagnosticsSilentWaitTimeoutMs: number;
143
146
  diagnosticsFullWaitTimeoutMs: number;
144
147
  diagnosticsRequestTimeoutMs: number;
145
148
  initializeTimeoutMs: number;
@@ -198,6 +201,8 @@ export function resolveConfig(raw: LspConfig): ResolvedLspConfig {
198
201
  diagnosticsDebounceMs: toMs(raw.diagnosticsDebounceMs) ?? clientDefaults.diagnosticsDebounceMs,
199
202
  diagnosticsDocumentWaitTimeoutMs:
200
203
  toMs(raw.diagnosticsDocumentWaitTimeoutMs) ?? clientDefaults.diagnosticsDocumentWaitTimeoutMs,
204
+ diagnosticsSilentWaitTimeoutMs:
205
+ toMs(raw.diagnosticsSilentWaitTimeoutMs) ?? clientDefaults.diagnosticsSilentWaitTimeoutMs,
201
206
  diagnosticsFullWaitTimeoutMs:
202
207
  toMs(raw.diagnosticsFullWaitTimeoutMs) ?? clientDefaults.diagnosticsFullWaitTimeoutMs,
203
208
  diagnosticsRequestTimeoutMs:
@@ -791,6 +796,7 @@ export function createLspService(
791
796
  diagnosticsDebounceMs: config.diagnosticsDebounceMs,
792
797
  diagnosticsDocumentWaitTimeoutMs:
793
798
  adapter.diagnosticsWaitMs ?? config.diagnosticsDocumentWaitTimeoutMs,
799
+ diagnosticsSilentWaitTimeoutMs: config.diagnosticsSilentWaitTimeoutMs,
794
800
  diagnosticsFullWaitTimeoutMs: config.diagnosticsFullWaitTimeoutMs,
795
801
  diagnosticsRequestTimeoutMs: config.diagnosticsRequestTimeoutMs,
796
802
  initializeTimeoutMs: adapter.startupTimeoutMs ?? config.initializeTimeoutMs,
@@ -27,12 +27,13 @@ description: Use when 编写、检查或排查 LSP 语言服务器配置 ——
27
27
 
28
28
  ```
29
29
  version / servers / enabled / disabled / watch / maxOpenDocuments /
30
- diagnosticsDebounceMs / diagnosticsDocumentWaitTimeoutMs /
30
+ diagnosticsDebounceMs / diagnosticsDocumentWaitTimeoutMs / diagnosticsSilentWaitTimeoutMs /
31
31
  diagnosticsFullWaitTimeoutMs / diagnosticsRequestTimeoutMs / initializeTimeoutMs
32
32
  ```
33
33
 
34
34
  - `enabled`:只启用列出的服务器 id(缺省 = 全部启用);`disabled`:从启用集中排除
35
- - 时长字段:毫秒数字,或带单位的字符串(`"300ms"` / `"5s"` / `"1m"` / `"2h"`,空单位按 ms);默认值见 `clientDefaults`(client.ts):debounce 150ms、document 等待 5s、full 等待 10s、pull 请求 3s、initialize 45s、`maxOpenDocuments` 32
35
+ - 时长字段:毫秒数字,或带单位的字符串(`"300ms"` / `"5s"` / `"1m"` / `"2h"`,空单位按 ms);默认值见 `clientDefaults`(client.ts):debounce 150ms、document 等待 5s、安静期 1.5s、full 等待 10s、pull 请求 3s、initialize 45s、`maxOpenDocuments` 32
36
+ - `diagnosticsSilentWaitTimeoutMs`:只用于「该文档上一份诊断为空」的情形——内容变化后最多只等这段安静期,不再等满 `diagnosticsDocumentWaitTimeoutMs`。typescript-language-server 在诊断集合空→空时不重复推送且不实现 pull 诊断,这类文档等满整个窗口也只会得到同样的「无诊断」,白等一次编辑;集合变成非空时服务器必定推送,所以调小它不会漏掉变更引入的错误,调大则更保守(默认 1.5s,约等于实测热态推送延迟的三倍)。设为不小于 `diagnosticsDocumentWaitTimeoutMs` 即等价于不做这个缩短
36
37
  - `watch`:`enabled` / `debounceMs`(缺省 300)/ `maxBatch`(缺省 500)/ `ignore`(glob,相对**每个被监听的项目根**的 POSIX 路径)。注意 `flushMs` 只在默认值里、**不可配**
37
38
  - 监听范围不是整个 cwd,而是**当前活跃服务器 client 的项目根**:root 在 cwd 内就只监听 root(被其他 root 包含的 root 不重复监听,root 是 cwd 的祖先时退化为 cwd);没有活跃 client 就不监听。因此容器 cwd(`~/projects` 下多个仓库)里只有活跃服务器所在的项目会被 watch;`ignore` 的匹配基准也随之是各自的项目根。资源耗尽(ENOSPC)时该 root 的监听器会停止并提示一次,调大 `fs.inotify.max_user_watches` 后跑 `/lsp-reload` 重试
38
39