@trim21/personal-pi-extensions 0.1.482 → 0.1.486

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 -9
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.486",
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";
@@ -349,6 +351,78 @@ export function registerCallgraphTool(pi: ExtensionAPI, ctx: AftToolContext): vo
349
351
  });
350
352
  }
351
353
 
354
+ /**
355
+ * 语义索引进度订阅:索引冷构建期间 Rust 侧每秒推送一次 status_changed 帧
356
+ * (status_debounce_loop),aft-bridge 广播给 subscribeStatus 监听器。搜索
357
+ * 请求阻塞等待索引就绪(最长 SEMANTIC_INDEX_WAIT_TIMEOUT_MS)时,把进度
358
+ * 经 onUpdate 流式显示给用户,而不是无声干等。
359
+ */
360
+
361
+ /** `BinaryBridge.subscribeStatus` 的能力探测类型:AftProjectTransport 接口上未暴露。 */
362
+ interface StatusSubscribableBridge {
363
+ subscribeStatus?(listener: (snapshot: StatusSnapshot) => void): () => void;
364
+ }
365
+
366
+ function subscribeBridgeStatus(
367
+ bridge: AftProjectTransport,
368
+ listener: (snapshot: StatusSnapshot) => void,
369
+ ): (() => void) | undefined {
370
+ const subscribable = bridge as AftProjectTransport & StatusSubscribableBridge;
371
+ if (typeof subscribable.subscribeStatus !== "function") {
372
+ return undefined;
373
+ }
374
+ return subscribable.subscribeStatus(listener);
375
+ }
376
+
377
+ /**
378
+ * Rust 侧 status 快照里语义索引构建进度(SemanticHealthComponentSnapshot 的
379
+ * Building 分支)。上游控制的 schema:只声明真正读取的字段,未声明键忽略。
380
+ */
381
+ const semanticIndexProgressSchema = Type.Object({
382
+ status: Type.Literal("building"),
383
+ stage: Type.Optional(Type.String()),
384
+ embedded_chunks: Type.Optional(Type.Integer()),
385
+ total_chunks: Type.Optional(Type.Integer()),
386
+ current_batch: Type.Optional(Type.Integer()),
387
+ total_batches: Type.Optional(Type.Integer()),
388
+ });
389
+
390
+ type SemanticIndexProgress = Static<typeof semanticIndexProgressSchema>;
391
+
392
+ /** status 快照 → 一行构建进度文本;非 Building 或形状不符返回 undefined(不显示)。 */
393
+ export function formatSemanticIndexProgress(snapshot: StatusSnapshot): string | undefined {
394
+ const raw = snapshot.semantic_index;
395
+ if (raw === undefined) {
396
+ return undefined;
397
+ }
398
+ let progress: SemanticIndexProgress;
399
+ try {
400
+ progress = Value.Parse(semanticIndexProgressSchema, raw);
401
+ } catch {
402
+ return undefined;
403
+ }
404
+ const parts = [`语义索引构建中${progress.stage === undefined ? "" : ` (${progress.stage})`}`];
405
+ if (
406
+ progress.embedded_chunks !== undefined &&
407
+ progress.total_chunks !== undefined &&
408
+ progress.total_chunks > 0
409
+ ) {
410
+ const percent = Math.min(
411
+ 100,
412
+ Math.round((progress.embedded_chunks / progress.total_chunks) * 100),
413
+ );
414
+ parts.push(`${progress.embedded_chunks}/${progress.total_chunks} chunks (${percent}%)`);
415
+ }
416
+ if (
417
+ progress.current_batch !== undefined &&
418
+ progress.total_batches !== undefined &&
419
+ progress.total_batches > 0
420
+ ) {
421
+ parts.push(`batch ${progress.current_batch}/${progress.total_batches}`);
422
+ }
423
+ return parts.join(" · ");
424
+ }
425
+
352
426
  const SearchParams = Type.Object(
353
427
  {
354
428
  query: Type.String({
@@ -380,7 +454,7 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
380
454
  promptSnippet: "Search code by meaning or exact text",
381
455
  promptGuidelines: [SEARCH_PROMPT],
382
456
  parameters: SearchParams,
383
- async execute(_id, params, _signal, _onUpdate, extCtx) {
457
+ async execute(_id, params, _signal, onUpdate, extCtx) {
384
458
  if (typeof params.query !== "string" || params.query.trim().length === 0) {
385
459
  throw new Error("'query' must be a non-empty string");
386
460
  }
@@ -390,13 +464,29 @@ export function registerSearchTool(pi: ExtensionAPI, ctx: AftToolContext): void
390
464
  includeTests: params.includeTests,
391
465
  });
392
466
 
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
- });
467
+ const bridge = bridgeFor(ctx);
468
+ const stopProgress =
469
+ onUpdate === undefined
470
+ ? undefined
471
+ : subscribeBridgeStatus(bridge, (snapshot) => {
472
+ const text = formatSemanticIndexProgress(snapshot);
473
+ if (text !== undefined) {
474
+ onUpdate({ content: [{ type: "text", text }], details: undefined });
475
+ }
476
+ });
477
+ let response: Record<string, unknown>;
478
+ let text: string;
479
+ try {
480
+ ({ text, response } = await callAftTool(bridge, "search", rawArgs, extCtx, {
481
+ // 默认 search 传输超时仅 60s,会早于索引等待(600s)触发;覆盖为等待
482
+ // 上限 + 常规执行预算。超时只说明响应被挤掉而非 bridge 挂死,保留
483
+ // 常驻的语义索引/LSP 状态。
484
+ transportTimeoutMs: SEMANTIC_INDEX_WAIT_TIMEOUT_MS + 60_000,
485
+ keepBridgeOnTimeout: true,
486
+ }));
487
+ } finally {
488
+ stopProgress?.();
489
+ }
400
490
  const truncated = response.truncated === true;
401
491
  return {
402
492
  content: [{ type: "text", text }],