@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.
@@ -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
- this.snapshot = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", {
195
+ snapshot = await callServicePassive<Record<string, unknown>, TaskGraph>("tasks.graph", {
188
196
  limit: 500,
189
197
  project_root: this.projectRoot,
190
- session_id: this.sessionId,
198
+ session_id: sessionId,
191
199
  });
192
200
  } catch {
193
- this.snapshot = { nodes: [], rootIds: [] };
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
- /** undefined when there is no open work to show -- PapyrusWidgetGroup's own signal to omit this section entirely. */
234
+ /** Returns current tasks, retaining stale rows or an unavailable status while the service recovers. */
224
235
  buildSection(theme: Theme): WidgetSection | undefined {
225
- return buildTaskWidgetSection(theme, buildTaskWidgetProjection(this.snapshot), this.rotation);
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 callService<Record<string, unknown>, Artifact[]>("notes.list", {
281
- project_root: this.projectRoot,
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
- this.totalOpenCount = rows.length;
285
- this.notes = rows.slice(0, NOTE_WIDGET_OPEN_LIMIT).map((row) => ({ id: row.id, title: row.title }));
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
- this.totalOpenCount = 0;
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
- /** undefined when there are no open notes -- PapyrusWidgetGroup's own signal to omit this section entirely. */
328
+ /** Returns current notes, retaining stale rows or an unavailable status while the service recovers. */
303
329
  buildSection(): WidgetSection | undefined {
304
- return buildNoteWidgetSection(this.notes, this.totalOpenCount, this.rotation);
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 async function (pi: ExtensionAPI) {
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({ taskId: paused.artifact.id, sessionId, status: "paused", effort: extractDeclaredEffort(paused.artifact.extra) });
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
- // Lazy imports keep TUI components out of non-interactive startup paths.
721
- const [tasksModule, docsModule, notesModule, rulesModule, playbooksModule, discussModule] = await Promise.all([
722
- import("./task/tasks.ts"),
723
- import("./docs/docs.ts"),
724
- import("./note/notes.ts"),
725
- import("./rules/rules.ts"),
726
- import("./playbook/playbooks.ts"),
727
- import("./discuss/discuss.ts"),
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 tasksModule.showTasks(ctx);
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 docsModule.showDocs(ctx);
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 notesModule.captureNote(args, ctx);
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 notesModule.showNotes(ctx);
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 rulesModule.showRules(ctx);
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 playbooksModule.showPlaybooks(ctx);
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) => playbooksModule.playbookArgumentCompletions(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 playbooksModule.openPlaybookByName(args, ctx);
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 discussModule.showDiscussions(ctx);
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
- pi.on("session_start", async (_event, ctx) => {
801
- // Registers this session's identity with the daemon as early as possible -- before any
802
- // Focus-mutating call could plausibly happen -- shrinking (not eliminating; see
803
- // domain/session-identity.ts) the first-touch race window. Best-effort: the daemon may be
804
- // unavailable during startup, and every other Focus-mutating call already tolerates an
805
- // unregistered/never-armored session_id (opt-in armor), so a missed registration here is
806
- // not worth surfacing to the user.
807
- try {
808
- const sessionId = ctx.sessionManager.getSessionId();
809
- const { secret } = await callService<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
810
- session_id: sessionId,
811
- });
812
- cacheSessionSecret(sessionId, secret);
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(ctx.sessionManager.getSessionId());
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
- await overlay.refresh();
836
- overlay.startPolling(TASK_WIDGET_POLL_INTERVAL_MS);
837
- await noteOverlay.refresh();
838
- noteOverlay.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
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", async (_event, ctx) => {
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
- try {
858
- const sessionId = ctx.sessionManager.getSessionId();
859
- await callService("session.release", { session_id: sessionId, ...sessionSecretField(sessionId) });
860
- forgetSessionSecret(sessionId);
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
- async function activePlaybooks(projectRoot?: string, capabilities: readonly string[] = []): Promise<Artifact[]> {
38
- return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
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 function registerPlaybookBridge(pi: ExtensionAPI): void {
86
- const refresh = async (projectRoot?: string) => {
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", async (event) => {
130
- await refresh(event?.cwd);
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.0",
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.7",
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",