@danypops/pi-papyrus 0.43.1 → 0.43.4

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.
Files changed (37) hide show
  1. package/extension/src/artifact-browser.ts +54 -31
  2. package/extension/src/artifact-detail-view.ts +52 -42
  3. package/extension/src/artifact-format.ts +11 -5
  4. package/extension/src/artifact-relationship-lines.ts +4 -5
  5. package/extension/src/beautiful-mermaid-renderer.ts +3 -5
  6. package/extension/src/context-budget.ts +8 -5
  7. package/extension/src/context-hub-contribution.ts +6 -2
  8. package/extension/src/context-injection-telemetry.ts +2 -2
  9. package/extension/src/discuss-ask-layout.ts +3 -1
  10. package/extension/src/discuss-ask-view.ts +437 -111
  11. package/extension/src/discuss.ts +64 -15
  12. package/extension/src/discussion-detail-view.ts +44 -22
  13. package/extension/src/docs.ts +3 -2
  14. package/extension/src/domain-tools.ts +79 -34
  15. package/extension/src/index.ts +170 -66
  16. package/extension/src/markdown.ts +3 -7
  17. package/extension/src/note-widget.ts +1 -1
  18. package/extension/src/notes.ts +3 -8
  19. package/extension/src/playbook-bridge.ts +17 -6
  20. package/extension/src/playbooks.ts +17 -7
  21. package/extension/src/rules.ts +5 -5
  22. package/extension/src/service-client.ts +23 -8
  23. package/extension/src/skill-catalog-footprint.ts +1 -1
  24. package/extension/src/task-detail-format.ts +9 -9
  25. package/extension/src/task-detail-view.ts +36 -29
  26. package/extension/src/task-focus-events.ts +3 -2
  27. package/extension/src/task-graph.ts +16 -12
  28. package/extension/src/task-presentation.ts +2 -6
  29. package/extension/src/task-widget.ts +12 -8
  30. package/extension/src/tasks.ts +148 -57
  31. package/extension/src/tool-rendering/artifact-card.ts +16 -4
  32. package/extension/src/tool-rendering/artifact-list.ts +23 -24
  33. package/extension/src/tool-rendering/index.ts +2 -6
  34. package/extension/src/tool-rendering/render-model.ts +96 -56
  35. package/extension/src/vehicle-artifact-renderers.ts +102 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. package/package.json +5 -5
@@ -8,40 +8,43 @@
8
8
  * "Are we there yet?" — the agent sees its open work items.
9
9
  */
10
10
  import { randomUUID } from "node:crypto";
11
- import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
12
- import { Type } from "typebox";
13
- import { truncateToWidth } from "@earendil-works/pi-tui";
11
+ import { CONTEXT_DEFAULT_RESERVE_TOKENS, CONTEXT_HUB_CONTRIBUTION_CHANNEL, CONTEXT_HUB_CONTRIBUTION_SCHEMA } from "@danypops/jittor";
14
12
  import {
15
- CONTEXT_ESTIMATE_CHARACTERS_PER_TOKEN,
13
+ type Artifact,
14
+ type GateResult,
16
15
  NOTE_LIST_MAX_LIMIT,
17
16
  NOTE_WIDGET_POLL_INTERVAL_MS,
18
17
  PAPYRUS_CONTEXT_INJECTION_CHANNEL,
19
18
  TASK_DRIVER_MAX_TURNS,
20
19
  TASK_DRIVER_MAX_UNCHANGED_TURNS,
21
20
  TASK_WIDGET_POLL_INTERVAL_MS,
22
- type Artifact,
23
- type GateResult,
24
21
  type TaskGraph,
25
22
  type TaskStatus,
26
23
  } from "@danypops/papyrus";
27
- import { formatMetadata } from "./artifact-format.ts";
28
- import { callService, subscribeTaskPushChannel } from "./service-client.ts";
29
24
  import type { PushChannelClient } from "@danypops/vehicle-client/daemon-client";
30
- import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
31
- import { registerNotesVehicle } from "./vehicle-notes-client.ts";
25
+ import type { ExtensionAPI, ExtensionContext, ExtensionUIContext, Theme } from "@earendil-works/pi-coding-agent";
26
+ import { truncateToWidth } from "@earendil-works/pi-tui";
27
+ import { Type } from "typebox";
28
+ import {
29
+ ActiveTaskContinuation,
30
+ type ActiveTaskMarker,
31
+ automaticPauseReason,
32
+ shouldResumeFocusOnHumanInput,
33
+ } from "./active-task-continuation.ts";
34
+ import { formatMetadata } from "./artifact-format.ts";
32
35
  import { BoundedPoll } from "./bounded-poll.ts";
33
- import { renderNoteWidgetLines } from "./note-widget.ts";
34
- import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
35
- import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
36
- import { ActiveTaskContinuation, automaticPauseReason, shouldResumeFocusOnHumanInput, type ActiveTaskMarker } from "./active-task-continuation.ts";
37
- import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
38
- import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
39
- import { buildContextInjection } from "./context-injection-telemetry.ts";
40
36
  import { buildTaskItemTree, computeContextBudget } from "./context-budget.ts";
41
37
  import { PAPYRUS_CONTEXT_HUB_PRODUCER_NAME, papyrusContextSegment } from "./context-hub-contribution.ts";
42
- import { CONTEXT_DEFAULT_RESERVE_TOKENS, CONTEXT_HUB_CONTRIBUTION_CHANNEL, CONTEXT_HUB_CONTRIBUTION_SCHEMA } from "@danypops/jittor";
43
- import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
38
+ import { buildContextInjection } from "./context-injection-telemetry.ts";
39
+ import { ensureTypingCourtesyTracking, isLiveAskPending } from "./discuss-ask-view.ts";
40
+ import { registerDomainTools, resolveNameFields } from "./domain-tools.ts";
41
+ import { renderNoteWidgetLines } from "./note-widget.ts";
42
+ import { PLAYBOOK_BRIDGE_MAX_PLAYBOOKS, registerPlaybookBridge } from "./playbook-bridge.ts";
43
+ import { callService, subscribeTaskPushChannel } from "./service-client.ts";
44
44
  import { cacheSessionSecret, forgetSessionSecret, sessionSecretField } from "./session-identity.ts";
45
+ import { emitTaskFocusEvent, setTaskFocusEventBus } from "./task-focus-events.ts";
46
+ import { TASK_STATUS_PRESENTATION, taskTreeConnector } from "./task-presentation.ts";
47
+ import { buildTaskWidgetProjection, type TaskWidgetProjection } from "./task-widget.ts";
45
48
  import { renderPapyrusToolCall, renderPapyrusToolResult } from "./tool-rendering/index.ts";
46
49
  import {
47
50
  createArtifactDetails,
@@ -50,6 +53,7 @@ import {
50
53
  createModelContent,
51
54
  createPreviewDetails,
52
55
  } from "./tool-rendering/render-model.ts";
56
+ import { registerNotesVehicle } from "./vehicle-notes-client.ts";
53
57
 
54
58
  function text(value: string, details: unknown = {}) {
55
59
  const modelContent = createModelContent(value);
@@ -63,18 +67,25 @@ function artifactTextLabel(artifact: Artifact): string {
63
67
  function artifactTextLines(artifacts: readonly Artifact[]): string[] {
64
68
  const titleCounts = new Map<string, number>();
65
69
  for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
66
- return artifacts.map((artifact) => titleCounts.get(artifact.title)! > 1
67
- ? `${artifactTextLabel(artifact)} (${artifact.id})`
68
- : artifactTextLabel(artifact));
70
+ return artifacts.map((artifact) =>
71
+ titleCounts.get(artifact.title)! > 1 ? `${artifactTextLabel(artifact)} (${artifact.id})` : artifactTextLabel(artifact),
72
+ );
69
73
  }
70
74
 
71
75
  /** Resolves graph protocol ids into model-facing names; equal titles retain ids only to disambiguate. */
72
76
  async function artifactNamesById(ids: readonly string[]): Promise<Map<string, string>> {
73
77
  const uniqueIds = [...new Set(ids)];
74
- const artifacts = (await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))).filter((artifact): artifact is Artifact => artifact !== null);
78
+ const artifacts = (
79
+ await Promise.all(uniqueIds.map((id) => callService<Record<string, unknown>, Artifact | null>("artifact.show", { id })))
80
+ ).filter((artifact): artifact is Artifact => artifact !== null);
75
81
  const titleCounts = new Map<string, number>();
76
82
  for (const artifact of artifacts) titleCounts.set(artifact.title, (titleCounts.get(artifact.title) ?? 0) + 1);
77
- return new Map(artifacts.map((artifact) => [artifact.id, titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title]));
83
+ return new Map(
84
+ artifacts.map((artifact) => [
85
+ artifact.id,
86
+ titleCounts.get(artifact.title)! > 1 ? `${artifact.title} (${artifact.id})` : artifact.title,
87
+ ]),
88
+ );
78
89
  }
79
90
 
80
91
  // ---------------------------------------------------------------------------
@@ -121,10 +132,14 @@ export class TaskOverlay {
121
132
  }
122
133
  }
123
134
 
124
- setProjectRoot(projectRoot: string): void { this.projectRoot = projectRoot; }
135
+ setProjectRoot(projectRoot: string): void {
136
+ this.projectRoot = projectRoot;
137
+ }
125
138
  // Scopes the widget's "active" glyph to this Pi session's own Focus, so a second
126
139
  // concurrent agent's focused task never shows as active in this session's widget.
127
- setSessionId(sessionId: string): void { this.sessionId = sessionId; }
140
+ setSessionId(sessionId: string): void {
141
+ this.sessionId = sessionId;
142
+ }
128
143
 
129
144
  /**
130
145
  * Never throws: called from several pi.on(...) handlers, some of which (session_compact,
@@ -135,7 +150,11 @@ export class TaskOverlay {
135
150
  async refresh(): Promise<void> {
136
151
  if (!this.projectRoot) return;
137
152
  try {
138
- this.snapshot = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", { limit: 500, project_root: this.projectRoot, session_id: this.sessionId });
153
+ this.snapshot = await callService<Record<string, unknown>, TaskGraph>("tasks.graph", {
154
+ limit: 500,
155
+ project_root: this.projectRoot,
156
+ session_id: this.sessionId,
157
+ });
139
158
  } catch {
140
159
  this.snapshot = { nodes: [], rootIds: [] };
141
160
  }
@@ -156,7 +175,9 @@ export class TaskOverlay {
156
175
  */
157
176
  private ensurePushChannel(): void {
158
177
  if (this.pushChannel && this.pushChannel.state() !== "closed") return;
159
- this.pushChannel = subscribeTaskPushChannel(() => { void this.refresh(); });
178
+ this.pushChannel = subscribeTaskPushChannel(() => {
179
+ void this.refresh();
180
+ });
160
181
  }
161
182
 
162
183
  private render(): void {
@@ -203,7 +224,9 @@ export class TaskOverlay {
203
224
  * a second concurrent Pi session against the same daemon.
204
225
  */
205
226
  startPolling(intervalMs: number = TASK_WIDGET_POLL_INTERVAL_MS): void {
206
- this.poll.start(intervalMs, () => { void this.refresh(); });
227
+ this.poll.start(intervalMs, () => {
228
+ void this.refresh();
229
+ });
207
230
  }
208
231
 
209
232
  stopPolling(): void {
@@ -247,12 +270,17 @@ export class NoteOverlay {
247
270
  }
248
271
  }
249
272
 
250
- setProjectRoot(projectRoot: string): void { this.projectRoot = projectRoot; }
273
+ setProjectRoot(projectRoot: string): void {
274
+ this.projectRoot = projectRoot;
275
+ }
251
276
 
252
277
  async refresh(): Promise<void> {
253
278
  if (!this.projectRoot) return;
254
279
  try {
255
- const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", { project_root: this.projectRoot, limit: NOTE_LIST_MAX_LIMIT });
280
+ const rows = await callService<Record<string, unknown>, Artifact[]>("notes.list", {
281
+ project_root: this.projectRoot,
282
+ limit: NOTE_LIST_MAX_LIMIT,
283
+ });
256
284
  this.openCount = rows.length;
257
285
  } catch {
258
286
  this.openCount = 0;
@@ -298,7 +326,9 @@ export class NoteOverlay {
298
326
  }
299
327
 
300
328
  startPolling(intervalMs: number = NOTE_WIDGET_POLL_INTERVAL_MS): void {
301
- this.poll.start(intervalMs, () => { void this.refresh(); });
329
+ this.poll.start(intervalMs, () => {
330
+ void this.refresh();
331
+ });
302
332
  }
303
333
 
304
334
  stopPolling(): void {
@@ -346,17 +376,23 @@ export default async function (pi: ExtensionAPI) {
346
376
  if (isLiveAskPending()) return;
347
377
  try {
348
378
  const sessionId = ctx.sessionManager.getSessionId();
349
- const active = await callService<Record<string, unknown>, ActiveTaskMarker | null>("tasks.active", { project_root: ctx.cwd, session_id: sessionId });
379
+ const active = await callService<Record<string, unknown>, ActiveTaskMarker | null>("tasks.active", {
380
+ project_root: ctx.cwd,
381
+ session_id: sessionId,
382
+ });
350
383
  const decision = taskContinuation.evaluate(active, {
351
384
  idle: ctx.isIdle(),
352
385
  pendingMessages: ctx.hasPendingMessages(),
353
386
  });
354
387
  if (decision.action === "continue" && decision.prompt) {
355
- pi.sendMessage({
356
- customType: "papyrus-task-continuation",
357
- content: decision.prompt,
358
- display: false,
359
- }, { triggerTurn: true, deliverAs: "nextTurn" });
388
+ pi.sendMessage(
389
+ {
390
+ customType: "papyrus-task-continuation",
391
+ content: decision.prompt,
392
+ display: false,
393
+ },
394
+ { triggerTurn: true, deliverAs: "nextTurn" },
395
+ );
360
396
  } else if (decision.action === "pause") {
361
397
  const paused = await callService<Record<string, unknown>, { artifact: Artifact; status: string }>("tasks.pause", {
362
398
  actor: "system",
@@ -415,8 +451,12 @@ export default async function (pi: ExtensionAPI) {
415
451
  text: Type.Optional(Type.String({ description: "substring across title and body" })),
416
452
  limit: Type.Optional(Type.Number()),
417
453
  }),
418
- renderCall(args, theme) { return renderPapyrusToolCall("Query artifacts", args, theme); },
419
- renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
454
+ renderCall(args, theme) {
455
+ return renderPapyrusToolCall("Query artifacts", args, theme);
456
+ },
457
+ renderResult(result, options, theme, context) {
458
+ return renderPapyrusToolResult(result, options, theme, context);
459
+ },
420
460
  async execute(_id, params, _signal, _onUpdate, _ctx) {
421
461
  try {
422
462
  const rows = await callService<Record<string, unknown>, Artifact[]>("artifact.query", { ...params, limit: params.limit ?? 50 });
@@ -456,8 +496,12 @@ export default async function (pi: ExtensionAPI) {
456
496
  since: Type.Optional(Type.String({ description: "history: RFC3339 lower bound" })),
457
497
  limit: Type.Optional(Type.Number({ description: "history: bounded page size" })),
458
498
  }),
459
- renderCall(args, theme) { return renderPapyrusToolCall("Artifact graph", args, theme); },
460
- renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
499
+ renderCall(args, theme) {
500
+ return renderPapyrusToolCall("Artifact graph", args, theme);
501
+ },
502
+ renderResult(result, options, theme, context) {
503
+ return renderPapyrusToolResult(result, options, theme, context);
504
+ },
461
505
  async execute(_id, rawParams, _signal, _onUpdate, _ctx) {
462
506
  try {
463
507
  const params: Record<string, unknown> = { ...rawParams };
@@ -475,7 +519,11 @@ export default async function (pi: ExtensionAPI) {
475
519
  return text(output, createPreviewDetails("graph.link", "Artifact relationship", output));
476
520
  }
477
521
  if (params.action === "unlink") {
478
- const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", { from: params.from as string, relation: params.relation as string, to: params.to as string });
522
+ const result = await callService<Record<string, unknown>, { removed: boolean }>("graph.unlink", {
523
+ from: params.from as string,
524
+ relation: params.relation as string,
525
+ to: params.to as string,
526
+ });
479
527
  const names = await artifactNamesById([params.from as string, params.to as string]);
480
528
  const relationship = `"${names.get(params.from as string) ?? "unknown artifact"}" --${params.relation}--> "${names.get(params.to as string) ?? "unknown artifact"}"`;
481
529
  const output = result.removed ? `Unlinked ${relationship}` : `No such relationship: ${relationship}`;
@@ -505,12 +553,22 @@ export default async function (pi: ExtensionAPI) {
505
553
  }
506
554
  if (params.action === "history") {
507
555
  const page = await callService<Record<string, unknown>, { events: Array<Record<string, unknown>> }>("graph.history", {
508
- id: params.id, actor: params.actor, session_id: params.session_id, since: params.since, limit: params.limit,
556
+ id: params.id,
557
+ actor: params.actor,
558
+ session_id: params.session_id,
559
+ since: params.since,
560
+ limit: params.limit,
509
561
  });
510
- if (page.events.length === 0) return text("No recorded events.", createPreviewDetails("graph.history", "Mutation event log", "No recorded events."));
511
- const eventIds = page.events.map((event) => event["artifactId"]).filter((id): id is string => typeof id === "string");
562
+ if (page.events.length === 0)
563
+ return text("No recorded events.", createPreviewDetails("graph.history", "Mutation event log", "No recorded events."));
564
+ const eventIds = page.events.map((event) => event.artifactId).filter((id): id is string => typeof id === "string");
512
565
  const names = await artifactNamesById(eventIds);
513
- const output = page.events.map((event) => `${event["occurredAt"]} "${typeof event["artifactId"] === "string" ? names.get(event["artifactId"]) ?? "unknown artifact" : "unknown artifact"}" ${event["type"]} · ${event["actor"]}/${event["source"]}`).join("\n");
566
+ const output = page.events
567
+ .map(
568
+ (event) =>
569
+ `${event.occurredAt} "${typeof event.artifactId === "string" ? (names.get(event.artifactId) ?? "unknown artifact") : "unknown artifact"}" ${event.type} · ${event.actor}/${event.source}`,
570
+ )
571
+ .join("\n");
514
572
  return text(output, createPreviewDetails("graph.history", "Mutation event log", output));
515
573
  }
516
574
  throw new Error(`unknown action: ${params.action}; use link, tree, status, or history`);
@@ -530,8 +588,12 @@ export default async function (pi: ExtensionAPI) {
530
588
  depth: Type.Optional(Type.Number({ description: "edge traversal depth" })),
531
589
  max_nodes: Type.Optional(Type.Number({ description: "maximum traversed nodes" })),
532
590
  }),
533
- renderCall(args, theme) { return renderPapyrusToolCall("Show artifact", args, theme); },
534
- renderResult(result, options, theme, context) { return renderPapyrusToolResult(result, options, theme, context); },
591
+ renderCall(args, theme) {
592
+ return renderPapyrusToolCall("Show artifact", args, theme);
593
+ },
594
+ renderResult(result, options, theme, context) {
595
+ return renderPapyrusToolResult(result, options, theme, context);
596
+ },
535
597
  async execute(_id, params, _signal, _onUpdate, _ctx) {
536
598
  try {
537
599
  const a = await callService<Record<string, unknown>, Artifact | null>("artifact.show", {
@@ -543,7 +605,9 @@ export default async function (pi: ExtensionAPI) {
543
605
  if (!a) throw new Error(`artifact ${params.id} not found`);
544
606
  let out = `${artifactTextLabel(a)}\n\n${a.body}`;
545
607
  if (Object.keys(a.extra).length > 0) {
546
- out += `\n\nMetadata:\n${formatMetadata(a.extra).map((line) => ` ${line}`).join("\n")}`;
608
+ out += `\n\nMetadata:\n${formatMetadata(a.extra)
609
+ .map((line) => ` ${line}`)
610
+ .join("\n")}`;
547
611
  }
548
612
  if (a.edges?.length) {
549
613
  const names = await artifactNamesById(a.edges.flatMap((edge) => [edge.from, edge.to]));
@@ -585,7 +649,9 @@ export default async function (pi: ExtensionAPI) {
585
649
  });
586
650
  pi.registerCommand("docs", {
587
651
  description: "Browse and manage Papyrus documents (interactive)",
588
- handler: async (_args, ctx) => { await docsModule.showDocs(ctx); },
652
+ handler: async (_args, ctx) => {
653
+ await docsModule.showDocs(ctx);
654
+ },
589
655
  });
590
656
  pi.registerCommand("note", {
591
657
  description: "Capture a deferred request directly in Papyrus",
@@ -604,20 +670,29 @@ export default async function (pi: ExtensionAPI) {
604
670
  });
605
671
  pi.registerCommand("rules", {
606
672
  description: "Browse, preview, and toggle Papyrus rules (interactive)",
607
- handler: async (_args, ctx) => { await rulesModule.showRules(ctx); },
673
+ handler: async (_args, ctx) => {
674
+ await rulesModule.showRules(ctx);
675
+ },
608
676
  });
609
677
  pi.registerCommand("playbooks", {
610
678
  description: "Browse, edit, and invoke Papyrus playbooks -- trigger/steps guidance an agent reads and follows (interactive)",
611
- handler: async (_args, ctx) => { await playbooksModule.showPlaybooks(ctx); },
679
+ handler: async (_args, ctx) => {
680
+ await playbooksModule.showPlaybooks(ctx);
681
+ },
612
682
  });
613
683
  pi.registerCommand("playbook", {
614
- description: "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",
684
+ description:
685
+ "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",
615
686
  getArgumentCompletions: (argumentPrefix) => playbooksModule.playbookArgumentCompletions(argumentPrefix),
616
- handler: async (args, ctx) => { await playbooksModule.openPlaybookByName(args, ctx); },
687
+ handler: async (args, ctx) => {
688
+ await playbooksModule.openPlaybookByName(args, ctx);
689
+ },
617
690
  });
618
691
  pi.registerCommand("discuss", {
619
692
  description: "Browse Papyrus Discussions and reply, defer, resume, settle, or block/unblock a task (interactive)",
620
- handler: async (_args, ctx) => { await discussModule.showDiscussions(ctx); },
693
+ handler: async (_args, ctx) => {
694
+ await discussModule.showDiscussions(ctx);
695
+ },
621
696
  });
622
697
 
623
698
  // ── Task widget (TodoOverlay pattern: factory form, requestRender) ──
@@ -643,7 +718,9 @@ export default async function (pi: ExtensionAPI) {
643
718
  // not worth surfacing to the user.
644
719
  try {
645
720
  const sessionId = ctx.sessionManager.getSessionId();
646
- const { secret } = await callService<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", { session_id: sessionId });
721
+ const { secret } = await callService<Record<string, unknown>, { sessionId: string; secret: string }>("session.register", {
722
+ session_id: sessionId,
723
+ });
647
724
  cacheSessionSecret(sessionId, secret);
648
725
  } catch {
649
726
  // intentionally silent -- see comment above
@@ -667,9 +744,15 @@ export default async function (pi: ExtensionAPI) {
667
744
  noteOverlay.startPolling(NOTE_WIDGET_POLL_INTERVAL_MS);
668
745
  });
669
746
 
670
- pi.on("session_before_compact", () => { taskContinuation.onCompaction(); });
671
- pi.on("session_compact", async () => { await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]); });
672
- pi.on("session_tree", async () => { await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]); });
747
+ pi.on("session_before_compact", () => {
748
+ taskContinuation.onCompaction();
749
+ });
750
+ pi.on("session_compact", async () => {
751
+ await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
752
+ });
753
+ pi.on("session_tree", async () => {
754
+ await Promise.all([overlay?.refresh(), noteOverlay?.refresh()]);
755
+ });
673
756
  pi.on("session_shutdown", async (_event, ctx) => {
674
757
  overlay?.dispose();
675
758
  overlay = undefined;
@@ -705,16 +788,27 @@ export default async function (pi: ExtensionAPI) {
705
788
  taskContinuation.onHumanInput();
706
789
  try {
707
790
  const sessionId = ctx.sessionManager.getSessionId();
708
- const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string; pauseReason?: string } | null>("tasks.focused", { session_id: sessionId });
791
+ const focus = await callService<Record<string, unknown>, { artifact: Artifact; status: string; pauseReason?: string } | null>(
792
+ "tasks.focused",
793
+ { session_id: sessionId },
794
+ );
709
795
  if (focus && shouldResumeFocusOnHumanInput(focus.status, focus.pauseReason)) {
710
- await callService("tasks.unpause", { actor: "system", source: "task-continuation", reason: "human input resumed automatic task continuation", session_id: sessionId, ...sessionSecretField(sessionId) });
796
+ await callService("tasks.unpause", {
797
+ actor: "system",
798
+ source: "task-continuation",
799
+ reason: "human input resumed automatic task continuation",
800
+ session_id: sessionId,
801
+ ...sessionSecretField(sessionId),
802
+ });
711
803
  emitTaskFocusEvent({ taskId: focus.artifact.id, sessionId, status: "unpaused" });
712
804
  }
713
805
  } catch {
714
806
  // The daemon may be unavailable during startup, reload, or shutdown.
715
807
  }
716
808
  });
717
- pi.on("agent_start", () => { taskContinuation.onAgentStart(); });
809
+ pi.on("agent_start", () => {
810
+ taskContinuation.onAgentStart();
811
+ });
718
812
  pi.on("agent_settled", async (_event, ctx) => {
719
813
  await driveActiveTasks(ctx);
720
814
  await logSessionContextSnapshot(ctx);
@@ -729,9 +823,19 @@ export default async function (pi: ExtensionAPI) {
729
823
  try {
730
824
  const sessionId = ctx.sessionManager.getSessionId();
731
825
  const [rules, playbooks, summary, taskGraph] = await Promise.all([
732
- callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", { project_root: ctx.cwd, session_id: sessionId }),
733
- callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", { status: "active", limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS }),
734
- callService<Record<string, unknown>, string | null>("tasks.context", { project_root: ctx.cwd, session_id: sessionId, verbosity: "summary" }),
826
+ callService<Record<string, unknown>, Array<Pick<Artifact, "id" | "title" | "body" | "extra">>>("rules.injectable", {
827
+ project_root: ctx.cwd,
828
+ session_id: sessionId,
829
+ }),
830
+ callService<Record<string, unknown>, Array<Pick<Artifact, "title" | "extra">>>("playbooks.list", {
831
+ status: "active",
832
+ limit: PLAYBOOK_BRIDGE_MAX_PLAYBOOKS,
833
+ }),
834
+ callService<Record<string, unknown>, string | null>("tasks.context", {
835
+ project_root: ctx.cwd,
836
+ session_id: sessionId,
837
+ verbosity: "summary",
838
+ }),
735
839
  callService<Record<string, unknown>, TaskGraph>("tasks.graph", { project_root: ctx.cwd, session_id: sessionId }),
736
840
  ]);
737
841
  const injection = buildContextInjection({
@@ -49,12 +49,8 @@ export function renderMarkdownBody(
49
49
  activeTheme: ActiveTheme,
50
50
  activeMarkdownTheme: ActiveMarkdownTheme = activePiMarkdownTheme,
51
51
  ): string[] {
52
- const markdown = new Markdown(
53
- body || "(no body)",
54
- 0,
55
- 0,
56
- createPapyrusMarkdownTheme(activeTheme, activeMarkdownTheme),
57
- { color: (text) => activeTheme().fg("text", text) },
58
- );
52
+ const markdown = new Markdown(body || "(no body)", 0, 0, createPapyrusMarkdownTheme(activeTheme, activeMarkdownTheme), {
53
+ color: (text) => activeTheme().fg("text", text),
54
+ });
59
55
  return markdown.render(Math.max(1, width));
60
56
  }
@@ -1,5 +1,5 @@
1
- import { truncateToWidth } from "@earendil-works/pi-tui";
2
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
+ import { truncateToWidth } from "@earendil-works/pi-tui";
3
3
 
4
4
  /** Hidden at 0, matching TaskOverlay's own "nothing open" hiding rule. */
5
5
  export function renderNoteWidgetLines(theme: Theme, openCount: number, width: number): string[] {
@@ -1,11 +1,11 @@
1
+ import { type Artifact, NOTE_DISPOSITIONS, NOTE_LIST_MAX_LIMIT } from "@danypops/papyrus";
1
2
  import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
- import { NOTE_DISPOSITIONS, NOTE_LIST_MAX_LIMIT, type Artifact } from "@danypops/papyrus";
3
3
  import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
4
4
  import { NOTE_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
5
5
  import { callService } from "./service-client.ts";
6
6
 
7
7
  export function noteRowMeta(note: Artifact): string {
8
- const history = Array.isArray(note.extra["noteHistory"]) ? note.extra["noteHistory"].length : 0;
8
+ const history = Array.isArray(note.extra.noteHistory) ? note.extra.noteHistory.length : 0;
9
9
  return `${history} event${history === 1 ? "" : "s"}`;
10
10
  }
11
11
 
@@ -52,12 +52,7 @@ export async function showNotes(ctx: ExtensionCommandContext): Promise<void> {
52
52
  statusOrder: ["draft", "active", "archived"],
53
53
  presentation: NOTE_STATUS_PRESENTATION,
54
54
  rowMeta: noteRowMeta,
55
- actions: (note) => [
56
- "Show details",
57
- ...(note.status === "draft" ? ["Consume"] : []),
58
- "Promote",
59
- "Archive",
60
- ],
55
+ actions: (note) => ["Show details", ...(note.status === "draft" ? ["Consume"] : []), "Promote", "Archive"],
61
56
  handleAction: async (choice, note, commandCtx) => {
62
57
  if (choice === "Show details") {
63
58
  await showArtifactDetails(commandCtx, note.id, "notes.show", { project_root: commandCtx.cwd });
@@ -17,14 +17,19 @@
17
17
  * time rather than baking in stale content, so even a lingering stale name fails cleanly with a
18
18
  * real error instead of running deleted content.
19
19
  */
20
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
20
+
21
21
  import type { Artifact } from "@danypops/papyrus";
22
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
22
23
  import { callService } from "./service-client.ts";
23
24
 
24
25
  export const PLAYBOOK_BRIDGE_MAX_PLAYBOOKS = 100;
25
26
 
26
27
  function slugify(title: string): string {
27
- const slug = title.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "").slice(0, 64);
28
+ const slug = title
29
+ .toLowerCase()
30
+ .replace(/[^a-z0-9]+/g, "-")
31
+ .replace(/^-+|-+$/g, "")
32
+ .slice(0, 64);
28
33
  return slug.length > 0 ? slug : "playbook";
29
34
  }
30
35
 
@@ -44,7 +49,7 @@ export function playbookCommandName(title: string): string {
44
49
  * already work this way).
45
50
  */
46
51
  export function playbookInjectionPreview(playbook: Pick<Artifact, "title" | "extra">): string {
47
- const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
52
+ const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
48
53
  return `• ${playbook.title} (when: ${trigger})`;
49
54
  }
50
55
 
@@ -56,7 +61,7 @@ export async function planPlaybookCommandRegistrations(): Promise<Array<{ name:
56
61
  let name = playbookCommandName(playbook.title);
57
62
  if (usedNames.has(name)) name = `${name}-${playbook.id.slice(0, 8)}`; // a real title collision, not the common case
58
63
  usedNames.add(name);
59
- const trigger = typeof playbook.extra["trigger"] === "string" ? playbook.extra["trigger"] : "manual invocation";
64
+ const trigger = typeof playbook.extra.trigger === "string" ? playbook.extra.trigger : "manual invocation";
60
65
  return { name, id: playbook.id, title: playbook.title, trigger };
61
66
  });
62
67
  }
@@ -77,7 +82,10 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
77
82
  // returns rendered text to drop into the editor (that's playbooks.preview
78
83
  // now). The editor gets a short kickoff prompt instead; the actual step
79
84
  // content surfaces via the normal Task Focus system-prompt pointer.
80
- const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; missingArguments?: string[] }>("playbooks.invoke", { id });
85
+ const invocation = await callService<Record<string, unknown>, { entryTaskId?: string; missingArguments?: string[] }>(
86
+ "playbooks.invoke",
87
+ { id },
88
+ );
81
89
  if (invocation.missingArguments) {
82
90
  ctx.ui.notify(`"${title}" needs: ${invocation.missingArguments.join(", ")}`, "error");
83
91
  return;
@@ -95,5 +103,8 @@ export function registerPlaybookBridge(pi: ExtensionAPI): void {
95
103
  // "no new/updated playbook commands this cycle", not a broken session start.
96
104
  }
97
105
  };
98
- pi.on("resources_discover", async () => { await refresh(); return {}; });
106
+ pi.on("resources_discover", async () => {
107
+ await refresh();
108
+ return {};
109
+ });
99
110
  }
@@ -1,6 +1,6 @@
1
- import type { AutocompleteItem } from "@earendil-works/pi-tui";
2
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
3
1
  import type { Artifact } from "@danypops/papyrus";
2
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
3
+ import type { AutocompleteItem } from "@earendil-works/pi-tui";
4
4
  import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
5
5
  import { PLAYBOOK_STATUS_PRESENTATION } from "./artifact-status-presentation.ts";
6
6
  import { matchArtifactByName } from "./domain-tools.ts";
@@ -9,7 +9,10 @@ import { callService } from "./service-client.ts";
9
9
  const PLAYBOOK_COMPLETION_MAX_CANDIDATES = 100;
10
10
 
11
11
  async function activePlaybooks(): Promise<Artifact[]> {
12
- return callService<Record<string, unknown>, Artifact[]>("playbooks.list", { status: "active", limit: PLAYBOOK_COMPLETION_MAX_CANDIDATES });
12
+ return callService<Record<string, unknown>, Artifact[]>("playbooks.list", {
13
+ status: "active",
14
+ limit: PLAYBOOK_COMPLETION_MAX_CANDIDATES,
15
+ });
13
16
  }
14
17
 
15
18
  /** `/playbook <tab>` completions -- title-prefix match, since that's what a human actually types, not a full-text search of body content. */
@@ -20,7 +23,11 @@ export async function playbookArgumentCompletions(argumentPrefix: string): Promi
20
23
  return rows
21
24
  .filter((row) => row.title.toLowerCase().startsWith(needle))
22
25
  .sort((a, b) => a.title.localeCompare(b.title))
23
- .map((row) => ({ value: row.title, label: row.title, description: typeof row.extra["trigger"] === "string" ? row.extra["trigger"] : undefined }));
26
+ .map((row) => ({
27
+ value: row.title,
28
+ label: row.title,
29
+ description: typeof row.extra.trigger === "string" ? row.extra.trigger : undefined,
30
+ }));
24
31
  } catch {
25
32
  return null; // a Papyrus daemon hiccup degrades to "no suggestions", never breaks the command line
26
33
  }
@@ -44,7 +51,10 @@ async function invokeAndReport(id: string, label: string, ctx: ExtensionCommandC
44
51
 
45
52
  /** `/playbook <name>` (no args opens the full browser instead): resolves by exact title, then invokes it directly -- one step, not browse-then-select-then-invoke. */
46
53
  export async function openPlaybookByName(name: string, ctx: ExtensionCommandContext): Promise<void> {
47
- if (!name.trim()) { await showPlaybooks(ctx); return; }
54
+ if (!name.trim()) {
55
+ await showPlaybooks(ctx);
56
+ return;
57
+ }
48
58
  try {
49
59
  const id = matchArtifactByName(await activePlaybooks(), name);
50
60
  await invokeAndReport(id, name.trim(), ctx);
@@ -60,8 +70,8 @@ function strings(value: unknown): string[] {
60
70
  }
61
71
 
62
72
  export function playbookRowMeta(playbook: Artifact): string {
63
- const trigger = typeof playbook.extra["trigger"] === "string" ? `when ${playbook.extra["trigger"]}` : "manual invocation";
64
- const tools = strings(playbook.extra["tools"]);
73
+ const trigger = typeof playbook.extra.trigger === "string" ? `when ${playbook.extra.trigger}` : "manual invocation";
74
+ const tools = strings(playbook.extra.tools);
65
75
  return [trigger, tools.join(", ")].filter(Boolean).join(" \u00b7 ");
66
76
  }
67
77
 
@@ -1,19 +1,19 @@
1
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
2
1
  import type { Artifact } from "@danypops/papyrus";
2
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
3
3
  import { showArtifactBrowser, showArtifactDetails } from "./artifact-browser.ts";
4
4
  import { RULE_STATUS_PRESENTATION, severityColor } from "./artifact-status-presentation.ts";
5
5
  import { callService } from "./service-client.ts";
6
6
 
7
7
  export function ruleRowMeta(rule: Artifact, theme: Theme): string {
8
- const severity = typeof rule.extra["severity"] === "string" ? rule.extra["severity"] : "info";
8
+ const severity = typeof rule.extra.severity === "string" ? rule.extra.severity : "info";
9
9
  const severityText = theme.fg(severityColor(severity), severity.toUpperCase());
10
- const condition = typeof rule.extra["condition"] === "string" ? `when ${rule.extra["condition"]}` : "always";
10
+ const condition = typeof rule.extra.condition === "string" ? `when ${rule.extra.condition}` : "always";
11
11
  return `${severityText} · ${condition}`;
12
12
  }
13
13
 
14
14
  export function ruleInjectionPreview(rule: Pick<Artifact, "title" | "body" | "extra">): string {
15
- const condition = typeof rule.extra["condition"] === "string" ? ` (when: ${rule.extra["condition"]})` : "";
16
- const action = rule.body || (typeof rule.extra["action"] === "string" ? rule.extra["action"] : "");
15
+ const condition = typeof rule.extra.condition === "string" ? ` (when: ${rule.extra.condition})` : "";
16
+ const action = rule.body || (typeof rule.extra.action === "string" ? rule.extra.action : "");
17
17
  return `• ${rule.title}${condition}\n ${action}`;
18
18
  }
19
19