@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