@danypops/pi-papyrus 0.43.3 → 0.43.4

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
  }
@@ -91,6 +102,10 @@ export class ArtifactCard implements Component {
91
102
  const lines = [truncateToWidth(header, safeWidth)];
92
103
  lines.push(truncateToWidth(this.theme.fg("text", artifact.title), safeWidth));
93
104
 
105
+ if (this.details.focus) {
106
+ lines.push(truncateToWidth(focusLine(this.details.focus, this.theme), safeWidth));
107
+ }
108
+
94
109
  if (this.expanded) {
95
110
  const metadata = [artifact.subtype, ...artifact.labels].filter(Boolean).join(" · ");
96
111
  if (metadata) lines.push(truncateToWidth(this.theme.fg("muted", metadata), safeWidth));
@@ -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) && isCompleteness(value.completeness) ? (value as unknown as ArtifactToolDetails) : undefined;
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",
3
+ "version": "0.43.4",
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,7 +20,7 @@
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.3.3",
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",