@danypops/pi-papyrus 0.43.3 → 0.43.5
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.
|
@@ -32,6 +32,17 @@ function statusColor(status: string): SemanticColor {
|
|
|
32
32
|
return "muted";
|
|
33
33
|
}
|
|
34
34
|
|
|
35
|
+
function focusLine(focus: ArtifactToolDetails["focus"], theme: Theme): string {
|
|
36
|
+
if (!focus) return "";
|
|
37
|
+
// Focus's own active/paused dimension is separate from the artifact's
|
|
38
|
+
// lifecycle status shown in the header above -- never merge the two.
|
|
39
|
+
if (focus.status === "paused") {
|
|
40
|
+
const reason = focus.pauseReason ? ` — ${focus.pauseReason}` : "";
|
|
41
|
+
return theme.fg("warning", `‖ focus paused${reason}`);
|
|
42
|
+
}
|
|
43
|
+
return theme.fg("accent", `▶ focus ${focus.status}`);
|
|
44
|
+
}
|
|
45
|
+
|
|
35
46
|
export function kindGlyph(kind: string): string {
|
|
36
47
|
return KIND_GLYPHS[kind] ?? "•";
|
|
37
48
|
}
|
|
@@ -48,10 +59,6 @@ export function emptyState(noun: string): string {
|
|
|
48
59
|
return `No ${noun}.`;
|
|
49
60
|
}
|
|
50
61
|
|
|
51
|
-
export function treeConnector(last: boolean): string {
|
|
52
|
-
return last ? "└─" : "├─";
|
|
53
|
-
}
|
|
54
|
-
|
|
55
62
|
export function expandHint(): string {
|
|
56
63
|
return "expand for details";
|
|
57
64
|
}
|
|
@@ -91,6 +98,10 @@ export class ArtifactCard implements Component {
|
|
|
91
98
|
const lines = [truncateToWidth(header, safeWidth)];
|
|
92
99
|
lines.push(truncateToWidth(this.theme.fg("text", artifact.title), safeWidth));
|
|
93
100
|
|
|
101
|
+
if (this.details.focus) {
|
|
102
|
+
lines.push(truncateToWidth(focusLine(this.details.focus, this.theme), safeWidth));
|
|
103
|
+
}
|
|
104
|
+
|
|
94
105
|
if (this.expanded) {
|
|
95
106
|
const metadata = [artifact.subtype, ...artifact.labels].filter(Boolean).join(" · ");
|
|
96
107
|
if (metadata) lines.push(truncateToWidth(this.theme.fg("muted", metadata), safeWidth));
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { TOOL_COLLAPSED_ROW_LIMIT } from "@danypops/papyrus";
|
|
2
2
|
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
3
|
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
-
import {
|
|
4
|
+
import { type TreeNode, TreeView } from "malevich-tui-components";
|
|
5
|
+
import { countSummary, expandHint, kindGlyph, statusGlyph } from "./artifact-card.ts";
|
|
5
6
|
import type { ArtifactListToolDetails, GraphToolDetails, ToolArtifactSummary } from "./render-model.ts";
|
|
6
7
|
|
|
7
8
|
function pluralKind(rows: readonly ToolArtifactSummary[]): string {
|
|
@@ -85,44 +86,79 @@ export class ArtifactListCard implements Component {
|
|
|
85
86
|
}
|
|
86
87
|
}
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
89
|
+
/** A fixed-text Component embedding one already-styled, width-truncated
|
|
90
|
+
* line -- used for the dependency-edge annotation Malevich's TreeView
|
|
91
|
+
* renders indented beneath a node. */
|
|
92
|
+
function textLine(text: string): Component {
|
|
93
|
+
return {
|
|
94
|
+
render: (width: number) => text.split("\n").map((line) => truncateToWidth(line, width)),
|
|
95
|
+
invalidate: () => {},
|
|
96
|
+
};
|
|
92
97
|
}
|
|
93
98
|
|
|
94
|
-
|
|
99
|
+
/** Projects GraphToolDetails' containment edges (`relation === "contains"`)
|
|
100
|
+
* into Malevich TreeNodes, rendered via the real, shared TreeView instead
|
|
101
|
+
* of hand-rolled connector math. Any non-containment edge (depends_on,
|
|
102
|
+
* references, blocks, ...) naming a node as its `to` -- previously
|
|
103
|
+
* silently dropped entirely -- surfaces as a "depends on: ..." annotation
|
|
104
|
+
* embedded under that node, matching DagView's own edge-annotation wording. */
|
|
105
|
+
function toTreeNodes(details: GraphToolDetails, theme: Theme, expanded: boolean): TreeNode[] {
|
|
95
106
|
const byId = new Map(details.nodes.map((node) => [node.id, node]));
|
|
96
107
|
const childIds = new Map<string, string[]>();
|
|
97
108
|
const contained = new Set<string>();
|
|
109
|
+
const dependencySources = new Map<string, string[]>();
|
|
98
110
|
for (const edge of details.edges) {
|
|
99
|
-
if (edge.relation
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
111
|
+
if (edge.relation === "contains") {
|
|
112
|
+
if (!byId.has(edge.from) || !byId.has(edge.to)) continue;
|
|
113
|
+
const children = childIds.get(edge.from) ?? [];
|
|
114
|
+
children.push(edge.to);
|
|
115
|
+
childIds.set(edge.from, children);
|
|
116
|
+
contained.add(edge.to);
|
|
117
|
+
} else if (byId.has(edge.to)) {
|
|
118
|
+
const sources = dependencySources.get(edge.to) ?? [];
|
|
119
|
+
sources.push(edge.from);
|
|
120
|
+
dependencySources.set(edge.to, sources);
|
|
121
|
+
}
|
|
104
122
|
}
|
|
105
|
-
|
|
106
|
-
const rows: HierarchyRow[] = [];
|
|
123
|
+
|
|
107
124
|
const visited = new Set<string>();
|
|
108
|
-
const
|
|
109
|
-
if (visited.has(node.id)) return;
|
|
125
|
+
const buildNode = (node: ToolArtifactSummary): TreeNode => {
|
|
110
126
|
visited.add(node.id);
|
|
111
|
-
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
const
|
|
117
|
-
|
|
118
|
-
}
|
|
127
|
+
const identity = expanded ? `${node.id} ` : "";
|
|
128
|
+
const label = `${theme.fg("accent", kindGlyph(node.kind))} ${theme.fg("muted", statusGlyph(node.status))} ${theme.fg("accent", identity)}${theme.fg("text", node.title)}`;
|
|
129
|
+
const children: TreeNode[] = [];
|
|
130
|
+
for (const id of childIds.get(node.id) ?? []) {
|
|
131
|
+
if (visited.has(id)) continue;
|
|
132
|
+
const child = byId.get(id);
|
|
133
|
+
if (child) children.push(buildNode(child));
|
|
134
|
+
}
|
|
135
|
+
const metadata = expanded ? rowMetadata(node) : "";
|
|
136
|
+
const sources = dependencySources.get(node.id);
|
|
137
|
+
const annotations = [
|
|
138
|
+
...(metadata ? [theme.fg("dim", metadata)] : []),
|
|
139
|
+
...(sources && sources.length > 0
|
|
140
|
+
? [theme.fg("dim", `depends on: ${sources.map((id) => byId.get(id)?.title ?? id).join(", ")}`)]
|
|
141
|
+
: []),
|
|
142
|
+
];
|
|
143
|
+
return {
|
|
144
|
+
label,
|
|
145
|
+
children: children.length > 0 ? children : undefined,
|
|
146
|
+
component: annotations.length > 0 ? textLine(annotations.join("\n")) : undefined,
|
|
147
|
+
};
|
|
119
148
|
};
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
149
|
+
|
|
150
|
+
const tree: TreeNode[] = [];
|
|
151
|
+
for (const root of details.nodes.filter((node) => !contained.has(node.id))) {
|
|
152
|
+
if (!visited.has(root.id)) tree.push(buildNode(root));
|
|
153
|
+
}
|
|
154
|
+
for (const node of details.nodes) {
|
|
155
|
+
if (!visited.has(node.id)) tree.push(buildNode(node));
|
|
156
|
+
}
|
|
157
|
+
return tree;
|
|
123
158
|
}
|
|
124
159
|
|
|
125
|
-
/** Bounded task containment preview; dependency
|
|
160
|
+
/** Bounded task containment preview; dependency edges surface as an
|
|
161
|
+
* annotation under the node they target (see toTreeNodes). */
|
|
126
162
|
export class TaskHierarchyPreview implements Component {
|
|
127
163
|
private details: GraphToolDetails;
|
|
128
164
|
private theme: Theme;
|
|
@@ -146,26 +182,12 @@ export class TaskHierarchyPreview implements Component {
|
|
|
146
182
|
render(width: number): string[] {
|
|
147
183
|
const safeWidth = Math.max(1, width);
|
|
148
184
|
if (this.cachedLines && this.cachedWidth === safeWidth) return this.cachedLines;
|
|
149
|
-
const
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
];
|
|
156
|
-
for (const row of rows) {
|
|
157
|
-
const identity = this.expanded ? `${row.node.id} ` : "";
|
|
158
|
-
lines.push(
|
|
159
|
-
truncateToWidth(
|
|
160
|
-
`${row.prefix}${row.connector}${row.connector ? " " : ""}${this.theme.fg("accent", kindGlyph(row.node.kind))} ${this.theme.fg("muted", statusGlyph(row.node.status))} ${this.theme.fg("accent", identity)}${this.theme.fg("text", row.node.title)}`,
|
|
161
|
-
safeWidth,
|
|
162
|
-
),
|
|
163
|
-
);
|
|
164
|
-
if (this.expanded) {
|
|
165
|
-
const metadata = rowMetadata(row.node);
|
|
166
|
-
if (metadata) lines.push(truncateToWidth(this.theme.fg("dim", `${row.prefix} ${metadata}`), safeWidth));
|
|
167
|
-
}
|
|
168
|
-
}
|
|
185
|
+
const header = truncateToWidth(
|
|
186
|
+
this.theme.fg("toolTitle", this.theme.bold(`${this.details.nodes.length} tasks · ${this.details.edges.length} edges`)),
|
|
187
|
+
safeWidth,
|
|
188
|
+
);
|
|
189
|
+
const tree = new TreeView({ nodes: toTreeNodes(this.details, this.theme, this.expanded) });
|
|
190
|
+
const lines = [header, ...tree.render(safeWidth)];
|
|
169
191
|
this.cachedWidth = safeWidth;
|
|
170
192
|
this.cachedLines = lines;
|
|
171
193
|
return lines;
|
|
@@ -37,10 +37,22 @@ interface ToolDetailsBase {
|
|
|
37
37
|
kind: string;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
/** Distinct from the artifact's own lifecycle status (todo/in-progress/done/...) --
|
|
41
|
+
* this is Task Focus's own separate active/paused dimension, carried by
|
|
42
|
+
* tasks.focused/tasks.pause/tasks.unpause's {artifact, status, updatedAt}
|
|
43
|
+
* wrapper shape. Never conflate the two: an artifact can be "in-progress"
|
|
44
|
+
* while its focus is "paused". */
|
|
45
|
+
export interface ArtifactFocusAnnotation {
|
|
46
|
+
status: string;
|
|
47
|
+
updatedAt: string;
|
|
48
|
+
pauseReason?: string;
|
|
49
|
+
}
|
|
50
|
+
|
|
40
51
|
export interface ArtifactToolDetails extends ToolDetailsBase {
|
|
41
52
|
kind: "artifact";
|
|
42
53
|
artifact: ToolArtifact;
|
|
43
54
|
completeness: ResultCompleteness;
|
|
55
|
+
focus?: ArtifactFocusAnnotation;
|
|
44
56
|
}
|
|
45
57
|
|
|
46
58
|
export interface ArtifactListToolDetails extends ToolDetailsBase {
|
|
@@ -150,7 +162,7 @@ function artifactSummary(artifact: Artifact): ToolArtifactSummary {
|
|
|
150
162
|
};
|
|
151
163
|
}
|
|
152
164
|
|
|
153
|
-
export function createArtifactDetails(operation: string, artifact: Artifact): ArtifactToolDetails {
|
|
165
|
+
export function createArtifactDetails(operation: string, artifact: Artifact, focus?: ArtifactFocusAnnotation): ArtifactToolDetails {
|
|
154
166
|
const body = boundedText(artifact.body, TOOL_DETAILS_BODY_MAX_CHARACTERS);
|
|
155
167
|
return {
|
|
156
168
|
schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
|
|
@@ -163,6 +175,7 @@ export function createArtifactDetails(operation: string, artifact: Artifact): Ar
|
|
|
163
175
|
updatedAt: artifact.updated_at,
|
|
164
176
|
},
|
|
165
177
|
completeness: body.completeness,
|
|
178
|
+
...(focus ? { focus } : {}),
|
|
166
179
|
};
|
|
167
180
|
}
|
|
168
181
|
|
|
@@ -334,6 +347,15 @@ function isToolArtifact(value: unknown): value is ToolArtifact {
|
|
|
334
347
|
);
|
|
335
348
|
}
|
|
336
349
|
|
|
350
|
+
function isFocusAnnotation(value: unknown): value is ArtifactFocusAnnotation {
|
|
351
|
+
return (
|
|
352
|
+
isRecord(value) &&
|
|
353
|
+
isBoundedString(value.status) &&
|
|
354
|
+
isBoundedString(value.updatedAt) &&
|
|
355
|
+
(value.pauseReason === undefined || isBoundedString(value.pauseReason))
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
|
|
337
359
|
function isGraphEdge(value: unknown): value is ToolGraphEdge {
|
|
338
360
|
return isRecord(value) && isBoundedString(value.from) && isBoundedString(value.relation) && isBoundedString(value.to);
|
|
339
361
|
}
|
|
@@ -371,7 +393,11 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
|
|
|
371
393
|
|
|
372
394
|
switch (value.kind) {
|
|
373
395
|
case "artifact":
|
|
374
|
-
return isToolArtifact(value.artifact) &&
|
|
396
|
+
return isToolArtifact(value.artifact) &&
|
|
397
|
+
isCompleteness(value.completeness) &&
|
|
398
|
+
(value.focus === undefined || isFocusAnnotation(value.focus))
|
|
399
|
+
? (value as unknown as ArtifactToolDetails)
|
|
400
|
+
: undefined;
|
|
375
401
|
case "artifact-list":
|
|
376
402
|
return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary) &&
|
|
377
403
|
Number.isSafeInteger(value.total) &&
|
|
@@ -16,9 +16,11 @@ import type { Artifact } from "@danypops/papyrus";
|
|
|
16
16
|
import type { VehicleToolRenderers } from "@danypops/vehicle-client-pi";
|
|
17
17
|
import { renderVehicleResult } from "@danypops/vehicle-client-pi/vehicle-render";
|
|
18
18
|
import type { VehicleOperationDescriptor } from "@danypops/vehicle-core";
|
|
19
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
20
|
+
import { type Component, Text } from "@earendil-works/pi-tui";
|
|
19
21
|
import { ArtifactCard } from "./tool-rendering/artifact-card.ts";
|
|
20
22
|
import { ArtifactListCard } from "./tool-rendering/artifact-list.ts";
|
|
21
|
-
import { createArtifactDetails, createArtifactListDetails } from "./tool-rendering/render-model.ts";
|
|
23
|
+
import { type ArtifactFocusAnnotation, createArtifactDetails, createArtifactListDetails } from "./tool-rendering/render-model.ts";
|
|
22
24
|
|
|
23
25
|
function isArtifact(value: unknown): value is Artifact {
|
|
24
26
|
if (typeof value !== "object" || value === null) return false;
|
|
@@ -40,6 +42,35 @@ function isArtifactArray(value: unknown): value is Artifact[] {
|
|
|
40
42
|
return Array.isArray(value) && value.every(isArtifact);
|
|
41
43
|
}
|
|
42
44
|
|
|
45
|
+
/** tasks.focused/tasks.pause/tasks.unpause's own wrapper shape -- an Artifact
|
|
46
|
+
* plus Task Focus's separate active/paused dimension. Detected the same
|
|
47
|
+
* name-independent, shape-based way as isArtifact/isArtifactArray above. */
|
|
48
|
+
interface TaskFocusOutput {
|
|
49
|
+
artifact: Artifact;
|
|
50
|
+
status: string;
|
|
51
|
+
updatedAt: string;
|
|
52
|
+
pauseReason?: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isTaskFocus(value: unknown): value is TaskFocusOutput {
|
|
56
|
+
if (typeof value !== "object" || value === null) return false;
|
|
57
|
+
const row = value as Record<string, unknown>;
|
|
58
|
+
return (
|
|
59
|
+
isArtifact(row.artifact) &&
|
|
60
|
+
typeof row.status === "string" &&
|
|
61
|
+
typeof row.updatedAt === "string" &&
|
|
62
|
+
(row.pauseReason === undefined || typeof row.pauseReason === "string")
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function focusAnnotation(output: TaskFocusOutput): ArtifactFocusAnnotation {
|
|
67
|
+
return { status: output.status, updatedAt: output.updatedAt, ...(output.pauseReason ? { pauseReason: output.pauseReason } : {}) };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function renderNoFocusedTask(theme: Theme): Component {
|
|
71
|
+
return new Text(theme.fg("dim", "No focused task."), 0, 0);
|
|
72
|
+
}
|
|
73
|
+
|
|
43
74
|
export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
|
|
44
75
|
return {
|
|
45
76
|
renderResult(result, options, theme, context) {
|
|
@@ -51,6 +82,19 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
|
|
|
51
82
|
if (isArtifact(output)) {
|
|
52
83
|
return new ArtifactCard(createArtifactDetails(descriptor.name, output), theme, options.expanded);
|
|
53
84
|
}
|
|
85
|
+
if (isTaskFocus(output)) {
|
|
86
|
+
return new ArtifactCard(
|
|
87
|
+
createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output)),
|
|
88
|
+
theme,
|
|
89
|
+
options.expanded,
|
|
90
|
+
);
|
|
91
|
+
}
|
|
92
|
+
// tasks.focused specifically returns null for "nothing focused" --
|
|
93
|
+
// scoped to this one operation so an unrelated null-output operation
|
|
94
|
+
// (e.g. a not-found lookup) is never mislabeled as a focus state.
|
|
95
|
+
if (output === null && descriptor.name === "tasks.focused") {
|
|
96
|
+
return renderNoFocusedTask(theme);
|
|
97
|
+
}
|
|
54
98
|
}
|
|
55
99
|
return renderVehicleResult(descriptor, result, options, theme, context);
|
|
56
100
|
},
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danypops/pi-papyrus",
|
|
3
|
-
"version": "0.43.
|
|
3
|
+
"version": "0.43.5",
|
|
4
4
|
"description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"keywords": ["pi-package"],
|
|
@@ -20,11 +20,11 @@
|
|
|
20
20
|
"@danypops/jittor": "^0.14.0",
|
|
21
21
|
"@danypops/papyrus": "^0.42.0",
|
|
22
22
|
"@danypops/vehicle-client": "^0.2.0",
|
|
23
|
-
"@danypops/vehicle-client-pi": "^0.
|
|
23
|
+
"@danypops/vehicle-client-pi": "^0.5.1",
|
|
24
24
|
"@danypops/vehicle-core": "^0.3.0",
|
|
25
25
|
"@danypops/vehicle-server": "^0.4.1",
|
|
26
26
|
"beautiful-mermaid": "1.1.3",
|
|
27
|
-
"malevich-tui-components": "^0.
|
|
27
|
+
"malevich-tui-components": "^0.20.2"
|
|
28
28
|
},
|
|
29
29
|
"devDependencies": {
|
|
30
30
|
"@earendil-works/pi-coding-agent": "^0.80.10",
|