@danypops/pi-lector 0.11.2 → 0.12.0
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/README.md
CHANGED
|
@@ -20,9 +20,10 @@ it's still building, instead of requiring an explicit "start indexing" call.
|
|
|
20
20
|
When a session starts inside a Git repository, pi-lector checks a durable, bounded
|
|
21
21
|
source-content manifest without blocking startup. The footer reports not cached,
|
|
22
22
|
caching, or cached across every workspace touched so far this session; completion
|
|
23
|
-
also emits a one-shot notification.
|
|
24
|
-
|
|
23
|
+
also emits a one-shot notification. The adapter subscribes to daemon job completion,
|
|
24
|
+
keeps bounded status polling as the disconnect fallback, and closes both on session shutdown.
|
|
25
|
+
The agent receives each state transition once in its context.
|
|
25
26
|
|
|
26
27
|
For an explicit, custom-bound population outside of Pi (a larger scan than the
|
|
27
28
|
default 500 files / 100 symbols per file), use `lector workspace populate-symbol-graph`
|
|
28
|
-
and `lector job
|
|
29
|
+
and `lector job wait` directly.
|
package/extension/src/index.ts
CHANGED
|
@@ -132,6 +132,7 @@ import {
|
|
|
132
132
|
createWorkspaceCacheOperations,
|
|
133
133
|
describeCacheState,
|
|
134
134
|
monitorWorkspaceCache,
|
|
135
|
+
waitForJobCompletion,
|
|
135
136
|
} from "./workspace-cache/operations.ts";
|
|
136
137
|
import { formatJobSnapshotResult, formatWorkspaceCacheCall, formatWorkspaceCacheStatusResult } from "./workspace-cache/rendering.ts";
|
|
137
138
|
import { createLectorWriteOperations } from "./write/operations.ts";
|
|
@@ -1108,7 +1109,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
1108
1109
|
});
|
|
1109
1110
|
|
|
1110
1111
|
interface WorkspaceCacheToolDetails {
|
|
1111
|
-
readonly action: "status" | "populate" | "job_status";
|
|
1112
|
+
readonly action: "status" | "populate" | "wait" | "job_status";
|
|
1112
1113
|
readonly status?: WorkspaceCacheStatus;
|
|
1113
1114
|
readonly job?: JobSnapshot<PopulateSymbolGraphResult>;
|
|
1114
1115
|
}
|
|
@@ -1117,14 +1118,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
1117
1118
|
name: "workspace_cache",
|
|
1118
1119
|
label: "Workspace Cache",
|
|
1119
1120
|
description:
|
|
1120
|
-
"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
|
|
1121
|
+
"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.",
|
|
1121
1122
|
promptSnippet: "Check or force-populate the workspace's persisted symbol graph",
|
|
1122
1123
|
promptGuidelines: [
|
|
1123
1124
|
"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.",
|
|
1124
|
-
"action=populate returns
|
|
1125
|
+
"When action=populate returns a queued/running job, call action=wait once with that jobId; do not run shell sleep or manually poll job_status.",
|
|
1125
1126
|
],
|
|
1126
1127
|
parameters: Type.Object({
|
|
1127
|
-
action: Type.Union([Type.Literal("status"), Type.Literal("populate"), Type.Literal("job_status")]),
|
|
1128
|
+
action: Type.Union([Type.Literal("status"), Type.Literal("populate"), Type.Literal("wait"), Type.Literal("job_status")]),
|
|
1128
1129
|
directory: Type.Optional(
|
|
1129
1130
|
Type.String({ description: "Required for action=status/populate -- absolute or cwd-relative path used to resolve the workspace" }),
|
|
1130
1131
|
),
|
|
@@ -1137,17 +1138,33 @@ export default function (pi: ExtensionAPI) {
|
|
|
1137
1138
|
waitMs: Type.Optional(
|
|
1138
1139
|
Type.Number({
|
|
1139
1140
|
description:
|
|
1140
|
-
"action=populate
|
|
1141
|
+
"action=populate: initial daemon wait, defaults to 3000 and capped at 30000. action=wait: total subscription/fallback bound, defaults to 300000 and capped at 300000",
|
|
1141
1142
|
}),
|
|
1142
1143
|
),
|
|
1143
|
-
jobId: Type.Optional(Type.String({ description: "Required for action=job_status -- a jobId returned by
|
|
1144
|
+
jobId: Type.Optional(Type.String({ description: "Required for action=wait/job_status -- a jobId returned by action=populate" })),
|
|
1144
1145
|
}),
|
|
1145
|
-
async execute(_toolCallId, params): Promise<AgentToolResult<WorkspaceCacheToolDetails>> {
|
|
1146
|
+
async execute(_toolCallId, params, signal): Promise<AgentToolResult<WorkspaceCacheToolDetails>> {
|
|
1146
1147
|
if (params.action === "job_status") {
|
|
1147
1148
|
if (!params.jobId) throw new Error("workspace_cache action=job_status requires jobId");
|
|
1148
|
-
const job = await
|
|
1149
|
+
const job = await cacheOperations.jobStatus(params.jobId);
|
|
1149
1150
|
return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "job_status", job } };
|
|
1150
1151
|
}
|
|
1152
|
+
if (params.action === "wait") {
|
|
1153
|
+
if (!params.jobId) throw new Error("workspace_cache action=wait requires jobId");
|
|
1154
|
+
const waitMs = params.waitMs ?? 300_000;
|
|
1155
|
+
if (!Number.isSafeInteger(waitMs) || waitMs < 1 || waitMs > 300_000)
|
|
1156
|
+
throw new Error("workspace_cache action=wait waitMs must be an integer from 1 to 300000");
|
|
1157
|
+
const pollIntervalMs = 5_000;
|
|
1158
|
+
const completed = await waitForJobCompletion(cacheOperations, params.jobId, {
|
|
1159
|
+
pollIntervalMs,
|
|
1160
|
+
maxPolls: Math.ceil(waitMs / pollIntervalMs),
|
|
1161
|
+
shouldContinue: () => !signal?.aborted,
|
|
1162
|
+
signal,
|
|
1163
|
+
});
|
|
1164
|
+
if (signal?.aborted) throw new DOMException("workspace cache wait canceled", "AbortError");
|
|
1165
|
+
const job = completed ?? (await cacheOperations.jobStatus(params.jobId));
|
|
1166
|
+
return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "wait", job } };
|
|
1167
|
+
}
|
|
1151
1168
|
if (!params.directory) throw new Error(`workspace_cache action=${params.action} requires directory`);
|
|
1152
1169
|
const directory = resolve(cwd, params.directory);
|
|
1153
1170
|
const maxFiles = params.maxFiles ?? 500;
|
|
@@ -1156,11 +1173,11 @@ export default function (pi: ExtensionAPI) {
|
|
|
1156
1173
|
const status = await cacheOperations.status(directory, maxFiles, maxSymbolsPerFile);
|
|
1157
1174
|
return { content: [{ type: "text", text: JSON.stringify(status) }], details: { action: "status", status } };
|
|
1158
1175
|
}
|
|
1159
|
-
const job = await
|
|
1176
|
+
const job = await cacheOperations.submit(directory, maxFiles, maxSymbolsPerFile, params.waitMs ?? 3_000);
|
|
1160
1177
|
return { content: [{ type: "text", text: JSON.stringify(job) }], details: { action: "populate", job } };
|
|
1161
1178
|
},
|
|
1162
1179
|
renderCall(args, theme, context) {
|
|
1163
|
-
const action = args.action === "populate" || args.action === "job_status" ? args.action : "status";
|
|
1180
|
+
const action = args.action === "populate" || args.action === "wait" || args.action === "job_status" ? args.action : "status";
|
|
1164
1181
|
const text = context.lastComponent instanceof Text ? context.lastComponent : new Text("", 0, 0);
|
|
1165
1182
|
text.setText(formatWorkspaceCacheCall(action, args, theme));
|
|
1166
1183
|
return text;
|
|
@@ -1,10 +1,18 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type JobSnapshot, type PopulateSymbolGraphResult, resolveLectorDaemonConnection, type WorkspaceCacheStatus } from "@danypops/lector";
|
|
2
|
+
import { connectPushChannel } from "@danypops/vehicle-client/daemon-client";
|
|
2
3
|
import { lectorClient, withWorkspace, workspaceForProjectDirectory } from "../lector-client.ts";
|
|
3
4
|
|
|
5
|
+
export interface JobWatchHandle {
|
|
6
|
+
close(): void;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
export type JobWatchOutcome = { readonly status: "subscribed"; readonly handle: JobWatchHandle } | { readonly status: "unavailable" };
|
|
10
|
+
|
|
4
11
|
export interface WorkspaceCacheOperations {
|
|
5
12
|
status(directory: string, maxFiles: number, maxSymbolsPerFile: number): Promise<WorkspaceCacheStatus>;
|
|
6
|
-
submit(directory: string, maxFiles: number, maxSymbolsPerFile: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
13
|
+
submit(directory: string, maxFiles: number, maxSymbolsPerFile: number, waitMs?: number): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
7
14
|
jobStatus(jobId: string): Promise<JobSnapshot<PopulateSymbolGraphResult>>;
|
|
15
|
+
watchJob?(jobId: string, onJob: (job: JobSnapshot<PopulateSymbolGraphResult>) => void): Promise<JobWatchOutcome>;
|
|
8
16
|
}
|
|
9
17
|
|
|
10
18
|
export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
@@ -18,7 +26,7 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
|
18
26
|
},
|
|
19
27
|
);
|
|
20
28
|
},
|
|
21
|
-
submit(directory, maxFiles, maxSymbolsPerFile) {
|
|
29
|
+
submit(directory, maxFiles, maxSymbolsPerFile, waitMs = 0) {
|
|
22
30
|
return withWorkspace(
|
|
23
31
|
() => workspaceForProjectDirectory(directory),
|
|
24
32
|
async ({ workspaceId }) => {
|
|
@@ -26,7 +34,7 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
|
26
34
|
const { job } = await client.callOnce("job.submit", {
|
|
27
35
|
operation: "workspace.populateSymbolGraph",
|
|
28
36
|
input: { workspaceId, maxFiles, maxSymbolsPerFile },
|
|
29
|
-
waitMs
|
|
37
|
+
waitMs,
|
|
30
38
|
});
|
|
31
39
|
return job;
|
|
32
40
|
},
|
|
@@ -37,6 +45,33 @@ export function createWorkspaceCacheOperations(): WorkspaceCacheOperations {
|
|
|
37
45
|
const { job } = await client.call("job.status", { jobId });
|
|
38
46
|
return job;
|
|
39
47
|
},
|
|
48
|
+
async watchJob(jobId, onJob) {
|
|
49
|
+
try {
|
|
50
|
+
const client = await lectorClient();
|
|
51
|
+
const { topic } = await client.call("job.watch", { jobId });
|
|
52
|
+
const initialTarget = resolveLectorDaemonConnection();
|
|
53
|
+
const channel = connectPushChannel({
|
|
54
|
+
url: () => {
|
|
55
|
+
const target = resolveLectorDaemonConnection();
|
|
56
|
+
return `ws://${target.host}:${target.port}/push`;
|
|
57
|
+
},
|
|
58
|
+
token: initialTarget.token,
|
|
59
|
+
topics: [topic],
|
|
60
|
+
onMessage(receivedTopic) {
|
|
61
|
+
if (receivedTopic !== topic) return;
|
|
62
|
+
void client
|
|
63
|
+
.call("job.status", { jobId })
|
|
64
|
+
.then(({ job }) => onJob(job))
|
|
65
|
+
.catch(() => {
|
|
66
|
+
// The bounded status cadence remains authoritative when push refresh fails.
|
|
67
|
+
});
|
|
68
|
+
},
|
|
69
|
+
});
|
|
70
|
+
return { status: "subscribed", handle: { close: () => channel.close() } };
|
|
71
|
+
} catch {
|
|
72
|
+
return { status: "unavailable" };
|
|
73
|
+
}
|
|
74
|
+
},
|
|
40
75
|
};
|
|
41
76
|
}
|
|
42
77
|
|
|
@@ -74,6 +109,49 @@ export interface MonitorWorkspaceCacheOptions {
|
|
|
74
109
|
readonly sleep?: (ms: number) => Promise<void>;
|
|
75
110
|
}
|
|
76
111
|
|
|
112
|
+
export interface WaitForJobCompletionOptions {
|
|
113
|
+
readonly pollIntervalMs: number;
|
|
114
|
+
readonly maxPolls: number;
|
|
115
|
+
readonly shouldContinue: () => boolean;
|
|
116
|
+
readonly signal?: AbortSignal;
|
|
117
|
+
readonly sleep?: (ms: number) => Promise<void>;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Waits on Vehicle push delivery and checks status on a bounded cadence when push is unavailable or disconnected. */
|
|
121
|
+
export async function waitForJobCompletion(
|
|
122
|
+
operations: WorkspaceCacheOperations,
|
|
123
|
+
jobId: string,
|
|
124
|
+
options: WaitForJobCompletionOptions,
|
|
125
|
+
): Promise<JobSnapshot<PopulateSymbolGraphResult> | undefined> {
|
|
126
|
+
const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
127
|
+
let pushedResolve: ((job: JobSnapshot<PopulateSymbolGraphResult>) => void) | undefined;
|
|
128
|
+
const pushed = new Promise<JobSnapshot<PopulateSymbolGraphResult>>((resolve) => {
|
|
129
|
+
pushedResolve = resolve;
|
|
130
|
+
});
|
|
131
|
+
let watch: JobWatchHandle | undefined;
|
|
132
|
+
let resolveAbort!: () => void;
|
|
133
|
+
const aborted = new Promise<void>((resolve) => {
|
|
134
|
+
resolveAbort = resolve;
|
|
135
|
+
});
|
|
136
|
+
const onAbort = () => resolveAbort();
|
|
137
|
+
options.signal?.addEventListener("abort", onAbort, { once: true });
|
|
138
|
+
try {
|
|
139
|
+
const outcome = await operations.watchJob?.(jobId, (job) => pushedResolve?.(job));
|
|
140
|
+
if (outcome?.status === "subscribed") watch = outcome.handle;
|
|
141
|
+
for (let poll = 0; poll < options.maxPolls && options.shouldContinue() && !options.signal?.aborted; poll++) {
|
|
142
|
+
const current = await operations.jobStatus(jobId);
|
|
143
|
+
if (current.status === "succeeded" || current.status === "failed") return current;
|
|
144
|
+
const next = await Promise.race([pushed, sleep(options.pollIntervalMs).then(() => undefined), aborted.then(() => undefined)]);
|
|
145
|
+
if (next?.status === "succeeded" || next?.status === "failed") return next;
|
|
146
|
+
}
|
|
147
|
+
if (options.shouldContinue() && !options.signal?.aborted) return operations.jobStatus(jobId);
|
|
148
|
+
return undefined;
|
|
149
|
+
} finally {
|
|
150
|
+
options.signal?.removeEventListener("abort", onAbort);
|
|
151
|
+
watch?.close();
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
77
155
|
/** Drives one bounded session cache lifecycle; Pi event handlers only render its states. */
|
|
78
156
|
export async function monitorWorkspaceCache(operations: WorkspaceCacheOperations, options: MonitorWorkspaceCacheOptions): Promise<void> {
|
|
79
157
|
const sleep = options.sleep ?? ((ms: number) => new Promise<void>((resolve) => setTimeout(resolve, ms)));
|
|
@@ -108,14 +186,12 @@ export async function monitorWorkspaceCache(operations: WorkspaceCacheOperations
|
|
|
108
186
|
}
|
|
109
187
|
options.onState({ status: "caching", jobId });
|
|
110
188
|
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
}
|
|
120
|
-
}
|
|
189
|
+
const job = await waitForJobCompletion(operations, jobId, {
|
|
190
|
+
pollIntervalMs: options.pollIntervalMs,
|
|
191
|
+
maxPolls: options.maxPolls,
|
|
192
|
+
shouldContinue: options.shouldContinue,
|
|
193
|
+
sleep,
|
|
194
|
+
});
|
|
195
|
+
if (job?.status === "failed") throw new Error(`${job.error.code}: ${job.error.message}`);
|
|
196
|
+
if (job?.status === "succeeded") reportCompleted(job);
|
|
121
197
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import type { JobSnapshot, PopulateSymbolGraphResult, WorkspaceCacheStatus } from "@danypops/lector";
|
|
2
2
|
import type { LectorTheme } from "../lector-tui-theme.ts";
|
|
3
3
|
|
|
4
|
-
type WorkspaceCacheAction = "status" | "populate" | "job_status";
|
|
4
|
+
type WorkspaceCacheAction = "status" | "populate" | "wait" | "job_status";
|
|
5
5
|
|
|
6
6
|
export function formatWorkspaceCacheCall(
|
|
7
7
|
action: WorkspaceCacheAction,
|
|
@@ -9,9 +9,9 @@ export function formatWorkspaceCacheCall(
|
|
|
9
9
|
theme: LectorTheme,
|
|
10
10
|
): string {
|
|
11
11
|
const label = theme.fg("toolTitle", theme.bold("workspace_cache"));
|
|
12
|
-
if (action === "job_status") {
|
|
12
|
+
if (action === "job_status" || action === "wait") {
|
|
13
13
|
const jobId = typeof args.jobId === "string" ? args.jobId : "";
|
|
14
|
-
return `${label} ${theme.fg("accent",
|
|
14
|
+
return `${label} ${theme.fg("accent", action)} ${theme.fg("dim", jobId)}`;
|
|
15
15
|
}
|
|
16
16
|
const directory = typeof args.directory === "string" ? args.directory : "";
|
|
17
17
|
const maxFiles = typeof args.maxFiles === "number" ? String(args.maxFiles) : "default";
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.12.0",
|
|
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
|
},
|
|
20
20
|
"dependencies": {
|
|
21
21
|
"@danypops/vehicle-client": "^0.2.0",
|
|
22
|
-
"@danypops/lector": "^0.
|
|
22
|
+
"@danypops/lector": "^0.16.0",
|
|
23
23
|
"malevich-tui-components": "^0.19.0"
|
|
24
24
|
},
|
|
25
25
|
"devDependencies": {
|