@danypops/pi-papyrus 0.54.0 → 0.54.2
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.
- package/extension/src/artifact/artifact-browser.ts +6 -2
- package/extension/src/tool-rendering/render-model/artifact.ts +82 -0
- package/extension/src/tool-rendering/render-model/discussion.ts +49 -0
- package/extension/src/tool-rendering/render-model/execution-plan.ts +88 -0
- package/extension/src/tool-rendering/render-model/gate-run.ts +55 -0
- package/extension/src/tool-rendering/render-model/graph.ts +43 -0
- package/extension/src/tool-rendering/render-model/index.ts +235 -0
- package/extension/src/tool-rendering/render-model/lease.ts +40 -0
- package/extension/src/tool-rendering/render-model/misc.ts +45 -0
- package/extension/src/tool-rendering/render-model/playbook.ts +107 -0
- package/extension/src/tool-rendering/render-model/shared.ts +163 -0
- package/extension/src/tool-rendering/render-model/task-completion.ts +92 -0
- package/extension/src/tool-rendering/render-model.ts +17 -827
- package/extension/src/tools/renderers/discussion.ts +92 -0
- package/extension/src/tools/renderers/index.ts +236 -0
- package/extension/src/tools/renderers/lease.ts +57 -0
- package/extension/src/tools/renderers/playbook.ts +69 -0
- package/extension/src/tools/renderers/shared.ts +94 -0
- package/extension/src/tools/renderers/task-completion.ts +98 -0
- package/extension/src/tools/renderers/task-execution.ts +79 -0
- package/extension/src/tools/vehicle-artifact-renderers.ts +14 -669
- package/package.json +2 -2
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import type { Artifact } from "@danypops/papyrus";
|
|
2
|
+
import { expandHint } from "@danypops/vehicle-client-pi/expand-hint";
|
|
3
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
4
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
5
|
+
import { buildDetailLines, type DetailField, type DetailSection, statelessComponent } from "malevich-tui-components";
|
|
6
|
+
import { detailViewTheme, measure, statusColor, statusGlyph } from "../../tool-rendering/artifact-card.ts";
|
|
7
|
+
import { isArtifact, isArtifactArray, type RenderableDiscussionParent } from "./shared.ts";
|
|
8
|
+
|
|
9
|
+
/** A Discussion round -- discuss.open/reply/show/rounds' own transcript entry. Detected the
|
|
10
|
+
* same name-independent, shape-based way as the others in this directory. */
|
|
11
|
+
export interface DiscussionRoundOutput {
|
|
12
|
+
roundNumber: number;
|
|
13
|
+
actor: string;
|
|
14
|
+
content: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export function isDiscussionRound(value: unknown): value is DiscussionRoundOutput {
|
|
18
|
+
if (typeof value !== "object" || value === null) return false;
|
|
19
|
+
const row = value as Record<string, unknown>;
|
|
20
|
+
return typeof row.roundNumber === "number" && typeof row.actor === "string" && typeof row.content === "string";
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export function isDiscussionRoundArray(value: unknown): value is DiscussionRoundOutput[] {
|
|
24
|
+
return Array.isArray(value) && value.every(isDiscussionRound);
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export interface DiscussionAndRoundsOutput {
|
|
28
|
+
discussion: Artifact;
|
|
29
|
+
rounds: DiscussionRoundOutput[];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isDiscussionAndRounds(value: unknown): value is DiscussionAndRoundsOutput {
|
|
33
|
+
if (typeof value !== "object" || value === null) return false;
|
|
34
|
+
const row = value as Record<string, unknown>;
|
|
35
|
+
return isArtifact(row.discussion) && isDiscussionRoundArray(row.rounds);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
export interface DiscussionRoundsOnlyOutput {
|
|
39
|
+
rounds: DiscussionRoundOutput[];
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export function isDiscussionRoundsOnly(value: unknown): value is DiscussionRoundsOnlyOutput {
|
|
43
|
+
if (typeof value !== "object" || value === null) return false;
|
|
44
|
+
const row = value as Record<string, unknown>;
|
|
45
|
+
return row.discussion === undefined && isDiscussionRoundArray(row.rounds);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface DiscussionListOutput {
|
|
49
|
+
discussions: Artifact[];
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
export function isDiscussionListOutput(value: unknown): value is DiscussionListOutput {
|
|
53
|
+
if (typeof value !== "object" || value === null) return false;
|
|
54
|
+
const row = value as Record<string, unknown>;
|
|
55
|
+
return isArtifactArray(row.discussions);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export function roundsSection(rounds: readonly DiscussionRoundOutput[]): DetailSection {
|
|
59
|
+
return {
|
|
60
|
+
heading: `Rounds (${rounds.length}):`,
|
|
61
|
+
items: rounds.map((round) => ({ byline: `${round.actor} · round ${round.roundNumber}`, body: round.content })),
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
export function renderDiscussionAndRounds(
|
|
66
|
+
output: { discussion: RenderableDiscussionParent; rounds: readonly DiscussionRoundOutput[] },
|
|
67
|
+
theme: Theme,
|
|
68
|
+
expanded: boolean,
|
|
69
|
+
): Component {
|
|
70
|
+
const discussion = output.discussion;
|
|
71
|
+
return statelessComponent((width) => {
|
|
72
|
+
const safeWidth = Math.max(1, width);
|
|
73
|
+
const fields: DetailField[] = [
|
|
74
|
+
{ label: "Title", value: discussion.title },
|
|
75
|
+
{ label: "Status", value: theme.fg(statusColor(discussion.status), `${statusGlyph(discussion.status)} ${discussion.status}`) },
|
|
76
|
+
];
|
|
77
|
+
const sections: DetailSection[] = expanded && output.rounds.length > 0 ? [roundsSection(output.rounds)] : [];
|
|
78
|
+
const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: detailViewTheme(theme), measure });
|
|
79
|
+
if (!expanded && output.rounds.length > 0) {
|
|
80
|
+
const count = output.rounds.length;
|
|
81
|
+
lines.push(truncateToWidth(theme.fg("dim", `${count} round${count === 1 ? "" : "s"} · ${expandHint()}`), safeWidth));
|
|
82
|
+
}
|
|
83
|
+
return lines;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export function renderDiscussionRoundsOnly(output: DiscussionRoundsOnlyOutput, theme: Theme): Component {
|
|
88
|
+
return statelessComponent((width) => {
|
|
89
|
+
const sections: DetailSection[] = output.rounds.length > 0 ? [roundsSection(output.rounds)] : [{ lines: ["No rounds."] }];
|
|
90
|
+
return buildDetailLines(Math.max(1, width), { sections, theme: detailViewTheme(theme), measure });
|
|
91
|
+
});
|
|
92
|
+
}
|
|
@@ -0,0 +1,236 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Curated result rendering for Papyrus's own Vehicle-projected operations
|
|
3
|
+
* (notes.*, tasks.*, docs.*, rules.*, playbooks.*, artifact.*): reuses the
|
|
4
|
+
* same ArtifactCard/ArtifactListCard components the pre-Vehicle native tool
|
|
5
|
+
* used, instead of the generic Vehicle renderer's raw full-column table
|
|
6
|
+
* dump -- a human reading a task/note list doesn't need id/subtype/extra/
|
|
7
|
+
* timestamps up front, only title + status, with the rest available on
|
|
8
|
+
* expand. Detection is by output shape, not operation name: every operation
|
|
9
|
+
* registered through this client is one of Papyrus's own artifact domains,
|
|
10
|
+
* so "looks like an Artifact" is a safe, name-independent signal here.
|
|
11
|
+
* Falls back to the generic Vehicle renderer for any other output shape
|
|
12
|
+
* (progress, transitions, gate runs, errors).
|
|
13
|
+
*
|
|
14
|
+
* This file is the one place that assembles every result-kind's own detector/renderer into the
|
|
15
|
+
* two exported Registry entry points (papyrusVehicleRenderers/papyrusVehiclePresentations) -- the
|
|
16
|
+
* "wide internal, narrow public" shape: each sibling module in this directory owns one kind's own
|
|
17
|
+
* real complexity, this one only ever needs to know each kind's own type + guard + renderer, never
|
|
18
|
+
* re-derive it. Split out of the former single 670-line vehicle-artifact-renderers.ts as part of a
|
|
19
|
+
* SOLID-audit-driven decomposition (see Doc "Modularity playbook: building-block-shaped
|
|
20
|
+
* TypeScript modules for papyrus/pi-papyrus" and the "pi-papyrus vehicle-artifact-renderers.ts
|
|
21
|
+
* split" child of "Epic: Modularize papyrus/pi-papyrus god-files into building-block modules").
|
|
22
|
+
*/
|
|
23
|
+
|
|
24
|
+
import { TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS } from "@danypops/papyrus";
|
|
25
|
+
import type { PiVehicleInvocationRequest, PiVehiclePresentationContract, VehicleToolRenderers } from "@danypops/vehicle-client-pi";
|
|
26
|
+
import { renderVehicleCall, renderVehicleResult } from "@danypops/vehicle-client-pi/vehicle-render";
|
|
27
|
+
import type { JsonValue, VehicleOperationDescriptor } from "@danypops/vehicle-core";
|
|
28
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
29
|
+
import { type Component, Text } from "@earendil-works/pi-tui";
|
|
30
|
+
import { ArtifactCard } from "../../tool-rendering/artifact-card.ts";
|
|
31
|
+
import { ArtifactListCard } from "../../tool-rendering/artifact-list.ts";
|
|
32
|
+
import {
|
|
33
|
+
createArtifactDetails,
|
|
34
|
+
createArtifactListDetails,
|
|
35
|
+
createDiscussionDetails,
|
|
36
|
+
createExecutionPlanDetails,
|
|
37
|
+
createLeaseDetails,
|
|
38
|
+
createNoFocusDetails,
|
|
39
|
+
createPlaybookInvocationDetails,
|
|
40
|
+
createPlaybookMissingArgumentsDetails,
|
|
41
|
+
createPreviewDetails,
|
|
42
|
+
createTaskCompletionDetails,
|
|
43
|
+
parsePapyrusToolDetails,
|
|
44
|
+
} from "../../tool-rendering/render-model.ts";
|
|
45
|
+
import { recordRenderDiagnostic, shapeFingerprint } from "../render-diagnostics.ts";
|
|
46
|
+
import {
|
|
47
|
+
isDiscussionAndRounds,
|
|
48
|
+
isDiscussionListOutput,
|
|
49
|
+
isDiscussionRoundsOnly,
|
|
50
|
+
renderDiscussionAndRounds,
|
|
51
|
+
renderDiscussionRoundsOnly,
|
|
52
|
+
} from "./discussion.ts";
|
|
53
|
+
import { isTaskLeaseView, renderLease } from "./lease.ts";
|
|
54
|
+
import {
|
|
55
|
+
isPlaybookInvocationResult,
|
|
56
|
+
isPlaybookMissingArguments,
|
|
57
|
+
renderPlaybookInvocationResult,
|
|
58
|
+
renderPlaybookMissingArguments,
|
|
59
|
+
} from "./playbook.ts";
|
|
60
|
+
import { boundedJsonPreview, focusAnnotation, isArtifact, isArtifactArray, isTaskFocus, renderNoFocusedTask } from "./shared.ts";
|
|
61
|
+
import { isTaskCompletion, renderTaskCompletion } from "./task-completion.ts";
|
|
62
|
+
import { isTaskExecutionPlan, renderTaskExecutionPlan } from "./task-execution.ts";
|
|
63
|
+
|
|
64
|
+
export function papyrusVehicleRenderers(descriptor: VehicleOperationDescriptor): VehicleToolRenderers {
|
|
65
|
+
return {
|
|
66
|
+
// Pure pass-through to the generic renderer -- the only reason this exists at all is
|
|
67
|
+
// the /reload investigation (papyrus task 4930cd9b): its absence from the diagnostic
|
|
68
|
+
// log for a real invocation (see onInvoked in vehicle-notes-client.ts) is itself
|
|
69
|
+
// evidence Pi never found ANY renderer -- ours or vehicle-client-pi's generic default
|
|
70
|
+
// -- for that specific tool call, distinct from this renderer running and choosing
|
|
71
|
+
// the generic path internally (which DOES show up here).
|
|
72
|
+
renderCall(args, theme, context) {
|
|
73
|
+
recordRenderDiagnostic({ event: "render-call-invoked", operation: descriptor.name });
|
|
74
|
+
return renderVehicleCall(descriptor, args, theme, context);
|
|
75
|
+
},
|
|
76
|
+
renderResult(result, options, theme, context) {
|
|
77
|
+
if (!options.isPartial && !context.isError) {
|
|
78
|
+
const output = (result.details as { output?: unknown } | undefined)?.output;
|
|
79
|
+
// /reload rendering-fallback investigation (papyrus task 4930cd9b) -- correlates
|
|
80
|
+
// against vehicle-notes-client.ts's onInvoked/vehicle-ready diagnostics by
|
|
81
|
+
// descriptor.name and wall-clock time.
|
|
82
|
+
recordRenderDiagnostic({
|
|
83
|
+
event: "render-result-dispatch",
|
|
84
|
+
operation: descriptor.name,
|
|
85
|
+
isArtifact: isArtifact(output),
|
|
86
|
+
isArtifactArray: isArtifactArray(output),
|
|
87
|
+
output: shapeFingerprint(output),
|
|
88
|
+
});
|
|
89
|
+
if (isArtifactArray(output)) {
|
|
90
|
+
return new ArtifactListCard(createArtifactListDetails(descriptor.name, output), theme, options.expanded);
|
|
91
|
+
}
|
|
92
|
+
if (isArtifact(output)) {
|
|
93
|
+
return new ArtifactCard(createArtifactDetails(descriptor.name, output), theme, options.expanded);
|
|
94
|
+
}
|
|
95
|
+
if (isTaskFocus(output)) {
|
|
96
|
+
return new ArtifactCard(
|
|
97
|
+
createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output)),
|
|
98
|
+
theme,
|
|
99
|
+
options.expanded,
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
// tasks.focused specifically returns null for "nothing focused" --
|
|
103
|
+
// scoped to this one operation so an unrelated null-output operation
|
|
104
|
+
// (e.g. a not-found lookup) is never mislabeled as a focus state.
|
|
105
|
+
if (output === null && descriptor.name === "tasks.focused") {
|
|
106
|
+
return renderNoFocusedTask(theme);
|
|
107
|
+
}
|
|
108
|
+
if (isTaskExecutionPlan(output)) {
|
|
109
|
+
return renderTaskExecutionPlan(output, theme, options.expanded);
|
|
110
|
+
}
|
|
111
|
+
if (isPlaybookInvocationResult(output)) {
|
|
112
|
+
return renderPlaybookInvocationResult(output, theme, options.expanded);
|
|
113
|
+
}
|
|
114
|
+
if (isPlaybookMissingArguments(output)) {
|
|
115
|
+
return renderPlaybookMissingArguments(output, theme);
|
|
116
|
+
}
|
|
117
|
+
if (isDiscussionAndRounds(output)) {
|
|
118
|
+
return renderDiscussionAndRounds(output, theme, options.expanded);
|
|
119
|
+
}
|
|
120
|
+
if (isDiscussionRoundsOnly(output)) {
|
|
121
|
+
return renderDiscussionRoundsOnly(output, theme);
|
|
122
|
+
}
|
|
123
|
+
if (isDiscussionListOutput(output)) {
|
|
124
|
+
return new ArtifactListCard(createArtifactListDetails(descriptor.name, output.discussions), theme, options.expanded);
|
|
125
|
+
}
|
|
126
|
+
if (isTaskCompletion(output)) {
|
|
127
|
+
return renderTaskCompletion(output, theme, options.expanded);
|
|
128
|
+
}
|
|
129
|
+
recordRenderDiagnostic({ event: "render-result-fell-through-to-generic", operation: descriptor.name });
|
|
130
|
+
}
|
|
131
|
+
return renderVehicleResult(descriptor, result, options, theme, context);
|
|
132
|
+
},
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
/**
|
|
137
|
+
* Projects a raw Papyrus operation output into a bounded, versioned PapyrusToolDetails DTO
|
|
138
|
+
* before Pi ever persists it -- the seam papyrusVehicleRenderers' own renderResult never had
|
|
139
|
+
* (it only converts shape at render time, from whatever the legacy {vehicle, output} path
|
|
140
|
+
* already persisted verbatim, lease tokens and all). Every branch here mirrors the same
|
|
141
|
+
* shape-detection papyrusVehicleRenderers's own renderResult uses, so the two stay in lockstep;
|
|
142
|
+
* anything genuinely unmatched still becomes a real, bounded PreviewToolDetails rather than an
|
|
143
|
+
* unprojected raw passthrough -- the one requirement this whole seam exists to satisfy.
|
|
144
|
+
*/
|
|
145
|
+
function projectPapyrusPresentation(descriptor: VehicleOperationDescriptor, output: unknown): JsonValue {
|
|
146
|
+
if (isArtifactArray(output)) return createArtifactListDetails(descriptor.name, output) as unknown as JsonValue;
|
|
147
|
+
if (isArtifact(output)) return createArtifactDetails(descriptor.name, output) as unknown as JsonValue;
|
|
148
|
+
if (isTaskFocus(output)) return createArtifactDetails(descriptor.name, output.artifact, focusAnnotation(output)) as unknown as JsonValue;
|
|
149
|
+
if (output === null && descriptor.name === "tasks.focused") return createNoFocusDetails(descriptor.name) as unknown as JsonValue;
|
|
150
|
+
if (isTaskExecutionPlan(output))
|
|
151
|
+
return createExecutionPlanDetails(descriptor.name, output.nodes, output.layers, output.cycleIds) as unknown as JsonValue;
|
|
152
|
+
if (isPlaybookInvocationResult(output)) {
|
|
153
|
+
return createPlaybookInvocationDetails(descriptor.name, {
|
|
154
|
+
playbookId: output.playbookId,
|
|
155
|
+
runId: output.runId,
|
|
156
|
+
created: output.created,
|
|
157
|
+
rootTaskIds: output.rootTaskIds,
|
|
158
|
+
entryTaskId: output.entryTaskId,
|
|
159
|
+
execution: output.execution,
|
|
160
|
+
}) as unknown as JsonValue;
|
|
161
|
+
}
|
|
162
|
+
if (isPlaybookMissingArguments(output)) {
|
|
163
|
+
return createPlaybookMissingArgumentsDetails(descriptor.name, output.playbookId, output.missingArguments) as unknown as JsonValue;
|
|
164
|
+
}
|
|
165
|
+
if (isDiscussionAndRounds(output))
|
|
166
|
+
return createDiscussionDetails(descriptor.name, output.rounds, output.discussion) as unknown as JsonValue;
|
|
167
|
+
if (isDiscussionRoundsOnly(output)) return createDiscussionDetails(descriptor.name, output.rounds) as unknown as JsonValue;
|
|
168
|
+
if (isDiscussionListOutput(output)) return createArtifactListDetails(descriptor.name, output.discussions) as unknown as JsonValue;
|
|
169
|
+
if (isTaskCompletion(output)) return createTaskCompletionDetails(descriptor.name, output) as unknown as JsonValue;
|
|
170
|
+
if (isTaskLeaseView(output)) return createLeaseDetails(descriptor.name, output) as unknown as JsonValue;
|
|
171
|
+
return createPreviewDetails(descriptor.name, descriptor.name, boundedJsonPreview(output)) as unknown as JsonValue;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function renderFromPapyrusPresentation(
|
|
175
|
+
presentation: NonNullable<ReturnType<typeof parsePapyrusToolDetails>>,
|
|
176
|
+
theme: Theme,
|
|
177
|
+
expanded: boolean,
|
|
178
|
+
): Component {
|
|
179
|
+
switch (presentation.kind) {
|
|
180
|
+
case "artifact-list":
|
|
181
|
+
return new ArtifactListCard(presentation, theme, expanded);
|
|
182
|
+
case "artifact":
|
|
183
|
+
return new ArtifactCard(presentation, theme, expanded);
|
|
184
|
+
case "no-focus":
|
|
185
|
+
return renderNoFocusedTask(theme);
|
|
186
|
+
case "execution-plan":
|
|
187
|
+
return renderTaskExecutionPlan(presentation, theme, expanded);
|
|
188
|
+
case "playbook-invocation":
|
|
189
|
+
return renderPlaybookInvocationResult(presentation, theme, expanded);
|
|
190
|
+
case "playbook-missing-arguments":
|
|
191
|
+
return renderPlaybookMissingArguments(presentation, theme);
|
|
192
|
+
case "discussion":
|
|
193
|
+
return presentation.discussion
|
|
194
|
+
? renderDiscussionAndRounds({ discussion: presentation.discussion, rounds: presentation.rounds }, theme, expanded)
|
|
195
|
+
: renderDiscussionRoundsOnly(presentation, theme);
|
|
196
|
+
case "task-completion":
|
|
197
|
+
return renderTaskCompletion(presentation, theme, expanded);
|
|
198
|
+
case "lease":
|
|
199
|
+
return renderLease(presentation, theme);
|
|
200
|
+
case "preview":
|
|
201
|
+
return new Text(theme.fg("toolOutput", presentation.content), 0, 0);
|
|
202
|
+
case "transition":
|
|
203
|
+
case "graph":
|
|
204
|
+
case "gate-run":
|
|
205
|
+
case "invocation":
|
|
206
|
+
case "error":
|
|
207
|
+
// Reachable only if a future caller starts producing these kinds through this seam
|
|
208
|
+
// (today's Papyrus Vehicle outputs never do) -- a bounded JSON preview is still a
|
|
209
|
+
// real, safe rendering rather than a crash.
|
|
210
|
+
return new Text(theme.fg("toolOutput", boundedJsonPreview(presentation)), 0, 0);
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
/**
|
|
215
|
+
* Pairs the projector above with a renderResult that reads the already-projected,
|
|
216
|
+
* already-bounded `details.presentation` DTO instead of raw `details.output` -- the seam
|
|
217
|
+
* pi-papyrus task "project typed bounded render details before Vehicle persists" exists for.
|
|
218
|
+
* Falls back to papyrusVehicleRenderers' own renderResult (which still reads `details.output`)
|
|
219
|
+
* for a partial/progress update, an error result, or a historical session row persisted before
|
|
220
|
+
* this seam existed -- both keep working exactly as before, unchanged.
|
|
221
|
+
*/
|
|
222
|
+
export function papyrusVehiclePresentations(descriptor: VehicleOperationDescriptor): PiVehiclePresentationContract {
|
|
223
|
+
return {
|
|
224
|
+
projector: {
|
|
225
|
+
maxBytes: TOOL_DETAILS_MAX_SERIALIZED_CHARACTERS,
|
|
226
|
+
project: (output: unknown, _request: PiVehicleInvocationRequest) => projectPapyrusPresentation(descriptor, output),
|
|
227
|
+
},
|
|
228
|
+
renderResult(result, options, theme, context) {
|
|
229
|
+
if (!options.isPartial && !context.isError) {
|
|
230
|
+
const presentation = parsePapyrusToolDetails((result.details as { presentation?: unknown } | undefined)?.presentation);
|
|
231
|
+
if (presentation) return renderFromPapyrusPresentation(presentation, theme, options.expanded);
|
|
232
|
+
}
|
|
233
|
+
return papyrusVehicleRenderers(descriptor).renderResult!(result, options, theme, context);
|
|
234
|
+
},
|
|
235
|
+
};
|
|
236
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import type { Component } from "@earendil-works/pi-tui";
|
|
3
|
+
import { buildDetailLines, type DetailField, statelessComponent } from "malevich-tui-components";
|
|
4
|
+
import { detailViewTheme, measure } from "../../tool-rendering/artifact-card.ts";
|
|
5
|
+
|
|
6
|
+
/** tasks.claim/heartbeat_lease/release_lease/lease's own name-first view. Detected the same
|
|
7
|
+
* name-independent, shape-based way as the others in this directory. */
|
|
8
|
+
export interface TaskLeaseViewOutput {
|
|
9
|
+
taskName: string;
|
|
10
|
+
taskTitle: string;
|
|
11
|
+
owner: string;
|
|
12
|
+
token: string;
|
|
13
|
+
claimedAt: string;
|
|
14
|
+
leaseExpiresAt: string;
|
|
15
|
+
heartbeatAt?: string;
|
|
16
|
+
note?: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export function isTaskLeaseView(value: unknown): value is TaskLeaseViewOutput {
|
|
20
|
+
if (typeof value !== "object" || value === null) return false;
|
|
21
|
+
const row = value as Record<string, unknown>;
|
|
22
|
+
return (
|
|
23
|
+
typeof row.taskName === "string" &&
|
|
24
|
+
typeof row.taskTitle === "string" &&
|
|
25
|
+
typeof row.owner === "string" &&
|
|
26
|
+
typeof row.token === "string" &&
|
|
27
|
+
typeof row.claimedAt === "string" &&
|
|
28
|
+
typeof row.leaseExpiresAt === "string" &&
|
|
29
|
+
(row.heartbeatAt === undefined || typeof row.heartbeatAt === "string") &&
|
|
30
|
+
(row.note === undefined || typeof row.note === "string")
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Renders a lease's own safe fields -- never the raw token, which the model channel (not this persisted, human-facing one) carries for a later heartbeat/release call. */
|
|
35
|
+
export function renderLease(
|
|
36
|
+
lease: {
|
|
37
|
+
taskName: string;
|
|
38
|
+
taskTitle: string;
|
|
39
|
+
owner: string;
|
|
40
|
+
claimedAt: string;
|
|
41
|
+
leaseExpiresAt: string;
|
|
42
|
+
heartbeatAt?: string;
|
|
43
|
+
note?: string;
|
|
44
|
+
},
|
|
45
|
+
theme: Theme,
|
|
46
|
+
): Component {
|
|
47
|
+
return statelessComponent((width) => {
|
|
48
|
+
const fields: DetailField[] = [
|
|
49
|
+
{ label: "Task", value: `${lease.taskName} \u2014 ${lease.taskTitle}` },
|
|
50
|
+
{ label: "Owner", value: lease.owner },
|
|
51
|
+
{ label: "Expires", value: lease.leaseExpiresAt },
|
|
52
|
+
...(lease.heartbeatAt ? [{ label: "Last heartbeat", value: lease.heartbeatAt }] : []),
|
|
53
|
+
...(lease.note ? [{ label: "Note", value: lease.note }] : []),
|
|
54
|
+
];
|
|
55
|
+
return buildDetailLines(Math.max(1, width), { fields, alignFields: true, theme: detailViewTheme(theme), measure });
|
|
56
|
+
});
|
|
57
|
+
}
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
2
|
+
import { type Component, Text, truncateToWidth } from "@earendil-works/pi-tui";
|
|
3
|
+
import { dagViewFromExecutionPlan, isTaskExecutionPlan, type TaskExecutionPlanOutput } from "./task-execution.ts";
|
|
4
|
+
|
|
5
|
+
/** playbooks.invoke's own PlaybookInvocationResult shape -- a materialized execution plan
|
|
6
|
+
* (same shape tasks.plan renders) plus which docs/rules/tasks were created and which one to
|
|
7
|
+
* focus. Detected the same name-independent, shape-based way as the others in this directory. */
|
|
8
|
+
export interface PlaybookInvocationResultOutput {
|
|
9
|
+
playbookId: string;
|
|
10
|
+
runId: string;
|
|
11
|
+
created: { docs: string[]; rules: string[]; tasks: string[] };
|
|
12
|
+
rootTaskIds: string[];
|
|
13
|
+
entryTaskId: string;
|
|
14
|
+
execution: TaskExecutionPlanOutput;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface PlaybookMissingArgumentsOutput {
|
|
18
|
+
playbookId: string;
|
|
19
|
+
missingArguments: string[];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export function isPlaybookInvocationResult(value: unknown): value is PlaybookInvocationResultOutput {
|
|
23
|
+
if (typeof value !== "object" || value === null) return false;
|
|
24
|
+
const row = value as Record<string, unknown>;
|
|
25
|
+
return (
|
|
26
|
+
typeof row.playbookId === "string" &&
|
|
27
|
+
typeof row.runId === "string" &&
|
|
28
|
+
typeof row.entryTaskId === "string" &&
|
|
29
|
+
Array.isArray(row.rootTaskIds) &&
|
|
30
|
+
typeof row.created === "object" &&
|
|
31
|
+
row.created !== null &&
|
|
32
|
+
Array.isArray((row.created as Record<string, unknown>).docs) &&
|
|
33
|
+
Array.isArray((row.created as Record<string, unknown>).rules) &&
|
|
34
|
+
Array.isArray((row.created as Record<string, unknown>).tasks) &&
|
|
35
|
+
isTaskExecutionPlan(row.execution)
|
|
36
|
+
);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
export function isPlaybookMissingArguments(value: unknown): value is PlaybookMissingArgumentsOutput {
|
|
40
|
+
if (typeof value !== "object" || value === null) return false;
|
|
41
|
+
const row = value as Record<string, unknown>;
|
|
42
|
+
return (
|
|
43
|
+
typeof row.playbookId === "string" &&
|
|
44
|
+
Array.isArray(row.missingArguments) &&
|
|
45
|
+
row.missingArguments.every((entry) => typeof entry === "string")
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export function renderPlaybookInvocationResult(result: PlaybookInvocationResultOutput, theme: Theme, expanded: boolean): Component {
|
|
50
|
+
const dag = dagViewFromExecutionPlan(result.execution, theme, expanded);
|
|
51
|
+
const counts = [
|
|
52
|
+
["task", result.created.tasks.length],
|
|
53
|
+
["rule", result.created.rules.length],
|
|
54
|
+
["doc", result.created.docs.length],
|
|
55
|
+
] as const;
|
|
56
|
+
const summary = counts
|
|
57
|
+
.filter(([, count]) => count > 0)
|
|
58
|
+
.map(([noun, count]) => `${count} ${noun}${count === 1 ? "" : "s"}`)
|
|
59
|
+
.join(", ");
|
|
60
|
+
return {
|
|
61
|
+
render: (width: number) => [...dag.render(width), truncateToWidth(theme.fg("dim", summary || "Nothing created."), width)],
|
|
62
|
+
invalidate: () => dag.invalidate(),
|
|
63
|
+
};
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function renderPlaybookMissingArguments(result: PlaybookMissingArgumentsOutput, theme: Theme): Component {
|
|
67
|
+
const line = theme.fg("warning", `Missing required argument(s): ${result.missingArguments.join(", ")}`);
|
|
68
|
+
return new Text(line, 0, 0);
|
|
69
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Base shape-detection kernel shared by every renderers/*.ts result-kind module: the generic
|
|
3
|
+
* "looks like an Artifact" duck-typing every other detector in this directory builds on, plus the
|
|
4
|
+
* Task Focus wrapper shape and small rendering primitives with no kind-specific machinery of their
|
|
5
|
+
* own. Split out of the former single vehicle-artifact-renderers.ts as part of a SOLID-audit-driven
|
|
6
|
+
* decomposition (see Doc "Modularity playbook: building-block-shaped TypeScript modules for
|
|
7
|
+
* papyrus/pi-papyrus" and the "pi-papyrus vehicle-artifact-renderers.ts split" child of "Epic:
|
|
8
|
+
* Modularize papyrus/pi-papyrus god-files into building-block modules").
|
|
9
|
+
*/
|
|
10
|
+
import type { Artifact } from "@danypops/papyrus";
|
|
11
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
12
|
+
import { type Component, Text } from "@earendil-works/pi-tui";
|
|
13
|
+
import type { ArtifactFocusAnnotation } from "../../tool-rendering/render-model.ts";
|
|
14
|
+
|
|
15
|
+
/** Every field an Artifact and its lean list-default ArtifactSummary (tasks.list/docs.list/
|
|
16
|
+
* rules.list/playbooks.list without full:true -- see summarizeArtifact()) both always carry. */
|
|
17
|
+
export function hasArtifactCoreFields(value: unknown): value is Omit<Artifact, "body" | "extra"> {
|
|
18
|
+
if (typeof value !== "object" || value === null) return false;
|
|
19
|
+
const row = value as Record<string, unknown>;
|
|
20
|
+
return (
|
|
21
|
+
typeof row.id === "string" &&
|
|
22
|
+
typeof row.kind === "string" &&
|
|
23
|
+
typeof row.title === "string" &&
|
|
24
|
+
typeof row.status === "string" &&
|
|
25
|
+
typeof row.subtype === "string" &&
|
|
26
|
+
Array.isArray(row.labels) &&
|
|
27
|
+
typeof row.created_at === "string" &&
|
|
28
|
+
typeof row.updated_at === "string"
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export function isArtifact(value: unknown): value is Artifact {
|
|
33
|
+
return hasArtifactCoreFields(value) && typeof (value as Record<string, unknown>).body === "string";
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Regression (real, live-observed): tasks.list's own documented default ("Returns a lean
|
|
38
|
+
* summary (no body/extra) unless full: true is passed") returns ArtifactSummary rows, which
|
|
39
|
+
* omit body entirely. createArtifactListDetails/artifactSummary (render-model.ts) never read
|
|
40
|
+
* .body for list rendering -- only single-artifact createArtifactDetails does -- so requiring
|
|
41
|
+
* body here (matching isArtifact) silently fell every default (lean, the common case) list
|
|
42
|
+
* call for tasks.list/docs.list/rules.list/playbooks.list through to the generic raw Vehicle
|
|
43
|
+
* table renderer instead of the curated ArtifactListCard.
|
|
44
|
+
*/
|
|
45
|
+
export function isArtifactArray(value: unknown): value is Artifact[] {
|
|
46
|
+
return Array.isArray(value) && value.every(hasArtifactCoreFields);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/** tasks.focused/tasks.pause/tasks.unpause's own wrapper shape -- an Artifact
|
|
50
|
+
* plus Task Focus's separate active/paused dimension. Detected the same
|
|
51
|
+
* name-independent, shape-based way as isArtifact/isArtifactArray above. */
|
|
52
|
+
export interface TaskFocusOutput {
|
|
53
|
+
artifact: Artifact;
|
|
54
|
+
status: string;
|
|
55
|
+
updatedAt: string;
|
|
56
|
+
pauseReason?: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
export function isTaskFocus(value: unknown): value is TaskFocusOutput {
|
|
60
|
+
if (typeof value !== "object" || value === null) return false;
|
|
61
|
+
const row = value as Record<string, unknown>;
|
|
62
|
+
return (
|
|
63
|
+
isArtifact(row.artifact) &&
|
|
64
|
+
typeof row.status === "string" &&
|
|
65
|
+
typeof row.updatedAt === "string" &&
|
|
66
|
+
(row.pauseReason === undefined || typeof row.pauseReason === "string")
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export function focusAnnotation(output: TaskFocusOutput): ArtifactFocusAnnotation {
|
|
71
|
+
return { status: output.status, updatedAt: output.updatedAt, ...(output.pauseReason ? { pauseReason: output.pauseReason } : {}) };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
export function renderNoFocusedTask(theme: Theme): Component {
|
|
75
|
+
return new Text(theme.fg("dim", "No focused task."), 0, 0);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** What renderDiscussionAndRounds/renderTaskCompletion actually read -- satisfied by both a raw
|
|
79
|
+
* Artifact (the live duck-typed output path) and the leaner projected ToolArtifactSummary (the
|
|
80
|
+
* typed-DTO path), with no cast needed at either call site. */
|
|
81
|
+
export interface RenderableDiscussionParent {
|
|
82
|
+
title: string;
|
|
83
|
+
status: string;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/** Deliberately does not catch: a value JSON.stringify can't serialize (e.g. a circular
|
|
87
|
+
* reference) has no safe textual fallback, so this propagates and the projector's own caller
|
|
88
|
+
* (invokeVehicleOperation) fails the whole call closed rather than persisting a placeholder --
|
|
89
|
+
* the same "never silently substitute raw/unsafe output" contract every other projection
|
|
90
|
+
* failure already carries. */
|
|
91
|
+
export function boundedJsonPreview(value: unknown): string {
|
|
92
|
+
const text = JSON.stringify(value, null, 2);
|
|
93
|
+
return typeof text === "string" ? text : String(value);
|
|
94
|
+
}
|
|
@@ -0,0 +1,98 @@
|
|
|
1
|
+
import type { Artifact } from "@danypops/papyrus";
|
|
2
|
+
import type { Theme } from "@earendil-works/pi-coding-agent";
|
|
3
|
+
import { type Component, truncateToWidth } from "@earendil-works/pi-tui";
|
|
4
|
+
import { buildDetailLines, type DetailField, type DetailSection, statelessComponent } from "malevich-tui-components";
|
|
5
|
+
import { detailViewTheme, measure, statusColor, statusGlyph } from "../../tool-rendering/artifact-card.ts";
|
|
6
|
+
import { isArtifact, type RenderableDiscussionParent } from "./shared.ts";
|
|
7
|
+
|
|
8
|
+
/** tasks.complete's own TaskCompletion shape -- a completed (or rejected) task plus its own
|
|
9
|
+
* gate/checklist proof run and any dependents still left blocked. Detected the same
|
|
10
|
+
* name-independent, shape-based way as the others in this directory. */
|
|
11
|
+
export interface TaskGateResultOutput {
|
|
12
|
+
gate: unknown;
|
|
13
|
+
passed: boolean;
|
|
14
|
+
output: string;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
export interface TaskChecklistReviewOutput {
|
|
18
|
+
item: string;
|
|
19
|
+
accepted: boolean;
|
|
20
|
+
reason?: string;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export interface TaskBlockageOutput {
|
|
24
|
+
artifact: Artifact;
|
|
25
|
+
dependencyIds: string[];
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export interface TaskCompletionOutput {
|
|
29
|
+
artifact: Artifact;
|
|
30
|
+
gates: TaskGateResultOutput[];
|
|
31
|
+
checklist: TaskChecklistReviewOutput[];
|
|
32
|
+
completed: boolean;
|
|
33
|
+
focused: Artifact | null;
|
|
34
|
+
blocked: TaskBlockageOutput[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** What renderTaskCompletion actually reads -- satisfied by both the raw duck-typed TaskCompletionOutput and the leaner projected TaskCompletionToolDetails. */
|
|
38
|
+
export interface RenderableTaskCompletion {
|
|
39
|
+
artifact: RenderableDiscussionParent;
|
|
40
|
+
gates: readonly { passed: boolean; output: string }[];
|
|
41
|
+
checklist: readonly TaskChecklistReviewOutput[];
|
|
42
|
+
completed: boolean;
|
|
43
|
+
focused?: RenderableDiscussionParent | null;
|
|
44
|
+
blocked: readonly { artifact: RenderableDiscussionParent; dependencyIds: readonly string[] }[];
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
export function isTaskCompletion(value: unknown): value is TaskCompletionOutput {
|
|
48
|
+
if (typeof value !== "object" || value === null) return false;
|
|
49
|
+
const row = value as Record<string, unknown>;
|
|
50
|
+
return (
|
|
51
|
+
isArtifact(row.artifact) &&
|
|
52
|
+
Array.isArray(row.gates) &&
|
|
53
|
+
Array.isArray(row.checklist) &&
|
|
54
|
+
typeof row.completed === "boolean" &&
|
|
55
|
+
(row.focused === null || isArtifact(row.focused)) &&
|
|
56
|
+
Array.isArray(row.blocked)
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
export function renderTaskCompletion(result: RenderableTaskCompletion, theme: Theme, expanded: boolean): Component {
|
|
61
|
+
const task = result.artifact;
|
|
62
|
+
return statelessComponent((width) => {
|
|
63
|
+
const safeWidth = Math.max(1, width);
|
|
64
|
+
const fields: DetailField[] = [
|
|
65
|
+
{ label: "Title", value: task.title },
|
|
66
|
+
{ label: "Status", value: theme.fg(statusColor(task.status), `${statusGlyph(task.status)} ${task.status}`) },
|
|
67
|
+
];
|
|
68
|
+
const sections: DetailSection[] = [];
|
|
69
|
+
if (result.gates.length > 0) {
|
|
70
|
+
sections.push({
|
|
71
|
+
heading: "Gates:",
|
|
72
|
+
lines: result.gates.map((gate) => theme.fg(gate.passed ? "success" : "error", `${gate.passed ? "✓" : "✗"} ${gate.output}`)),
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
if (result.checklist.length > 0) {
|
|
76
|
+
sections.push({
|
|
77
|
+
heading: "Checklist:",
|
|
78
|
+
lines: result.checklist.map((entry) =>
|
|
79
|
+
theme.fg(
|
|
80
|
+
entry.accepted ? "success" : "error",
|
|
81
|
+
`${entry.accepted ? "✓" : "✗"} ${entry.item}${entry.reason ? ` — ${entry.reason}` : ""}`,
|
|
82
|
+
),
|
|
83
|
+
),
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
if (result.blocked.length > 0) {
|
|
87
|
+
sections.push({
|
|
88
|
+
heading: "Still blocked:",
|
|
89
|
+
lines: result.blocked.map((entry) => theme.fg("warning", `◼ ${entry.artifact.title}`)),
|
|
90
|
+
});
|
|
91
|
+
}
|
|
92
|
+
const lines = buildDetailLines(safeWidth, { fields, sections, alignFields: true, theme: detailViewTheme(theme), measure });
|
|
93
|
+
if (result.focused && expanded) {
|
|
94
|
+
lines.push(truncateToWidth(theme.fg("accent", `▶ focus ${result.focused.title}`), safeWidth));
|
|
95
|
+
}
|
|
96
|
+
return lines;
|
|
97
|
+
});
|
|
98
|
+
}
|