@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.
- package/extension/src/client-diagnostics.ts +46 -0
- package/extension/src/editor/editor-state.ts +1 -1
- package/extension/src/editor/index.ts +22 -0
- package/extension/src/editor/{neovim-editor-component.ts → modal-editor-component.ts} +5 -5
- package/extension/src/index.ts +21 -6
- package/extension/src/lector-client.ts +36 -0
- package/extension/src/symbol-annotation/operations.ts +16 -14
- package/extension/src/workspace-cache/operations.ts +20 -3
- package/package.json +6 -3
|
@@ -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
|
|
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
|
|
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,
|
|
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
|
|
38
|
+
export class ModalEditorComponent implements Component {
|
|
39
39
|
private readonly state: EditorState;
|
|
40
|
-
private readonly host:
|
|
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:
|
|
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;
|
package/extension/src/index.ts
CHANGED
|
@@ -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 {
|
|
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:
|
|
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
|
|
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
|
|
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,
|
|
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.
|
|
13
|
-
* via
|
|
14
|
-
*
|
|
15
|
-
*
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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
|
-
() =>
|
|
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(
|
|
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.
|
|
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
|
|
24
|
+
"@danypops/vehicle-client": "^0.8.2",
|
|
22
25
|
"@danypops/lector": "^0.18.0",
|
|
23
|
-
"malevich-tui-components": "^0.
|
|
26
|
+
"malevich-tui-components": "^0.25.0",
|
|
24
27
|
"picomatch": "^4.0.5"
|
|
25
28
|
},
|
|
26
29
|
"devDependencies": {
|