@danypops/papyrus 0.10.0 → 0.10.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/README.md CHANGED
@@ -116,7 +116,7 @@ Internally, application services depend on the `ArtifactStore` and `GateRunner`
116
116
  - `/rules` — severity/condition rows, exact injection preview, enable/disable, and task gating
117
117
  - `/skills` — trigger/tools rows, invocation into the editor, and artifact templates
118
118
 
119
- All frontends use daemon-backed domain operations; none opens SQLite from the Pi process.
119
+ All frontends use daemon-backed domain operations; none opens SQLite from the Pi process. **Show details** opens a bounded navigable view across Tasks, Notes, Docs, Rules, legacy Skills, templates, and workflow Skills. `↑/↓` scrolls, `←/→` pans wide relationships, and Esc returns to the browser; non-interactive clients receive stable text.
120
120
 
121
121
  ## Notes
122
122
 
@@ -4,9 +4,12 @@ import { Container, Input, Spacer, truncateToWidth, visibleWidth } from "@earend
4
4
  import { SEED_RELATIONS } from "../../src/constants.ts";
5
5
  import type { Artifact } from "../../src/domain/artifact.ts";
6
6
  import type { OperationName } from "../../src/service.ts";
7
- import { formatMetadata } from "./artifact-format.ts";
7
+ import { artifactDetailsText } from "./artifact-detail-format.ts";
8
+ import { showArtifactDetailView } from "./artifact-detail-view.ts";
8
9
  import { callService } from "./service-client.ts";
9
10
 
11
+ export { artifactDetailsText } from "./artifact-detail-format.ts";
12
+
10
13
  const BROWSER_QUERY_LIMIT = 500;
11
14
  const BROWSER_VISIBLE_ROWS = 20;
12
15
  const DETAIL_GRAPH_DEPTH = 4;
@@ -51,31 +54,34 @@ async function loadArtifacts(config: ArtifactBrowserConfig): Promise<Artifact[]>
51
54
  });
52
55
  }
53
56
 
57
+ export type ArtifactDetailLoader = (
58
+ operation: OperationName,
59
+ input: Record<string, unknown>,
60
+ ) => Promise<Artifact | null>;
61
+
62
+ const loadArtifactDetails: ArtifactDetailLoader = (operation, input) =>
63
+ callService<Record<string, unknown>, Artifact | null>(operation, input);
64
+
54
65
  export async function showArtifactDetails(
55
66
  ctx: ExtensionCommandContext,
56
67
  id: string,
57
68
  operation: OperationName = "artifact.show",
58
69
  input: Record<string, unknown> = {},
70
+ load: ArtifactDetailLoader = loadArtifactDetails,
59
71
  ): Promise<void> {
60
- const artifact = await callService<Record<string, unknown>, Artifact | null>(operation, {
61
- id,
62
- ...input,
63
- tree: true,
64
- depth: DETAIL_GRAPH_DEPTH,
65
- max_nodes: DETAIL_GRAPH_NODES,
66
- });
67
- if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
68
- let output = `${artifact.title}\n${artifact.id} [${artifact.kind}|${artifact.status}]`;
69
- if (artifact.subtype) output += ` · ${artifact.subtype}`;
70
- if (artifact.body) output += `\n\n${artifact.body}`;
71
- if (artifact.labels.length > 0) output += `\n\nLabels: ${artifact.labels.join(", ")}`;
72
- if (Object.keys(artifact.extra).length > 0) {
73
- output += `\n\nMetadata:\n${formatMetadata(artifact.extra).map((line) => ` ${line}`).join("\n")}`;
74
- }
75
- if (artifact.edges?.length) {
76
- output += `\n\nEdges:\n${artifact.edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`;
72
+ try {
73
+ const artifact = await load(operation, {
74
+ id,
75
+ ...input,
76
+ tree: true,
77
+ depth: DETAIL_GRAPH_DEPTH,
78
+ max_nodes: DETAIL_GRAPH_NODES,
79
+ });
80
+ if (!artifact) { ctx.ui.notify(`Artifact ${id} not found`, "error"); return; }
81
+ await showArtifactDetailView(ctx, artifact);
82
+ } catch (error) {
83
+ ctx.ui.notify(`Show details failed: ${error instanceof Error ? error.message : error}`, "error");
77
84
  }
78
- ctx.ui.notify(output, "info");
79
85
  }
80
86
 
81
87
  export async function linkFromArtifact(ctx: ExtensionCommandContext, fromId: string, fixedRelation?: string): Promise<void> {
@@ -0,0 +1,16 @@
1
+ import type { Artifact } from "../../src/domain/artifact.ts";
2
+ import { formatMetadata } from "./artifact-format.ts";
3
+
4
+ export function artifactDetailsText(artifact: Artifact): string {
5
+ let output = `${artifact.title}\n${artifact.id} [${artifact.kind}|${artifact.status}]`;
6
+ if (artifact.subtype) output += ` · ${artifact.subtype}`;
7
+ output += `\n\n${artifact.body || "(no body)"}`;
8
+ if (artifact.labels.length > 0) output += `\n\nLabels: ${artifact.labels.join(", ")}`;
9
+ if (Object.keys(artifact.extra).length > 0) {
10
+ output += `\n\nMetadata:\n${formatMetadata(artifact.extra).map((line) => ` ${line}`).join("\n")}`;
11
+ }
12
+ if (artifact.edges?.length) {
13
+ output += `\n\nRelationships:\n${artifact.edges.map((edge) => ` ${edge.from} --${edge.relation}--> ${edge.to}`).join("\n")}`;
14
+ }
15
+ return output;
16
+ }
@@ -0,0 +1,95 @@
1
+ import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
2
+ import { matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, type TUI } from "@earendil-works/pi-tui";
3
+ import {
4
+ ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS,
5
+ ARTIFACT_DETAIL_MAX_VISIBLE_LINES,
6
+ ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
7
+ ARTIFACT_DETAIL_RESERVED_ROWS,
8
+ } from "../../src/constants.ts";
9
+ import type { Artifact } from "../../src/domain/artifact.ts";
10
+ import { artifactDetailsText } from "./artifact-detail-format.ts";
11
+
12
+ interface ArtifactDetailLine {
13
+ text: string;
14
+ wide: boolean;
15
+ }
16
+
17
+ class ArtifactDetailViewport {
18
+ private offsetX = 0;
19
+ private offsetY = 0;
20
+ private renderedWidth = 0;
21
+ private lines: ArtifactDetailLine[] = [];
22
+ private readonly visibleLines: number;
23
+ private readonly narrative: string;
24
+ private readonly relationships: string[];
25
+
26
+ constructor(
27
+ private readonly tui: TUI,
28
+ private readonly theme: Theme,
29
+ artifact: Artifact,
30
+ private readonly close: () => void,
31
+ ) {
32
+ this.visibleLines = Math.max(
33
+ ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
34
+ Math.min(ARTIFACT_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - ARTIFACT_DETAIL_RESERVED_ROWS),
35
+ );
36
+ this.narrative = artifactDetailsText({ ...artifact, edges: undefined });
37
+ this.relationships = (artifact.edges ?? []).map((edge) => `${edge.from} --${edge.relation}--> ${edge.to}`);
38
+ }
39
+
40
+ invalidate(): void { this.renderedWidth = 0; }
41
+
42
+ render(width: number): string[] {
43
+ const contentWidth = Math.max(1, width - 2);
44
+ this.buildLines(contentWidth);
45
+ const wideWidth = this.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
46
+ this.offsetX = Math.min(this.offsetX, Math.max(0, wideWidth - contentWidth));
47
+ this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
48
+ const end = Math.min(this.lines.length, this.offsetY + this.visibleLines);
49
+ const border = this.theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
50
+ const footer = [
51
+ wideWidth > contentWidth ? `←/→ relationships · column ${this.offsetX + 1}/${wideWidth}` : "",
52
+ this.lines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.lines.length}` : "",
53
+ "Esc back",
54
+ ].filter(Boolean).join(" · ");
55
+ return [
56
+ border,
57
+ truncateToWidth(this.theme.bold("Artifact details"), width, ""),
58
+ border,
59
+ ...this.lines.slice(this.offsetY, end).map((line) => line.wide
60
+ ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
61
+ : truncateToWidth(` ${line.text}`, width, "")),
62
+ truncateToWidth(this.theme.fg("dim", footer), width, ""),
63
+ border,
64
+ ];
65
+ }
66
+
67
+ handleInput(data: string): void {
68
+ if (matchesKey(data, "escape") || matchesKey(data, "ctrl+c")) { this.close(); return; }
69
+ if (matchesKey(data, "up")) this.offsetY = Math.max(0, this.offsetY - 1);
70
+ else if (matchesKey(data, "down")) this.offsetY = Math.min(Math.max(0, this.lines.length - this.visibleLines), this.offsetY + 1);
71
+ else if (matchesKey(data, "left")) this.offsetX = Math.max(0, this.offsetX - ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS);
72
+ else if (matchesKey(data, "right")) this.offsetX += ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS;
73
+ else return;
74
+ this.tui.requestRender();
75
+ }
76
+
77
+ private buildLines(width: number): void {
78
+ if (this.renderedWidth === width) return;
79
+ this.renderedWidth = width;
80
+ const narrative = this.narrative.split("\n").flatMap((line) =>
81
+ (line.length === 0 ? [""] : wrapTextWithAnsi(line, width)).map((text) => ({ text, wide: false })));
82
+ const relationshipSection = this.relationships.length > 0
83
+ ? [{ text: "", wide: false }, { text: "Relationships:", wide: false }, ...this.relationships.map((text) => ({ text, wide: true }))]
84
+ : [];
85
+ this.lines = [...narrative, ...relationshipSection];
86
+ this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
87
+ }
88
+ }
89
+
90
+ export async function showArtifactDetailView(ctx: ExtensionCommandContext, artifact: Artifact): Promise<void> {
91
+ const output = artifactDetailsText(artifact);
92
+ if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
93
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
94
+ new ArtifactDetailViewport(tui, theme, artifact, done));
95
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.10.0",
3
+ "version": "0.10.1",
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"],
package/src/constants.ts CHANGED
@@ -29,6 +29,11 @@ export const TASK_DETAIL_MIN_VISIBLE_LINES = 8;
29
29
  export const TASK_DETAIL_MAX_VISIBLE_LINES = 24;
30
30
  export const TASK_DETAIL_RESERVED_ROWS = 8;
31
31
  export const TASK_DETAIL_HORIZONTAL_PAN_COLUMNS = 4;
32
+ /** Bounded navigable detail views for non-Task artifacts. */
33
+ export const ARTIFACT_DETAIL_MIN_VISIBLE_LINES = 8;
34
+ export const ARTIFACT_DETAIL_MAX_VISIBLE_LINES = 24;
35
+ export const ARTIFACT_DETAIL_RESERVED_ROWS = 8;
36
+ export const ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS = 4;
32
37
  export const TASK_GRAPH_MIN_VISIBLE_LINES = 8;
33
38
  export const TASK_GRAPH_MAX_VISIBLE_LINES = 30;
34
39
  export const TASK_GRAPH_RESERVED_ROWS = 8;