@danypops/pi-papyrus 0.45.7 → 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.
@@ -19,7 +19,7 @@ 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
21
  import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
22
- import { type DagEdge, type DagNode, DagView } from "malevich-tui-components";
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";
@@ -212,6 +212,185 @@ function renderPlaybookMissingArguments(result: PlaybookMissingArgumentsOutput,
212
212
  return new Text(line, 0, 0);
213
213
  }
214
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
+
215
394
  export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
216
395
  return {
217
396
  renderResult(result, options, theme, context) {
@@ -245,6 +424,18 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
245
424
  if (isPlaybookMissingArguments(output)) {
246
425
  return renderPlaybookMissingArguments(output, theme);
247
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
+ }
248
439
  }
249
440
  return renderVehicleResult(descriptor, result, options, theme, context);
250
441
  },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@danypops/pi-papyrus",
3
- "version": "0.45.7",
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"],