@danypops/papyrus 0.10.1 → 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 +1 -1
- package/extension/src/artifact-detail-format.ts +25 -10
- package/extension/src/artifact-detail-view.ts +34 -17
- package/extension/src/markdown.ts +60 -0
- package/extension/src/task-detail-format.ts +29 -10
- package/extension/src/task-detail-view.ts +39 -14
- package/package.json +1 -1
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. **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.
|
|
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
|
|
|
@@ -1,16 +1,31 @@
|
|
|
1
1
|
import type { Artifact } from "../../src/domain/artifact.ts";
|
|
2
2
|
import { formatMetadata } from "./artifact-format.ts";
|
|
3
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
|
+
|
|
4
24
|
export function artifactDetailsText(artifact: Artifact): string {
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
output += `\n\
|
|
8
|
-
if (
|
|
9
|
-
if (
|
|
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
|
-
}
|
|
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")}`;
|
|
15
30
|
return output;
|
|
16
31
|
}
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { ExtensionCommandContext
|
|
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
|
ARTIFACT_DETAIL_HORIZONTAL_PAN_COLUMNS,
|
|
@@ -7,7 +7,8 @@ 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 { artifactDetailsText } from "./artifact-detail-format.ts";
|
|
10
|
+
import { artifactDetailContent, artifactDetailsText, type ArtifactDetailContent } from "./artifact-detail-format.ts";
|
|
11
|
+
import { renderMarkdownBody, type ActiveTheme } from "./markdown.ts";
|
|
11
12
|
|
|
12
13
|
interface ArtifactDetailLine {
|
|
13
14
|
text: string;
|
|
@@ -20,12 +21,11 @@ class ArtifactDetailViewport {
|
|
|
20
21
|
private renderedWidth = 0;
|
|
21
22
|
private lines: ArtifactDetailLine[] = [];
|
|
22
23
|
private readonly visibleLines: number;
|
|
23
|
-
private readonly
|
|
24
|
-
private readonly relationships: string[];
|
|
24
|
+
private readonly content: ArtifactDetailContent;
|
|
25
25
|
|
|
26
26
|
constructor(
|
|
27
27
|
private readonly tui: TUI,
|
|
28
|
-
private readonly
|
|
28
|
+
private readonly activeTheme: ActiveTheme,
|
|
29
29
|
artifact: Artifact,
|
|
30
30
|
private readonly close: () => void,
|
|
31
31
|
) {
|
|
@@ -33,8 +33,7 @@ class ArtifactDetailViewport {
|
|
|
33
33
|
ARTIFACT_DETAIL_MIN_VISIBLE_LINES,
|
|
34
34
|
Math.min(ARTIFACT_DETAIL_MAX_VISIBLE_LINES, tui.terminal.rows - ARTIFACT_DETAIL_RESERVED_ROWS),
|
|
35
35
|
);
|
|
36
|
-
this.
|
|
37
|
-
this.relationships = (artifact.edges ?? []).map((edge) => `${edge.from} --${edge.relation}--> ${edge.to}`);
|
|
36
|
+
this.content = artifactDetailContent(artifact);
|
|
38
37
|
}
|
|
39
38
|
|
|
40
39
|
invalidate(): void { this.renderedWidth = 0; }
|
|
@@ -42,11 +41,12 @@ class ArtifactDetailViewport {
|
|
|
42
41
|
render(width: number): string[] {
|
|
43
42
|
const contentWidth = Math.max(1, width - 2);
|
|
44
43
|
this.buildLines(contentWidth);
|
|
45
|
-
const wideWidth = this.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
|
|
44
|
+
const wideWidth = this.content.relationships.reduce((maximum, line) => Math.max(maximum, visibleWidth(line)), 0);
|
|
46
45
|
this.offsetX = Math.min(this.offsetX, Math.max(0, wideWidth - contentWidth));
|
|
47
46
|
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
48
47
|
const end = Math.min(this.lines.length, this.offsetY + this.visibleLines);
|
|
49
|
-
const
|
|
48
|
+
const theme = this.activeTheme();
|
|
49
|
+
const border = theme.fg("borderMuted", "─".repeat(Math.max(1, width)));
|
|
50
50
|
const footer = [
|
|
51
51
|
wideWidth > contentWidth ? `←/→ relationships · column ${this.offsetX + 1}/${wideWidth}` : "",
|
|
52
52
|
this.lines.length > this.visibleLines ? `↑/↓ scroll · ${this.offsetY + 1}-${end}/${this.lines.length}` : "",
|
|
@@ -54,12 +54,12 @@ class ArtifactDetailViewport {
|
|
|
54
54
|
].filter(Boolean).join(" · ");
|
|
55
55
|
return [
|
|
56
56
|
border,
|
|
57
|
-
truncateToWidth(
|
|
57
|
+
truncateToWidth(theme.fg("accent", theme.bold("Artifact details")), width, ""),
|
|
58
58
|
border,
|
|
59
59
|
...this.lines.slice(this.offsetY, end).map((line) => line.wide
|
|
60
60
|
? ` ${sliceByColumn(line.text, this.offsetX, contentWidth, true)}`
|
|
61
61
|
: truncateToWidth(` ${line.text}`, width, "")),
|
|
62
|
-
truncateToWidth(
|
|
62
|
+
truncateToWidth(theme.fg("dim", footer), width, ""),
|
|
63
63
|
border,
|
|
64
64
|
];
|
|
65
65
|
}
|
|
@@ -77,12 +77,29 @@ class ArtifactDetailViewport {
|
|
|
77
77
|
private buildLines(width: number): void {
|
|
78
78
|
if (this.renderedWidth === width) return;
|
|
79
79
|
this.renderedWidth = width;
|
|
80
|
-
const
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
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
|
+
]
|
|
84
101
|
: [];
|
|
85
|
-
this.lines = [...
|
|
102
|
+
this.lines = [...identity, ...body, ...labels, ...metadata, ...relationships];
|
|
86
103
|
this.offsetY = Math.min(this.offsetY, Math.max(0, this.lines.length - this.visibleLines));
|
|
87
104
|
}
|
|
88
105
|
}
|
|
@@ -91,5 +108,5 @@ export async function showArtifactDetailView(ctx: ExtensionCommandContext, artif
|
|
|
91
108
|
const output = artifactDetailsText(artifact);
|
|
92
109
|
if (ctx.mode !== "tui") { ctx.ui.notify(output, "info"); return; }
|
|
93
110
|
await ctx.ui.custom<void>((tui, theme, _keybindings, done) =>
|
|
94
|
-
new ArtifactDetailViewport(tui, theme, artifact, done));
|
|
111
|
+
new ArtifactDetailViewport(tui, () => ctx.ui.theme ?? theme, artifact, done));
|
|
95
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
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
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)
|
|
84
|
+
if (checklist.length > 0) sections.push(checklist);
|
|
79
85
|
const gates = gateLines(task.extra["gates"]);
|
|
80
|
-
if (gates.length > 0)
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
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
|
|
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
|
|
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
|
|
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.
|
|
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
|
|
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(
|
|
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(
|
|
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
|
|
86
|
-
|
|
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
|
-
|
|
91
|
-
|
|
109
|
+
...wrap("Relationships:", "muted"),
|
|
110
|
+
...wrap(" Dependencies point prerequisite → dependent.", "dim"),
|
|
92
111
|
]
|
|
93
112
|
: [];
|
|
94
|
-
this.detailLines = [
|
|
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