@danypops/pi-papyrus 0.60.0 → 0.60.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.
@@ -76,6 +76,7 @@ export class ArtifactListCard implements Component {
76
76
  const omitted = Math.max(0, this.details.total - display.length);
77
77
  if (omitted > 0) lines.push(truncateToWidth(this.theme.fg("dim", `${omitted} more · ${expandHint()}`), safeWidth));
78
78
  }
79
+ if (this.details.hasMore) lines.push(truncateToWidth(this.theme.fg("dim", "More pages available"), safeWidth));
79
80
  this.cachedWidth = safeWidth;
80
81
  this.cachedLines = lines;
81
82
  return lines;
@@ -188,7 +189,20 @@ export class TaskHierarchyPreview implements Component {
188
189
  safeWidth,
189
190
  );
190
191
  const tree = new TreeView({ nodes: toTreeNodes(this.details, this.theme, this.expanded) });
191
- const lines = [header, ...tree.render(safeWidth)];
192
+ const rendered = tree.render(safeWidth);
193
+ const displayed = rendered.slice(0, this.expanded ? 200 : TOOL_COLLAPSED_ROW_LIMIT);
194
+ const lines = [header, ...displayed];
195
+ if (rendered.length > displayed.length) {
196
+ lines.push(
197
+ truncateToWidth(
198
+ this.theme.fg("dim", `${rendered.length - displayed.length} more lines · ${this.expanded ? "display limit" : expandHint()}`),
199
+ safeWidth,
200
+ ),
201
+ );
202
+ }
203
+ if (this.details.nodeCompleteness.truncated || this.details.edgeCompleteness.truncated) {
204
+ lines.push(truncateToWidth(this.theme.fg("dim", "Partial graph · presentation limit"), safeWidth));
205
+ }
192
206
  this.cachedWidth = safeWidth;
193
207
  this.cachedLines = lines;
194
208
  return lines;
@@ -22,6 +22,7 @@ export interface ArtifactListToolDetails extends ToolDetailsBase {
22
22
  kind: "artifact-list";
23
23
  rows: ToolArtifactSummary[];
24
24
  total: number;
25
+ hasMore?: boolean;
25
26
  completeness: ResultCompleteness;
26
27
  }
27
28
 
@@ -83,6 +83,7 @@ export function parsePapyrusToolDetails(value: unknown): PapyrusToolDetails | un
83
83
  return isBoundedArray(value.rows, TOOL_DETAILS_MAX_ITEMS, isArtifactSummary) &&
84
84
  Number.isSafeInteger(value.total) &&
85
85
  Number(value.total) >= value.rows.length &&
86
+ (value.hasMore === undefined || typeof value.hasMore === "boolean") &&
86
87
  isCompleteness(value.completeness)
87
88
  ? (value as unknown as ArtifactListToolDetails)
88
89
  : undefined;
@@ -1,3 +1,4 @@
1
+ import { stripVTControlCharacters } from "node:util";
1
2
  import { TOOL_DETAILS_BODY_MAX_CHARACTERS } from "@danypops/papyrus";
2
3
  import { boundedText, PAPYRUS_TOOL_DETAILS_SCHEMA, type ResultCompleteness, type ToolDetailsBase } from "./shared.ts";
3
4
 
@@ -8,8 +9,15 @@ export interface SemanticTextToolDetails extends ToolDetailsBase {
8
9
  completeness: ResultCompleteness;
9
10
  }
10
11
 
12
+ /** Preserves diagnostic text while removing terminal commands and normalizing line breaks and tabs. */
13
+ export function plainTerminalText(text: string): string {
14
+ return stripVTControlCharacters(text)
15
+ .replace(/\r\n?/g, "\n")
16
+ .replace(/\p{Cc}/gu, (character) => (character === "\n" ? "\n" : character === "\t" ? " " : ""));
17
+ }
18
+
11
19
  export function createSemanticTextDetails(operation: string, text: string): SemanticTextToolDetails {
12
- const bounded = boundedText(text, TOOL_DETAILS_BODY_MAX_CHARACTERS);
20
+ const bounded = boundedText(plainTerminalText(text), TOOL_DETAILS_BODY_MAX_CHARACTERS);
13
21
  return {
14
22
  schemaVersion: PAPYRUS_TOOL_DETAILS_SCHEMA,
15
23
  kind: "semantic-text",
@@ -0,0 +1,38 @@
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, Text, truncateToWidth } from "@earendil-works/pi-tui";
5
+ import { createSemanticTextDetails, type SemanticTextToolDetails } from "./render-model/semantic-text.ts";
6
+
7
+ const MAX_EXPANDED_TEXT_ROWS = 200;
8
+
9
+ /** Renders diagnostic text with host-owned styles and bounded collapsed or expanded rows. */
10
+ export class SemanticTextCard implements Component {
11
+ private readonly details: SemanticTextToolDetails;
12
+
13
+ constructor(
14
+ details: SemanticTextToolDetails,
15
+ private readonly theme: Theme,
16
+ private readonly expanded: boolean,
17
+ ) {
18
+ const normalized = createSemanticTextDetails(details.operation, details.text);
19
+ this.details = { ...normalized, completeness: details.completeness.truncated ? details.completeness : normalized.completeness };
20
+ }
21
+
22
+ render(width: number): string[] {
23
+ if (width < 1) return [];
24
+ const lines = new Text(this.details.text, 0, 0).render(width);
25
+ const limit = this.expanded ? MAX_EXPANDED_TEXT_ROWS : TOOL_COLLAPSED_ROW_LIMIT;
26
+ const visible = lines.slice(0, limit).map((line) => truncateToWidth(this.theme.fg("toolOutput", line), width));
27
+ const omitted = lines.length - visible.length;
28
+ if (omitted > 0) {
29
+ const hint = this.expanded ? `${omitted} more lines omitted · display limit` : `${omitted} more lines · ${expandHint()}`;
30
+ visible.push(truncateToWidth(this.theme.fg("dim", hint), width));
31
+ } else if (this.details.completeness.truncated) {
32
+ visible.push(truncateToWidth(this.theme.fg("dim", "Output truncated"), width));
33
+ }
34
+ return visible;
35
+ }
36
+
37
+ invalidate(): void {}
38
+ }
@@ -23,7 +23,7 @@ import type { JsonValue, VehicleOperationDescriptor } from "@danypops/vehicle-co
23
23
  import type { Theme } from "@earendil-works/pi-coding-agent";
24
24
  import { type Component, Text } from "@earendil-works/pi-tui";
25
25
  import { ArtifactCard } from "../../tool-rendering/artifact-card.ts";
26
- import { ArtifactListCard } from "../../tool-rendering/artifact-list.ts";
26
+ import { ArtifactListCard, TaskHierarchyPreview } from "../../tool-rendering/artifact-list.ts";
27
27
  import {
28
28
  createArtifactDetails,
29
29
  createArtifactListDetails,
@@ -40,6 +40,7 @@ import {
40
40
  type PapyrusToolDetails,
41
41
  parsePapyrusToolDetails,
42
42
  } from "../../tool-rendering/render-model.ts";
43
+ import { SemanticTextCard } from "../../tool-rendering/semantic-text.ts";
43
44
  import { recordRenderDiagnostic, shapeFingerprint } from "../render-diagnostics.ts";
44
45
  import { batchOutcomeSummary } from "./batch.ts";
45
46
  import {
@@ -66,6 +67,7 @@ import {
66
67
  renderNoFocusedTask,
67
68
  semanticText,
68
69
  } from "./shared.ts";
70
+ import { taskGraphPresentation, taskPagePresentation } from "./task-collections.ts";
69
71
  import { isTaskCompletion, renderTaskCompletion } from "./task-completion.ts";
70
72
  import { isTaskExecutionPlan, renderTaskExecutionPlan } from "./task-execution.ts";
71
73
 
@@ -134,6 +136,9 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
134
136
  if (isTaskCompletion(output)) {
135
137
  return renderTaskCompletion(output, theme, options.expanded);
136
138
  }
139
+ if (isSemanticTextOutput(output)) {
140
+ return new SemanticTextCard(createSemanticTextDetails(descriptor.name, semanticText(output)), theme, options.expanded);
141
+ }
137
142
  recordRenderDiagnostic({ event: "render-result-fell-through-to-generic", operation: descriptor.name });
138
143
  }
139
144
  return renderVehicleResult(descriptor, result, options, theme, context);
@@ -151,6 +156,11 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
151
156
  * instead of silently persisting and rendering raw JSON.
152
157
  */
153
158
  function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, output: unknown): PapyrusToolDetails {
159
+ if (descriptor.name === "tasks.graph" || descriptor.name === "tasks.list_page") {
160
+ const presentation = descriptor.name === "tasks.graph" ? taskGraphPresentation(output) : taskPagePresentation(output);
161
+ if (presentation) return presentation;
162
+ throw new Error(`${descriptor.name} produced no legal presentation variant`);
163
+ }
154
164
  if (descriptor.name === "batch.execute") {
155
165
  const summary = batchOutcomeSummary(output);
156
166
  if (summary !== undefined) return createSemanticTextDetails(descriptor.name, summary);
@@ -217,9 +227,10 @@ function renderFromPapyrusPresentation(
217
227
  case "preview":
218
228
  return new Text(theme.fg("toolOutput", presentation.content), 0, 0);
219
229
  case "semantic-text":
220
- return new Text(theme.fg("toolOutput", presentation.text), 0, 0);
221
- case "transition":
230
+ return new SemanticTextCard(presentation, theme, expanded);
222
231
  case "graph":
232
+ return new TaskHierarchyPreview(presentation, theme, expanded);
233
+ case "transition":
223
234
  case "gate-run":
224
235
  case "invocation":
225
236
  case "error":
@@ -0,0 +1,64 @@
1
+ import { TASK_EXECUTION_MAX_EDGES, TASK_EXECUTION_MAX_NODES, TASK_LIST_PAGE_MAX_LIMIT, type TaskNode } from "@danypops/papyrus";
2
+ import { type ArtifactListToolDetails, createArtifactListDetails } from "../../tool-rendering/render-model/artifact.ts";
3
+ import { createGraphDetails, type GraphToolDetails, type ToolGraphEdge } from "../../tool-rendering/render-model/graph.ts";
4
+ import { isRecord } from "../../tool-rendering/render-model/shared.ts";
5
+ import { isArtifact, isArtifactArray } from "./shared.ts";
6
+
7
+ function identifiers(value: unknown): value is string[] {
8
+ return (
9
+ Array.isArray(value) &&
10
+ value.length <= TASK_EXECUTION_MAX_NODES &&
11
+ value.every((id) => typeof id === "string" && id.length > 0 && id.length <= 500)
12
+ );
13
+ }
14
+
15
+ function graphNode(value: unknown): value is TaskNode {
16
+ return (
17
+ isRecord(value) &&
18
+ isArtifact(value.task) &&
19
+ identifiers(value.parentIds) &&
20
+ identifiers(value.childIds) &&
21
+ identifiers(value.dependencyIds)
22
+ );
23
+ }
24
+
25
+ /** Projects one cursor page while keeping its continuation token in the operation result only. */
26
+ export function taskPagePresentation(output: unknown): ArtifactListToolDetails | undefined {
27
+ if (!isRecord(output) || !Array.isArray(output.items) || output.items.length > TASK_LIST_PAGE_MAX_LIMIT || !isArtifactArray(output.items))
28
+ return undefined;
29
+ if (
30
+ output.nextCursor !== undefined &&
31
+ (typeof output.nextCursor !== "string" || output.nextCursor.length === 0 || output.nextCursor.length > 16384)
32
+ )
33
+ return undefined;
34
+ return { ...createArtifactListDetails("tasks.list_page", output.items), hasMore: output.nextCursor !== undefined };
35
+ }
36
+
37
+ /** Projects selected tasks and their typed relationships into a bounded hierarchy. */
38
+ export function taskGraphPresentation(output: unknown): GraphToolDetails | undefined {
39
+ if (
40
+ !isRecord(output) ||
41
+ !Array.isArray(output.nodes) ||
42
+ output.nodes.length > TASK_EXECUTION_MAX_NODES ||
43
+ !identifiers(output.rootIds) ||
44
+ !output.nodes.every(graphNode)
45
+ )
46
+ return undefined;
47
+ const edges = new Map<string, ToolGraphEdge>();
48
+ let linkCount = 0;
49
+ for (const node of output.nodes) {
50
+ linkCount += node.parentIds.length + node.childIds.length + node.dependencyIds.length;
51
+ if (linkCount > TASK_EXECUTION_MAX_EDGES * 3) return undefined;
52
+ const add = (from: string, relation: string, to: string) => {
53
+ edges.set(JSON.stringify([from, relation, to]), { from, relation, to });
54
+ };
55
+ for (const parent of node.parentIds) add(parent, "contains", node.task.id);
56
+ for (const child of node.childIds) add(node.task.id, "contains", child);
57
+ for (const dependency of node.dependencyIds) add(dependency, "unlocks", node.task.id);
58
+ }
59
+ return createGraphDetails(
60
+ "tasks.graph",
61
+ output.nodes.map((node) => node.task),
62
+ [...edges.values()],
63
+ );
64
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.60.0",
3
+ "version": "0.60.2",
4
4
  "description": "Pi host extension for Papyrus: native tools, TUI panels, and context injection over the daemon-backed graph store",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -13,14 +13,14 @@
13
13
  "typecheck": "tsc --noEmit -p tsconfig.json"
14
14
  },
15
15
  "peerDependencies": {
16
- "@danypops/vehicle-client-pi": "^0.46.0",
16
+ "@danypops/vehicle-client-pi": "^0.48.3",
17
17
  "@earendil-works/pi-coding-agent": "*",
18
18
  "@earendil-works/pi-tui": "*",
19
19
  "typebox": "*"
20
20
  },
21
21
  "dependencies": {
22
22
  "@danypops/jittor": "^0.19.2",
23
- "@danypops/papyrus": "^0.60.12",
23
+ "@danypops/papyrus": "^0.60.13",
24
24
  "@danypops/vehicle-client": "^0.10.8",
25
25
  "@danypops/vehicle-core": "^0.19.1",
26
26
  "@danypops/vehicle-server": "^0.27.1",
@@ -30,7 +30,7 @@
30
30
  "devDependencies": {
31
31
  "@danypops/pi-extension-harness": "^0.8.3",
32
32
  "@danypops/pi-tui-harness": "^0.0.2",
33
- "@danypops/vehicle-client-pi": "^0.46.0",
33
+ "@danypops/vehicle-client-pi": "^0.48.3",
34
34
  "@danypops/vehicle-conformance": "^0.3.0",
35
35
  "@earendil-works/pi-coding-agent": "^0.80.10",
36
36
  "bun-types": "latest",