@danypops/pi-papyrus 0.43.1 → 0.43.3

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 +1 -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 +69 -55
  35. package/extension/src/vehicle-artifact-renderers.ts +58 -0
  36. package/extension/src/vehicle-notes-client.ts +35 -7
  37. package/package.json +5 -5
@@ -1,4 +1,5 @@
1
1
  import {
2
+ type Artifact,
2
3
  TOOL_DETAILS_BODY_MAX_CHARACTERS,
3
4
  TOOL_DETAILS_FIELD_MAX_CHARACTERS,
4
5
  TOOL_DETAILS_MAX_EDGES,
@@ -6,7 +7,6 @@ import {
6
7
  TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
7
8
  TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS,
8
9
  TOOL_MODEL_CONTENT_MAX_CHARACTERS,
9
- type Artifact,
10
10
  } from "@danypops/papyrus";
11
11
 
12
12
  export const PAPYRUS_TOOL_DETAILS_SCHEMA = "papyrus.tool-details/v1" as const;
@@ -198,11 +198,7 @@ export function createTransitionDetails(
198
198
  };
199
199
  }
200
200
 
201
- export function createGraphDetails(
202
- operation: string,
203
- artifacts: readonly Artifact[],
204
- edges: readonly ToolGraphEdge[],
205
- ): GraphToolDetails {
201
+ export function createGraphDetails(operation: string, artifacts: readonly Artifact[], edges: readonly ToolGraphEdge[]): GraphToolDetails {
206
202
  const nodes = artifacts.slice(0, TOOL_DETAILS_MAX_ITEMS).map(artifactSummary);
207
203
  const boundedEdges = edges.slice(0, TOOL_DETAILS_MAX_EDGES).map((edge) => ({ ...edge }));
208
204
  return {
@@ -237,11 +233,7 @@ export function createGateRunDetails(
237
233
  };
238
234
  }
239
235
 
240
- export function createInvocationDetails(
241
- operation: string,
242
- runId: string,
243
- created: ToolInvocationCreated,
244
- ): InvocationToolDetails {
236
+ export function createInvocationDetails(operation: string, runId: string, created: ToolInvocationCreated): InvocationToolDetails {
245
237
  const bounded: ToolInvocationCreated = {
246
238
  tasks: created.tasks.slice(0, TOOL_DETAILS_MAX_ITEMS),
247
239
  docs: created.docs.slice(0, TOOL_DETAILS_MAX_ITEMS),
@@ -318,13 +310,15 @@ function isCompleteness(value: unknown): value is ResultCompleteness {
318
310
  }
319
311
 
320
312
  function isArtifactSummary(value: unknown): value is ToolArtifactSummary {
321
- return isRecord(value)
322
- && isBoundedString(value.id)
323
- && isBoundedString(value.kind)
324
- && isBoundedString(value.title)
325
- && isBoundedString(value.status)
326
- && isBoundedString(value.subtype)
327
- && isStringArray(value.labels);
313
+ return (
314
+ isRecord(value) &&
315
+ isBoundedString(value.id) &&
316
+ isBoundedString(value.kind) &&
317
+ isBoundedString(value.title) &&
318
+ isBoundedString(value.status) &&
319
+ isBoundedString(value.subtype) &&
320
+ isStringArray(value.labels)
321
+ );
328
322
  }
329
323
 
330
324
  function isToolArtifact(value: unknown): value is ToolArtifact {
@@ -332,10 +326,12 @@ function isToolArtifact(value: unknown): value is ToolArtifact {
332
326
  const body = value.body;
333
327
  const createdAt = value.createdAt;
334
328
  const updatedAt = value.updatedAt;
335
- return isArtifactSummary(value)
336
- && isBoundedString(body, TOOL_DETAILS_BODY_MAX_CHARACTERS)
337
- && isBoundedString(createdAt)
338
- && isBoundedString(updatedAt);
329
+ return (
330
+ isArtifactSummary(value) &&
331
+ isBoundedString(body, TOOL_DETAILS_BODY_MAX_CHARACTERS) &&
332
+ isBoundedString(createdAt) &&
333
+ isBoundedString(updatedAt)
334
+ );
339
335
  }
340
336
 
341
337
  function isGraphEdge(value: unknown): value is ToolGraphEdge {
@@ -343,11 +339,13 @@ function isGraphEdge(value: unknown): value is ToolGraphEdge {
343
339
  }
344
340
 
345
341
  function isGateRow(value: unknown): value is ToolGateRow {
346
- return isRecord(value)
347
- && typeof value.passed === "boolean"
348
- && isBoundedString(value.type)
349
- && isBoundedString(value.target)
350
- && isBoundedString(value.output, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS);
342
+ return (
343
+ isRecord(value) &&
344
+ typeof value.passed === "boolean" &&
345
+ isBoundedString(value.type) &&
346
+ isBoundedString(value.target) &&
347
+ isBoundedString(value.output, TOOL_DETAILS_ROW_OUTPUT_MAX_CHARACTERS)
348
+ );
351
349
  }
352
350
 
353
351
  function isBoundedArray<T>(value: unknown, maximum: number, predicate: (entry: unknown) => entry is T): value is T[] {
@@ -362,48 +360,64 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
362
360
  } catch {
363
361
  return undefined;
364
362
  }
365
- if (serializedLength > TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS || !isRecord(value)
366
- || value.schemaVersion !== PAPYRUS_TOOL_DETAILS_SCHEMA
367
- || !isBoundedString(value.operation)
368
- || !isBoundedString(value.kind)) return undefined;
363
+ if (
364
+ serializedLength > TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS ||
365
+ !isRecord(value) ||
366
+ value.schemaVersion !== PAPYRUS_TOOL_DETAILS_SCHEMA ||
367
+ !isBoundedString(value.operation) ||
368
+ !isBoundedString(value.kind)
369
+ )
370
+ return undefined;
369
371
 
370
372
  switch (value.kind) {
371
373
  case "artifact":
372
- return isToolArtifact(value.artifact) && isCompleteness(value.completeness)
373
- ? value as unknown as ArtifactToolDetails : undefined;
374
+ return isToolArtifact(value.artifact) && isCompleteness(value.completeness) ? (value as unknown as ArtifactToolDetails) : undefined;
374
375
  case "artifact-list":
375
- return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
376
- && Number.isSafeInteger(value.total) && Number(value.total) >= value.rows.length
377
- && isCompleteness(value.completeness)
378
- ? value as unknown as ArtifactListToolDetails : undefined;
376
+ return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary) &&
377
+ Number.isSafeInteger(value.total) &&
378
+ Number(value.total) >= value.rows.length &&
379
+ isCompleteness(value.completeness)
380
+ ? (value as unknown as ArtifactListToolDetails)
381
+ : undefined;
379
382
  case "transition":
380
383
  return isArtifactSummary(value.artifact) && isBoundedString(value.fromStatus) && isBoundedString(value.toStatus)
381
- ? value as unknown as TransitionToolDetails : undefined;
384
+ ? (value as unknown as TransitionToolDetails)
385
+ : undefined;
382
386
  case "graph":
383
- return isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary)
384
- && isBoundedArray(value.edges, TOOL_DETAILS_MAX_EDGES, isGraphEdge)
385
- && isCompleteness(value.nodeCompleteness) && isCompleteness(value.edgeCompleteness)
386
- ? value as unknown as GraphToolDetails : undefined;
387
+ return isBoundedArray(value.nodes, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary) &&
388
+ isBoundedArray(value.edges, TOOL_DETAILS_MAX_EDGES, isGraphEdge) &&
389
+ isCompleteness(value.nodeCompleteness) &&
390
+ isCompleteness(value.edgeCompleteness)
391
+ ? (value as unknown as GraphToolDetails)
392
+ : undefined;
387
393
  case "gate-run":
388
- return isBoundedString(value.artifactId)
389
- && isBoundedString(value.artifactTitle)
390
- && isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isGateRow)
391
- && isCompleteness(value.completeness)
392
- ? value as unknown as GateRunToolDetails : undefined;
394
+ return isBoundedString(value.artifactId) &&
395
+ isBoundedString(value.artifactTitle) &&
396
+ isBoundedArray(value.gates, TOOL_DETAILS_MAX_ITEMS, isGateRow) &&
397
+ isCompleteness(value.completeness)
398
+ ? (value as unknown as GateRunToolDetails)
399
+ : undefined;
393
400
  case "invocation": {
394
401
  if (!isRecord(value.created)) return undefined;
395
- return isBoundedString(value.runId)
396
- && isStringArray(value.created.tasks) && isStringArray(value.created.docs)
397
- && isStringArray(value.created.rules) && isStringArray(value.created.roots)
398
- && isCompleteness(value.completeness)
399
- ? value as unknown as InvocationToolDetails : undefined;
402
+ return isBoundedString(value.runId) &&
403
+ isStringArray(value.created.tasks) &&
404
+ isStringArray(value.created.docs) &&
405
+ isStringArray(value.created.rules) &&
406
+ isStringArray(value.created.roots) &&
407
+ isCompleteness(value.completeness)
408
+ ? (value as unknown as InvocationToolDetails)
409
+ : undefined;
400
410
  }
401
411
  case "preview":
402
- return isBoundedString(value.title) && isBoundedString(value.content, TOOL_DETAILS_BODY_MAX_CHARACTERS) && isCompleteness(value.completeness)
403
- ? value as unknown as PreviewToolDetails : undefined;
412
+ return isBoundedString(value.title) &&
413
+ isBoundedString(value.content, TOOL_DETAILS_BODY_MAX_CHARACTERS) &&
414
+ isCompleteness(value.completeness)
415
+ ? (value as unknown as PreviewToolDetails)
416
+ : undefined;
404
417
  case "error":
405
418
  return isBoundedString(value.code) && isBoundedString(value.message, TOOL_DETAILS_BODY_MAX_CHARACTERS)
406
- ? value as unknown as ErrorToolDetails : undefined;
419
+ ? (value as unknown as ErrorToolDetails)
420
+ : undefined;
407
421
  default:
408
422
  return undefined;
409
423
  }
@@ -0,0 +1,58 @@
1
+ /**
2
+ * Curated result rendering for Papyrus's own Vehicle-projected operations
3
+ * (notes.*, tasks.*, docs.*, rules.*, playbooks.*, artifact.*): reuses the
4
+ * same ArtifactCard/ArtifactListCard components the pre-Vehicle native tool
5
+ * used, instead of the generic Vehicle renderer's raw full-column table
6
+ * dump -- a human reading a task/note list doesn't need id/subtype/extra/
7
+ * timestamps up front, only title + status, with the rest available on
8
+ * expand. Detection is by output shape, not operation name: every operation
9
+ * registered through this client is one of Papyrus's own artifact domains,
10
+ * so "looks like an Artifact" is a safe, name-independent signal here.
11
+ * Falls back to the generic Vehicle renderer for any other output shape
12
+ * (progress, transitions, gate runs, errors).
13
+ */
14
+
15
+ import type { Artifact } from "@danypops/papyrus";
16
+ import type { VehicleToolRenderers } from "@danypops/vehicle-client-pi";
17
+ import { renderVehicleResult } from "@danypops/vehicle-client-pi/vehicle-render";
18
+ import type { VehicleOperationDescriptor } from "@danypops/vehicle-core";
19
+ import { ArtifactCard } from "./tool-rendering/artifact-card.ts";
20
+ import { ArtifactListCard } from "./tool-rendering/artifact-list.ts";
21
+ import { createArtifactDetails, createArtifactListDetails } from "./tool-rendering/render-model.ts";
22
+
23
+ function isArtifact(value: unknown): value is Artifact {
24
+ if (typeof value !== "object" || value === null) return false;
25
+ const row = value as Record<string, unknown>;
26
+ return (
27
+ typeof row.id === "string" &&
28
+ typeof row.kind === "string" &&
29
+ typeof row.title === "string" &&
30
+ typeof row.status === "string" &&
31
+ typeof row.subtype === "string" &&
32
+ typeof row.body === "string" &&
33
+ Array.isArray(row.labels) &&
34
+ typeof row.created_at === "string" &&
35
+ typeof row.updated_at === "string"
36
+ );
37
+ }
38
+
39
+ function isArtifactArray(value: unknown): value is Artifact[] {
40
+ return Array.isArray(value) && value.every(isArtifact);
41
+ }
42
+
43
+ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
44
+ return {
45
+ renderResult(result, options, theme, context) {
46
+ if (!options.isPartial && !context.isError) {
47
+ const output = (result.details as { output?: unknown } | undefined)?.output;
48
+ if (isArtifactArray(output)) {
49
+ return new ArtifactListCard(createArtifactListDetails(descriptor.name, output), theme, options.expanded);
50
+ }
51
+ if (isArtifact(output)) {
52
+ return new ArtifactCard(createArtifactDetails(descriptor.name, output), theme, options.expanded);
53
+ }
54
+ }
55
+ return renderVehicleResult(descriptor, result, options, theme, context);
56
+ },
57
+ };
58
+ }
@@ -10,18 +10,37 @@
10
10
  * Uses service-client.ts's currentVehicleClientTarget() (test-injectable) rather
11
11
  * than resolveVehicleClientTarget() directly, so a test exercising the full
12
12
  * extension entrypoint doesn't resolve a real daemonStateDir().
13
+ *
14
+ * The client itself is wrapped in createReconnectingVehicleClient(), re-resolving
15
+ * currentVehicleClientTarget() on every reconnect attempt rather than closing over
16
+ * one target captured here at session_start -- confirmed live: the daemon rebinds
17
+ * a new random port on every restart, and a bare RemoteVehicleClient built once had
18
+ * no way to notice its baseUrl had died, breaking every Vehicle tool call for the
19
+ * rest of the Pi session until a full extension reload.
13
20
  */
14
- import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
21
+
22
+ import { createReconnectingVehicleClient } from "@danypops/vehicle-client/daemon-client";
15
23
  import { RemoteVehicleClient } from "@danypops/vehicle-client/http";
16
24
  import { registerVehicleTools } from "@danypops/vehicle-client-pi";
25
+ import type { ExtensionAPI } from "@earendil-works/pi-coding-agent";
17
26
  import { currentVehicleClientTarget } from "./service-client.ts";
18
27
  import { sessionSecretField } from "./session-identity.ts";
19
28
  import { emitTaskFocusEvent } from "./task-focus-events.ts";
29
+ import { papyrusVehicleRenderers } from "./vehicle-artifact-renderers.ts";
20
30
 
21
31
  const REGISTERED_PERMISSIONS = [
22
- "notes:read", "notes:write", "rules:read", "rules:write", "docs:read", "docs:write",
23
- "playbooks:read", "playbooks:write", "tasks:read", "tasks:write",
24
- "artifact:read", "artifact:write",
32
+ "notes:read",
33
+ "notes:write",
34
+ "rules:read",
35
+ "rules:write",
36
+ "docs:read",
37
+ "docs:write",
38
+ "playbooks:read",
39
+ "playbooks:write",
40
+ "tasks:read",
41
+ "tasks:write",
42
+ "artifact:read",
43
+ "artifact:write",
25
44
  ];
26
45
 
27
46
  /** 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. */
@@ -31,10 +50,15 @@ export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
31
50
  const target = currentVehicleClientTarget();
32
51
  if (!target) return;
33
52
  try {
34
- const client = new RemoteVehicleClient({ baseUrl: target.baseUrl, token: target.token });
53
+ const client = createReconnectingVehicleClient(async () => {
54
+ const resolved = currentVehicleClientTarget();
55
+ if (!resolved) throw new Error("Papyrus daemon is not running");
56
+ return new RemoteVehicleClient({ baseUrl: resolved.baseUrl, token: resolved.token });
57
+ });
35
58
  await registerVehicleTools(pi, client, {
36
59
  permissions: REGISTERED_PERMISSIONS,
37
60
  principal: { id: "pi-papyrus" },
61
+ renderers: papyrusVehicleRenderers,
38
62
  // playbooks.invoke's own module handler, and tasks.focus/pause/unpause/clear_focus's
39
63
  // own module handlers, authorize an internal Task Focus write via
40
64
  // sessionIdentity.assertAuthorized(session_id, session_secret) -- see
@@ -52,7 +76,10 @@ export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
52
76
  // explicitly overrides session_id to a DIFFERENT session never gets this
53
77
  // session's secret smuggled in on its behalf.
54
78
  const requestedSessionId = (input as { session_id?: unknown } | undefined)?.session_id;
55
- const sessionId = typeof requestedSessionId === "string" && requestedSessionId.length > 0 ? requestedSessionId : context.sessionManager.getSessionId();
79
+ const sessionId =
80
+ typeof requestedSessionId === "string" && requestedSessionId.length > 0
81
+ ? requestedSessionId
82
+ : context.sessionManager.getSessionId();
56
83
  const { session_secret: sessionSecret } = sessionSecretField(sessionId);
57
84
  // Omit sessionSecret entirely when nothing is cached (unregistered session) --
58
85
  // {sessionSecret: null} would fail the module's own optionalString(input,
@@ -73,7 +100,8 @@ export async function registerNotesVehicle(pi: ExtensionAPI): Promise<void> {
73
100
  }
74
101
  if (descriptor.name === "tasks.pause" || descriptor.name === "tasks.unpause") {
75
102
  const focus = output as { artifact: { id: string } } | undefined;
76
- if (focus?.artifact?.id) emitTaskFocusEvent({ taskId: focus.artifact.id, status: descriptor.name === "tasks.pause" ? "paused" : "unpaused" });
103
+ if (focus?.artifact?.id)
104
+ emitTaskFocusEvent({ taskId: focus.artifact.id, status: descriptor.name === "tasks.pause" ? "paused" : "unpaused" });
77
105
  return;
78
106
  }
79
107
  if (descriptor.name === "tasks.clear_focus") {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.43.1",
3
+ "version": "0.43.3",
4
4
  "description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -19,10 +19,10 @@
19
19
  "dependencies": {
20
20
  "@danypops/jittor": "^0.14.0",
21
21
  "@danypops/papyrus": "^0.42.0",
22
- "@danypops/vehicle-client": "^0.1.1",
23
- "@danypops/vehicle-client-pi": "^0.3.1",
24
- "@danypops/vehicle-core": "^0.2.0",
25
- "@danypops/vehicle-server": "^0.1.1",
22
+ "@danypops/vehicle-client": "^0.2.0",
23
+ "@danypops/vehicle-client-pi": "^0.3.3",
24
+ "@danypops/vehicle-core": "^0.3.0",
25
+ "@danypops/vehicle-server": "^0.4.1",
26
26
  "beautiful-mermaid": "1.1.3",
27
27
  "malevich-tui-components": "^0.16.1"
28
28
  },