@trim21/personal-pi-extensions 0.1.482 → 0.1.487

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.
Files changed (2) hide show
  1. package/package.json +1 -1
  2. package/src/aft/tools.ts +99 -11
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@trim21/personal-pi-extensions",
3
- "version": "0.1.482",
3
+ "version": "0.1.487",
4
4
  "type": "module",
5
5
  "description": "Custom pi coding-agent extensions: bwrap sandbox, workspace guard, opencode edit, and more",
6
6
  "keywords": [
package/src/aft/tools.ts CHANGED
@@ -16,9 +16,11 @@ import {
16
16
  coerceTargetParam,
17
17
  formatCallgraphSections,
18
18
  PLAIN_CALLGRAPH_THEME,
19
+ type StatusSnapshot,
19
20
  } from "@cortexkit/aft-bridge";
20
21
  import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
- import { Type } from "typebox";
22
+ import { type Static, Type } from "typebox";
23
+ import { Value } from "typebox/value";
22
24
 
23
25
  import { formatDisplayPath, formatSubtitlePath, resolvePathArg } from "../lib/path.js";
24
26
  import { type ToolPendant } from "../lib/pendant.js";
@@ -150,7 +152,6 @@ export function registerOutlineTool(pi: ExtensionAPI, ctx: AftToolContext): void
150
152
  truncated,
151
153
  params,
152
154
  pendant: {
153
- title: "aft_outline",
154
155
  subtitle,
155
156
  } satisfies ToolPendant,
156
157
  },
@@ -222,7 +223,6 @@ export function registerZoomTool(pi: ExtensionAPI, ctx: AftToolContext): void {
222
223
  truncated,
223
224
  params,
224
225
  pendant: {
225
- title: "aft_zoom",
226
226
  subtitle,
227
227
  markdown: buildPendantMarkdown({
228
228
  title: "aft_zoom",
@@ -349,6 +349,78 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
349
349
  });
350
350
  }
351
351
 
352
+ /**
353
+ * 语义索引进度订阅:索引冷构建期间 Rust 侧每秒推送一次 status_changed 帧
354
+ * (status_debounce_loop),aft-bridge 广播给 subscribeStatus 监听器。搜索
355
+ * 请求阻塞等待索引就绪(最长 SEMANTIC_INDEX_WAIT_TIMEOUT_MS)时,把进度
356
+ * 经 onUpdate 流式显示给用户,而不是无声干等。
357
+ */
358
+
359
+ /** `BinaryBridge.subscribeStatus` 的能力探测类型:AftProjectTransport 接口上未暴露。 */
360
+ interface StatusSubscribableBridge {
361
+ subscribeStatus?(listener: (snapshot: StatusSnapshot) => void): () => void;
362
+ }
363
+
364
+ function subscribeBridgeStatus(
365
+ bridge: AftProjectTransport,
366
+ listener: (snapshot: StatusSnapshot) => void,
367
+ ): (() => void) | undefined {
368
+ const subscribable = bridge as AftProjectTransport & StatusSubscribableBridge;
369
+ if (typeof subscribable.subscribeStatus !== "function") {
370
+ return undefined;
371
+ }
372
+ return subscribable.subscribeStatus(listener);
373
+ }
374
+
375
+ /**
376
+ * Rust 侧 status 快照里语义索引构建进度(SemanticHealthComponentSnapshot 的
377
+ * Building 分支)。上游控制的 schema:只声明真正读取的字段,未声明键忽略。
378
+ */
379
+ const semanticIndexProgressSchema = Type.Object({
380
+ status: Type.Literal("building"),
381
+ stage: Type.Optional(Type.String()),
382
+ embedded_chunks: Type.Optional(Type.Integer()),
383
+ total_chunks: Type.Optional(Type.Integer()),
384
+ current_batch: Type.Optional(Type.Integer()),
385
+ total_batches: Type.Optional(Type.Integer()),
386
+ });
387
+
388
+ type SemanticIndexProgress = Static<typeof semanticIndexProgressSchema>;
389
+
390
+ /** status 快照 → 一行构建进度文本;非 Building 或形状不符返回 undefined(不显示)。 */
391
+ export function formatSemanticIndexProgress(snapshot: StatusSnapshot): string | undefined {
392
+ const raw = snapshot.semantic_index;
393
+ if (raw === undefined) {
394
+ return undefined;
395
+ }
396
+ let progress: SemanticIndexProgress;
397
+ try {
398
+ progress = Value.Parse(semanticIndexProgressSchema, raw);
399
+ } catch {
400
+ return undefined;
401
+ }
402
+ const parts = [`语义索引构建中${progress.stage === undefined ? "" : ` (${progress.stage})`}`];
403
+ if (
404
+ progress.embedded_chunks !== undefined &&
405
+ progress.total_chunks !== undefined &&
406
+ progress.total_chunks > 0
407
+ ) {
408
+ const percent = Math.min(
409
+ 100,
410
+ Math.round((progress.embedded_chunks / progress.total_chunks) * 100),
411
+ );
412
+ parts.push(`${progress.embedded_chunks}/${progress.total_chunks} chunks (${percent}%)`);
413
+ }
414
+ if (
415
+ progress.current_batch !== undefined &&
416
+ progress.total_batches !== undefined &&
417
+ progress.total_batches > 0
418
+ ) {
419
+ parts.push(`batch ${progress.current_batch}/${progress.total_batches}`);
420
+ }
421
+ return parts.join(" · ");
422
+ }
423
+
352
424
  const SearchParams = Type.Object(
353
425
  {
354
426
  query: Type.String({
@@ -380,7 +452,7 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
380
452
  promptSnippet: "Search code by meaning or exact text",
381
453
  promptGuidelines: [SEARCH_PROMPT],
382
454
  parameters: SearchParams,
383
- async execute(_id, params, _signal, _onUpdate, extCtx) {
455
+ async execute(_id, params, _signal, onUpdate, extCtx) {
384
456
  if (typeof params.query !== "string" || params.query.trim().length === 0) {
385
457
  throw new Error("'query' must be a non-empty string");
386
458
  }
@@ -390,13 +462,29 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
390
462
  includeTests: params.includeTests,
391
463
  });
392
464
 
393
- const { text, response } = await callAftTool(bridgeFor(ctx), "search", rawArgs, extCtx, {
394
- // 默认 search 传输超时仅 60s,会早于索引等待(600s)触发;覆盖为等待
395
- // 上限 + 常规执行预算。超时只说明响应被挤掉而非 bridge 挂死,保留
396
- // 常驻的语义索引/LSP 状态。
397
- transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
398
- keepBridgeOnTimeout: true,
399
- });
465
+ const bridge = bridgeFor(ctx);
466
+ const stopProgress =
467
+ onUpdate === undefined
468
+ ? undefined
469
+ : subscribeBridgeStatus(bridge, (snapshot) => {
470
+ const text = formatSemanticIndexProgress(snapshot);
471
+ if (text !== undefined) {
472
+ onUpdate({ content: [{ type: "text", text }], details: undefined });
473
+ }
474
+ });
475
+ let response: Record<string, unknown>;
476
+ let text: string;
477
+ try {
478
+ ({ text, response } = await callAftTool(bridge, "search", rawArgs, extCtx, {
479
+ // 默认 search 传输超时仅 60s,会早于索引等待(600s)触发;覆盖为等待
480
+ // 上限 + 常规执行预算。超时只说明响应被挤掉而非 bridge 挂死,保留
481
+ // 常驻的语义索引/LSP 状态。
482
+ transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
483
+ keepBridgeOnTimeout: true,
484
+ }));
485
+ } finally {
486
+ stopProgress?.();
487
+ }
400
488
  const truncated = response.truncated === true;
401
489
  return {
402
490
  content: [{ type: "text", text }],