@danypops/pi-papyrus 0.59.0 → 0.59.2
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/index.ts
CHANGED
|
@@ -42,7 +42,7 @@ import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss/discus
|
|
|
42
42
|
import { resolveNameFields } from "./domain-tools.ts";
|
|
43
43
|
import { buildNoteWidgetSection, type NoteWidgetRow } from "./note/note-widget.ts";
|
|
44
44
|
import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook/playbook-bridge.ts";
|
|
45
|
-
import { callService, subscribeTaskPushChannel } from "./service-client.ts";
|
|
45
|
+
import { callService, callServicePassive, subscribeTaskPushChannel } from "./service-client.ts";
|
|
46
46
|
import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
|
|
47
47
|
import {
|
|
48
48
|
ActiveTaskContinuation,
|
|
@@ -152,8 +152,10 @@ export function buildTaskWidgetSection(
|
|
|
152
152
|
export class TaskOverlay {
|
|
153
153
|
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
154
154
|
private snapshot: TaskGraph = { nodes: [], rootIds: [] };
|
|
155
|
+
private degraded = false;
|
|
155
156
|
private projectRoot: string | undefined;
|
|
156
157
|
private sessionId: string | undefined;
|
|
158
|
+
private generation = 0;
|
|
157
159
|
private readonly poll = new BoundedPoll();
|
|
158
160
|
private pushChannel: PushChannelClient | undefined;
|
|
159
161
|
private readonly rotation = new AutoRotatingWindow({
|
|
@@ -167,11 +169,13 @@ export class TaskOverlay {
|
|
|
167
169
|
}
|
|
168
170
|
|
|
169
171
|
setProjectRoot(projectRoot: string): void {
|
|
172
|
+
if (projectRoot !== this.projectRoot) this.generation++;
|
|
170
173
|
this.projectRoot = projectRoot;
|
|
171
174
|
}
|
|
172
175
|
// Scopes the widget's "active" glyph to this Pi session's own Focus, so a second
|
|
173
176
|
// concurrent agent's focused task never shows as active in this session's widget.
|
|
174
177
|
setSessionId(sessionId: string): void {
|
|
178
|
+
if (sessionId !== this.sessionId) this.generation++;
|
|
175
179
|
this.sessionId = sessionId;
|
|
176
180
|
}
|
|
177
181
|
|
|
@@ -183,15 +187,22 @@ export class TaskOverlay {
|
|
|
183
187
|
*/
|
|
184
188
|
async refresh(): Promise<void> {
|
|
185
189
|
if (!this.projectRoot) return;
|
|
190
|
+
const generation = this.generation;
|
|
191
|
+
const sessionId = this.sessionId;
|
|
192
|
+
let snapshot = this.snapshot;
|
|
193
|
+
let degraded = false;
|
|
186
194
|
try {
|
|
187
|
-
|
|
195
|
+
snapshot = await callServicePassive<Record<string, unknown>, TaskGraph>("tasks.graph", {
|
|
188
196
|
limit: 500,
|
|
189
197
|
project_root: this.projectRoot,
|
|
190
|
-
session_id:
|
|
198
|
+
session_id: sessionId,
|
|
191
199
|
});
|
|
192
200
|
} catch {
|
|
193
|
-
|
|
201
|
+
degraded = true;
|
|
194
202
|
}
|
|
203
|
+
if (generation !== this.generation) return;
|
|
204
|
+
this.snapshot = snapshot;
|
|
205
|
+
this.degraded = degraded;
|
|
195
206
|
try {
|
|
196
207
|
this.widgetGroup?.requestUpdate();
|
|
197
208
|
} catch {
|
|
@@ -217,12 +228,15 @@ export class TaskOverlay {
|
|
|
217
228
|
/** Theme-free existence check -- PapyrusWidgetGroup's own eager (pre-paint) hide decision needs
|
|
218
229
|
* this without needing a theme, which is only ever available inside the widget's own render(width). */
|
|
219
230
|
hasOpenWork(): boolean {
|
|
220
|
-
return buildTaskWidgetProjection(this.snapshot).openTotal > 0;
|
|
231
|
+
return this.degraded || buildTaskWidgetProjection(this.snapshot).openTotal > 0;
|
|
221
232
|
}
|
|
222
233
|
|
|
223
|
-
/**
|
|
234
|
+
/** Returns current tasks, retaining stale rows or an unavailable status while the service recovers. */
|
|
224
235
|
buildSection(theme: Theme): WidgetSection | undefined {
|
|
225
|
-
|
|
236
|
+
const section = buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot), this.rotation);
|
|
237
|
+
if (section) return this.degraded ? { ...section, label: `${section.label} · stale` } : section;
|
|
238
|
+
if (this.degraded) return { label: "Tasks · unavailable", render: () => ["Papyrus service unavailable; retrying."] };
|
|
239
|
+
return undefined;
|
|
226
240
|
}
|
|
227
241
|
|
|
228
242
|
/**
|
|
@@ -240,6 +254,7 @@ export class TaskOverlay {
|
|
|
240
254
|
}
|
|
241
255
|
|
|
242
256
|
dispose(): void {
|
|
257
|
+
this.generation++;
|
|
243
258
|
this.stopPolling();
|
|
244
259
|
this.pushChannel?.close();
|
|
245
260
|
this.pushChannel = undefined;
|
|
@@ -258,7 +273,9 @@ export class NoteOverlay {
|
|
|
258
273
|
private widgetGroup: PapyrusWidgetGroup | undefined;
|
|
259
274
|
private notes: NoteWidgetRow[] = [];
|
|
260
275
|
private totalOpenCount = 0;
|
|
276
|
+
private degraded = false;
|
|
261
277
|
private projectRoot: string | undefined;
|
|
278
|
+
private generation = 0;
|
|
262
279
|
private readonly poll = new BoundedPoll();
|
|
263
280
|
private readonly rotation = new AutoRotatingWindow({
|
|
264
281
|
totalRows: 0,
|
|
@@ -271,22 +288,31 @@ export class NoteOverlay {
|
|
|
271
288
|
}
|
|
272
289
|
|
|
273
290
|
setProjectRoot(projectRoot: string): void {
|
|
291
|
+
if (projectRoot !== this.projectRoot) this.generation++;
|
|
274
292
|
this.projectRoot = projectRoot;
|
|
275
293
|
}
|
|
276
294
|
|
|
277
295
|
async refresh(): Promise<void> {
|
|
278
296
|
if (!this.projectRoot) return;
|
|
297
|
+
const generation = this.generation;
|
|
298
|
+
const projectRoot = this.projectRoot;
|
|
299
|
+
let notes = this.notes;
|
|
300
|
+
let totalOpenCount = this.totalOpenCount;
|
|
301
|
+
let degraded = false;
|
|
279
302
|
try {
|
|
280
|
-
const rows = await
|
|
281
|
-
project_root:
|
|
303
|
+
const rows = await callServicePassive<Record<string, unknown>, Artifact[]>("notes.list", {
|
|
304
|
+
project_root: projectRoot,
|
|
282
305
|
limit: NOTE_LIST_MAX_LIMIT,
|
|
283
306
|
});
|
|
284
|
-
|
|
285
|
-
|
|
307
|
+
totalOpenCount = rows.length;
|
|
308
|
+
notes = rows.slice(0, NOTE_WIDGET_OPEN_LIMIT).map((row) => ({ id: row.id, title: row.title }));
|
|
286
309
|
} catch {
|
|
287
|
-
|
|
288
|
-
this.notes = [];
|
|
310
|
+
degraded = true;
|
|
289
311
|
}
|
|
312
|
+
if (generation !== this.generation) return;
|
|
313
|
+
this.totalOpenCount = totalOpenCount;
|
|
314
|
+
this.notes = notes;
|
|
315
|
+
this.degraded = degraded;
|
|
290
316
|
try {
|
|
291
317
|
this.widgetGroup?.requestUpdate();
|
|
292
318
|
} catch {
|
|
@@ -296,12 +322,15 @@ export class NoteOverlay {
|
|
|
296
322
|
|
|
297
323
|
/** Theme-free existence check -- see TaskOverlay's own hasOpenWork() for why this needs to stay theme-free. */
|
|
298
324
|
hasOpenNotes(): boolean {
|
|
299
|
-
return this.totalOpenCount > 0;
|
|
325
|
+
return this.degraded || this.totalOpenCount > 0;
|
|
300
326
|
}
|
|
301
327
|
|
|
302
|
-
/**
|
|
328
|
+
/** Returns current notes, retaining stale rows or an unavailable status while the service recovers. */
|
|
303
329
|
buildSection(): WidgetSection | undefined {
|
|
304
|
-
|
|
330
|
+
const section = buildNoteWidgetSection(this.notes, this.totalOpenCount, this.rotation);
|
|
331
|
+
if (section) return this.degraded ? { ...section, label: `${section.label} · stale` } : section;
|
|
332
|
+
if (this.degraded) return { label: "Notes · unavailable", render: () => ["Papyrus service unavailable; retrying."] };
|
|
333
|
+
return undefined;
|
|
305
334
|
}
|
|
306
335
|
|
|
307
336
|
startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
|
|
@@ -315,6 +344,7 @@ export class NoteOverlay {
|
|
|
315
344
|
}
|
|
316
345
|
|
|
317
346
|
dispose(): void {
|
|
347
|
+
this.generation++;
|
|
318
348
|
this.stopPolling();
|
|
319
349
|
this.widgetGroup = undefined;
|
|
320
350
|
this.projectRoot = undefined;
|
|
@@ -441,7 +471,7 @@ export class PapyrusWidgetGroup {
|
|
|
441
471
|
// Entry point
|
|
442
472
|
// ---------------------------------------------------------------------------
|
|
443
473
|
|
|
444
|
-
export default
|
|
474
|
+
export default function (pi: ExtensionAPI) {
|
|
445
475
|
setTaskFocusEventBus(pi);
|
|
446
476
|
registerPlaybookBridge(pi);
|
|
447
477
|
let contextInjectionSequence = 0;
|
|
@@ -492,7 +522,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
492
522
|
session_id: sessionId,
|
|
493
523
|
...sessionSecretField(sessionId),
|
|
494
524
|
});
|
|
495
|
-
emitTaskFocusEvent({
|
|
525
|
+
emitTaskFocusEvent({
|
|
526
|
+
taskId: paused.artifact.id,
|
|
527
|
+
sessionId,
|
|
528
|
+
status: "paused",
|
|
529
|
+
effort: extractDeclaredEffort(paused.artifact.extra),
|
|
530
|
+
});
|
|
496
531
|
if (ctx.hasUI) ctx.ui.notify(`Papyrus task driving paused: ${decision.reason}. Human input resumes it automatically.`, "warning");
|
|
497
532
|
}
|
|
498
533
|
} catch {
|
|
@@ -717,15 +752,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
717
752
|
|
|
718
753
|
// ── Interactive artifact browsers ──────────────────────────────────
|
|
719
754
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
755
|
+
// Command modules are loaded on first use. Registration itself stays synchronous,
|
|
756
|
+
// so interactive-only TUI code cannot delay extension startup.
|
|
757
|
+
const loadTasks = () => import("./task/tasks.ts");
|
|
758
|
+
const loadDocs = () => import("./docs/docs.ts");
|
|
759
|
+
const loadNotes = () => import("./note/notes.ts");
|
|
760
|
+
const loadRules = () => import("./rules/rules.ts");
|
|
761
|
+
let loadedPlaybooks: Awaited<ReturnType<typeof importPlaybooks>> | undefined;
|
|
762
|
+
let playbooksPromise: ReturnType<typeof importPlaybooks> | undefined;
|
|
763
|
+
function importPlaybooks() {
|
|
764
|
+
return import("./playbook/playbooks.ts");
|
|
765
|
+
}
|
|
766
|
+
function loadPlaybooks() {
|
|
767
|
+
playbooksPromise ??= importPlaybooks().then((module) => {
|
|
768
|
+
loadedPlaybooks = module;
|
|
769
|
+
return module;
|
|
770
|
+
});
|
|
771
|
+
return playbooksPromise;
|
|
772
|
+
}
|
|
773
|
+
const loadDiscuss = () => import("./discuss/discuss.ts");
|
|
729
774
|
let overlay: TaskOverlay | undefined;
|
|
730
775
|
let noteOverlay: NoteOverlay | undefined;
|
|
731
776
|
let widgetGroup: PapyrusWidgetGroup | undefined;
|
|
@@ -735,20 +780,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
735
780
|
handler: async (_args, ctx) => {
|
|
736
781
|
overlay?.setProjectRoot(ctx.cwd);
|
|
737
782
|
overlay?.setSessionId(ctx.sessionManager.getSessionId());
|
|
738
|
-
await
|
|
783
|
+
await (await loadTasks()).showTasks(ctx);
|
|
739
784
|
await overlay?.refresh();
|
|
740
785
|
},
|
|
741
786
|
});
|
|
742
787
|
pi.registerCommand("docs", {
|
|
743
788
|
description: "Browse and manage Papyrus documents (interactive)",
|
|
744
789
|
handler: async (_args, ctx) => {
|
|
745
|
-
await
|
|
790
|
+
await (await loadDocs()).showDocs(ctx);
|
|
746
791
|
},
|
|
747
792
|
});
|
|
748
793
|
pi.registerCommand("note", {
|
|
749
794
|
description: "Capture a deferred request directly in Papyrus",
|
|
750
795
|
handler: async (args, ctx) => {
|
|
751
|
-
await
|
|
796
|
+
await (await loadNotes()).captureNote(args, ctx);
|
|
752
797
|
await noteOverlay?.refresh();
|
|
753
798
|
},
|
|
754
799
|
});
|
|
@@ -756,34 +801,38 @@ export default async function (pi: ExtensionAPI) {
|
|
|
756
801
|
description: "Browse and triage the project Notes inbox",
|
|
757
802
|
handler: async (_args, ctx) => {
|
|
758
803
|
noteOverlay?.setProjectRoot(ctx.cwd);
|
|
759
|
-
await
|
|
804
|
+
await (await loadNotes()).showNotes(ctx);
|
|
760
805
|
await noteOverlay?.refresh();
|
|
761
806
|
},
|
|
762
807
|
});
|
|
763
808
|
pi.registerCommand("rules", {
|
|
764
809
|
description: "Browse, preview, and toggle Papyrus rules (interactive)",
|
|
765
810
|
handler: async (_args, ctx) => {
|
|
766
|
-
await
|
|
811
|
+
await (await loadRules()).showRules(ctx);
|
|
767
812
|
},
|
|
768
813
|
});
|
|
769
814
|
pi.registerCommand("playbooks", {
|
|
770
815
|
description: "Browse, edit, and invoke Papyrus playbooks -- trigger/steps guidance an agent reads and follows (interactive)",
|
|
771
816
|
handler: async (_args, ctx) => {
|
|
772
|
-
await
|
|
817
|
+
await (await loadPlaybooks()).showPlaybooks(ctx);
|
|
773
818
|
},
|
|
774
819
|
});
|
|
775
820
|
pi.registerCommand("playbook", {
|
|
776
821
|
description:
|
|
777
822
|
"Open one Papyrus playbook directly by name (tab-completes active playbook titles) and place its invocation in the editor; no argument opens the full /playbooks browser instead",
|
|
778
|
-
getArgumentCompletions: (argumentPrefix) =>
|
|
823
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
824
|
+
if (loadedPlaybooks) return loadedPlaybooks.playbookArgumentCompletions(argumentPrefix);
|
|
825
|
+
void loadPlaybooks();
|
|
826
|
+
return [];
|
|
827
|
+
},
|
|
779
828
|
handler: async (args, ctx) => {
|
|
780
|
-
await
|
|
829
|
+
await (await loadPlaybooks()).openPlaybookByName(args, ctx);
|
|
781
830
|
},
|
|
782
831
|
});
|
|
783
832
|
pi.registerCommand("discuss", {
|
|
784
833
|
description: "Browse Papyrus Discussions and reply, defer, resume, settle, or block/unblock a task (interactive)",
|
|
785
834
|
handler: async (_args, ctx) => {
|
|
786
|
-
await
|
|
835
|
+
await (await loadDiscuss()).showDiscussions(ctx);
|
|
787
836
|
},
|
|
788
837
|
});
|
|
789
838
|
|
|
@@ -797,22 +846,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
797
846
|
|
|
798
847
|
// ── Task widget (TodoOverlay pattern: factory form, requestRender) ──
|
|
799
848
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
})
|
|
812
|
-
|
|
813
|
-
} catch {
|
|
814
|
-
// intentionally silent -- see comment above
|
|
815
|
-
}
|
|
849
|
+
let sessionGeneration = 0;
|
|
850
|
+
pi.on("session_start", (_event, ctx) => {
|
|
851
|
+
const generation = ++sessionGeneration;
|
|
852
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
853
|
+
// Identity registration is best-effort lifecycle bookkeeping. It starts now but
|
|
854
|
+
// cannot hold Pi's first paint behind a daemon connection or retry budget.
|
|
855
|
+
void callServicePassive<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
856
|
+
session_id: sessionId,
|
|
857
|
+
})
|
|
858
|
+
.then(({ secret }) => {
|
|
859
|
+
if (generation === sessionGeneration) cacheSessionSecret(sessionId, secret);
|
|
860
|
+
})
|
|
861
|
+
.catch(() => {});
|
|
816
862
|
if (!ctx.hasUI) return;
|
|
817
863
|
// Attached from session start, not lazily on first ask -- a per-ask listener would only see
|
|
818
864
|
// keystrokes from the moment that tool call happens to begin, missing typing already in
|
|
@@ -824,7 +870,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
824
870
|
overlay ??= new TaskOverlay();
|
|
825
871
|
overlay.setWidgetGroup(widgetGroup);
|
|
826
872
|
overlay.setProjectRoot(ctx.cwd);
|
|
827
|
-
overlay.setSessionId(
|
|
873
|
+
overlay.setSessionId(sessionId);
|
|
828
874
|
|
|
829
875
|
noteOverlay ??= new NoteOverlay();
|
|
830
876
|
noteOverlay.setWidgetGroup(widgetGroup);
|
|
@@ -832,10 +878,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
832
878
|
|
|
833
879
|
widgetGroup.setOverlays(overlay, noteOverlay);
|
|
834
880
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
881
|
+
void Promise.all([overlay.refresh(), noteOverlay.refresh()]).then(() => {
|
|
882
|
+
if (generation !== sessionGeneration) return;
|
|
883
|
+
overlay?.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
|
|
884
|
+
noteOverlay?.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
|
|
885
|
+
});
|
|
839
886
|
});
|
|
840
887
|
|
|
841
888
|
pi.on("session_before_compact", () => {
|
|
@@ -847,20 +894,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
847
894
|
pi.on("session_tree", async () => {
|
|
848
895
|
await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
|
|
849
896
|
});
|
|
850
|
-
pi.on("session_shutdown",
|
|
897
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
898
|
+
sessionGeneration++;
|
|
851
899
|
overlay?.dispose();
|
|
852
900
|
overlay = undefined;
|
|
853
901
|
noteOverlay?.dispose();
|
|
854
902
|
noteOverlay = undefined;
|
|
855
903
|
widgetGroup?.dispose();
|
|
856
904
|
widgetGroup = undefined;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
} catch {
|
|
862
|
-
// intentionally silent -- see session_start's comment above
|
|
863
|
-
}
|
|
905
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
906
|
+
const secret = sessionSecretField(sessionId);
|
|
907
|
+
forgetSessionSecret(sessionId);
|
|
908
|
+
void callServicePassive("session.release", { session_id: sessionId, ...secret }).catch(() => {});
|
|
864
909
|
});
|
|
865
910
|
|
|
866
911
|
// Update widgets after any papyrus tool call
|
|
@@ -21,7 +21,7 @@
|
|
|
21
21
|
import type { Artifact } from "@danypops/papyrus";
|
|
22
22
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
23
23
|
import { buildActivationContext } from "../context/activation-context.ts";
|
|
24
|
-
import { callService } from "../service-client.ts";
|
|
24
|
+
import { callService, callServicePassive } from "../service-client.ts";
|
|
25
25
|
|
|
26
26
|
export const PLAYBOOK_BRIDGE_MAX_PLAYBOOKS = 100;
|
|
27
27
|
|
|
@@ -34,8 +34,14 @@ function slugify(title: string): string {
|
|
|
34
34
|
return slug.length > 0 ? slug : "playbook";
|
|
35
35
|
}
|
|
36
36
|
|
|
37
|
-
|
|
38
|
-
|
|
37
|
+
type ServiceCall = <Input extends Record<string, unknown>, Output>(operation: "playbooks.list", input: Input) => Promise<Output>;
|
|
38
|
+
|
|
39
|
+
async function activePlaybooks(
|
|
40
|
+
projectRoot?: string,
|
|
41
|
+
capabilities: readonly string[] = [],
|
|
42
|
+
serviceCall: ServiceCall = callService,
|
|
43
|
+
): Promise<Artifact[]> {
|
|
44
|
+
return serviceCall<Record<string, unknown>, Artifact[]>("playbooks.list", {
|
|
39
45
|
status: "active",
|
|
40
46
|
full: true,
|
|
41
47
|
limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS,
|
|
@@ -70,8 +76,9 @@ export function playbookInjectionPreview(playbook: Pick<Artifact, "title" | "ext
|
|
|
70
76
|
export async function planPlaybookCommandRegistrations(
|
|
71
77
|
projectRoot?: string,
|
|
72
78
|
capabilities: readonly string[] = [],
|
|
79
|
+
serviceCall: ServiceCall = callService,
|
|
73
80
|
): Promise<Array<{ name: string; id: string; title: string; trigger: string }>> {
|
|
74
|
-
const playbooks = await activePlaybooks(projectRoot, capabilities);
|
|
81
|
+
const playbooks = await activePlaybooks(projectRoot, capabilities, serviceCall);
|
|
75
82
|
const usedNames = new Set<string>();
|
|
76
83
|
return playbooks.map((playbook) => {
|
|
77
84
|
let name = playbookCommandName(playbook.title);
|
|
@@ -82,10 +89,18 @@ export async function planPlaybookCommandRegistrations(
|
|
|
82
89
|
});
|
|
83
90
|
}
|
|
84
91
|
|
|
85
|
-
export
|
|
86
|
-
|
|
92
|
+
export interface PlaybookBridgeHandle {
|
|
93
|
+
/** Explicit test/shutdown boundary for the most recently scheduled discovery refresh. */
|
|
94
|
+
waitForRefresh(): Promise<void>;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
export function registerPlaybookBridge(pi: ExtensionAPI): PlaybookBridgeHandle {
|
|
98
|
+
let generation = 0;
|
|
99
|
+
let latestRefresh = Promise.resolve();
|
|
100
|
+
const refresh = async (projectRoot: string | undefined, scheduledGeneration: number) => {
|
|
87
101
|
try {
|
|
88
|
-
const registrations = await planPlaybookCommandRegistrations(projectRoot, pi.getActiveTools?.() ?? []);
|
|
102
|
+
const registrations = await planPlaybookCommandRegistrations(projectRoot, pi.getActiveTools?.() ?? [], callServicePassive);
|
|
103
|
+
if (scheduledGeneration !== generation) return;
|
|
89
104
|
for (const { name, id, title, trigger } of registrations) {
|
|
90
105
|
pi.registerCommand(name, {
|
|
91
106
|
description: trigger,
|
|
@@ -126,8 +141,13 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
|
|
|
126
141
|
// "no new/updated playbook commands this cycle", not a broken session start.
|
|
127
142
|
}
|
|
128
143
|
};
|
|
129
|
-
pi.on("resources_discover",
|
|
130
|
-
|
|
144
|
+
pi.on("resources_discover", (event) => {
|
|
145
|
+
const scheduledGeneration = ++generation;
|
|
146
|
+
latestRefresh = refresh(event?.cwd, scheduledGeneration);
|
|
131
147
|
return {};
|
|
132
148
|
});
|
|
149
|
+
pi.on("session_shutdown", () => {
|
|
150
|
+
generation++;
|
|
151
|
+
});
|
|
152
|
+
return { waitForRefresh: () => latestRefresh };
|
|
133
153
|
}
|
|
@@ -25,6 +25,13 @@ const client: RetryingClient<PapyrusClient> = createRetryingClient<PapyrusClient
|
|
|
25
25
|
connectRetry: true,
|
|
26
26
|
});
|
|
27
27
|
|
|
28
|
+
// Passive lifecycle work must fail fast when the daemon is absent. The regular
|
|
29
|
+
// client keeps its restart-surviving retry budget for explicit user/tool calls.
|
|
30
|
+
const passiveClient: RetryingClient<PapyrusClient> = createRetryingClient<PapyrusClient>(() => connector(), {
|
|
31
|
+
label: "Papyrus passive",
|
|
32
|
+
connectRetry: false,
|
|
33
|
+
});
|
|
34
|
+
|
|
28
35
|
export async function papyrusClient(): Promise<PapyrusClient> {
|
|
29
36
|
return client.call(async (resolved) => resolved);
|
|
30
37
|
}
|
|
@@ -33,14 +40,24 @@ export async function callService<Input extends Record<string, unknown>, Output>
|
|
|
33
40
|
return client.call((resolved) => resolved.call<Input, Output>(operation, input));
|
|
34
41
|
}
|
|
35
42
|
|
|
43
|
+
/** Fail-fast daemon call for widgets, discovery, and lifecycle bookkeeping. */
|
|
44
|
+
export async function callServicePassive<Input extends Record<string, unknown>, Output>(
|
|
45
|
+
operation: OperationName,
|
|
46
|
+
input: Input,
|
|
47
|
+
): Promise<Output> {
|
|
48
|
+
return passiveClient.call((resolved) => resolved.call<Input, Output>(operation, input));
|
|
49
|
+
}
|
|
50
|
+
|
|
36
51
|
export function setPapyrusClientConnectorForTests(value: ClientConnector): void {
|
|
37
52
|
connector = value;
|
|
38
53
|
client.reset();
|
|
54
|
+
passiveClient.reset();
|
|
39
55
|
}
|
|
40
56
|
|
|
41
57
|
export function resetPapyrusClientForTests(): void {
|
|
42
58
|
connector = () => connectPapyrusClient();
|
|
43
59
|
client.reset();
|
|
60
|
+
passiveClient.reset();
|
|
44
61
|
}
|
|
45
62
|
|
|
46
63
|
let pushChannelTargetResolver: typeof resolvePushChannelTarget = resolvePushChannelTarget;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.59.
|
|
3
|
+
"version": "0.59.2",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"license": "MIT",
|
|
6
6
|
"type": "module",
|
|
@@ -20,7 +20,7 @@
|
|
|
20
20
|
},
|
|
21
21
|
"dependencies": {
|
|
22
22
|
"@danypops/jittor": "^0.19.2",
|
|
23
|
-
"@danypops/papyrus": "^0.60.
|
|
23
|
+
"@danypops/papyrus": "^0.60.10",
|
|
24
24
|
"@danypops/vehicle-client": "^0.10.3",
|
|
25
25
|
"@danypops/vehicle-core": "^0.18.5",
|
|
26
26
|
"@danypops/vehicle-server": "^0.25.2",
|
|
@@ -28,6 +28,7 @@
|
|
|
28
28
|
"malevich-tui-components": "^0.32.1"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
|
+
"@danypops/pi-extension-harness": "^0.8.3",
|
|
31
32
|
"@danypops/pi-tui-harness": "^0.0.2",
|
|
32
33
|
"@danypops/vehicle-client-pi": "^0.46.0",
|
|
33
34
|
"@danypops/vehicle-conformance": "^0.3.0",
|