@danypops/pi-papyrus 0.58.0 → 0.59.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.
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,
|
|
@@ -154,6 +154,7 @@ export class TaskOverlay {
|
|
|
154
154
|
private snapshot: TaskGraph = { nodes: [], rootIds: [] };
|
|
155
155
|
private projectRoot: string | undefined;
|
|
156
156
|
private sessionId: string | undefined;
|
|
157
|
+
private generation = 0;
|
|
157
158
|
private readonly poll = new BoundedPoll();
|
|
158
159
|
private pushChannel: PushChannelClient | undefined;
|
|
159
160
|
private readonly rotation = new AutoRotatingWindow({
|
|
@@ -167,11 +168,13 @@ export class TaskOverlay {
|
|
|
167
168
|
}
|
|
168
169
|
|
|
169
170
|
setProjectRoot(projectRoot: string): void {
|
|
171
|
+
if (projectRoot !== this.projectRoot) this.generation++;
|
|
170
172
|
this.projectRoot = projectRoot;
|
|
171
173
|
}
|
|
172
174
|
// Scopes the widget's "active" glyph to this Pi session's own Focus, so a second
|
|
173
175
|
// concurrent agent's focused task never shows as active in this session's widget.
|
|
174
176
|
setSessionId(sessionId: string): void {
|
|
177
|
+
if (sessionId !== this.sessionId) this.generation++;
|
|
175
178
|
this.sessionId = sessionId;
|
|
176
179
|
}
|
|
177
180
|
|
|
@@ -183,15 +186,20 @@ export class TaskOverlay {
|
|
|
183
186
|
*/
|
|
184
187
|
async refresh(): Promise<void> {
|
|
185
188
|
if (!this.projectRoot) return;
|
|
189
|
+
const generation = this.generation;
|
|
190
|
+
const sessionId = this.sessionId;
|
|
191
|
+
let snapshot: TaskGraph;
|
|
186
192
|
try {
|
|
187
|
-
|
|
193
|
+
snapshot = await callServicePassive<Record<string, unknown>, TaskGraph>("tasks.graph", {
|
|
188
194
|
limit: 500,
|
|
189
195
|
project_root: this.projectRoot,
|
|
190
|
-
session_id:
|
|
196
|
+
session_id: sessionId,
|
|
191
197
|
});
|
|
192
198
|
} catch {
|
|
193
|
-
|
|
199
|
+
snapshot = { nodes: [], rootIds: [] };
|
|
194
200
|
}
|
|
201
|
+
if (generation !== this.generation) return;
|
|
202
|
+
this.snapshot = snapshot;
|
|
195
203
|
try {
|
|
196
204
|
this.widgetGroup?.requestUpdate();
|
|
197
205
|
} catch {
|
|
@@ -240,6 +248,7 @@ export class TaskOverlay {
|
|
|
240
248
|
}
|
|
241
249
|
|
|
242
250
|
dispose(): void {
|
|
251
|
+
this.generation++;
|
|
243
252
|
this.stopPolling();
|
|
244
253
|
this.pushChannel?.close();
|
|
245
254
|
this.pushChannel = undefined;
|
|
@@ -259,6 +268,7 @@ export class NoteOverlay {
|
|
|
259
268
|
private notes: NoteWidgetRow[] = [];
|
|
260
269
|
private totalOpenCount = 0;
|
|
261
270
|
private projectRoot: string | undefined;
|
|
271
|
+
private generation = 0;
|
|
262
272
|
private readonly poll = new BoundedPoll();
|
|
263
273
|
private readonly rotation = new AutoRotatingWindow({
|
|
264
274
|
totalRows: 0,
|
|
@@ -271,22 +281,29 @@ export class NoteOverlay {
|
|
|
271
281
|
}
|
|
272
282
|
|
|
273
283
|
setProjectRoot(projectRoot: string): void {
|
|
284
|
+
if (projectRoot !== this.projectRoot) this.generation++;
|
|
274
285
|
this.projectRoot = projectRoot;
|
|
275
286
|
}
|
|
276
287
|
|
|
277
288
|
async refresh(): Promise<void> {
|
|
278
289
|
if (!this.projectRoot) return;
|
|
290
|
+
const generation = this.generation;
|
|
291
|
+
const projectRoot = this.projectRoot;
|
|
292
|
+
let notes: NoteWidgetRow[] = [];
|
|
293
|
+
let totalOpenCount = 0;
|
|
279
294
|
try {
|
|
280
|
-
const rows = await
|
|
281
|
-
project_root:
|
|
295
|
+
const rows = await callServicePassive<Record<string, unknown>, Artifact[]>("notes.list", {
|
|
296
|
+
project_root: projectRoot,
|
|
282
297
|
limit: NOTE_LIST_MAX_LIMIT,
|
|
283
298
|
});
|
|
284
|
-
|
|
285
|
-
|
|
299
|
+
totalOpenCount = rows.length;
|
|
300
|
+
notes = rows.slice(0, NOTE_WIDGET_OPEN_LIMIT).map((row) => ({ id: row.id, title: row.title }));
|
|
286
301
|
} catch {
|
|
287
|
-
|
|
288
|
-
this.notes = [];
|
|
302
|
+
// A missing daemon is the normal passive-startup case.
|
|
289
303
|
}
|
|
304
|
+
if (generation !== this.generation) return;
|
|
305
|
+
this.totalOpenCount = totalOpenCount;
|
|
306
|
+
this.notes = notes;
|
|
290
307
|
try {
|
|
291
308
|
this.widgetGroup?.requestUpdate();
|
|
292
309
|
} catch {
|
|
@@ -315,6 +332,7 @@ export class NoteOverlay {
|
|
|
315
332
|
}
|
|
316
333
|
|
|
317
334
|
dispose(): void {
|
|
335
|
+
this.generation++;
|
|
318
336
|
this.stopPolling();
|
|
319
337
|
this.widgetGroup = undefined;
|
|
320
338
|
this.projectRoot = undefined;
|
|
@@ -441,7 +459,7 @@ export class PapyrusWidgetGroup {
|
|
|
441
459
|
// Entry point
|
|
442
460
|
// ---------------------------------------------------------------------------
|
|
443
461
|
|
|
444
|
-
export default
|
|
462
|
+
export default function (pi: ExtensionAPI) {
|
|
445
463
|
setTaskFocusEventBus(pi);
|
|
446
464
|
registerPlaybookBridge(pi);
|
|
447
465
|
let contextInjectionSequence = 0;
|
|
@@ -492,7 +510,12 @@ export default async function (pi: ExtensionAPI) {
|
|
|
492
510
|
session_id: sessionId,
|
|
493
511
|
...sessionSecretField(sessionId),
|
|
494
512
|
});
|
|
495
|
-
emitTaskFocusEvent({
|
|
513
|
+
emitTaskFocusEvent({
|
|
514
|
+
taskId: paused.artifact.id,
|
|
515
|
+
sessionId,
|
|
516
|
+
status: "paused",
|
|
517
|
+
effort: extractDeclaredEffort(paused.artifact.extra),
|
|
518
|
+
});
|
|
496
519
|
if (ctx.hasUI) ctx.ui.notify(`Papyrus task driving paused: ${decision.reason}. Human input resumes it automatically.`, "warning");
|
|
497
520
|
}
|
|
498
521
|
} catch {
|
|
@@ -717,15 +740,25 @@ export default async function (pi: ExtensionAPI) {
|
|
|
717
740
|
|
|
718
741
|
// ── Interactive artifact browsers ──────────────────────────────────
|
|
719
742
|
|
|
720
|
-
//
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
726
|
-
|
|
727
|
-
|
|
728
|
-
|
|
743
|
+
// Command modules are loaded on first use. Registration itself stays synchronous,
|
|
744
|
+
// so interactive-only TUI code cannot delay extension startup.
|
|
745
|
+
const loadTasks = () => import("./task/tasks.ts");
|
|
746
|
+
const loadDocs = () => import("./docs/docs.ts");
|
|
747
|
+
const loadNotes = () => import("./note/notes.ts");
|
|
748
|
+
const loadRules = () => import("./rules/rules.ts");
|
|
749
|
+
let loadedPlaybooks: Awaited<ReturnType<typeof importPlaybooks>> | undefined;
|
|
750
|
+
let playbooksPromise: ReturnType<typeof importPlaybooks> | undefined;
|
|
751
|
+
function importPlaybooks() {
|
|
752
|
+
return import("./playbook/playbooks.ts");
|
|
753
|
+
}
|
|
754
|
+
function loadPlaybooks() {
|
|
755
|
+
playbooksPromise ??= importPlaybooks().then((module) => {
|
|
756
|
+
loadedPlaybooks = module;
|
|
757
|
+
return module;
|
|
758
|
+
});
|
|
759
|
+
return playbooksPromise;
|
|
760
|
+
}
|
|
761
|
+
const loadDiscuss = () => import("./discuss/discuss.ts");
|
|
729
762
|
let overlay: TaskOverlay | undefined;
|
|
730
763
|
let noteOverlay: NoteOverlay | undefined;
|
|
731
764
|
let widgetGroup: PapyrusWidgetGroup | undefined;
|
|
@@ -735,20 +768,20 @@ export default async function (pi: ExtensionAPI) {
|
|
|
735
768
|
handler: async (_args, ctx) => {
|
|
736
769
|
overlay?.setProjectRoot(ctx.cwd);
|
|
737
770
|
overlay?.setSessionId(ctx.sessionManager.getSessionId());
|
|
738
|
-
await
|
|
771
|
+
await (await loadTasks()).showTasks(ctx);
|
|
739
772
|
await overlay?.refresh();
|
|
740
773
|
},
|
|
741
774
|
});
|
|
742
775
|
pi.registerCommand("docs", {
|
|
743
776
|
description: "Browse and manage Papyrus documents (interactive)",
|
|
744
777
|
handler: async (_args, ctx) => {
|
|
745
|
-
await
|
|
778
|
+
await (await loadDocs()).showDocs(ctx);
|
|
746
779
|
},
|
|
747
780
|
});
|
|
748
781
|
pi.registerCommand("note", {
|
|
749
782
|
description: "Capture a deferred request directly in Papyrus",
|
|
750
783
|
handler: async (args, ctx) => {
|
|
751
|
-
await
|
|
784
|
+
await (await loadNotes()).captureNote(args, ctx);
|
|
752
785
|
await noteOverlay?.refresh();
|
|
753
786
|
},
|
|
754
787
|
});
|
|
@@ -756,34 +789,38 @@ export default async function (pi: ExtensionAPI) {
|
|
|
756
789
|
description: "Browse and triage the project Notes inbox",
|
|
757
790
|
handler: async (_args, ctx) => {
|
|
758
791
|
noteOverlay?.setProjectRoot(ctx.cwd);
|
|
759
|
-
await
|
|
792
|
+
await (await loadNotes()).showNotes(ctx);
|
|
760
793
|
await noteOverlay?.refresh();
|
|
761
794
|
},
|
|
762
795
|
});
|
|
763
796
|
pi.registerCommand("rules", {
|
|
764
797
|
description: "Browse, preview, and toggle Papyrus rules (interactive)",
|
|
765
798
|
handler: async (_args, ctx) => {
|
|
766
|
-
await
|
|
799
|
+
await (await loadRules()).showRules(ctx);
|
|
767
800
|
},
|
|
768
801
|
});
|
|
769
802
|
pi.registerCommand("playbooks", {
|
|
770
803
|
description: "Browse, edit, and invoke Papyrus playbooks -- trigger/steps guidance an agent reads and follows (interactive)",
|
|
771
804
|
handler: async (_args, ctx) => {
|
|
772
|
-
await
|
|
805
|
+
await (await loadPlaybooks()).showPlaybooks(ctx);
|
|
773
806
|
},
|
|
774
807
|
});
|
|
775
808
|
pi.registerCommand("playbook", {
|
|
776
809
|
description:
|
|
777
810
|
"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) =>
|
|
811
|
+
getArgumentCompletions: (argumentPrefix) => {
|
|
812
|
+
if (loadedPlaybooks) return loadedPlaybooks.playbookArgumentCompletions(argumentPrefix);
|
|
813
|
+
void loadPlaybooks();
|
|
814
|
+
return [];
|
|
815
|
+
},
|
|
779
816
|
handler: async (args, ctx) => {
|
|
780
|
-
await
|
|
817
|
+
await (await loadPlaybooks()).openPlaybookByName(args, ctx);
|
|
781
818
|
},
|
|
782
819
|
});
|
|
783
820
|
pi.registerCommand("discuss", {
|
|
784
821
|
description: "Browse Papyrus Discussions and reply, defer, resume, settle, or block/unblock a task (interactive)",
|
|
785
822
|
handler: async (_args, ctx) => {
|
|
786
|
-
await
|
|
823
|
+
await (await loadDiscuss()).showDiscussions(ctx);
|
|
787
824
|
},
|
|
788
825
|
});
|
|
789
826
|
|
|
@@ -797,22 +834,19 @@ export default async function (pi: ExtensionAPI) {
|
|
|
797
834
|
|
|
798
835
|
// ── Task widget (TodoOverlay pattern: factory form, requestRender) ──
|
|
799
836
|
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
//
|
|
805
|
-
//
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
})
|
|
812
|
-
|
|
813
|
-
} catch {
|
|
814
|
-
// intentionally silent -- see comment above
|
|
815
|
-
}
|
|
837
|
+
let sessionGeneration = 0;
|
|
838
|
+
pi.on("session_start", (_event, ctx) => {
|
|
839
|
+
const generation = ++sessionGeneration;
|
|
840
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
841
|
+
// Identity registration is best-effort lifecycle bookkeeping. It starts now but
|
|
842
|
+
// cannot hold Pi's first paint behind a daemon connection or retry budget.
|
|
843
|
+
void callServicePassive<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
|
|
844
|
+
session_id: sessionId,
|
|
845
|
+
})
|
|
846
|
+
.then(({ secret }) => {
|
|
847
|
+
if (generation === sessionGeneration) cacheSessionSecret(sessionId, secret);
|
|
848
|
+
})
|
|
849
|
+
.catch(() => {});
|
|
816
850
|
if (!ctx.hasUI) return;
|
|
817
851
|
// Attached from session start, not lazily on first ask -- a per-ask listener would only see
|
|
818
852
|
// keystrokes from the moment that tool call happens to begin, missing typing already in
|
|
@@ -824,7 +858,7 @@ export default async function (pi: ExtensionAPI) {
|
|
|
824
858
|
overlay ??= new TaskOverlay();
|
|
825
859
|
overlay.setWidgetGroup(widgetGroup);
|
|
826
860
|
overlay.setProjectRoot(ctx.cwd);
|
|
827
|
-
overlay.setSessionId(
|
|
861
|
+
overlay.setSessionId(sessionId);
|
|
828
862
|
|
|
829
863
|
noteOverlay ??= new NoteOverlay();
|
|
830
864
|
noteOverlay.setWidgetGroup(widgetGroup);
|
|
@@ -832,10 +866,11 @@ export default async function (pi: ExtensionAPI) {
|
|
|
832
866
|
|
|
833
867
|
widgetGroup.setOverlays(overlay, noteOverlay);
|
|
834
868
|
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
869
|
+
void Promise.all([overlay.refresh(), noteOverlay.refresh()]).then(() => {
|
|
870
|
+
if (generation !== sessionGeneration) return;
|
|
871
|
+
overlay?.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
|
|
872
|
+
noteOverlay?.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
|
|
873
|
+
});
|
|
839
874
|
});
|
|
840
875
|
|
|
841
876
|
pi.on("session_before_compact", () => {
|
|
@@ -847,20 +882,18 @@ export default async function (pi: ExtensionAPI) {
|
|
|
847
882
|
pi.on("session_tree", async () => {
|
|
848
883
|
await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
|
|
849
884
|
});
|
|
850
|
-
pi.on("session_shutdown",
|
|
885
|
+
pi.on("session_shutdown", (_event, ctx) => {
|
|
886
|
+
sessionGeneration++;
|
|
851
887
|
overlay?.dispose();
|
|
852
888
|
overlay = undefined;
|
|
853
889
|
noteOverlay?.dispose();
|
|
854
890
|
noteOverlay = undefined;
|
|
855
891
|
widgetGroup?.dispose();
|
|
856
892
|
widgetGroup = undefined;
|
|
857
|
-
|
|
858
|
-
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
} catch {
|
|
862
|
-
// intentionally silent -- see session_start's comment above
|
|
863
|
-
}
|
|
893
|
+
const sessionId = ctx.sessionManager.getSessionId();
|
|
894
|
+
const secret = sessionSecretField(sessionId);
|
|
895
|
+
forgetSessionSecret(sessionId);
|
|
896
|
+
void callServicePassive("session.release", { session_id: sessionId, ...secret }).catch(() => {});
|
|
864
897
|
});
|
|
865
898
|
|
|
866
899
|
// 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;
|
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
* had died.
|
|
24
24
|
*/
|
|
25
25
|
|
|
26
|
+
import { createAgentNotifier } from "@danypops/vehicle-client-pi/agent-poll-ticker";
|
|
26
27
|
import { createReconnectingVehicleClient, daemonInstanceIdentity } from "@danypops/vehicle-client/daemon-client";
|
|
27
28
|
import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
|
|
28
29
|
import {
|
|
@@ -31,6 +32,7 @@ import {
|
|
|
31
32
|
registerVehicleToolsWhenReady,
|
|
32
33
|
type VehicleReadyEvent,
|
|
33
34
|
} from "@danypops/vehicle-client-pi";
|
|
35
|
+
import { VehicleApprovalOutcomePoll } from "@danypops/vehicle-client-pi/vehicle-approval-outcome-poll";
|
|
34
36
|
import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
|
|
35
37
|
import { discussLiveFollowUp } from "../discuss/discuss-live-follow-up.ts";
|
|
36
38
|
import { currentVehicleClientTarget } from "../service-client.ts";
|
|
@@ -61,6 +63,13 @@ export const PAPYRUS_VEHICLE_PERMISSIONS = [
|
|
|
61
63
|
/** Task Focus's own internal write needs a real, per-session secret -- see below. Every other tasks.* operation reads session_id purely for read-scoping and needs no secret. */
|
|
62
64
|
const FOCUS_MUTATION_OPERATIONS = new Set(["tasks.focus", "tasks.pause", "tasks.unpause", "tasks.clear_focus"]);
|
|
63
65
|
|
|
66
|
+
/** Matches pi-tickets' own watch-events poll cadence -- no established Papyrus-specific reason
|
|
67
|
+
* to differ, and this Vehicle doesn't call configureApprovals() on anything today, so nothing yet
|
|
68
|
+
* actually exercises this path; it exists so a future gated operation (e.g. a genuinely
|
|
69
|
+
* destructive artifact mutation) gets outcome-visibility for free rather than needing this same
|
|
70
|
+
* wiring added later. */
|
|
71
|
+
const APPROVAL_OUTCOME_POLL_INTERVAL_MS = 30_000;
|
|
72
|
+
|
|
64
73
|
/**
|
|
65
74
|
* Vehicle Shell's core set (see @danypops/vehicle-client-pi's registerVehicleTools `shell`
|
|
66
75
|
* option): the handful of operations used in nearly every session, active from turn one with no
|
|
@@ -134,6 +143,22 @@ export function registerNotesVehicle(pi: ExtensionAPI): Promise<RegisteredPiVehi
|
|
|
134
143
|
connectRetry: true,
|
|
135
144
|
},
|
|
136
145
|
);
|
|
146
|
+
// Push half of the Approval Gate's outcome-visibility story -- see
|
|
147
|
+
// @danypops/vehicle-client-pi's own vehicle-approval-outcome-poll.ts doc comment (the Papyrus
|
|
148
|
+
// Discussion parallel this mirrors) and pi-tickets' identical wiring in vehicle-client.ts.
|
|
149
|
+
const approvalOutcomePoll = new VehicleApprovalOutcomePoll(client, createAgentNotifier(pi));
|
|
150
|
+
let approvalOutcomePollTimer: ReturnType<typeof setInterval> | undefined;
|
|
151
|
+
pi.on("session_start", (_event, ctx) => {
|
|
152
|
+
if (!ctx.hasUI) return;
|
|
153
|
+
if (approvalOutcomePollTimer) return;
|
|
154
|
+
approvalOutcomePollTimer = setInterval(() => void approvalOutcomePoll.poll(), APPROVAL_OUTCOME_POLL_INTERVAL_MS);
|
|
155
|
+
void approvalOutcomePoll.poll(); // an outcome may already be resolved before this session ever started polling
|
|
156
|
+
});
|
|
157
|
+
pi.on("session_shutdown", () => {
|
|
158
|
+
if (!approvalOutcomePollTimer) return;
|
|
159
|
+
clearInterval(approvalOutcomePollTimer);
|
|
160
|
+
approvalOutcomePollTimer = undefined;
|
|
161
|
+
});
|
|
137
162
|
return registerVehicleToolsWhenReady(pi, () => Promise.resolve(currentVehicleClientTarget() ? client : undefined), {
|
|
138
163
|
// registerVehicleMetricsOperations(..., "papyrus") in @danypops/papyrus's own daemon.ts uses
|
|
139
164
|
// the default "metrics" prefix -- without this, its metrics.query/metrics.recordClientEvent
|
|
@@ -205,6 +230,7 @@ export function registerNotesVehicle(pi: ExtensionAPI): Promise<RegisteredPiVehi
|
|
|
205
230
|
// couldn't batch a live ask alongside other tool calls in the same turn and
|
|
206
231
|
// let those run before the human sees the prompt -- same reasoning here.
|
|
207
232
|
executionMode: (descriptor) => (descriptor.name === "discuss.open" || descriptor.name === "discuss.reply" ? "sequential" : undefined),
|
|
233
|
+
onApprovalPending: (requestId, descriptor) => approvalOutcomePoll.record(requestId, descriptor.name),
|
|
208
234
|
onInvoked: ({ descriptor }, output) => {
|
|
209
235
|
// See vehicle-notes-client.ts's log wiring above -- correlates a real invocation's
|
|
210
236
|
// timestamp against when registration actually completed.
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.59.1",
|
|
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",
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
"typecheck": "tsc --noEmit -p tsconfig.json"
|
|
14
14
|
},
|
|
15
15
|
"peerDependencies": {
|
|
16
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
16
|
+
"@danypops/vehicle-client-pi": "^0.46.0",
|
|
17
17
|
"@earendil-works/pi-coding-agent": "*",
|
|
18
18
|
"@earendil-works/pi-tui": "*",
|
|
19
19
|
"typebox": "*"
|
|
@@ -28,8 +28,9 @@
|
|
|
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
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
33
|
+
"@danypops/vehicle-client-pi": "^0.46.0",
|
|
33
34
|
"@danypops/vehicle-conformance": "^0.3.0",
|
|
34
35
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|
|
35
36
|
"bun-types": "latest",
|