@danypops/pi-papyrus 0.45.7 → 0.45.9

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.
@@ -1,8 +1,21 @@
1
1
  import type { Theme } from "@earendil-works/pi-coding-agent";
2
2
  import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
3
- import { buildDetailLines, type DetailField, type DetailSection } from "malevich-tui-components";
3
+ import { buildDetailLines, type DetailField, type DetailSection, type DetailViewTheme } from "malevich-tui-components";
4
4
  import type { ArtifactToolDetails } from "./render-model.ts";
5
5
 
6
+ /** Shared by every buildDetailLines caller in this extension (ArtifactCard, and
7
+ * vehicle-artifact-renderers.ts's discuss/tasks.complete renderers) -- one Theme -> DetailViewTheme
8
+ * mapping instead of the same four-field object literal re-typed at each call site. */
9
+ export function detailViewTheme(theme: Theme): DetailViewTheme {
10
+ return {
11
+ field: (s) => theme.fg("text", s),
12
+ heading: (s) => theme.fg("toolTitle", theme.bold(s)),
13
+ byline: (s) => theme.fg("muted", s),
14
+ body: (s) => theme.fg("text", s),
15
+ line: (s) => theme.fg("warning", s),
16
+ };
17
+ }
18
+
6
19
  const KIND_GLYPHS: Readonly<Record<string, string>> = {
7
20
  task: "◇",
8
21
  doc: "▤",
@@ -100,6 +113,7 @@ export class ArtifactCard implements Component {
100
113
  const status = this.theme.fg(statusColor(artifact.status), `${statusGlyph(artifact.status)} ${artifact.status}`);
101
114
  return [
102
115
  { label: "Title", value: artifact.title },
116
+ { label: "Alias", value: artifact.alias ?? artifact.id },
103
117
  { label: "Kind", value: `${kindGlyph(artifact.kind)} ${artifact.kind}` },
104
118
  { label: "Status", value: status },
105
119
  ...(this.expanded ? [{ label: "ID", value: artifact.id }] : []),
@@ -128,13 +142,7 @@ export class ArtifactCard implements Component {
128
142
  fields: this.fields(),
129
143
  sections: this.sections(),
130
144
  alignFields: true,
131
- theme: {
132
- field: (s) => theme.fg("text", s),
133
- heading: (s) => theme.fg("toolTitle", theme.bold(s)),
134
- byline: (s) => theme.fg("muted", s),
135
- body: (s) => theme.fg("text", s),
136
- line: (s) => theme.fg("warning", s),
137
- },
145
+ theme: detailViewTheme(theme),
138
146
  });
139
147
 
140
148
  if (this.details.focus) {
@@ -18,6 +18,8 @@ export interface ResultCompleteness {
18
18
 
19
19
  export interface ToolArtifactSummary {
20
20
  id: string;
21
+ /** Optional only for a detail object persisted before this field existed -- ArtifactCard falls back to id when absent. */
22
+ alias?: string;
21
23
  kind: string;
22
24
  title: string;
23
25
  status: string;
@@ -154,6 +156,7 @@ function boundedText(value: string, maximum: number): { value: string; completen
154
156
  function artifactSummary(artifact: Artifact): ToolArtifactSummary {
155
157
  return {
156
158
  id: artifact.id,
159
+ alias: artifact.alias,
157
160
  kind: artifact.kind,
158
161
  title: artifact.title,
159
162
  status: artifact.status,
@@ -19,8 +19,8 @@ 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";
23
- import { ArtifactCard, expandHint, statusColor, statusGlyph } from "./tool-rendering/artifact-card.ts";
22
+ import { buildDetailLines, type DagEdge, type DagNode, DagView, type DetailField, type DetailSection } from "malevich-tui-components";
23
+ import { ArtifactCard, detailViewTheme, 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";
26
26
 
@@ -212,6 +212,175 @@ 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
+ function roundsSection(rounds: readonly DiscussionRoundOutput[]): DetailSection {
265
+ return {
266
+ heading: `Rounds (${rounds.length}):`,
267
+ items: rounds.map((round) => ({ byline: `${round.actor} · round ${round.roundNumber}`, body: round.content })),
268
+ };
269
+ }
270
+
271
+ /** A one-shot render function with nothing to invalidate -- every buildDetailLines-based
272
+ * renderer in this file (unlike ArtifactCard/DagView) has no cache to clear. */
273
+ function statelessComponent(render: (width: number) => string[]): Component {
274
+ return { render, invalidate: () => {} };
275
+ }
276
+
277
+ function renderDiscussionAndRounds(output: DiscussionAndRoundsOutput, theme: Theme, expanded: boolean): Component {
278
+ const discussion = output.discussion;
279
+ return statelessComponent((width) => {
280
+ const safeWidth = Math.max(1, width);
281
+ const fields: DetailField[] = [
282
+ { label: "Title", value: discussion.title },
283
+ { label: "Status", value: theme.fg(statusColor(discussion.status), `${statusGlyph(discussion.status)} ${discussion.status}`) },
284
+ ];
285
+ const sections: DetailSection[] = expanded && output.rounds.length > 0 ? [roundsSection(output.rounds)] : [];
286
+ const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: detailViewTheme(theme) });
287
+ if (!expanded && output.rounds.length > 0) {
288
+ const count = output.rounds.length;
289
+ lines.push(truncateToWidth(theme.fg("dim", `${count} round${count === 1 ? "" : "s"} · ${expandHint()}`), safeWidth));
290
+ }
291
+ return lines;
292
+ });
293
+ }
294
+
295
+ function renderDiscussionRoundsOnly(output: DiscussionRoundsOnlyOutput, theme: Theme): Component {
296
+ return statelessComponent((width) => {
297
+ const sections: DetailSection[] = output.rounds.length > 0 ? [roundsSection(output.rounds)] : [{ lines: ["No rounds."] }];
298
+ return buildDetailLines(Math.max(1, width), { sections, theme: detailViewTheme(theme) });
299
+ });
300
+ }
301
+
302
+ /** tasks.complete's own TaskCompletion shape -- a completed (or rejected) task plus its own
303
+ * gate/checklist proof run and any dependents still left blocked. Detected the same
304
+ * name-independent, shape-based way as the others in this file. */
305
+ interface TaskGateResultOutput {
306
+ gate: unknown;
307
+ passed: boolean;
308
+ output: string;
309
+ }
310
+
311
+ interface TaskChecklistReviewOutput {
312
+ item: string;
313
+ accepted: boolean;
314
+ reason?: string;
315
+ }
316
+
317
+ interface TaskBlockageOutput {
318
+ artifact: Artifact;
319
+ dependencyIds: string[];
320
+ }
321
+
322
+ interface TaskCompletionOutput {
323
+ artifact: Artifact;
324
+ gates: TaskGateResultOutput[];
325
+ checklist: TaskChecklistReviewOutput[];
326
+ completed: boolean;
327
+ focused: Artifact | null;
328
+ blocked: TaskBlockageOutput[];
329
+ }
330
+
331
+ function isTaskCompletion(value: unknown): value is TaskCompletionOutput {
332
+ if (typeof value !== "object" || value === null) return false;
333
+ const row = value as Record<string, unknown>;
334
+ return (
335
+ isArtifact(row.artifact) &&
336
+ Array.isArray(row.gates) &&
337
+ Array.isArray(row.checklist) &&
338
+ typeof row.completed === "boolean" &&
339
+ (row.focused === null || isArtifact(row.focused)) &&
340
+ Array.isArray(row.blocked)
341
+ );
342
+ }
343
+
344
+ function renderTaskCompletion(result: TaskCompletionOutput, theme: Theme, expanded: boolean): Component {
345
+ const task = result.artifact;
346
+ return statelessComponent((width) => {
347
+ const safeWidth = Math.max(1, width);
348
+ const fields: DetailField[] = [
349
+ { label: "Title", value: task.title },
350
+ { label: "Status", value: theme.fg(statusColor(task.status), `${statusGlyph(task.status)} ${task.status}`) },
351
+ ];
352
+ const sections: DetailSection[] = [];
353
+ if (result.gates.length > 0) {
354
+ sections.push({
355
+ heading: "Gates:",
356
+ lines: result.gates.map((gate) => theme.fg(gate.passed ? "success" : "error", `${gate.passed ? "✓" : "✗"} ${gate.output}`)),
357
+ });
358
+ }
359
+ if (result.checklist.length > 0) {
360
+ sections.push({
361
+ heading: "Checklist:",
362
+ lines: result.checklist.map((entry) =>
363
+ theme.fg(
364
+ entry.accepted ? "success" : "error",
365
+ `${entry.accepted ? "✓" : "✗"} ${entry.item}${entry.reason ? ` — ${entry.reason}` : ""}`,
366
+ ),
367
+ ),
368
+ });
369
+ }
370
+ if (result.blocked.length > 0) {
371
+ sections.push({
372
+ heading: "Still blocked:",
373
+ lines: result.blocked.map((entry) => theme.fg("warning", `◼ ${entry.artifact.title}`)),
374
+ });
375
+ }
376
+ const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: detailViewTheme(theme) });
377
+ if (result.focused && expanded) {
378
+ lines.push(truncateToWidth(theme.fg("accent", `▶ focus ${result.focused.title}`), safeWidth));
379
+ }
380
+ return lines;
381
+ });
382
+ }
383
+
215
384
  export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
216
385
  return {
217
386
  renderResult(result, options, theme, context) {
@@ -245,6 +414,18 @@ export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor):
245
414
  if (isPlaybookMissingArguments(output)) {
246
415
  return renderPlaybookMissingArguments(output, theme);
247
416
  }
417
+ if (isDiscussionAndRounds(output)) {
418
+ return renderDiscussionAndRounds(output, theme, options.expanded);
419
+ }
420
+ if (isDiscussionRoundsOnly(output)) {
421
+ return renderDiscussionRoundsOnly(output, theme);
422
+ }
423
+ if (isDiscussionListOutput(output)) {
424
+ return new ArtifactListCard(createArtifactListDetails(descriptor.name, output.discussions), theme, options.expanded);
425
+ }
426
+ if (isTaskCompletion(output)) {
427
+ return renderTaskCompletion(output, theme, options.expanded);
428
+ }
248
429
  }
249
430
  return renderVehicleResult(descriptor, result, options, theme, context);
250
431
  },
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.9",
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"],