@danypops/pi-lector 0.12.15 → 0.13.1
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,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Idempotent start/stop wrapper over setInterval -- a second start() is a no-op rather than a
|
|
3
|
+
* competing timer, and stop() is safe to call even if never started. Same shape as pi-papyrus's,
|
|
4
|
+
* pi-pipes', pi-packed's, and pi-tickets' own BoundedPoll.
|
|
5
|
+
*/
|
|
6
|
+
export class BoundedPoll {
|
|
7
|
+
private timer: ReturnType<typeof setInterval> | undefined;
|
|
8
|
+
|
|
9
|
+
start(intervalMs: number, tick: () => void): void {
|
|
10
|
+
if (this.timer) return;
|
|
11
|
+
this.timer = setInterval(tick, intervalMs);
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
stop(): void {
|
|
15
|
+
if (!this.timer) return;
|
|
16
|
+
clearInterval(this.timer);
|
|
17
|
+
this.timer = undefined;
|
|
18
|
+
}
|
|
19
|
+
}
|
package/extension/src/index.ts
CHANGED
|
@@ -129,6 +129,7 @@ import { formatSearchCall, formatSearchResult } from "./search/rendering.ts";
|
|
|
129
129
|
import { type AnnotationAnchorInput, createLectorSymbolAnnotationOperations } from "./symbol-annotation/operations.ts";
|
|
130
130
|
import { formatAnnotationDetail, formatAnnotationListSummary, formatAnnotationSummary } from "./symbol-annotation/rendering.ts";
|
|
131
131
|
import type { LectorVehicleCall } from "./vehicle-client.ts";
|
|
132
|
+
import { CachingOverlay } from "./workspace-cache/caching-overlay.ts";
|
|
132
133
|
import {
|
|
133
134
|
type CachePresentationState,
|
|
134
135
|
cacheContextMessage,
|
|
@@ -278,6 +279,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
278
279
|
};
|
|
279
280
|
});
|
|
280
281
|
|
|
282
|
+
let cachingOverlay: CachingOverlay | undefined;
|
|
281
283
|
pi.on("session_shutdown", (_event, ctx) => {
|
|
282
284
|
sessionGeneration++;
|
|
283
285
|
cacheStatesByRoot.clear();
|
|
@@ -285,6 +287,7 @@ export default function (pi: ExtensionAPI) {
|
|
|
285
287
|
lastInjectedSummary = undefined;
|
|
286
288
|
uiContext = undefined;
|
|
287
289
|
ctx.ui.setStatus("lector-cache", undefined);
|
|
290
|
+
cachingOverlay?.dispose();
|
|
288
291
|
});
|
|
289
292
|
|
|
290
293
|
pi.on("session_start", (_event, ctx) => {
|
|
@@ -295,6 +298,14 @@ export default function (pi: ExtensionAPI) {
|
|
|
295
298
|
lastInjectedSummary = undefined;
|
|
296
299
|
uiContext = ctx;
|
|
297
300
|
setNewWorkspaceObserver((root) => startMonitoringRoot(root, ctx));
|
|
301
|
+
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();
|
|
305
|
+
cachingOverlay.setUI(ctx.ui);
|
|
306
|
+
void cachingOverlay.refresh();
|
|
307
|
+
cachingOverlay.startPolling();
|
|
308
|
+
}
|
|
298
309
|
const thisGeneration = sessionGeneration;
|
|
299
310
|
void nearestGitWorkspaceRoot(cwd)
|
|
300
311
|
.then((projectRoot) => {
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Persistent above-editor widget for currently-active symbol-graph caching jobs -- mirrors
|
|
3
|
+
* pi-papyrus's own TaskOverlay/NoteOverlay, pi-pipes' own JobsOverlay, and pi-packed's own
|
|
4
|
+
* DoctorOverlay: factory-form ctx.ui.setWidget registration, requestRender on refresh, hides the
|
|
5
|
+
* widget entirely (setWidget(key, undefined)) rather than an empty box once nothing is caching.
|
|
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.
|
|
9
|
+
*/
|
|
10
|
+
import type { ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
|
|
11
|
+
import type { TUI } from "@earendil-works/pi-tui";
|
|
12
|
+
import { AutoRotatingWindow } from "malevich-tui-components";
|
|
13
|
+
import { BoundedPoll } from "../bounded-poll.js";
|
|
14
|
+
import { lectorClient, type RetryingLectorClient } from "../lector-client.js";
|
|
15
|
+
import { buildCachingWidgetProjection, type CachingWidgetProjection, LECTOR_CACHING_WIDGET_VISIBLE_ROWS, renderCachingWidgetLines } from "./caching-widget.js";
|
|
16
|
+
|
|
17
|
+
const WIDGET_KEY = "pi-lector-caching";
|
|
18
|
+
|
|
19
|
+
/** Matches pi-papyrus's/pi-pipes' own 15-20s cadence -- workspace.activeCachingJobs is a cheap in-memory read. */
|
|
20
|
+
export const CACHING_WIDGET_POLL_INTERVAL_MS = 15_000;
|
|
21
|
+
|
|
22
|
+
/** How often the widget's own auto-rotating overflow page advances. */
|
|
23
|
+
export const CACHING_WIDGET_ROTATION_INTERVAL_MS = 6_000;
|
|
24
|
+
|
|
25
|
+
const EMPTY_PROJECTION: CachingWidgetProjection = { rows: [], total: 0 };
|
|
26
|
+
|
|
27
|
+
export class CachingOverlay {
|
|
28
|
+
private uiCtx: ExtensionUIContext | undefined;
|
|
29
|
+
private registered = false;
|
|
30
|
+
private tui: TUI | undefined;
|
|
31
|
+
private projection: CachingWidgetProjection = EMPTY_PROJECTION;
|
|
32
|
+
private readonly poll = new BoundedPoll();
|
|
33
|
+
/** Repaint-only ticker (no data refetch) so the widget's own auto-rotating page visibly
|
|
34
|
+
* advances even when nothing else has changed. */
|
|
35
|
+
private readonly rotationPoll = new BoundedPoll();
|
|
36
|
+
private readonly rotation = new AutoRotatingWindow({
|
|
37
|
+
totalRows: 0,
|
|
38
|
+
pageSize: LECTOR_CACHING_WIDGET_VISIBLE_ROWS,
|
|
39
|
+
intervalMs: CACHING_WIDGET_ROTATION_INTERVAL_MS,
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
constructor(private readonly connect: () => Promise<RetryingLectorClient> = lectorClient) {}
|
|
43
|
+
|
|
44
|
+
setUI(ctx: ExtensionUIContext): void {
|
|
45
|
+
if (ctx !== this.uiCtx) {
|
|
46
|
+
this.uiCtx = ctx;
|
|
47
|
+
this.registered = false;
|
|
48
|
+
this.tui = undefined;
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/** Never throws: called from a poll timer and from session_start, neither of which should turn
|
|
53
|
+
* a best-effort status widget into a crashed extension host over a daemon that isn't running
|
|
54
|
+
* yet or a rendering bug. */
|
|
55
|
+
async refresh(): Promise<void> {
|
|
56
|
+
try {
|
|
57
|
+
const client = await this.connect();
|
|
58
|
+
const result = await client.call("workspace.activeCachingJobs", {});
|
|
59
|
+
this.projection = buildCachingWidgetProjection(result.jobs);
|
|
60
|
+
} catch {
|
|
61
|
+
this.projection = EMPTY_PROJECTION;
|
|
62
|
+
}
|
|
63
|
+
try {
|
|
64
|
+
this.render();
|
|
65
|
+
} catch {
|
|
66
|
+
// A rendering bug must not crash the extension host over a best-effort status widget.
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
private render(): void {
|
|
71
|
+
if (!this.uiCtx) return;
|
|
72
|
+
|
|
73
|
+
if (this.projection.total === 0) {
|
|
74
|
+
if (this.registered) {
|
|
75
|
+
this.uiCtx.setWidget(WIDGET_KEY, undefined);
|
|
76
|
+
this.registered = false;
|
|
77
|
+
this.tui = undefined;
|
|
78
|
+
this.rotationPoll.stop();
|
|
79
|
+
}
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (!this.registered) {
|
|
84
|
+
this.uiCtx.setWidget(
|
|
85
|
+
WIDGET_KEY,
|
|
86
|
+
(tui: TUI, theme: Theme) => {
|
|
87
|
+
this.tui = tui;
|
|
88
|
+
return {
|
|
89
|
+
render: (width: number) => renderCachingWidgetLines(theme, this.projection, width, this.rotation),
|
|
90
|
+
invalidate: () => {
|
|
91
|
+
// Theme changed -- force re-registration, matching every other overlay in this ecosystem.
|
|
92
|
+
this.registered = false;
|
|
93
|
+
this.tui = undefined;
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
},
|
|
97
|
+
{ placement: "aboveEditor" },
|
|
98
|
+
);
|
|
99
|
+
this.registered = true;
|
|
100
|
+
this.rotationPoll.start(CACHING_WIDGET_ROTATION_INTERVAL_MS, () => this.tui?.requestRender());
|
|
101
|
+
} else {
|
|
102
|
+
this.tui?.requestRender();
|
|
103
|
+
}
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
startPolling(intervalMs: number = CACHING_WIDGET_POLL_INTERVAL_MS): void {
|
|
107
|
+
this.poll.start(intervalMs, () => {
|
|
108
|
+
void this.refresh();
|
|
109
|
+
});
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
stopPolling(): void {
|
|
113
|
+
this.poll.stop();
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
dispose(): void {
|
|
117
|
+
this.stopPolling();
|
|
118
|
+
this.rotationPoll.stop();
|
|
119
|
+
this.uiCtx?.setWidget(WIDGET_KEY, undefined);
|
|
120
|
+
this.registered = false;
|
|
121
|
+
this.tui = undefined;
|
|
122
|
+
this.uiCtx = undefined;
|
|
123
|
+
}
|
|
124
|
+
}
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Pure projection/render pair for the Caching widget -- mirrors pi-papyrus's own task-widget.ts /
|
|
3
|
+
* pi-pipes' own jobs-widget.ts / pi-packed's own doctor-widget.ts split: the daemon's
|
|
4
|
+
* workspace.activeCachingJobs result in, a bounded intermediate shape out, no I/O, no TUI, fully
|
|
5
|
+
* unit-testable without a real daemon or terminal. See caching-overlay.ts for the stateful
|
|
6
|
+
* ctx.ui.setWidget-registered class that drives these from a live poll.
|
|
7
|
+
*/
|
|
8
|
+
import { vehicleWidgetTitle } from "@danypops/vehicle-client-pi/widget-header";
|
|
9
|
+
import { truncateToWidth, visibleWidth, wrapTextWithAnsi } from "@earendil-works/pi-tui";
|
|
10
|
+
import { type AutoRotatingWindow, renderCardRow, type TextMeasure } from "malevich-tui-components";
|
|
11
|
+
|
|
12
|
+
const measure: TextMeasure = { visibleWidth, truncateToWidth, wrapTextWithAnsi };
|
|
13
|
+
|
|
14
|
+
/** The daemon's own manifest name (see packages/lector/src/service.ts's `new VehicleRegistry({ name: "lector" })`). */
|
|
15
|
+
const VEHICLE_NAME = "lector";
|
|
16
|
+
|
|
17
|
+
/** Visible rows per page before the auto-rotating overflow hint pages to the next. */
|
|
18
|
+
export const LECTOR_CACHING_WIDGET_VISIBLE_ROWS = 5;
|
|
19
|
+
|
|
20
|
+
export interface CachingWidgetRow {
|
|
21
|
+
workspaceId: string;
|
|
22
|
+
status: "queued" | "running" | "waiting-for-resources";
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export interface CachingWidgetProjection {
|
|
26
|
+
rows: CachingWidgetRow[];
|
|
27
|
+
total: number;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function buildCachingWidgetProjection(jobs: readonly CachingWidgetRow[]): CachingWidgetProjection {
|
|
31
|
+
return { rows: [...jobs], total: jobs.length };
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function cachingRowLine(theme: { fg(color: string, text: string): string }, row: CachingWidgetRow, width: number): string {
|
|
35
|
+
const glyph =
|
|
36
|
+
row.status === "waiting-for-resources"
|
|
37
|
+
? theme.fg("warning", "\u23f8")
|
|
38
|
+
: row.status === "queued"
|
|
39
|
+
? theme.fg("muted", "\u2022")
|
|
40
|
+
: theme.fg("accent", "\u25b6");
|
|
41
|
+
return truncateToWidth(`${glyph} ${row.workspaceId}`, width, "\u2026");
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/** "Lector · Caching · <N>", plus a "page/total ⟳" suffix once genuinely paging. */
|
|
45
|
+
function cachingCardLabel(projection: CachingWidgetProjection, rotation?: AutoRotatingWindow): string {
|
|
46
|
+
const base = vehicleWidgetTitle(VEHICLE_NAME, "Caching", `${projection.total}`);
|
|
47
|
+
return rotation?.isPaging ? `${base} \u00b7 ${rotation.pageIndex + 1}/${rotation.pageCount} \u27f3` : base;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Renders the widget as a single bordered card -- `[]` (hide the whole widget) when nothing is
|
|
51
|
+
* currently caching, matching every other overlay's own "hide when nothing to show" convention. */
|
|
52
|
+
export function renderCachingWidgetLines(
|
|
53
|
+
theme: { fg(color: string, text: string): string },
|
|
54
|
+
projection: CachingWidgetProjection,
|
|
55
|
+
width: number,
|
|
56
|
+
rotation?: AutoRotatingWindow,
|
|
57
|
+
): string[] {
|
|
58
|
+
if (projection.total === 0) return [];
|
|
59
|
+
rotation?.setTotalRows(projection.rows.length);
|
|
60
|
+
const { start, end } = rotation?.currentPageBounds() ?? { start: 0, end: projection.rows.length };
|
|
61
|
+
const visibleRows = projection.rows.slice(start, end);
|
|
62
|
+
|
|
63
|
+
return renderCardRow(
|
|
64
|
+
[
|
|
65
|
+
{
|
|
66
|
+
label: cachingCardLabel(projection, rotation),
|
|
67
|
+
render: (innerWidth: number) => visibleRows.map((row) => cachingRowLine(theme, row, innerWidth)),
|
|
68
|
+
},
|
|
69
|
+
],
|
|
70
|
+
width,
|
|
71
|
+
{ measure, frameStyle: (s) => theme.fg("borderMuted", s) },
|
|
72
|
+
);
|
|
73
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-lector",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.1",
|
|
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",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"@danypops/vehicle-client-pi": "^0.43.0"
|
|
23
23
|
},
|
|
24
24
|
"dependencies": {
|
|
25
|
+
"@danypops/lector": "^0.19.9",
|
|
25
26
|
"@danypops/vehicle-client": "^0.10.3",
|
|
26
27
|
"@danypops/vehicle-core": "^0.17.1",
|
|
27
|
-
"
|
|
28
|
-
"malevich-tui-components": "^0.25.0",
|
|
28
|
+
"malevich-tui-components": "^0.32.1",
|
|
29
29
|
"picomatch": "^4.0.5"
|
|
30
30
|
},
|
|
31
31
|
"devDependencies": {
|