@danypops/pi-papyrus 0.45.6 → 0.45.8

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.
@@ -18,8 +18,8 @@ import type { VehicleToolRenderers } from "@danypops/vehicle-client-pi";
18
18
  import { renderVehicleResult } from "@danypops/vehicle-client-pi/vehicle-render";
19
19
  import type { VehicleOperationDescriptor } from "@danypops/vehicle-core";
20
20
  import type { Theme } from "@earendil-works/pi-coding-agent";
21
- import { type Component, Text } from "@earendil-works/pi-tui";
22
- import { type DagEdge, type DagNode, DagView } from "malevich-tui-components";
21
+ import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
22
+ import { buildDetailLines, type DagEdge, type DagNode, DagView, type DetailField, type DetailSection } from "malevich-tui-components";
23
23
  import { ArtifactCard, expandHint, statusColor, statusGlyph } from "./tool-rendering/artifact-card.ts";
24
24
  import { ArtifactListCard } from "./tool-rendering/artifact-list.ts";
25
25
  import { type ArtifactFocusAnnotation, createArtifactDetails, createArtifactListDetails } from "./tool-rendering/render-model.ts";
@@ -121,7 +121,7 @@ function isTaskExecutionPlan(value: unknown): value is TaskExecutionPlanOutput {
121
121
  );
122
122
  }
123
123
 
124
- function renderTaskExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, expanded: boolean): Component {
124
+ function dagViewFromExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, expanded: boolean): DagView {
125
125
  const nodes: DagNode[] = plan.nodes.map((node) => ({
126
126
  id: node.id,
127
127
  label: `${theme.fg(statusColor(node.state), statusGlyph(node.state))} ${theme.fg("text", node.title)}`,
@@ -142,6 +142,255 @@ function renderTaskExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, ex
142
142
  });
143
143
  }
144
144
 
145
+ function renderTaskExecutionPlan(plan: TaskExecutionPlanOutput, theme: Theme, expanded: boolean): Component {
146
+ return dagViewFromExecutionPlan(plan, theme, expanded);
147
+ }
148
+
149
+ /** playbooks.invoke's own PlaybookInvocationResult shape -- a materialized execution plan
150
+ * (same shape tasks.plan renders) plus which docs/rules/tasks were created and which one to
151
+ * focus. Detected the same name-independent, shape-based way as the others in this file. */
152
+ interface PlaybookInvocationResultOutput {
153
+ playbookId: string;
154
+ runId: string;
155
+ created: { docs: string[]; rules: string[]; tasks: string[] };
156
+ rootTaskIds: string[];
157
+ entryTaskId: string;
158
+ execution: TaskExecutionPlanOutput;
159
+ }
160
+
161
+ interface PlaybookMissingArgumentsOutput {
162
+ playbookId: string;
163
+ missingArguments: string[];
164
+ }
165
+
166
+ function isPlaybookInvocationResult(value: unknown): value is PlaybookInvocationResultOutput {
167
+ if (typeof value !== "object" || value === null) return false;
168
+ const row = value as Record<string, unknown>;
169
+ return (
170
+ typeof row.playbookId === "string" &&
171
+ typeof row.runId === "string" &&
172
+ typeof row.entryTaskId === "string" &&
173
+ Array.isArray(row.rootTaskIds) &&
174
+ typeof row.created === "object" &&
175
+ row.created !== null &&
176
+ Array.isArray((row.created as Record<string, unknown>).docs) &&
177
+ Array.isArray((row.created as Record<string, unknown>).rules) &&
178
+ Array.isArray((row.created as Record<string, unknown>).tasks) &&
179
+ isTaskExecutionPlan(row.execution)
180
+ );
181
+ }
182
+
183
+ function isPlaybookMissingArguments(value: unknown): value is PlaybookMissingArgumentsOutput {
184
+ if (typeof value !== "object" || value === null) return false;
185
+ const row = value as Record<string, unknown>;
186
+ return (
187
+ typeof row.playbookId === "string" &&
188
+ Array.isArray(row.missingArguments) &&
189
+ row.missingArguments.every((entry) => typeof entry === "string")
190
+ );
191
+ }
192
+
193
+ function renderPlaybookInvocationResult(result: PlaybookInvocationResultOutput, theme: Theme, expanded: boolean): Component {
194
+ const dag = dagViewFromExecutionPlan(result.execution, theme, expanded);
195
+ const counts = [
196
+ ["task", result.created.tasks.length],
197
+ ["rule", result.created.rules.length],
198
+ ["doc", result.created.docs.length],
199
+ ] as const;
200
+ const summary = counts
201
+ .filter(([, count]) => count > 0)
202
+ .map(([noun, count]) => `${count} ${noun}${count === 1 ? "" : "s"}`)
203
+ .join(", ");
204
+ return {
205
+ render: (width: number) => [...dag.render(width), truncateToWidth(theme.fg("dim", summary || "Nothing created."), width)],
206
+ invalidate: () => dag.invalidate(),
207
+ };
208
+ }
209
+
210
+ function renderPlaybookMissingArguments(result: PlaybookMissingArgumentsOutput, theme: Theme): Component {
211
+ const line = theme.fg("warning", `Missing required argument(s): ${result.missingArguments.join(", ")}`);
212
+ return new Text(line, 0, 0);
213
+ }
214
+
215
+ /** A Discussion round -- discuss.open/reply/show/rounds' own transcript entry. Detected the
216
+ * same name-independent, shape-based way as the others in this file. */
217
+ interface DiscussionRoundOutput {
218
+ roundNumber: number;
219
+ actor: string;
220
+ content: string;
221
+ }
222
+
223
+ function isDiscussionRound(value: unknown): value is DiscussionRoundOutput {
224
+ if (typeof value !== "object" || value === null) return false;
225
+ const row = value as Record<string, unknown>;
226
+ return typeof row.roundNumber === "number" && typeof row.actor === "string" && typeof row.content === "string";
227
+ }
228
+
229
+ function isDiscussionRoundArray(value: unknown): value is DiscussionRoundOutput[] {
230
+ return Array.isArray(value) && value.every(isDiscussionRound);
231
+ }
232
+
233
+ interface DiscussionAndRoundsOutput {
234
+ discussion: Artifact;
235
+ rounds: DiscussionRoundOutput[];
236
+ }
237
+
238
+ function isDiscussionAndRounds(value: unknown): value is DiscussionAndRoundsOutput {
239
+ if (typeof value !== "object" || value === null) return false;
240
+ const row = value as Record<string, unknown>;
241
+ return isArtifact(row.discussion) && isDiscussionRoundArray(row.rounds);
242
+ }
243
+
244
+ interface DiscussionRoundsOnlyOutput {
245
+ rounds: DiscussionRoundOutput[];
246
+ }
247
+
248
+ function isDiscussionRoundsOnly(value: unknown): value is DiscussionRoundsOnlyOutput {
249
+ if (typeof value !== "object" || value === null) return false;
250
+ const row = value as Record<string, unknown>;
251
+ return row.discussion === undefined && isDiscussionRoundArray(row.rounds);
252
+ }
253
+
254
+ interface DiscussionListOutput {
255
+ discussions: Artifact[];
256
+ }
257
+
258
+ function isDiscussionListOutput(value: unknown): value is DiscussionListOutput {
259
+ if (typeof value !== "object" || value === null) return false;
260
+ const row = value as Record<string, unknown>;
261
+ return isArtifactArray(row.discussions);
262
+ }
263
+
264
+ const discussTheme = (theme: Theme) => ({
265
+ field: (s: string) => theme.fg("text", s),
266
+ heading: (s: string) => theme.fg("toolTitle", theme.bold(s)),
267
+ byline: (s: string) => theme.fg("muted", s),
268
+ body: (s: string) => theme.fg("text", s),
269
+ });
270
+
271
+ function roundsSection(rounds: readonly DiscussionRoundOutput[]): DetailSection {
272
+ return {
273
+ heading: `Rounds (${rounds.length}):`,
274
+ items: rounds.map((round) => ({ byline: `${round.actor} · round ${round.roundNumber}`, body: round.content })),
275
+ };
276
+ }
277
+
278
+ function renderDiscussionAndRounds(output: DiscussionAndRoundsOutput, theme: Theme, expanded: boolean): Component {
279
+ const discussion = output.discussion;
280
+ return {
281
+ render: (width: number) => {
282
+ const safeWidth = Math.max(1, width);
283
+ const fields: DetailField[] = [
284
+ { label: "Title", value: discussion.title },
285
+ { label: "Status", value: theme.fg(statusColor(discussion.status), `${statusGlyph(discussion.status)} ${discussion.status}`) },
286
+ ];
287
+ const sections: DetailSection[] = expanded && output.rounds.length > 0 ? [roundsSection(output.rounds)] : [];
288
+ const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: discussTheme(theme) });
289
+ if (!expanded && output.rounds.length > 0) {
290
+ const count = output.rounds.length;
291
+ lines.push(truncateToWidth(theme.fg("dim", `${count} round${count === 1 ? "" : "s"} · ${expandHint()}`), safeWidth));
292
+ }
293
+ return lines;
294
+ },
295
+ invalidate: () => {},
296
+ };
297
+ }
298
+
299
+ function renderDiscussionRoundsOnly(output: DiscussionRoundsOnlyOutput, theme: Theme): Component {
300
+ return {
301
+ render: (width: number) => {
302
+ const sections: DetailSection[] = output.rounds.length > 0 ? [roundsSection(output.rounds)] : [{ lines: ["No rounds."] }];
303
+ return buildDetailLines(Math.max(1, width), { sections, theme: discussTheme(theme) });
304
+ },
305
+ invalidate: () => {},
306
+ };
307
+ }
308
+
309
+ /** tasks.complete's own TaskCompletion shape -- a completed (or rejected) task plus its own
310
+ * gate/checklist proof run and any dependents still left blocked. Detected the same
311
+ * name-independent, shape-based way as the others in this file. */
312
+ interface TaskGateResultOutput {
313
+ gate: unknown;
314
+ passed: boolean;
315
+ output: string;
316
+ }
317
+
318
+ interface TaskChecklistReviewOutput {
319
+ item: string;
320
+ accepted: boolean;
321
+ reason?: string;
322
+ }
323
+
324
+ interface TaskBlockageOutput {
325
+ artifact: Artifact;
326
+ dependencyIds: string[];
327
+ }
328
+
329
+ interface TaskCompletionOutput {
330
+ artifact: Artifact;
331
+ gates: TaskGateResultOutput[];
332
+ checklist: TaskChecklistReviewOutput[];
333
+ completed: boolean;
334
+ focused: Artifact | null;
335
+ blocked: TaskBlockageOutput[];
336
+ }
337
+
338
+ function isTaskCompletion(value: unknown): value is TaskCompletionOutput {
339
+ if (typeof value !== "object" || value === null) return false;
340
+ const row = value as Record<string, unknown>;
341
+ return (
342
+ isArtifact(row.artifact) &&
343
+ Array.isArray(row.gates) &&
344
+ Array.isArray(row.checklist) &&
345
+ typeof row.completed === "boolean" &&
346
+ (row.focused === null || isArtifact(row.focused)) &&
347
+ Array.isArray(row.blocked)
348
+ );
349
+ }
350
+
351
+ function renderTaskCompletion(result: TaskCompletionOutput, theme: Theme, expanded: boolean): Component {
352
+ const task = result.artifact;
353
+ return {
354
+ render: (width: number) => {
355
+ const safeWidth = Math.max(1, width);
356
+ const fields: DetailField[] = [
357
+ { label: "Title", value: task.title },
358
+ { label: "Status", value: theme.fg(statusColor(task.status), `${statusGlyph(task.status)} ${task.status}`) },
359
+ ];
360
+ const sections: DetailSection[] = [];
361
+ if (result.gates.length > 0) {
362
+ sections.push({
363
+ heading: "Gates:",
364
+ lines: result.gates.map((gate) => theme.fg(gate.passed ? "success" : "error", `${gate.passed ? "✓" : "✗"} ${gate.output}`)),
365
+ });
366
+ }
367
+ if (result.checklist.length > 0) {
368
+ sections.push({
369
+ heading: "Checklist:",
370
+ lines: result.checklist.map((entry) =>
371
+ theme.fg(
372
+ entry.accepted ? "success" : "error",
373
+ `${entry.accepted ? "✓" : "✗"} ${entry.item}${entry.reason ? ` — ${entry.reason}` : ""}`,
374
+ ),
375
+ ),
376
+ });
377
+ }
378
+ if (result.blocked.length > 0) {
379
+ sections.push({
380
+ heading: "Still blocked:",
381
+ lines: result.blocked.map((entry) => theme.fg("warning", `◼ ${entry.artifact.title}`)),
382
+ });
383
+ }
384
+ const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: discussTheme(theme) });
385
+ if (result.focused && expanded) {
386
+ lines.push(truncateToWidth(theme.fg("accent", `▶ focus ${result.focused.title}`), safeWidth));
387
+ }
388
+ return lines;
389
+ },
390
+ invalidate: () => {},
391
+ };
392
+ }
393
+
145
394
  export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
146
395
  return {
147
396
  renderResult(result, options, theme, context) {
@@ -169,6 +418,24 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
169
418
  if (isTaskExecutionPlan(output)) {
170
419
  return renderTaskExecutionPlan(output, theme, options.expanded);
171
420
  }
421
+ if (isPlaybookInvocationResult(output)) {
422
+ return renderPlaybookInvocationResult(output, theme, options.expanded);
423
+ }
424
+ if (isPlaybookMissingArguments(output)) {
425
+ return renderPlaybookMissingArguments(output, theme);
426
+ }
427
+ if (isDiscussionAndRounds(output)) {
428
+ return renderDiscussionAndRounds(output, theme, options.expanded);
429
+ }
430
+ if (isDiscussionRoundsOnly(output)) {
431
+ return renderDiscussionRoundsOnly(output, theme);
432
+ }
433
+ if (isDiscussionListOutput(output)) {
434
+ return new ArtifactListCard(createArtifactListDetails(descriptor.name, output.discussions), theme, options.expanded);
435
+ }
436
+ if (isTaskCompletion(output)) {
437
+ return renderTaskCompletion(output, theme, options.expanded);
438
+ }
172
439
  }
173
440
  return renderVehicleResult(descriptor, result, options, theme, context);
174
441
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.45.6",
3
+ "version": "0.45.8",
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"],