@danypops/papyrus 0.10.0 → 0.11.0

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. User-authored bodies render as width-aware Markdown with headings, emphasis, links, quotes, lists, tables, inline/fenced code, syntax highlighting, and every color/decorative style derived dynamically from the active Pi theme. Generated lifecycle, metadata, checklist, gate, history, and relationship sections keep explicit semantic theme colors. `↑/↓` scrolls, `←/→` pans wide relationships, and Esc returns to the browser; non-interactive clients receive stable source 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,31 @@
1
+ import type { Artifact } from "../../src/domain/artifact.ts";
2
+ import { formatMetadata } from "./artifact-format.ts";
3
+
4
+ export interface ArtifactDetailContent {
5
+ title: string;
6
+ identity: string;
7
+ body: string;
8
+ labels: string[];
9
+ metadata: string[];
10
+ relationships: string[];
11
+ }
12
+
13
+ export function artifactDetailContent(artifact: Artifact): ArtifactDetailContent {
14
+ return {
15
+ title: artifact.title,
16
+ identity: `${artifact.id} [${artifact.kind}|${artifact.status}]${artifact.subtype ? ` · ${artifact.subtype}` : ""}`,
17
+ body: artifact.body || "(no body)",
18
+ labels: [...artifact.labels],
19
+ metadata: Object.keys(artifact.extra).length > 0 ? formatMetadata(artifact.extra) : [],
20
+ relationships: (artifact.edges ?? []).map((edge) => `${edge.from} --${edge.relation}--> ${edge.to}`),
21
+ };
22
+ }
23
+
24
+ export function artifactDetailsText(artifact: Artifact): string {
25
+ const content = artifactDetailContent(artifact);
26
+ let output = `${content.title}\n${content.identity}\n\n${content.body}`;
27
+ if (content.labels.length > 0) output += `\n\nLabels: ${content.labels.join(", ")}`;
28
+ if (content.metadata.length > 0) output += `\n\nMetadata:\n${content.metadata.map((line) => ` ${line}`).join("\n")}`;
29
+ if (content.relationships.length > 0) output += `\n\nRelationships:\n${content.relationships.map((line) => ` ${line}`).join("\n")}`;
30
+ return output;
31
+ }
@@ -0,0 +1,112 @@
1
+ import type { ExtensionCommandContext } 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 { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
11
+ import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
12
+
13
+ interface ArtifactDetailLine {
14
+ text: string;
15
+ wide: boolean;
16
+ }
17
+
18
+ class ArtifactDetailViewport {
19
+ private offsetX = 0;
20
+ private offsetY = 0;
21
+ private renderedWidth = 0;
22
+ private lines: ArtifactDetailLine[] = [];
23
+ private readonly visibleLines: number;
24
+ private readonly content: ArtifactDetailContent;
25
+
26
+ constructor(
27
+ private readonly tui: TUI,
28
+ private readonly activeTheme: ActiveTheme,
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.content = artifactDetailContent(artifact);
37
+ }
38
+
39
+ invalidate(): void { this.renderedWidth = 0; }
40
+
41
+ render(width: number): string[] {
42
+ const contentWidth = Math.max(1, width - 2);
43
+ this.buildLines(contentWidth);
44
+ const wideWidth = this.content.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
45
+ this.offsetX = Math.min(this.offsetX, Math.max(0, wideWidth - contentWidth));
46
+ this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
47
+ const end = Math.min(this.lines.length, this.offsetY + this.visibleLines);
48
+ const theme = this.activeTheme();
49
+ const border = 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(theme.fg("accent", 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(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 theme = this.activeTheme();
81
+ const wrap = (text: string, color: "text" | "muted" | "dim" = "text"): ArtifactDetailLine[] =>
82
+ (text.length === 0 ? [""] : wrapTextWithAnsi(theme.fg(color, text), width)).map((line) => ({ text: line, wide: false }));
83
+ const identity = [
84
+ ...wrap(theme.bold(this.content.title)),
85
+ ...wrap(this.content.identity, "muted"),
86
+ { text: "", wide: false },
87
+ ];
88
+ const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, wide: false }));
89
+ const labels = this.content.labels.length > 0
90
+ ? [{ text: "", wide: false }, ...wrap("Labels:", "muted"), ...wrap(this.content.labels.join(", "))]
91
+ : [];
92
+ const metadata = this.content.metadata.length > 0
93
+ ? [{ text: "", wide: false }, ...wrap("Metadata:", "muted"), ...this.content.metadata.flatMap((line) => wrap(` ${line}`, "dim"))]
94
+ : [];
95
+ const relationships = this.content.relationships.length > 0
96
+ ? [
97
+ { text: "", wide: false },
98
+ ...wrap("Relationships:", "muted"),
99
+ ...this.content.relationships.map((text) => ({ text: theme.fg("text", text), wide: true })),
100
+ ]
101
+ : [];
102
+ this.lines = [...identity, ...body, ...labels, ...metadata, ...relationships];
103
+ this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
104
+ }
105
+ }
106
+
107
+ export async function showArtifactDetailView(ctx: ExtensionCommandContext, artifact: Artifact): Promise<void> {
108
+ const output = artifactDetailsText(artifact);
109
+ if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
110
+ await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
111
+ new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, done));
112
+ }
@@ -0,0 +1,60 @@
1
+ import { getMarkdownTheme, type Theme } from "@earendil-works/pi-coding-agent";
2
+ import { Markdown, type MarkdownTheme } from "@earendil-works/pi-tui";
3
+
4
+ export type ActiveTheme = () => Theme;
5
+ export type ActiveMarkdownTheme = () => Pick<MarkdownTheme, "highlightCode">;
6
+
7
+ function activePiMarkdownTheme(): Pick<MarkdownTheme, "highlightCode"> {
8
+ try {
9
+ return getMarkdownTheme();
10
+ } catch {
11
+ return {};
12
+ }
13
+ }
14
+
15
+ export function createPapyrusMarkdownTheme(
16
+ activeTheme: ActiveTheme,
17
+ activeMarkdownTheme: ActiveMarkdownTheme = activePiMarkdownTheme,
18
+ ): MarkdownTheme {
19
+ return {
20
+ heading: (text) => activeTheme().fg("mdHeading", text),
21
+ link: (text) => activeTheme().fg("mdLink", text),
22
+ linkUrl: (text) => activeTheme().fg("mdLinkUrl", text),
23
+ code: (text) => activeTheme().fg("mdCode", text),
24
+ codeBlock: (text) => activeTheme().fg("mdCodeBlock", text),
25
+ codeBlockBorder: (text) => activeTheme().fg("mdCodeBlockBorder", text),
26
+ quote: (text) => activeTheme().fg("mdQuote", text),
27
+ quoteBorder: (text) => activeTheme().fg("mdQuoteBorder", text),
28
+ hr: (text) => activeTheme().fg("mdHr", text),
29
+ listBullet: (text) => activeTheme().fg("mdListBullet", text),
30
+ bold: (text) => activeTheme().bold(text),
31
+ italic: (text) => activeTheme().italic(text),
32
+ strikethrough: (text) => activeTheme().strikethrough(text),
33
+ underline: (text) => activeTheme().underline(text),
34
+ highlightCode: (code, language) => {
35
+ try {
36
+ const highlighted = activeMarkdownTheme().highlightCode?.(code, language);
37
+ if (highlighted) return highlighted;
38
+ } catch {
39
+ // The host theme may be unavailable in isolated rendering tests.
40
+ }
41
+ return code.split("\n").map((line) => activeTheme().fg("mdCodeBlock", line));
42
+ },
43
+ };
44
+ }
45
+
46
+ export function renderMarkdownBody(
47
+ body: string,
48
+ width: number,
49
+ activeTheme: ActiveTheme,
50
+ activeMarkdownTheme: ActiveMarkdownTheme = activePiMarkdownTheme,
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
+ );
59
+ return markdown.render(Math.max(1, width));
60
+ }
@@ -70,19 +70,38 @@ function historyLines(history: TaskEvent[]): string[] {
70
70
  return lines;
71
71
  }
72
72
 
73
- export function taskDetailsText(task: Artifact, relationshipGraphLines: string[] = [], history: TaskEvent[] = []): string {
74
- let output = `${TASK_STATUS_GLYPHS[task.status] ?? "?"} ${task.title}\n${task.id} [task|${task.status}]`;
75
- if (task.labels.length > 0) output += `\nLabels: ${task.labels.join(", ")}`;
76
- output += `\n\n${task.body || "(no body)"}`;
73
+ export interface TaskDetailContent {
74
+ headline: string;
75
+ identity: string;
76
+ labels: string[];
77
+ body: string;
78
+ sections: string[][];
79
+ }
80
+
81
+ export function taskDetailContent(task: Artifact, history: TaskEvent[] = []): TaskDetailContent {
82
+ const sections: string[][] = [];
77
83
  const checklist = checklistLines(task.extra["checklist"]);
78
- if (checklist.length > 0) output += `\n\n${checklist.join("\n")}`;
84
+ if (checklist.length > 0) sections.push(checklist);
79
85
  const gates = gateLines(task.extra["gates"]);
80
- if (gates.length > 0) output += `\n\n${gates.join("\n")}`;
86
+ if (gates.length > 0) sections.push(gates);
81
87
  const metadata = Object.fromEntries(Object.entries(task.extra).filter(([key]) => key !== "checklist" && key !== "gates"));
82
- if (Object.keys(metadata).length > 0) {
83
- output += `\n\nMetadata:\n${formatMetadata(metadata).map((line) => ` ${line}`).join("\n")}`;
84
- }
85
- output += `\n\n${historyLines(history).join("\n")}`;
88
+ if (Object.keys(metadata).length > 0) sections.push(["Metadata:", ...formatMetadata(metadata).map((line) => ` ${line}`)]);
89
+ sections.push(historyLines(history));
90
+ return {
91
+ headline: `${TASK_STATUS_GLYPHS[task.status] ?? "?"} ${task.title}`,
92
+ identity: `${task.id} [task|${task.status}]`,
93
+ labels: [...task.labels],
94
+ body: task.body || "(no body)",
95
+ sections,
96
+ };
97
+ }
98
+
99
+ export function taskDetailsText(task: Artifact, relationshipGraphLines: string[] = [], history: TaskEvent[] = []): string {
100
+ const content = taskDetailContent(task, history);
101
+ let output = `${content.headline}\n${content.identity}`;
102
+ if (content.labels.length > 0) output += `\nLabels: ${content.labels.join(", ")}`;
103
+ output += `\n\n${content.body}`;
104
+ for (const section of content.sections) output += `\n\n${section.join("\n")}`;
86
105
  if (task.edges?.length) {
87
106
  const graph = relationshipGraphLines.length > 0 ? relationshipGraphLines.join("\n") : " (graph unavailable)";
88
107
  output += `\n\nRelationships:\n Dependencies point prerequisite → dependent.\n${graph}`;
@@ -1,4 +1,4 @@
1
- import type { ExtensionCommandContext, Theme } from "@earendil-works/pi-coding-agent";
1
+ import type { ExtensionCommandContext } from "@earendil-works/pi-coding-agent";
2
2
  import { matchesKey, sliceByColumn, truncateToWidth, visibleWidth, wrapTextWithAnsi, type TUI } from "@earendil-works/pi-tui";
3
3
  import {
4
4
  TASK_DETAIL_HORIZONTAL_PAN_COLUMNS,
@@ -12,7 +12,9 @@ import type { GraphRenderer } from "../../src/ports/graph-renderer.ts";
12
12
  import { projectTaskRelationships } from "../../src/task-relationship-view.ts";
13
13
  import type { TaskGraph } from "../../src/task-service.ts";
14
14
  import { BeautifulMermaidRenderer } from "./beautiful-mermaid-renderer.ts";
15
- import { taskDetailsText } from "./task-detail-format.ts";
15
+ import { taskDetailContent, taskDetailsText, type TaskDetailContent } from "./task-detail-format.ts";
16
+ import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
17
+ import { TASK_STATUS_PRESENTATION } from "./task-presentation.ts";
16
18
 
17
19
  interface DetailLine {
18
20
  text: string;
@@ -25,11 +27,12 @@ class TaskDetailViewport {
25
27
  private renderedWidth = 0;
26
28
  private detailLines: DetailLine[] = [];
27
29
  private readonly visibleLines: number;
28
- private readonly narrative: string;
30
+ private readonly content: TaskDetailContent;
31
+ private readonly status: Artifact["status"];
29
32
 
30
33
  constructor(
31
34
  private readonly tui: TUI,
32
- private readonly theme: Theme,
35
+ private readonly activeTheme: ActiveTheme,
33
36
  task: Artifact,
34
37
  private readonly graphLines: string[],
35
38
  history: TaskEvent[],
@@ -39,7 +42,8 @@ class TaskDetailViewport {
39
42
  TASK_DETAIL_MIN_VISIBLE_LINES,
40
43
  Math.min(TASK_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - TASK_DETAIL_RESERVED_ROWS),
41
44
  );
42
- this.narrative = taskDetailsText({ ...task, edges: undefined }, [], history);
45
+ this.content = taskDetailContent(task, history);
46
+ this.status = task.status;
43
47
  }
44
48
 
45
49
  invalidate(): void { this.renderedWidth = 0; }
@@ -51,7 +55,8 @@ class TaskDetailViewport {
51
55
  this.offsetX = Math.min(this.offsetX, Math.max(0, graphWidth - contentWidth));
52
56
  this.offsetY = Math.min(this.offsetY, Math.max(0, this.detailLines.length - this.visibleLines));
53
57
  const end = Math.min(this.detailLines.length, this.offsetY + this.visibleLines);
54
- const border = this.theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
58
+ const theme = this.activeTheme();
59
+ const border = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
55
60
  const footer = [
56
61
  graphWidth > contentWidth ? `←/→ graph · column ${this.offsetX + 1}/${graphWidth}` : "",
57
62
  this.detailLines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.detailLines.length}` : "",
@@ -59,12 +64,12 @@ class TaskDetailViewport {
59
64
  ].filter(Boolean).join(" · ");
60
65
  return [
61
66
  border,
62
- truncateToWidth(this.theme.bold("Task details"), width, ""),
67
+ truncateToWidth(theme.fg("accent", theme.bold("Task details")), width, ""),
63
68
  border,
64
69
  ...this.detailLines.slice(this.offsetY, end).map((line) => line.graph
65
70
  ? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
66
71
  : truncateToWidth(` ${line.text}`, width, "")),
67
- truncateToWidth(this.theme.fg("dim", footer), width, ""),
72
+ truncateToWidth(theme.fg("dim", footer), width, ""),
68
73
  border,
69
74
  ];
70
75
  }
@@ -82,16 +87,36 @@ class TaskDetailViewport {
82
87
  private buildLines(width: number): void {
83
88
  if (this.renderedWidth === width) return;
84
89
  this.renderedWidth = width;
85
- const narrative = this.narrative.split("\n").flatMap((line) =>
86
- (line.length === 0 ? [""] : wrapTextWithAnsi(line, width)).map((text) => ({ text, graph: false })));
90
+ const theme = this.activeTheme();
91
+ const wrap = (text: string, color: "text" | "muted" | "dim" = "text"): DetailLine[] =>
92
+ (text.length === 0 ? [""] : wrapTextWithAnsi(theme.fg(color, text), width)).map((line) => ({ text: line, graph: false }));
93
+ const status = TASK_STATUS_PRESENTATION[this.status as keyof typeof TASK_STATUS_PRESENTATION];
94
+ const headline = status ? theme.fg(status.color, theme.bold(this.content.headline)) : theme.bold(this.content.headline);
95
+ const identity = [
96
+ ...wrapTextWithAnsi(headline, width).map((text) => ({ text, graph: false })),
97
+ ...wrap(this.content.identity, "muted"),
98
+ ...(this.content.labels.length > 0 ? wrap(`Labels: ${this.content.labels.join(", ")}`, "muted") : []),
99
+ { text: "", graph: false },
100
+ ];
101
+ const body = renderMarkdownBody(this.content.body, width, this.activeTheme).map((text) => ({ text, graph: false }));
102
+ const sections = this.content.sections.flatMap((section) => [
103
+ { text: "", graph: false },
104
+ ...section.flatMap((line, index) => wrap(line, index === 0 ? "muted" : "dim")),
105
+ ]);
87
106
  const relationshipHeader = this.graphLines.length > 0
88
107
  ? [
89
108
  { text: "", graph: false },
90
- { text: "Relationships:", graph: false },
91
- { text: " Dependencies point prerequisite → dependent.", graph: false },
109
+ ...wrap("Relationships:", "muted"),
110
+ ...wrap(" Dependencies point prerequisite → dependent.", "dim"),
92
111
  ]
93
112
  : [];
94
- this.detailLines = [...narrative, ...relationshipHeader, ...this.graphLines.map((text) => ({ text, graph: true }))];
113
+ this.detailLines = [
114
+ ...identity,
115
+ ...body,
116
+ ...sections,
117
+ ...relationshipHeader,
118
+ ...this.graphLines.map((text) => ({ text: theme.fg("text", text), graph: true })),
119
+ ];
95
120
  this.offsetY = Math.min(this.offsetY, Math.max(0, this.detailLines.length - this.visibleLines));
96
121
  }
97
122
  }
@@ -110,5 +135,5 @@ export async function showTaskDetails(
110
135
  return;
111
136
  }
112
137
  await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
113
- new TaskDetailViewport(tui, theme, task, relationshipGraph, history, done));
138
+ new TaskDetailViewport(tui, () => ctx.ui.theme ?? theme, task, relationshipGraph, history, done));
114
139
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/papyrus",
3
- "version": "0.10.0",
3
+ "version": "0.11.0",
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;