@danypops/pi-papyrus 0.54.0 → 0.54.2

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.
@@ -0,0 +1,79 @@
1
+ import { TOOL_COLLAPSED_ROW_LIMIT } from "@danypops/papyrus";
2
+ import { expandHint } from "@danypops/vehicle-client-pi/expand-hint";
3
+ import type { Theme } from "@earendil-works/pi-coding-agent";
4
+ import type { Component } from "@earendil-works/pi-tui";
5
+ import { type DagEdge, type DagNode, DagView } from "malevich-tui-components";
6
+ import { statusColor, statusGlyph } from "../../tool-rendering/artifact-card.ts";
7
+
8
+ /** tasks.plan's own TaskExecutionPlan shape (projectTaskExecution) -- a
9
+ * genuinely structured topological-execution view, never artifact-shaped,
10
+ * detected the same name-independent way as the others in this directory. */
11
+ export interface TaskExecutionNodeOutput {
12
+ id: string;
13
+ title: string;
14
+ status: string;
15
+ active: boolean;
16
+ state: string;
17
+ layer: number | null;
18
+ prerequisiteIds: string[];
19
+ successorIds: string[];
20
+ }
21
+
22
+ export interface TaskExecutionPlanOutput {
23
+ nodes: TaskExecutionNodeOutput[];
24
+ layers: string[][];
25
+ cycleIds: string[];
26
+ }
27
+
28
+ export function isTaskExecutionNode(value: unknown): value is TaskExecutionNodeOutput {
29
+ if (typeof value !== "object" || value === null) return false;
30
+ const row = value as Record<string, unknown>;
31
+ return (
32
+ typeof row.id === "string" &&
33
+ typeof row.title === "string" &&
34
+ typeof row.status === "string" &&
35
+ typeof row.active === "boolean" &&
36
+ typeof row.state === "string" &&
37
+ (row.layer === null || typeof row.layer === "number") &&
38
+ Array.isArray(row.prerequisiteIds) &&
39
+ Array.isArray(row.successorIds)
40
+ );
41
+ }
42
+
43
+ export function isTaskExecutionPlan(value: unknown): value is TaskExecutionPlanOutput {
44
+ if (typeof value !== "object" || value === null) return false;
45
+ const row = value as Record<string, unknown>;
46
+ return (
47
+ Array.isArray(row.nodes) &&
48
+ row.nodes.every(isTaskExecutionNode) &&
49
+ Array.isArray(row.layers) &&
50
+ row.layers.every((layer) => Array.isArray(layer) && layer.every((id) => typeof id === "string")) &&
51
+ Array.isArray(row.cycleIds) &&
52
+ row.cycleIds.every((id) => typeof id === "string")
53
+ );
54
+ }
55
+
56
+ export function dagViewFromExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, expanded: boolean): DagView {
57
+ const nodes: DagNode[] = plan.nodes.map((node) => ({
58
+ id: node.id,
59
+ label: `${theme.fg(statusColor(node.state), statusGlyph(node.state))} ${theme.fg("text", node.title)}`,
60
+ }));
61
+ const edges: DagEdge[] = plan.nodes.flatMap((node) => node.prerequisiteIds.map((from) => ({ from, to: node.id })));
62
+ return new DagView({
63
+ layers: plan.layers,
64
+ nodes,
65
+ edges,
66
+ cycleIds: plan.cycleIds,
67
+ defaultStyle: (s) => theme.fg("text", s),
68
+ edgeStyle: (s) => theme.fg("dim", s),
69
+ layerHeaderStyle: (s) => theme.fg("toolTitle", theme.bold(s)),
70
+ cycleHeaderStyle: (s) => theme.fg("error", theme.bold(s)),
71
+ expanded,
72
+ visibleNodeCount: TOOL_COLLAPSED_ROW_LIMIT,
73
+ moreLine: (hiddenCount) => theme.fg("dim", `${hiddenCount} more · ${expandHint()}`),
74
+ });
75
+ }
76
+
77
+ export function renderTaskExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, expanded: boolean): Component {
78
+ return dagViewFromExecutionPlan(plan, theme, expanded);
79
+ }