@danypops/pi-lector 0.12.6 → 0.12.8

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.
@@ -0,0 +1,46 @@
1
+ import type { RetryingClientDiagnosticEvent } from "@danypops/vehicle-client/daemon-client";
2
+
3
+ /**
4
+ * Formats one createRetryingClient diagnostic event as a single, compact line -- the same real
5
+ * RCA gap @danypops/vehicle-client's own onEvent hook was added to close: a scrubbed "connector
6
+ * unavailable" error at the tool-call boundary otherwise gives no way to tell, after the fact,
7
+ * whether it was a genuine fresh connect() failure, a circuit-breaker short-circuit (no connect
8
+ * attempted at all), or an in-flight operation's stale-connection retry. Never includes a stack
9
+ * trace -- name and message only, matching this house's other client-diagnostic channels
10
+ * (@danypops/vehicle-client-pi's own client-diagnostics.ts).
11
+ */
12
+ /** Never risks Object's default `[object Object]` stringification for a non-Error, non-string throw. */
13
+ function describeError(error: unknown): { name: string; message: string } {
14
+ if (error instanceof Error) return { name: error.name, message: error.message };
15
+ if (typeof error === "string") return { name: "string", message: error };
16
+ if (typeof error === "number" || typeof error === "boolean" || typeof error === "bigint") return { name: typeof error, message: String(error) };
17
+ return { name: typeof error, message: "(unprintable value)" };
18
+ }
19
+
20
+ export function formatClientDiagnosticEvent(event: RetryingClientDiagnosticEvent): string {
21
+ const parts = [`[lector-client] ${event.type}`];
22
+ if (event.attempt !== undefined) parts.push(`attempt=${event.attempt}`);
23
+ if (event.consecutiveFailures !== undefined) parts.push(`consecutiveFailures=${event.consecutiveFailures}`);
24
+ if (event.operationId !== undefined) parts.push(`operationId=${event.operationId}`);
25
+ if (event.error !== undefined) {
26
+ const { name, message } = describeError(event.error);
27
+ parts.push(`error=${name}: ${message}`);
28
+ }
29
+ return parts.join(" ");
30
+ }
31
+
32
+ /**
33
+ * Logs a createRetryingClient diagnostic event to stderr, only when LECTOR_CLIENT_DIAG is set --
34
+ * zero cost and zero output for every session that never opts in, matching the env-gated
35
+ * convention @danypops/vehicle-client-pi's own VEHICLE_CLIENT_DIAG already established in this
36
+ * house. Never throws: onEvent's own contract requires it stay side-effect-light, and a broken
37
+ * local console (or a formatting bug) must not take down a real client call.
38
+ */
39
+ export function logClientDiagnosticEvent(event: RetryingClientDiagnosticEvent): void {
40
+ if (!process.env.LECTOR_CLIENT_DIAG) return;
41
+ try {
42
+ console.error(formatClientDiagnosticEvent(event));
43
+ } catch {
44
+ // best-effort only -- see doc comment above.
45
+ }
46
+ }
@@ -7,7 +7,7 @@ export type EditorAction = { kind: "save" } | { kind: "save-and-quit" } | { kind
7
7
  const BACKSPACE_KEYS = new Set(["\x7f", "\b"]);
8
8
 
9
9
  /**
10
- * Pure neovim-style modal editing state machine: mode transitions, cursor motion, and buffer
10
+ * Pure modal editing state machine: mode transitions, cursor motion, and buffer
11
11
  * edits, with no terminal/ANSI rendering and no I/O -- save/quit/hover requests surface as
12
12
  * `pendingAction` for the hosting Component to actually perform (reading/writing through
13
13
  * Lector's hash-guarded workspace operations lives outside this class entirely). Kept
@@ -0,0 +1,22 @@
1
+ /**
2
+ * The real, host-agnostic editor surface -- deliberately its own subpath export
3
+ * (`@danypops/pi-lector/editor`), not part of this package's own Pi extension entry point
4
+ * (`extension/src/index.ts`, which is loaded only via Pi's own extension-discovery mechanism and
5
+ * has no package export of its own).
6
+ *
7
+ * Everything re-exported here has zero structural dependency on Pi's extension machinery --
8
+ * confirmed directly: ModalEditorComponent's own constructor only ever calls
9
+ * `tui.requestRender()`/`tui.terminal.rows` and `theme.fg()`/`theme.bg()` (EditorTheme is its own
10
+ * interface, deliberately narrowed away from pi-coding-agent's full Theme shape), and
11
+ * ModalEditorHost is a plain `{filePath, save, hover}` port with no Pi types anywhere in it.
12
+ * EditorState's own only dependency is `@danypops/lector`'s LiveBuffer -- no I/O, no rendering.
13
+ *
14
+ * This lets any real host (not just a Pi session that has loaded this package as an extension)
15
+ * construct and mount a real Lector editor Component directly, against its own tui/theme
16
+ * implementation and its own ModalEditorHost backed by whatever it wants (a Lector daemon
17
+ * client, a Vehicle operation, a plain filesystem call -- this package doesn't care).
18
+ */
19
+
20
+ export { type EditorAction, type EditorMode, EditorState } from "./editor-state.ts";
21
+ export type { EditorTheme } from "./editor-theme.ts";
22
+ export { ModalEditorComponent, type ModalEditorHost } from "./modal-editor-component.ts";
@@ -10,7 +10,7 @@ import type { EditorTheme } from "./editor-theme.ts";
10
10
 
11
11
  export type { EditorTheme } from "./editor-theme.ts";
12
12
 
13
- export interface NeovimEditorHost {
13
+ export interface ModalEditorHost {
14
14
  filePath: string;
15
15
  /** Saves the buffer's current text through Lector's hash-guarded write. Throws (surfaced as a status message, not a crash) on a genuinely concurrent external change. */
16
16
  save(text: string): Promise<void>;
@@ -28,16 +28,16 @@ const CAPTURE_COLOR: Record<string, ThemeColor> = {
28
28
  };
29
29
 
30
30
  /**
31
- * A real, full-file, neovim-style modal code editor Component -- not a CustomEditor subclass
31
+ * A real, full-file, modal code editor Component -- not a CustomEditor subclass
32
32
  * (that API replaces Pi's own chat input, not a full-file view; confirmed against
33
33
  * docs/tui.md's Pattern 7 and examples/extensions/modal-editor.ts). Renders as a `ctx.ui.custom`
34
34
  * overlay. Owns no authoritative state of its own past the open edit session: `EditorState`'s
35
35
  * LiveBuffer is the only in-memory copy, and every save round-trips through the host's
36
36
  * hash-guarded write -- never a second source of truth for the file's real disk content.
37
37
  */
38
- export class NeovimEditorComponent implements Component {
38
+ export class ModalEditorComponent implements Component {
39
39
  private readonly state: EditorState;
40
- private readonly host: NeovimEditorHost;
40
+ private readonly host: ModalEditorHost;
41
41
  private readonly tui: TUI;
42
42
  private readonly theme: EditorTheme;
43
43
  private readonly done: () => void;
@@ -47,7 +47,7 @@ export class NeovimEditorComponent implements Component {
47
47
  private statusMessage = "";
48
48
  private highlightCache: { text: string; spans: readonly HighlightSpan[] } | undefined;
49
49
 
50
- constructor(tui: TUI, theme: EditorTheme, host: NeovimEditorHost, content: string, done: () => void) {
50
+ constructor(tui: TUI, theme: EditorTheme, host: ModalEditorHost, content: string, done: () => void) {
51
51
  this.tui = tui;
52
52
  this.theme = theme;
53
53
  this.host = host;
@@ -1,3 +1,5 @@
1
+ import { readdirSync } from "node:fs";
2
+ import { homedir } from "node:os";
1
3
  import { resolve } from "node:path";
2
4
  import type {
3
5
  CachedRepositoryPage,
@@ -44,7 +46,7 @@ import { Type } from "typebox";
44
46
  /** Real ANSI-aware measurement for Table -- Malevich's own default is ASCII-only, unsafe against theme-styled cell/header text. */
45
47
  const tableMeasure: TextMeasure = { visibleWidth, truncateToWidth };
46
48
 
47
- import { isFilesystemRoot } from "@danypops/lector";
49
+ import { classifyAutoPopulationRoot, isFilesystemRoot } from "@danypops/lector";
48
50
  import { createLectorApplyPatchOperations } from "./apply-patch/operations.ts";
49
51
  import { formatApplyPatchCall, formatApplyPatchResult } from "./apply-patch/rendering.ts";
50
52
  import { createLectorCodeIntelligenceOperations } from "./code-intelligence/operations.ts";
@@ -75,7 +77,7 @@ import { createLectorEditOperations } from "./edit/operations.ts";
75
77
  import { openDirectoryExplorer } from "./editor/directory-explorer-operations.ts";
76
78
  import { ExplorerComponent, type ExplorerResult } from "./editor/explorer-component.ts";
77
79
  import { runExplorerFlow } from "./editor/explorer-flow.ts";
78
- import { NeovimEditorComponent, type NeovimEditorHost } from "./editor/neovim-editor-component.ts";
80
+ import { ModalEditorComponent, type ModalEditorHost } from "./editor/modal-editor-component.ts";
79
81
  import { openEditorFile } from "./editor/operations.ts";
80
82
  import { createExternalSearchOperations } from "./external-search/operations.ts";
81
83
  import {
@@ -214,10 +216,23 @@ export default function (pi: ExtensionAPI) {
214
216
  * intentional fallback for a raw read/write of a file outside any git repo can register
215
217
  * exactly this as a "new workspace", and auto-populating it would attempt a full
216
218
  * filesystem-wide symbol-graph scan -- confirmed live as a real, previously-shipped bug.
219
+ *
220
+ * Also short-circuits a broad host directory (home directory, an XDG config/cache/data
221
+ * root, a dotfile directory) the exact same way workspace.populateSymbolGraph's own
222
+ * server-side gate would refuse it -- avoids the round trip (and the queue-behind-real-
223
+ * projects ergonomics this caused live for ~/.pi/agent) entirely, using the identical
224
+ * classification Lector itself uses so the two never drift. A readdir failure (permission,
225
+ * race) is treated as "can't tell, don't block" -- the server-side gate is still authoritative.
217
226
  */
218
227
  function startMonitoringRoot(root: string, ctx: Parameters<Parameters<ExtensionAPI["on"]>[1]>[1]): void {
219
228
  if (isFilesystemRoot(root)) return;
220
229
  if (monitoringRoots.has(root)) return;
230
+ try {
231
+ const topLevelEntries = readdirSync(root);
232
+ if (classifyAutoPopulationRoot({ rootPath: root, homeDir: homedir(), topLevelEntries }) === "broad-non-project") return;
233
+ } catch {
234
+ // Can't read it here -- let the server-side gate be the authoritative answer.
235
+ }
221
236
  monitoringRoots.add(root);
222
237
  const thisGeneration = sessionGeneration;
223
238
  void monitorWorkspaceCache(cacheOperations, {
@@ -372,7 +387,7 @@ export default function (pi: ExtensionAPI) {
372
387
  }
373
388
 
374
389
  await commandCtx.ui.custom<void>((tui, theme, _keybindings, done) => {
375
- const host: NeovimEditorHost = {
390
+ const host: ModalEditorHost = {
376
391
  filePath: absolutePath,
377
392
  save: (text) => session.save(text),
378
393
  hover: async (line, character) => {
@@ -380,7 +395,7 @@ export default function (pi: ExtensionAPI) {
380
395
  return result.hover;
381
396
  },
382
397
  };
383
- return new NeovimEditorComponent(tui, theme, host, session.content, () => done(undefined));
398
+ return new ModalEditorComponent(tui, theme, host, session.content, () => done(undefined));
384
399
  }, editorOverlayOptions);
385
400
  }
386
401
 
@@ -405,7 +420,7 @@ export default function (pi: ExtensionAPI) {
405
420
  }
406
421
 
407
422
  pi.registerCommand("editor", {
408
- description: "Open a file in a neovim-style modal code editor, or a filesystem explorer with no path",
423
+ description: "Open a file in a modal code editor, or a filesystem explorer with no path",
409
424
  handler: async (args, commandCtx) => {
410
425
  const target = args.trim();
411
426
  if (!target) {
@@ -1126,7 +1141,7 @@ export default function (pi: ExtensionAPI) {
1126
1141
  name: "workspace_cache",
1127
1142
  label: "Workspace Cache",
1128
1143
  description:
1129
- "Checks or drives population of the workspace's persisted symbol graph -- the store reachable_from, symbol_annotations (anchor resolution), reference_based_rename, and workspace_map all read from, separate from the live language-server index find_symbols/hover/go_to_definition use. action=status reports not-cached/caching/partial/cached for the given bounds, without starting work. action=populate requests a scan and briefly waits for fast completion. action=wait subscribes to daemon job completion, with bounded status polling only when push delivery is unavailable. action=job_status is a point-in-time diagnostic read.",
1144
+ "Checks or drives population of the workspace's persisted symbol graph -- the store reachable_from, symbol_annotations (anchor resolution), reference_based_rename, and workspace_map all read from, separate from the live language-server index find_symbols/hover/go_to_definition use. action=status reports not-cached/caching/partial/cached for the given bounds, without starting work. action=populate requests a scan and briefly waits for fast completion; a source file changing mid-scan (e.g. a concurrent edit or rename) is retried automatically in the background for up to a minute before surfacing as a real failure, no manual re-run needed. action=wait subscribes to daemon job completion, with bounded status polling only when push delivery is unavailable. action=job_status is a point-in-time diagnostic read.",
1130
1145
  promptSnippet: "Check or force-populate the workspace's persisted symbol graph",
1131
1146
  promptGuidelines: [
1132
1147
  "Use action=populate with a larger maxFiles/maxSymbolsPerFile before relying on reachable_from/symbol_annotations/reference_based_rename against a workspace bigger than the default 500-file auto-scan -- their own errors (empty results, UnknownAnnotationAnchor, ReferenceBasedRenameRequiresFreshGraph) usually mean the graph never reached the files you need, not that population is simply still catching up.",
@@ -11,6 +11,7 @@ import {
11
11
  type WorkspaceResolutionRequest,
12
12
  } from "@danypops/lector";
13
13
  import { createRetryingClient, isLikelyStaleConnectionError, type RetryingClient } from "@danypops/vehicle-client/daemon-client";
14
+ import { logClientDiagnosticEvent } from "./client-diagnostics.ts";
14
15
 
15
16
  /**
16
17
  * Lazily connects to a running Lector daemon and caches, per resolution request, the
@@ -44,6 +45,9 @@ let connector: ClientConnector = () => connectLectorClient();
44
45
  const retryingClient: RetryingClient<LectorClient> = createRetryingClient(() => connector(), {
45
46
  label: "Lector",
46
47
  isStaleConnectionError: (error) => error instanceof LectorDaemonUnavailable || isLikelyStaleConnectionError(error),
48
+ // Purely observational, opt-in via LECTOR_CLIENT_DIAG -- see client-diagnostics.ts's own doc
49
+ // comment for the exact RCA gap this closes.
50
+ onEvent: logClientDiagnosticEvent,
47
51
  });
48
52
 
49
53
  /**
@@ -220,6 +224,38 @@ export function workspaceForPathOrDirectory(path: string): Promise<ResolvedWorks
220
224
  return resolveWorkspace({ strategy: "path-or-directory", path });
221
225
  }
222
226
 
227
+ /** Raised by workspaceForAnnotationPath for a path that does not exist on disk at all -- a distinct, explicit failure the caller must handle, never silently guessed as "must be a file, take its dirname()." */
228
+ export class AnnotationPathDoesNotExist extends Error {
229
+ constructor(readonly path: string) {
230
+ super(`"${path}" does not exist -- a symbol-annotation scope must be a real project directory or an existing source file`);
231
+ this.name = "AnnotationPathDoesNotExist";
232
+ }
233
+ }
234
+
235
+ /**
236
+ * For symbol-annotation's own `path` parameter, which genuinely means "which workspace does this
237
+ * belong to" and can honestly be either an existing project directory or one specific source
238
+ * file -- unlike workspaceForCodeIntelligencePath (files only, dirname() unconditional), a real
239
+ * project directory resolves via its own language markers, not its parent's. A genuinely
240
+ * nonexistent path throws AnnotationPathDoesNotExist rather than being silently treated as a file.
241
+ */
242
+ export async function workspaceForAnnotationPath(path: string): Promise<ResolvedWorkspace> {
243
+ const request: WorkspaceResolutionRequest = { strategy: "code-intelligence-path-or-directory", path };
244
+ const key = requestCacheKey(request);
245
+ const cached = resolutionCache.get(key);
246
+ if (cached) return cached;
247
+ const client = await lectorClient();
248
+ const outcome = await client.callOnce("workspace.resolvePath", request);
249
+ if (!outcome.found) {
250
+ if (outcome.reason === "nonexistent-path") throw new AnnotationPathDoesNotExist(path);
251
+ throw new Error(`workspace.resolvePath unexpectedly reported not-found for code-intelligence-path-or-directory: ${path}`);
252
+ }
253
+ const resolved: ResolvedWorkspace = { workspaceId: outcome.workspaceId, root: outcome.root };
254
+ resolutionCache.set(key, resolved);
255
+ if (outcome.created) onNewWorkspace?.(outcome.root);
256
+ return resolved;
257
+ }
258
+
223
259
  /**
224
260
  * The nearest ancestor of an already-resolved project root whose own package.json declares that
225
261
  * project as a workspace member via npm/yarn/bun's "workspaces" field -- undefined (no
@@ -1,5 +1,5 @@
1
1
  import type { OperationInputs, OperationOutputs } from "@danypops/lector";
2
- import { lectorClient, withWorkspace, workspaceForCodeIntelligencePath } from "../lector-client.ts";
2
+ import { lectorClient, withWorkspace, workspaceForAnnotationPath } from "../lector-client.ts";
3
3
 
4
4
  /** A bare symbol position an anchor is given as -- symbolNodeId and the anchor's baseline file hash are derived server-side, never supplied by the caller. */
5
5
  export interface AnnotationAnchorInput {
@@ -9,10 +9,12 @@ export interface AnnotationAnchorInput {
9
9
  }
10
10
 
11
11
  /**
12
- * Thin wrappers over Lector's annotation operations. Anchored to a workspace
13
- * via the first anchor's own path (workspaceForCodeIntelligencePath) --
14
- * every operation that needs a workspace already has at least one real
15
- * anchor position or an id whose workspace the caller already knows.
12
+ * Thin wrappers over Lector's annotation operations. Every operation resolves its workspace from
13
+ * its own `path` parameter via workspaceForAnnotationPath -- a real project directory or an
14
+ * existing source file, never dirname()'d unconditionally the way a plain code-intelligence
15
+ * path is. `path` here means "which workspace this call belongs to," not necessarily one of the
16
+ * operation's own anchors (create/refresh's anchors carry their own, separately-validated paths
17
+ * server-side).
16
18
  */
17
19
  export interface SymbolAnnotationOperations {
18
20
  create(
@@ -46,7 +48,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
46
48
  return {
47
49
  async create(path, subtype, title, body, anchors) {
48
50
  return withWorkspace(
49
- () => workspaceForCodeIntelligencePath(path),
51
+ () => workspaceForAnnotationPath(path),
50
52
  async ({ workspaceId }) => {
51
53
  const client = await lectorClient();
52
54
  return client.callOnce("workspace.createAnnotation", { workspaceId, subtype, title, body, anchors });
@@ -55,7 +57,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
55
57
  },
56
58
  async get(path, id) {
57
59
  return withWorkspace(
58
- () => workspaceForCodeIntelligencePath(path),
60
+ () => workspaceForAnnotationPath(path),
59
61
  async ({ workspaceId }) => {
60
62
  const client = await lectorClient();
61
63
  return client.call("workspace.getAnnotation", { workspaceId, id });
@@ -64,7 +66,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
64
66
  },
65
67
  async list(path, options = {}) {
66
68
  return withWorkspace(
67
- () => workspaceForCodeIntelligencePath(path),
69
+ () => workspaceForAnnotationPath(path),
68
70
  async ({ workspaceId }) => {
69
71
  const client = await lectorClient();
70
72
  return client.call("workspace.listAnnotations", {
@@ -79,7 +81,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
79
81
  },
80
82
  async refresh(path, id, subtype, title, body, anchors) {
81
83
  return withWorkspace(
82
- () => workspaceForCodeIntelligencePath(path),
84
+ () => workspaceForAnnotationPath(path),
83
85
  async ({ workspaceId }) => {
84
86
  const client = await lectorClient();
85
87
  return client.callOnce("workspace.refreshAnnotation", { workspaceId, id, subtype, title, body, anchors });
@@ -88,7 +90,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
88
90
  },
89
91
  async scrub(path, id) {
90
92
  return withWorkspace(
91
- () => workspaceForCodeIntelligencePath(path),
93
+ () => workspaceForAnnotationPath(path),
92
94
  async ({ workspaceId }) => {
93
95
  const client = await lectorClient();
94
96
  return client.callOnce("workspace.scrubAnnotation", { workspaceId, id });
@@ -97,7 +99,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
97
99
  },
98
100
  async restore(path, id) {
99
101
  return withWorkspace(
100
- () => workspaceForCodeIntelligencePath(path),
102
+ () => workspaceForAnnotationPath(path),
101
103
  async ({ workspaceId }) => {
102
104
  const client = await lectorClient();
103
105
  return client.callOnce("workspace.restoreAnnotation", { workspaceId, id });
@@ -106,7 +108,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
106
108
  },
107
109
  async contain(path, parentId, childId) {
108
110
  return withWorkspace(
109
- () => workspaceForCodeIntelligencePath(path),
111
+ () => workspaceForAnnotationPath(path),
110
112
  async ({ workspaceId }) => {
111
113
  const client = await lectorClient();
112
114
  return client.callOnce("workspace.containAnnotation", { workspaceId, parentId, childId });
@@ -115,7 +117,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
115
117
  },
116
118
  async uncontain(path, parentId, childId) {
117
119
  return withWorkspace(
118
- () => workspaceForCodeIntelligencePath(path),
120
+ () => workspaceForAnnotationPath(path),
119
121
  async ({ workspaceId }) => {
120
122
  const client = await lectorClient();
121
123
  return client.callOnce("workspace.uncontainAnnotation", { workspaceId, parentId, childId });
@@ -124,7 +126,7 @@ export function createLectorSymbolAnnotationOperations(): SymbolAnnotationOperat
124
126
  },
125
127
  async tree(path, rootId, maxDepth) {
126
128
  return withWorkspace(
127
- () => workspaceForCodeIntelligencePath(path),
129
+ () => workspaceForAnnotationPath(path),
128
130
  async ({ workspaceId }) => {
129
131
  const client = await lectorClient();
130
132
  return client.call("workspace.annotationTree", { workspaceId, rootId, maxDepth });
@@ -15,9 +15,26 @@ export interface JobWatchHandle {
15
15
 
16
16
  export type JobWatchOutcome = { readonly status: "subscribed"; readonly handle: JobWatchHandle } | { readonly status: "unavailable" };
17
17
 
18
+ /**
19
+ * The daemon's own default (0/omitted -- fail fast on a WorkspaceChangedDuringPopulation race)
20
+ * would surface a live-editing/rename race straight to this tool's own caller as an opaque
21
+ * error. This tool's whole point is "make this converge for me" -- a bounded background retry
22
+ * costs the caller nothing extra (it runs inside the job the caller is already waiting on or
23
+ * polling, not as additional synchronous tool-call latency), so it defaults on here specifically,
24
+ * unlike the raw daemon operation which stays fail-fast by default for programmatic callers that
25
+ * want today's exact contract.
26
+ */
27
+ const DEFAULT_POPULATE_RETRY_TIME_BUDGET_MS = 60_000;
28
+
18
29
  export interface WorkspaceCacheOperations {
19
30
  status(directory: string, maxFiles: number, maxSymbolsPerFile: number): Promise<WorkspaceCacheStatus>;
20
- submit(directory: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
31
+ submit(
32
+ directory: string,
33
+ maxFiles: number,
34
+ maxSymbolsPerFile: number,
35
+ waitMs?: number,
36
+ retryTimeBudgetMs?: number,
37
+ ): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
21
38
  jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
22
39
  watchJob?(jobId: string, onJob: (job: JobSnapshot<PopulateSymbolGraphResult>) => void): Promise<JobWatchOutcome>;
23
40
  }
@@ -33,14 +50,14 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
33
50
  },
34
51
  );
35
52
  },
36
- submit(directory, maxFiles, maxSymbolsPerFile, waitMs = 0) {
53
+ submit(directory, maxFiles, maxSymbolsPerFile, waitMs = 0, retryTimeBudgetMs = DEFAULT_POPULATE_RETRY_TIME_BUDGET_MS) {
37
54
  return withWorkspace(
38
55
  () => workspaceForProjectDirectory(directory),
39
56
  async ({ workspaceId }) => {
40
57
  const client = await lectorClient();
41
58
  const { job } = await client.callOnce("job.submit", {
42
59
  operation: "workspace.populateSymbolGraph",
43
- input: { workspaceId, maxFiles, maxSymbolsPerFile },
60
+ input: { workspaceId, maxFiles, maxSymbolsPerFile, retryTimeBudgetMs },
44
61
  waitMs,
45
62
  });
46
63
  return job;
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-lector",
3
- "version": "0.12.6",
3
+ "version": "0.12.8",
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",
@@ -12,15 +12,18 @@
12
12
  "pi": {
13
13
  "extensions": ["extension/src/index.ts"]
14
14
  },
15
+ "exports": {
16
+ "./editor": "./extension/src/editor/index.ts"
17
+ },
15
18
  "peerDependencies": {
16
19
  "@earendil-works/pi-coding-agent": "*",
17
20
  "@earendil-works/pi-tui": "*",
18
21
  "typebox": "*"
19
22
  },
20
23
  "dependencies": {
21
- "@danypops/vehicle-client": "^0.2.0",
24
+ "@danypops/vehicle-client": "^0.8.2",
22
25
  "@danypops/lector": "^0.18.0",
23
- "malevich-tui-components": "^0.19.0",
26
+ "malevich-tui-components": "^0.25.0",
24
27
  "picomatch": "^4.0.5"
25
28
  },
26
29
  "devDependencies": {