@danypops/papyrus 0.34.2 → 0.34.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.
@@ -10,19 +10,26 @@ export interface ArtifactDetailContent {
10
10
  relationships: string[];
11
11
  }
12
12
 
13
- export function artifactDetailContent(artifact: Artifact): ArtifactDetailContent {
13
+ export function artifactDetailContent(artifact: Artifact, relationshipLines: string[] = []): ArtifactDetailContent {
14
14
  return {
15
15
  title: artifact.title,
16
16
  identity: `${artifact.id} [${artifact.kind}|${artifact.status}]${artifact.subtype ? ` · ${artifact.subtype}` : ""}`,
17
17
  body: artifact.body || "(no body)",
18
18
  labels: [...artifact.labels],
19
19
  metadata: Object.keys(artifact.extra).length > 0 ? formatMetadata(artifact.extra) : [],
20
- relationships: (artifact.edges ?? []).map((edge) => `${edge.from} --${edge.relation}--> ${edge.to}`),
20
+ relationships: relationshipLines,
21
21
  };
22
22
  }
23
23
 
24
- export function artifactDetailsText(artifact: Artifact): string {
25
- const content = artifactDetailContent(artifact);
24
+ /**
25
+ * Deliberately plain, unlike ArtifactDetailViewport's TUI body (renderMarkdownBody): this path
26
+ * feeds ctx.ui.notify(), used outside interactive mode (RPC, piped/non-terminal callers) where
27
+ * ANSI escape codes are noise or corruption, not formatting. Raw Markdown source stays readable
28
+ * as plain text either way. relationshipLines is still upgraded (a real graph or a resolved
29
+ * arrow list, never raw ids) since that needs no color to read.
30
+ */
31
+ export function artifactDetailsText(artifact: Artifact, relationshipLines: string[] = []): string {
32
+ const content = artifactDetailContent(artifact, relationshipLines);
26
33
  let output = `${content.title}\n${content.identity}\n\n${content.body}`;
27
34
  if (content.labels.length > 0) output += `\n\nLabels: ${content.labels.join(", ")}`;
28
35
  if (content.metadata.length > 0) output += `\n\nMetadata:\n${content.metadata.map((line) => ` ${line}`).join("\n")}`;
@@ -7,7 +7,10 @@ import {
7
7
  ARTIFACT_DETAIL_RESERVED_ROWS,
8
8
  } from "../../src/constants.ts";
9
9
  import type { Artifact } from "../../src/domain/artifact.ts";
10
+ import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
10
11
  import { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
12
+ import { buildArtifactRelationshipLines } from "./artifact-relationship-lines.ts";
13
+ import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
11
14
  import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
12
15
 
13
16
  interface ArtifactDetailLine {
@@ -27,13 +30,14 @@ class ArtifactDetailViewport {
27
30
  private readonly tui: TUI,
28
31
  private readonly activeTheme: ActiveTheme,
29
32
  artifact: Artifact,
33
+ relationshipLines: string[],
30
34
  private readonly close: () => void,
31
35
  ) {
32
36
  this.visibleLines = Math.max(
33
37
  ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
34
38
  Math.min(ARTIFACT_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - ARTIFACT_DETAIL_RESERVED_ROWS),
35
39
  );
36
- this.content = artifactDetailContent(artifact);
40
+ this.content = artifactDetailContent(artifact, relationshipLines);
37
41
  }
38
42
 
39
43
  invalidate(): void { this.renderedWidth = 0; }
@@ -104,9 +108,14 @@ class ArtifactDetailViewport {
104
108
  }
105
109
  }
106
110
 
107
- export async function showArtifactDetailView(ctx: ExtensionCommandContext, artifact: Artifact): Promise<void> {
108
- const output = artifactDetailsText(artifact);
111
+ export async function showArtifactDetailView(
112
+ ctx: ExtensionCommandContext,
113
+ artifact: Artifact,
114
+ renderer: GraphRenderer = new BeautifulMermaidRenderer(),
115
+ ): Promise<void> {
116
+ const relationshipLines = buildArtifactRelationshipLines(artifact, renderer);
117
+ const output = artifactDetailsText(artifact, relationshipLines);
109
118
  if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
110
119
  await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
111
- new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, done));
120
+ new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, relationshipLines, done));
112
121
  }
@@ -0,0 +1,24 @@
1
+ import { GRAPH_RENDER_MAX_ROUTED_EDGES, GRAPH_RENDER_MAX_ROUTED_NODES } from "../../src/constants.ts";
2
+ import { projectArtifactRelationships } from "../../src/artifact-relationship-view.ts";
3
+ import type { Artifact } from "../../src/domain/artifact.ts";
4
+ import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
5
+
6
+ /**
7
+ * Renders an artifact's direct relationships as a small graph when the neighbor set is real
8
+ * (more than one node) and within the same bound Task graph rendering already enforces;
9
+ * otherwise falls back to a plain, still name-resolved arrow list -- never the renderer's own
10
+ * separate box-style fallback, so this view has exactly two shapes, not three.
11
+ */
12
+ export function buildArtifactRelationshipLines(artifact: Artifact, renderer: GraphRenderer): string[] {
13
+ const graph = projectArtifactRelationships(artifact);
14
+ if (graph.edges.length === 0) return [];
15
+ const withinBounds = graph.nodes.length > 1
16
+ && graph.nodes.length <= GRAPH_RENDER_MAX_ROUTED_NODES
17
+ && graph.edges.length <= GRAPH_RENDER_MAX_ROUTED_EDGES;
18
+ if (withinBounds) {
19
+ const rendered = renderer.render(graph);
20
+ if (rendered.lines.length > 0) return rendered.lines;
21
+ }
22
+ const labelById = new Map(graph.nodes.map((node) => [node.id, node.label]));
23
+ return graph.edges.map((edge) => `${labelById.get(edge.from) ?? edge.from} --${edge.label}--> ${labelById.get(edge.to) ?? edge.to}`);
24
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.34.2",
3
+ "version": "0.34.3",
4
4
  "description": "Daemon-backed graph artifacts, evidence-bearing tasks, rules, skills, and native TUI workflows for Pi",
5
5
  "type": "module",
6
6
  "keywords": ["pi-package"],
@@ -0,0 +1,23 @@
1
+ import type { Artifact } from "./domain/artifact.ts";
2
+ import type { DisplayGraph, DisplayGraphEdge, DisplayGraphNode } from "./domain/display-graph.ts";
3
+ import { fallbackLabel } from "./task-relationship-view.ts";
4
+
5
+ /**
6
+ * The generic-artifact counterpart to projectTaskRelationships: unlike a Task, which already
7
+ * has its containing TaskGraph's real titles in memory, a generic Doc/Rule/Skill/Playbook only
8
+ * has raw edge id pairs (artifact.show's tree fetch never resolves neighbor titles -- see
9
+ * ops.ts getArtifact). Reuses the same fallbackLabel heuristic rather than adding a network
10
+ * round-trip per neighbor, which would turn a rendering concern into a new daemon-adjacent one.
11
+ */
12
+ export function projectArtifactRelationships(artifact: Artifact): DisplayGraph {
13
+ const edges: DisplayGraphEdge[] = (artifact.edges ?? []).map((edge) => ({ from: edge.from, to: edge.to, label: edge.relation }));
14
+ const nodeIds = new Set<string>();
15
+ for (const edge of edges) {
16
+ nodeIds.add(edge.from);
17
+ nodeIds.add(edge.to);
18
+ }
19
+ const nodes: DisplayGraphNode[] = [...nodeIds].map((id) => id === artifact.id
20
+ ? { id, label: artifact.title, status: artifact.status }
21
+ : { id, label: fallbackLabel(id) });
22
+ return { direction: "LR", nodes, edges };
23
+ }
@@ -9,7 +9,8 @@ function normalizeEdge(edge: ArtifactEdge): DisplayGraphEdge {
9
9
  return { from: edge.from, to: edge.to, label: edge.relation };
10
10
  }
11
11
 
12
- function fallbackLabel(id: string): string {
12
+ /** Best-effort readable label from a bare id when no real title is known -- shared with artifact-relationship-view.ts's generic version of this same problem. */
13
+ export function fallbackLabel(id: string): string {
13
14
  return id.replace(/-[a-z0-9]{4}$/i, "").replaceAll("-", " ");
14
15
  }
15
16