@danypops/pi-lector 0.13.1 → 0.13.6

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.
@@ -52,7 +52,7 @@ export interface CodeIntelligenceOperations {
52
52
  workspaceMap(path: string, maxNodes: number, maxEdges: number, maxEntries: number, maxBytes: number): Promise<OperationOutputs["workspace.map"]>;
53
53
  }
54
54
 
55
- export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperations {
55
+ export function createLectorCodeIntelligenceOperations(ownerId?: string): CodeIntelligenceOperations {
56
56
  return {
57
57
  async goToDefinition(path, line, character) {
58
58
  return withWorkspace(
@@ -144,6 +144,7 @@ export function createLectorCodeIntelligenceOperations(): CodeIntelligenceOperat
144
144
  operation: "workspace.populateSymbolGraph",
145
145
  input: { workspaceId, maxFiles, maxSymbolsPerFile },
146
146
  waitMs,
147
+ ...(ownerId ? { ownerId } : {}),
147
148
  });
148
149
  return job;
149
150
  },
@@ -38,10 +38,11 @@ import {
38
38
  createWriteToolDefinition,
39
39
  type ExtensionAPI,
40
40
  type ExtensionCommandContext,
41
+ type ToolDefinition,
41
42
  } from "@earendil-works/pi-coding-agent";
42
43
  import { Text, truncateToWidth, visibleWidth } from "@earendil-works/pi-tui";
43
44
  import { renderBoundedTable, type TextMeasure } from "malevich-tui-components";
44
- import { Type } from "typebox";
45
+ import { type TSchema, Type } from "typebox";
45
46
 
46
47
  /** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
47
48
  const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
@@ -128,6 +129,7 @@ import { createLectorSearchOperations } from "./search/operations.ts";
128
129
  import { formatSearchCall, formatSearchResult } from "./search/rendering.ts";
129
130
  import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation/operations.ts";
130
131
  import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation/rendering.ts";
132
+ import { boundLectorToolText } from "./tool-output-bounds.ts";
131
133
  import type { LectorVehicleCall } from "./vehicle-client.ts";
132
134
  import { CachingOverlay } from "./workspace-cache/caching-overlay.ts";
133
135
  import {
@@ -168,7 +170,23 @@ function renderIntelligenceSource(body: string, provenance: IntelligenceProvenan
168
170
  * "start it with `lector serve`" error if none is reachable.
169
171
  */
170
172
  export default function (pi: ExtensionAPI) {
171
- const cacheOperations = createWorkspaceCacheOperations();
173
+ let cacheOperations = createWorkspaceCacheOperations();
174
+ // Registration is the ownership boundary: every custom Lector tool enters this set
175
+ // automatically, so future tools cannot silently bypass the final model-content cap.
176
+ // Built-in-compatible read/write/edit stay on Pi's own definitions and truncation path.
177
+ const customToolNames = new Set<string>();
178
+ function registerLectorTool<TParams extends TSchema, TDetails = unknown, TState = unknown>(tool: ToolDefinition<TParams, TDetails, TState>): void {
179
+ customToolNames.add(tool.name);
180
+ pi.registerTool(tool);
181
+ }
182
+ pi.on("tool_result", (event) => {
183
+ if (!customToolNames.has(event.toolName)) return;
184
+ const textBlocks = event.content.filter((block): block is Extract<(typeof event.content)[number], { type: "text" }> => block.type === "text");
185
+ if (textBlocks.length === 0) return;
186
+ const bounded = boundLectorToolText(textBlocks.map((block) => block.text).join("\n"));
187
+ if (!bounded.truncation) return;
188
+ return { content: [{ type: "text", text: bounded.text }, ...event.content.filter((block) => block.type !== "text")] };
189
+ });
172
190
  // One generation counter shared by every root's monitor loop, not per-root -- a new session
173
191
  // (or shutdown) invalidates every previous session's in-flight monitor regardless of which
174
192
  // root it tracked, and there is exactly one "current session" at a time.
@@ -292,6 +310,8 @@ export default function (pi: ExtensionAPI) {
292
310
 
293
311
  pi.on("session_start", (_event, ctx) => {
294
312
  const { cwd } = ctx;
313
+ const ownerId = ctx.sessionManager.getSessionId();
314
+ cacheOperations = createWorkspaceCacheOperations(ownerId);
295
315
  sessionGeneration++;
296
316
  cacheStatesByRoot.clear();
297
317
  monitoringRoots.clear();
@@ -299,9 +319,9 @@ export default function (pi: ExtensionAPI) {
299
319
  uiContext = ctx;
300
320
  setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
301
321
  if (ctx.hasUI) {
302
- // The persistent widget counterpart to the single-line "lector-cache" status above --
303
- // enumerates EVERY workspace currently caching, not just this session's own cwd root.
304
- cachingOverlay ??= new CachingOverlay();
322
+ // The persistent widget counterpart to the single-line "lector-cache" status above.
323
+ // Ownership is the Pi session, not cwd: one session may legitimately touch several roots.
324
+ cachingOverlay = new CachingOverlay(undefined, ownerId);
305
325
  cachingOverlay.setUI(ctx.ui);
306
326
  void cachingOverlay.refresh();
307
327
  cachingOverlay.startPolling();
@@ -323,7 +343,7 @@ export default function (pi: ExtensionAPI) {
323
343
  pi.registerTool(createEditToolDefinition(cwd, { operations: createLectorEditOperations() }));
324
344
 
325
345
  const findSymbolsOperations = createLectorFindSymbolsOperations();
326
- pi.registerTool({
346
+ registerLectorTool({
327
347
  name: "find_symbols",
328
348
  label: "Find Symbols",
329
349
  description:
@@ -385,7 +405,7 @@ export default function (pi: ExtensionAPI) {
385
405
  },
386
406
  });
387
407
 
388
- const codeIntelligenceOperations = createLectorCodeIntelligenceOperations();
408
+ const codeIntelligenceOperations = createLectorCodeIntelligenceOperations(ownerId);
389
409
 
390
410
  const editorOverlayOptions = { overlay: true, overlayOptions: { width: "100%", maxHeight: "100%", anchor: "center" } } as const;
391
411
 
@@ -451,7 +471,7 @@ export default function (pi: ExtensionAPI) {
451
471
  character: Type.Number({ description: "1-indexed character offset within the line" }),
452
472
  };
453
473
 
454
- pi.registerTool({
474
+ registerLectorTool({
455
475
  name: "go_to_definition",
456
476
  label: "Go to Definition",
457
477
  description: "Find where the symbol at an exact file position is actually declared, across files, through re-exports and aliasing.",
@@ -485,7 +505,7 @@ export default function (pi: ExtensionAPI) {
485
505
  },
486
506
  });
487
507
 
488
- pi.registerTool({
508
+ registerLectorTool({
489
509
  name: "go_to_implementation",
490
510
  label: "Go to Implementation",
491
511
  description:
@@ -523,7 +543,7 @@ export default function (pi: ExtensionAPI) {
523
543
  },
524
544
  });
525
545
 
526
- pi.registerTool({
546
+ registerLectorTool({
527
547
  name: "find_references",
528
548
  label: "Find References",
529
549
  description: "Find every project-wide usage of the symbol at an exact file position.",
@@ -568,7 +588,7 @@ export default function (pi: ExtensionAPI) {
568
588
  },
569
589
  });
570
590
 
571
- pi.registerTool({
591
+ registerLectorTool({
572
592
  name: "hover",
573
593
  label: "Hover",
574
594
  description: "Get type and documentation information for the symbol at an exact file position.",
@@ -608,7 +628,7 @@ export default function (pi: ExtensionAPI) {
608
628
  },
609
629
  });
610
630
 
611
- pi.registerTool({
631
+ registerLectorTool({
612
632
  name: "document_symbols",
613
633
  label: "Document Symbols",
614
634
  description: "List every symbol declared in one file, hierarchically -- an outline of its classes, functions, and their members.",
@@ -642,7 +662,7 @@ export default function (pi: ExtensionAPI) {
642
662
  },
643
663
  });
644
664
 
645
- pi.registerTool({
665
+ registerLectorTool({
646
666
  name: "diagnostics",
647
667
  label: "Diagnostics",
648
668
  description: "List every error and warning a language server currently knows about for one file, as of its last analysis.",
@@ -679,7 +699,7 @@ export default function (pi: ExtensionAPI) {
679
699
  },
680
700
  });
681
701
 
682
- pi.registerTool({
702
+ registerLectorTool({
683
703
  name: "call_hierarchy",
684
704
  label: "Call Hierarchy",
685
705
  description:
@@ -745,7 +765,7 @@ export default function (pi: ExtensionAPI) {
745
765
  },
746
766
  });
747
767
 
748
- pi.registerTool({
768
+ registerLectorTool({
749
769
  name: "reference_based_rename",
750
770
  label: "Reference-Based Rename",
751
771
  description:
@@ -808,7 +828,7 @@ export default function (pi: ExtensionAPI) {
808
828
  applied?: OperationOutputs["workspace.rename"];
809
829
  }
810
830
 
811
- pi.registerTool({
831
+ registerLectorTool({
812
832
  name: "rename",
813
833
  label: "Rename",
814
834
  description:
@@ -877,7 +897,7 @@ export default function (pi: ExtensionAPI) {
877
897
  uncontained?: boolean;
878
898
  }
879
899
 
880
- pi.registerTool({
900
+ registerLectorTool({
881
901
  name: "symbol_annotations",
882
902
  label: "Symbol Annotations",
883
903
  description:
@@ -1056,7 +1076,7 @@ export default function (pi: ExtensionAPI) {
1056
1076
  },
1057
1077
  });
1058
1078
 
1059
- pi.registerTool({
1079
+ registerLectorTool({
1060
1080
  name: "reachable_from",
1061
1081
  label: "Reachable From",
1062
1082
  description:
@@ -1105,7 +1125,7 @@ export default function (pi: ExtensionAPI) {
1105
1125
  },
1106
1126
  });
1107
1127
 
1108
- pi.registerTool({
1128
+ registerLectorTool({
1109
1129
  name: "workspace_map",
1110
1130
  label: "Workspace Map",
1111
1131
  description:
@@ -1162,7 +1182,7 @@ export default function (pi: ExtensionAPI) {
1162
1182
  readonly job?: JobSnapshot<PopulateSymbolGraphResult>;
1163
1183
  }
1164
1184
 
1165
- pi.registerTool({
1185
+ registerLectorTool({
1166
1186
  name: "workspace_cache",
1167
1187
  label: "Workspace Cache",
1168
1188
  description:
@@ -1253,7 +1273,7 @@ export default function (pi: ExtensionAPI) {
1253
1273
  });
1254
1274
 
1255
1275
  const gitOperations = createLectorGitOperations();
1256
- pi.registerTool({
1276
+ registerLectorTool({
1257
1277
  name: "git",
1258
1278
  label: "Git",
1259
1279
  description:
@@ -1396,7 +1416,7 @@ export default function (pi: ExtensionAPI) {
1396
1416
  });
1397
1417
 
1398
1418
  const searchOperations = createLectorSearchOperations();
1399
- pi.registerTool({
1419
+ registerLectorTool({
1400
1420
  name: "search_code",
1401
1421
  label: "Search Code",
1402
1422
  description:
@@ -1437,7 +1457,7 @@ export default function (pi: ExtensionAPI) {
1437
1457
  });
1438
1458
 
1439
1459
  const findFilesOperations = createLectorFindFilesOperations();
1440
- pi.registerTool({
1460
+ registerLectorTool({
1441
1461
  name: "find_files",
1442
1462
  label: "Find Files",
1443
1463
  description:
@@ -1483,7 +1503,7 @@ export default function (pi: ExtensionAPI) {
1483
1503
  });
1484
1504
 
1485
1505
  const lineEditOperations = createLectorLineEditOperations();
1486
- pi.registerTool({
1506
+ registerLectorTool({
1487
1507
  name: "line_edit",
1488
1508
  label: "Line Edit",
1489
1509
  description:
@@ -1554,7 +1574,7 @@ export default function (pi: ExtensionAPI) {
1554
1574
  });
1555
1575
 
1556
1576
  const applyPatchOperations = createLectorApplyPatchOperations();
1557
- pi.registerTool({
1577
+ registerLectorTool({
1558
1578
  name: "apply_patch",
1559
1579
  label: "Apply Patch",
1560
1580
  description:
@@ -1605,7 +1625,7 @@ export default function (pi: ExtensionAPI) {
1605
1625
  | { readonly action: "revert"; readonly reverted: { readonly path: string; readonly newHash: string | null } };
1606
1626
 
1607
1627
  const mutationHistoryOperations = createMutationHistoryOperations();
1608
- pi.registerTool({
1628
+ registerLectorTool({
1609
1629
  name: "mutation_history",
1610
1630
  label: "Mutation History",
1611
1631
  description:
@@ -1690,7 +1710,7 @@ export default function (pi: ExtensionAPI) {
1690
1710
  }
1691
1711
 
1692
1712
  const packageSourceOperations = createLectorPackageSourceOperations();
1693
- pi.registerTool({
1713
+ registerLectorTool({
1694
1714
  name: "package_source",
1695
1715
  label: "Package Source",
1696
1716
  description:
@@ -1796,7 +1816,7 @@ export default function (pi: ExtensionAPI) {
1796
1816
  const repoFetchOperations = createLectorRepoFetchOperations();
1797
1817
  const repoCacheListOperations = createRepoCacheListOperations();
1798
1818
  const repoCacheEvictOperations = createRepoCacheEvictOperations();
1799
- pi.registerTool({
1819
+ registerLectorTool({
1800
1820
  name: "repo_cache",
1801
1821
  label: "Repo Cache",
1802
1822
  description:
@@ -1899,7 +1919,7 @@ export default function (pi: ExtensionAPI) {
1899
1919
  | { readonly action: "sourcegraph_code"; readonly result: { candidates: readonly SourcegraphCodeCandidate[] } };
1900
1920
 
1901
1921
  const externalSearchOperations = createExternalSearchOperations();
1902
- pi.registerTool({
1922
+ registerLectorTool({
1903
1923
  name: "external_search",
1904
1924
  label: "External Search",
1905
1925
  description:
@@ -1956,7 +1976,7 @@ export default function (pi: ExtensionAPI) {
1956
1976
  });
1957
1977
 
1958
1978
  const crossWorkspaceSearchOperations = createLectorCrossWorkspaceSearchOperations();
1959
- pi.registerTool({
1979
+ registerLectorTool({
1960
1980
  name: "find_symbols_across_projects",
1961
1981
  label: "Find Symbols Across Projects",
1962
1982
  description:
@@ -1996,7 +2016,7 @@ export default function (pi: ExtensionAPI) {
1996
2016
  },
1997
2017
  });
1998
2018
 
1999
- pi.registerTool({
2019
+ registerLectorTool({
2000
2020
  name: "search_code_across_projects",
2001
2021
  label: "Search Code Across Projects",
2002
2022
  description:
@@ -0,0 +1,36 @@
1
+ import { DEFAULT_MAX_BYTES, DEFAULT_MAX_LINES, formatSize, type TruncationResult, truncateHead } from "@earendil-works/pi-coding-agent";
2
+
3
+ export interface BoundedLectorToolText {
4
+ readonly text: string;
5
+ readonly truncation: TruncationResult | undefined;
6
+ }
7
+
8
+ function noticeFor(truncation: TruncationResult): string {
9
+ return (
10
+ `[Lector tool output truncated: ${truncation.totalLines} lines total, ${formatSize(truncation.totalBytes)} total; ` +
11
+ `showing ${truncation.outputLines} lines and ${formatSize(truncation.outputBytes)}. Refine the query or lower its scope.]`
12
+ );
13
+ }
14
+
15
+ /**
16
+ * Applies Pi's process-level custom-tool output contract independently of Lector's domain bounds.
17
+ * Domain maxBytes values intentionally describe DTO payload fields, not the final serialized tool
18
+ * message; this final guard accounts for separators, provenance, JSON framing, and caller-selected
19
+ * bounds before content enters model context.
20
+ */
21
+ export function boundLectorToolText(text: string): BoundedLectorToolText {
22
+ const initial = truncateHead(text);
23
+ if (!initial.truncated) return { text, truncation: undefined };
24
+
25
+ let bounded = initial;
26
+ for (let attempt = 0; attempt < 3; attempt++) {
27
+ const notice = noticeFor(bounded);
28
+ const noticeBytes = Buffer.byteLength(`\n\n${notice}`, "utf8");
29
+ bounded = truncateHead(text, {
30
+ maxLines: Math.max(1, DEFAULT_MAX_LINES - 2),
31
+ maxBytes: Math.max(1, DEFAULT_MAX_BYTES - noticeBytes),
32
+ });
33
+ }
34
+ const notice = noticeFor(bounded);
35
+ return { text: bounded.content ? `${bounded.content}\n\n${notice}` : notice, truncation: bounded };
36
+ }
@@ -4,8 +4,8 @@
4
4
  * DoctorOverlay: factory-form ctx.ui.setWidget registration, requestRender on refresh, hides the
5
5
  * widget entirely (setWidget(key, undefined)) rather than an empty box once nothing is caching.
6
6
  *
7
- * workspace.activeCachingJobs enumerates every workspace with a currently active (queued/
8
- * running) population job -- see packages/lector/src/service/symbol-graph/cache-query-handlers.ts.
7
+ * workspace.activeCachingJobs is filtered by the owning Pi session, while its omitted-owner
8
+ * daemon contract remains available for global administration -- see cache-query-handlers.ts.
9
9
  */
10
10
  import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
11
11
  import type { TUI } from "@earendil-works/pi-tui";
@@ -39,7 +39,10 @@ export class CachingOverlay {
39
39
  intervalMs: CACHING_WIDGET_ROTATION_INTERVAL_MS,
40
40
  });
41
41
 
42
- constructor(private readonly connect: () => Promise<RetryingLectorClient> = lectorClient) {}
42
+ constructor(
43
+ private readonly connect: () => Promise<RetryingLectorClient> = lectorClient,
44
+ private readonly ownerId?: string,
45
+ ) {}
43
46
 
44
47
  setUI(ctx: ExtensionUIContext): void {
45
48
  if (ctx !== this.uiCtx) {
@@ -55,7 +58,7 @@ export class CachingOverlay {
55
58
  async refresh(): Promise<void> {
56
59
  try {
57
60
  const client = await this.connect();
58
- const result = await client.call("workspace.activeCachingJobs", {});
61
+ const result = await client.call("workspace.activeCachingJobs", this.ownerId ? { ownerId: this.ownerId } : {});
59
62
  this.projection = buildCachingWidgetProjection(result.jobs);
60
63
  } catch {
61
64
  this.projection = EMPTY_PROJECTION;
@@ -5,6 +5,8 @@
5
5
  * unit-testable without a real daemon or terminal. See caching-overlay.ts for the stateful
6
6
  * ctx.ui.setWidget-registered class that drives these from a live poll.
7
7
  */
8
+
9
+ import { basename } from "node:path";
8
10
  import { vehicleWidgetTitle } from "@danypops/vehicle-client-pi/widget-header";
9
11
  import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
10
12
  import { type AutoRotatingWindow, renderCardRow, type TextMeasure } from "malevich-tui-components";
@@ -20,6 +22,10 @@ export const LECTOR_CACHING_WIDGET_VISIBLE_ROWS = 5;
20
22
  export interface CachingWidgetRow {
21
23
  workspaceId: string;
22
24
  status: "queued" | "running" | "waiting-for-resources";
25
+ /** Absent for an already-unregistered workspace -- falls back to the bare workspaceId for display. */
26
+ rootPath?: string;
27
+ /** Absent until the first file of this run completes, or while still queued -- nothing walked yet. */
28
+ progress?: { filesProcessed: number; filesTotal: number };
23
29
  }
24
30
 
25
31
  export interface CachingWidgetProjection {
@@ -31,6 +37,11 @@ export function buildCachingWidgetProjection(jobs: readonly CachingWidgetRow[]):
31
37
  return { rows: [...jobs], total: jobs.length };
32
38
  }
33
39
 
40
+ /** The project directory's own basename when a path is known ("lector", not the full absolute path a narrow card has no room for) -- falls back to the bare workspaceId hash for a row with no rootPath at all. */
41
+ function cachingRowLabel(row: CachingWidgetRow): string {
42
+ return row.rootPath ? basename(row.rootPath) : row.workspaceId;
43
+ }
44
+
34
45
  function cachingRowLine(theme: { fg(color: string, text: string): string }, row: CachingWidgetRow, width: number): string {
35
46
  const glyph =
36
47
  row.status === "waiting-for-resources"
@@ -38,7 +49,8 @@ function cachingRowLine(theme: { fg(color: string, text: string): string }, row:
38
49
  : row.status === "queued"
39
50
  ? theme.fg("muted", "\u2022")
40
51
  : theme.fg("accent", "\u25b6");
41
- return truncateToWidth(`${glyph} ${row.workspaceId}`, width, "\u2026");
52
+ const progressSuffix = row.progress ? theme.fg("muted", ` (${row.progress.filesProcessed}/${row.progress.filesTotal})`) : "";
53
+ return truncateToWidth(`${glyph} ${cachingRowLabel(row)}${progressSuffix}`, width, "\u2026");
42
54
  }
43
55
 
44
56
  /** "Lector · Caching · <N>", plus a "page/total ⟳" suffix once genuinely paging. */
@@ -39,7 +39,7 @@ export interface WorkspaceCacheOperations {
39
39
  watchJob?(jobId: string, onJob: (job: JobSnapshot<PopulateSymbolGraphResult>) => void): Promise<JobWatchOutcome>;
40
40
  }
41
41
 
42
- export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
42
+ export function createWorkspaceCacheOperations(ownerId?: string): WorkspaceCacheOperations {
43
43
  return {
44
44
  status(directory, maxFiles, maxSymbolsPerFile) {
45
45
  return withWorkspace(
@@ -59,6 +59,7 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
59
59
  operation: "workspace.populateSymbolGraph",
60
60
  input: { workspaceId, maxFiles, maxSymbolsPerFile, retryTimeBudgetMs },
61
61
  waitMs,
62
+ ...(ownerId ? { ownerId } : {}),
62
63
  });
63
64
  return job;
64
65
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.13.1",
3
+ "version": "0.13.6",
4
4
  "description": "Pi host adapter for Lector: overrides read/write/edit with a daemon-backed, hash-guarded filesystem",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -19,7 +19,7 @@
19
19
  "@earendil-works/pi-coding-agent": "*",
20
20
  "@earendil-works/pi-tui": "*",
21
21
  "typebox": "*",
22
- "@danypops/vehicle-client-pi": "^0.43.0"
22
+ "@danypops/vehicle-client-pi": "^0.45.0"
23
23
  },
24
24
  "dependencies": {
25
25
  "@danypops/lector": "^0.19.9",
@@ -29,7 +29,7 @@
29
29
  "picomatch": "^4.0.5"
30
30
  },
31
31
  "devDependencies": {
32
- "@danypops/vehicle-client-pi": "^0.43.0",
32
+ "@danypops/vehicle-client-pi": "^0.45.0",
33
33
  "@danypops/vehicle-conformance": "^0.4.0",
34
34
  "@danypops/pi-extension-harness": "^0.2.0",
35
35
  "@danypops/pi-tui-harness": "^0.0.1",