@mcuste/pi-diagram 0.0.0
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/LICENSE +21 -0
- package/README.md +240 -0
- package/dist/artifacts.d.ts +59 -0
- package/dist/artifacts.d.ts.map +1 -0
- package/dist/artifacts.js +274 -0
- package/dist/artifacts.js.map +1 -0
- package/dist/cache.d.ts +43 -0
- package/dist/cache.d.ts.map +1 -0
- package/dist/cache.js +0 -0
- package/dist/cache.js.map +1 -0
- package/dist/d2/diagnostics.d.ts +25 -0
- package/dist/d2/diagnostics.d.ts.map +1 -0
- package/dist/d2/diagnostics.js +77 -0
- package/dist/d2/diagnostics.js.map +1 -0
- package/dist/d2/fonts.d.ts +23 -0
- package/dist/d2/fonts.d.ts.map +1 -0
- package/dist/d2/fonts.js +255 -0
- package/dist/d2/fonts.js.map +1 -0
- package/dist/d2/preflight.d.ts +20 -0
- package/dist/d2/preflight.d.ts.map +1 -0
- package/dist/d2/preflight.js +217 -0
- package/dist/d2/preflight.js.map +1 -0
- package/dist/d2/profiles.d.ts +34 -0
- package/dist/d2/profiles.d.ts.map +1 -0
- package/dist/d2/profiles.js +118 -0
- package/dist/d2/profiles.js.map +1 -0
- package/dist/d2/runner.d.ts +95 -0
- package/dist/d2/runner.d.ts.map +1 -0
- package/dist/d2/runner.js +350 -0
- package/dist/d2/runner.js.map +1 -0
- package/dist/display.d.ts +48 -0
- package/dist/display.d.ts.map +1 -0
- package/dist/display.js +120 -0
- package/dist/display.js.map +1 -0
- package/dist/index.d.ts +3 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +5 -0
- package/dist/index.js.map +1 -0
- package/dist/normalize.d.ts +23 -0
- package/dist/normalize.d.ts.map +1 -0
- package/dist/normalize.js +83 -0
- package/dist/normalize.js.map +1 -0
- package/dist/process.d.ts +38 -0
- package/dist/process.d.ts.map +1 -0
- package/dist/process.js +87 -0
- package/dist/process.js.map +1 -0
- package/dist/raster.d.ts +40 -0
- package/dist/raster.d.ts.map +1 -0
- package/dist/raster.js +193 -0
- package/dist/raster.js.map +1 -0
- package/dist/render.d.ts +55 -0
- package/dist/render.d.ts.map +1 -0
- package/dist/render.js +229 -0
- package/dist/render.js.map +1 -0
- package/dist/tools.d.ts +58 -0
- package/dist/tools.d.ts.map +1 -0
- package/dist/tools.js +284 -0
- package/dist/tools.js.map +1 -0
- package/package.json +101 -0
- package/src/artifacts.ts +418 -0
- package/src/cache.ts +0 -0
- package/src/d2/diagnostics.ts +114 -0
- package/src/d2/fonts.ts +289 -0
- package/src/d2/preflight.ts +270 -0
- package/src/d2/profiles.ts +157 -0
- package/src/d2/runner.ts +513 -0
- package/src/display.ts +201 -0
- package/src/index.ts +5 -0
- package/src/normalize.ts +119 -0
- package/src/process.ts +134 -0
- package/src/raster.ts +258 -0
- package/src/render.ts +338 -0
- package/src/tools.ts +455 -0
package/src/tools.ts
ADDED
|
@@ -0,0 +1,455 @@
|
|
|
1
|
+
import { type Static, type TSchema, Type } from "typebox";
|
|
2
|
+
import { parseArtifactNames, workspacePaths } from "./artifacts.js";
|
|
3
|
+
import { type Diagnostic, formatDiagnostic } from "./d2/diagnostics.js";
|
|
4
|
+
import { DEFAULT_PROFILE, type ProfileName } from "./d2/profiles.js";
|
|
5
|
+
import { D2Cli, type D2Renderer } from "./d2/runner.js";
|
|
6
|
+
import {
|
|
7
|
+
type Component,
|
|
8
|
+
type DiagramCallView,
|
|
9
|
+
type DisplayContext,
|
|
10
|
+
type DisplayTheme,
|
|
11
|
+
displayLoaded,
|
|
12
|
+
imagesSupported,
|
|
13
|
+
primeDisplay,
|
|
14
|
+
renderDiagramCall,
|
|
15
|
+
renderDiagramResult,
|
|
16
|
+
} from "./display.js";
|
|
17
|
+
import { ResvgRasterizer, type SvgRasterizer } from "./raster.js";
|
|
18
|
+
import { type DiagramRendering, type Representation, renderDiagram } from "./render.js";
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Capped well below what D2 can parse: a diagram that reads clearly in a terminal is a few dozen
|
|
22
|
+
* lines, and a larger one usually means the model is dumping a whole codebase.
|
|
23
|
+
*/
|
|
24
|
+
const MAX_SOURCE_LENGTH = 20_000;
|
|
25
|
+
const MAX_TITLE_LENGTH = 120;
|
|
26
|
+
const MAX_PATH_LENGTH = 255;
|
|
27
|
+
|
|
28
|
+
const DiagramSource = Type.String({
|
|
29
|
+
minLength: 1,
|
|
30
|
+
maxLength: MAX_SOURCE_LENGTH,
|
|
31
|
+
description: "Diagram source in D2. Imports, local images, and remote icons are rejected.",
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
const DiagramTitle = Type.String({
|
|
35
|
+
minLength: 1,
|
|
36
|
+
maxLength: MAX_TITLE_LENGTH,
|
|
37
|
+
description: "Short label shown with the diagram and used to name saved artifacts.",
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
/** One literal per profile, because a mapped union loses the names from the static type. */
|
|
41
|
+
const DiagramProfile = Type.Union(
|
|
42
|
+
[
|
|
43
|
+
Type.Literal("explain"),
|
|
44
|
+
Type.Literal("architecture"),
|
|
45
|
+
Type.Literal("data"),
|
|
46
|
+
Type.Literal("docs"),
|
|
47
|
+
Type.Literal("tree"),
|
|
48
|
+
Type.Literal("c4"),
|
|
49
|
+
Type.Literal("dependency"),
|
|
50
|
+
],
|
|
51
|
+
{
|
|
52
|
+
description:
|
|
53
|
+
"What the diagram is for. The harness maps this to theme and spacing; the model does not choose them.",
|
|
54
|
+
},
|
|
55
|
+
);
|
|
56
|
+
|
|
57
|
+
const DiagramRender = Type.Union(
|
|
58
|
+
[
|
|
59
|
+
Type.Literal("auto"),
|
|
60
|
+
Type.Literal("image"),
|
|
61
|
+
Type.Literal("unicode"),
|
|
62
|
+
Type.Literal("ascii"),
|
|
63
|
+
Type.Literal("source"),
|
|
64
|
+
],
|
|
65
|
+
{
|
|
66
|
+
description:
|
|
67
|
+
"Preferred representation in the transcript. `auto` shows an image where the terminal supports it and Unicode text otherwise.",
|
|
68
|
+
},
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
const DiagramFormat = Type.Union([
|
|
72
|
+
Type.Literal("source"),
|
|
73
|
+
Type.Literal("svg"),
|
|
74
|
+
Type.Literal("png"),
|
|
75
|
+
Type.Literal("txt"),
|
|
76
|
+
]);
|
|
77
|
+
|
|
78
|
+
const DiagramFormats = Type.Array(DiagramFormat, {
|
|
79
|
+
minItems: 1,
|
|
80
|
+
uniqueItems: true,
|
|
81
|
+
description:
|
|
82
|
+
"Files to produce. They are written outside the repository and their paths are returned. Defaults to editable source and an SVG.",
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
/** What the transcript shows, which is not always the text representation that was prepared. */
|
|
86
|
+
type DisplayedAs = Representation | "image";
|
|
87
|
+
|
|
88
|
+
const DiagramSave = Type.Object(
|
|
89
|
+
{
|
|
90
|
+
dir: Type.String({
|
|
91
|
+
minLength: 1,
|
|
92
|
+
maxLength: MAX_PATH_LENGTH,
|
|
93
|
+
description:
|
|
94
|
+
"Directory inside the workspace to copy the files into. There is no default: name where diagrams belong in this repository.",
|
|
95
|
+
}),
|
|
96
|
+
basename: Type.Optional(
|
|
97
|
+
Type.String({
|
|
98
|
+
minLength: 1,
|
|
99
|
+
maxLength: MAX_TITLE_LENGTH,
|
|
100
|
+
description: "Stable file name stem, without an extension. Defaults to the title.",
|
|
101
|
+
}),
|
|
102
|
+
),
|
|
103
|
+
},
|
|
104
|
+
{
|
|
105
|
+
additionalProperties: false,
|
|
106
|
+
description:
|
|
107
|
+
"Copy the files into the repository. Use it only when the user asked to keep the diagram.",
|
|
108
|
+
},
|
|
109
|
+
);
|
|
110
|
+
|
|
111
|
+
const DiagramParameters = Type.Object(
|
|
112
|
+
{
|
|
113
|
+
source: DiagramSource,
|
|
114
|
+
title: Type.Optional(DiagramTitle),
|
|
115
|
+
profile: Type.Optional(DiagramProfile),
|
|
116
|
+
render: Type.Optional(DiagramRender),
|
|
117
|
+
formats: Type.Optional(DiagramFormats),
|
|
118
|
+
save: Type.Optional(DiagramSave),
|
|
119
|
+
},
|
|
120
|
+
{ additionalProperties: false },
|
|
121
|
+
);
|
|
122
|
+
|
|
123
|
+
type DiagramParameters = Static<typeof DiagramParameters>;
|
|
124
|
+
|
|
125
|
+
const DIAGRAM_DESCRIPTION = [
|
|
126
|
+
"Draw a diagram from D2 source and show it in the terminal. Use it when architecture,",
|
|
127
|
+
"relationships, sequence, data flow, state transitions, schemas, or process flow are easier to",
|
|
128
|
+
"see than to read, and keep the diagram to what answers the question.",
|
|
129
|
+
"",
|
|
130
|
+
"D2 in brief:",
|
|
131
|
+
" edges client -> gateway: request",
|
|
132
|
+
" containers core: Core Services { api; worker }, then core.api -> core.worker",
|
|
133
|
+
" sequence flow: { shape: sequence_diagram; user -> api: submit }",
|
|
134
|
+
" tables users: { shape: sql_table; id: int {constraint: primary_key}; email: varchar }",
|
|
135
|
+
"",
|
|
136
|
+
"Not allowed: `@` imports, `icon`, `link`, `shape: image`, and `|...|` block labels.",
|
|
137
|
+
"Do not set colours, themes, or fonts. This tool owns how diagrams look.",
|
|
138
|
+
"Aim for 5 to 15 nodes, or up to about 25 with `profile: dependency`. Split anything larger.",
|
|
139
|
+
"",
|
|
140
|
+
"`profile` says what the diagram is for, and sets the layout, theme, and spacing:",
|
|
141
|
+
" explain an answer in this conversation, drawn by hand. The default",
|
|
142
|
+
" architecture systems and components, with room between the parts",
|
|
143
|
+
" data schemas, tables, class relationships",
|
|
144
|
+
" docs a diagram that will be checked into the repository",
|
|
145
|
+
" tree a hierarchy: an org chart, a call tree, a file layout",
|
|
146
|
+
" c4 the C4 convention, with the palette its readers expect",
|
|
147
|
+
" dependency a graph with more nodes than usual, drawn dense",
|
|
148
|
+
"",
|
|
149
|
+
"Files: `formats` produces .d2, .svg, .png, or .txt outside the repository and returns the",
|
|
150
|
+
"paths. `save: { dir }` also copies them into the repository, so Markdown can reference the",
|
|
151
|
+
"SVG. Only pass `save` when the user asked to keep the diagram; explaining something needs",
|
|
152
|
+
"neither. A terminal that can show images shows one without being asked.",
|
|
153
|
+
].join("\n");
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* `content` goes to the model and `details` only to the screen, so the diagram travels in
|
|
157
|
+
* `details` whenever this package draws the row itself.
|
|
158
|
+
*/
|
|
159
|
+
interface DiagramToolDetails {
|
|
160
|
+
readonly language: "d2";
|
|
161
|
+
readonly title?: string;
|
|
162
|
+
readonly profile: ProfileName;
|
|
163
|
+
readonly requested: Static<typeof DiagramRender>;
|
|
164
|
+
readonly renderedAs: DisplayedAs;
|
|
165
|
+
readonly image?: { readonly path: string; readonly widthPx: number; readonly heightPx: number };
|
|
166
|
+
/** The diagram as text, for the renderer and as the fallback for an image. */
|
|
167
|
+
readonly textPreview: string;
|
|
168
|
+
/** The D2 source, shown in the expanded row. */
|
|
169
|
+
readonly source: string;
|
|
170
|
+
readonly diagnostics?: readonly Diagnostic[];
|
|
171
|
+
readonly sourceHash: string;
|
|
172
|
+
readonly lineCount: number;
|
|
173
|
+
readonly widthCells: number;
|
|
174
|
+
readonly d2Version?: string;
|
|
175
|
+
readonly outputs?: Readonly<Record<string, string>>;
|
|
176
|
+
readonly notes?: readonly string[];
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
interface TextContent {
|
|
180
|
+
readonly type: "text";
|
|
181
|
+
readonly text: string;
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
interface ToolResult<TDetails> {
|
|
185
|
+
readonly content: readonly TextContent[];
|
|
186
|
+
readonly details: TDetails;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
interface ToolContext {
|
|
190
|
+
readonly cwd: string;
|
|
191
|
+
/** Only a terminal UI can display an image; print and RPC modes never can. */
|
|
192
|
+
readonly mode?: "tui" | "rpc" | "json" | "print";
|
|
193
|
+
readonly ui?: {
|
|
194
|
+
confirm(title: string, message: string): Promise<boolean>;
|
|
195
|
+
};
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
type ToolTier = "read" | "write" | "exec";
|
|
199
|
+
|
|
200
|
+
type ToolApprovalDecision =
|
|
201
|
+
| ToolTier
|
|
202
|
+
| {
|
|
203
|
+
readonly tier: ToolTier;
|
|
204
|
+
readonly reason?: string;
|
|
205
|
+
readonly policy?: "allow" | "deny" | "prompt";
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
interface ToolDefinition<TParameters extends TSchema, TDetails> {
|
|
209
|
+
readonly name: string;
|
|
210
|
+
readonly label: string;
|
|
211
|
+
readonly description: string;
|
|
212
|
+
readonly parameters: TParameters;
|
|
213
|
+
readonly approval: ToolApprovalDecision | ((args: unknown) => ToolApprovalDecision);
|
|
214
|
+
readonly loadMode: "essential" | "discoverable";
|
|
215
|
+
readonly concurrency?:
|
|
216
|
+
| "shared"
|
|
217
|
+
| "exclusive"
|
|
218
|
+
| ((args: Partial<Static<TParameters>>) => "shared" | "exclusive");
|
|
219
|
+
readonly executionMode?: "sequential" | "parallel";
|
|
220
|
+
readonly formatApprovalDetails?: (args: unknown) => string | readonly string[] | undefined;
|
|
221
|
+
/** Arguments can still be arriving, so every field is read defensively. */
|
|
222
|
+
readonly renderCall?: (args: Partial<Static<TParameters>>, theme: DisplayTheme) => Component;
|
|
223
|
+
/** Throwing here is the host's signal to render the result its own way. */
|
|
224
|
+
readonly renderResult?: (
|
|
225
|
+
result: { readonly content: readonly TextContent[]; readonly details: TDetails },
|
|
226
|
+
options: { readonly expanded: boolean; readonly isPartial: boolean },
|
|
227
|
+
theme: DisplayTheme,
|
|
228
|
+
context: DisplayContext,
|
|
229
|
+
) => Component;
|
|
230
|
+
execute(
|
|
231
|
+
toolCallId: string,
|
|
232
|
+
parameters: Static<TParameters>,
|
|
233
|
+
signal: AbortSignal | undefined,
|
|
234
|
+
onUpdate: unknown,
|
|
235
|
+
context: ToolContext,
|
|
236
|
+
): Promise<ToolResult<TDetails>>;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
export interface DiagramExtensionApi {
|
|
240
|
+
registerTool<TParameters extends TSchema, TDetails>(
|
|
241
|
+
definition: ToolDefinition<TParameters, TDetails>,
|
|
242
|
+
): void;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
export interface DiagramExtensionDependencies {
|
|
246
|
+
readonly renderer?: D2Renderer;
|
|
247
|
+
readonly rasterizer?: SvgRasterizer;
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
/** Only `save` reaches the repository. The temp store changes nothing worth approving. */
|
|
251
|
+
function approvalFor(args: unknown): ToolApprovalDecision {
|
|
252
|
+
return readSave(args) === undefined ? "read" : "write";
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/** Names the repository files at stake, so approving is a decision about specific paths. */
|
|
256
|
+
function approvalDetails(args: unknown): readonly string[] | undefined {
|
|
257
|
+
const save = readSave(args);
|
|
258
|
+
if (save === undefined) {
|
|
259
|
+
return undefined;
|
|
260
|
+
}
|
|
261
|
+
const read = (key: string): unknown =>
|
|
262
|
+
typeof args === "object" && args !== null ? Reflect.get(args, key) : undefined;
|
|
263
|
+
try {
|
|
264
|
+
const title = read("title");
|
|
265
|
+
const names = parseArtifactNames(
|
|
266
|
+
{ formats: read("formats"), save },
|
|
267
|
+
{ title: typeof title === "string" ? title : undefined, hash: "" },
|
|
268
|
+
);
|
|
269
|
+
return workspacePaths(names).map((path) => `Writes ${path}`);
|
|
270
|
+
} catch {
|
|
271
|
+
// The call itself will refuse with the reason; the prompt only needs the intent.
|
|
272
|
+
return typeof save.dir === "string"
|
|
273
|
+
? [`Writes diagram artifacts into ${save.dir}`]
|
|
274
|
+
: ["Writes diagram artifacts into the repository"];
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
function readSave(args: unknown): Record<string, unknown> | undefined {
|
|
279
|
+
if (typeof args !== "object" || args === null) {
|
|
280
|
+
return undefined;
|
|
281
|
+
}
|
|
282
|
+
const save = Reflect.get(args, "save");
|
|
283
|
+
return typeof save === "object" && save !== null && !Array.isArray(save)
|
|
284
|
+
? (save as Record<string, unknown>)
|
|
285
|
+
: undefined;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
/** The waiting row names the diagram and the policy, never the source. */
|
|
289
|
+
function callView(args: Partial<DiagramParameters>): DiagramCallView {
|
|
290
|
+
const title = typeof args.title === "string" ? args.title.trim() : "";
|
|
291
|
+
const source = typeof args.source === "string" ? args.source : "";
|
|
292
|
+
const lines = source.split("\n").filter((line) => line.trim().length > 0).length;
|
|
293
|
+
const directory = readSave(args)?.dir;
|
|
294
|
+
const saving = typeof directory === "string" ? `, saving into ${directory}` : "";
|
|
295
|
+
const profile = typeof args.profile === "string" ? args.profile : DEFAULT_PROFILE.name;
|
|
296
|
+
return {
|
|
297
|
+
subject: title === "" ? `${lines} line${lines === 1 ? "" : "s"}` : `"${title}"`,
|
|
298
|
+
note: `(${profile}${saving})`,
|
|
299
|
+
};
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
/** How each representation is named to the reader. */
|
|
303
|
+
const DRAWN: Readonly<Record<DisplayedAs, string>> = {
|
|
304
|
+
image: "an image",
|
|
305
|
+
unicode: "box drawing",
|
|
306
|
+
ascii: "plain ASCII",
|
|
307
|
+
source: "D2 source",
|
|
308
|
+
};
|
|
309
|
+
|
|
310
|
+
/**
|
|
311
|
+
* What the model reads. The diagram is left out when this package draws the row, and the summary
|
|
312
|
+
* says so, because an empty-looking result invites the model to render again.
|
|
313
|
+
*/
|
|
314
|
+
function contentFor(rendering: DiagramRendering, drawnHere: boolean): string {
|
|
315
|
+
const paths = rendering.saved.map((artifact) => artifact.path).join(", ");
|
|
316
|
+
const saved =
|
|
317
|
+
rendering.saved.length === 0
|
|
318
|
+
? undefined
|
|
319
|
+
: rendering.saved[0]?.location === "workspace"
|
|
320
|
+
? `saved in the repository: ${paths}`
|
|
321
|
+
: `saved outside the repository: ${paths}`;
|
|
322
|
+
const blocks = drawnHere ? [summaryFor(rendering)] : [rendering.title, rendering.text];
|
|
323
|
+
return [...blocks, saved, ...rendering.notes.map((note) => `note: ${note}`)]
|
|
324
|
+
.filter((block): block is string => Boolean(block))
|
|
325
|
+
.join("\n\n");
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
function summaryFor(rendering: DiagramRendering): string {
|
|
329
|
+
const named = rendering.title === undefined ? "the diagram" : `"${rendering.title}"`;
|
|
330
|
+
const as = DRAWN[rendering.image === undefined ? rendering.renderedAs : "image"];
|
|
331
|
+
return `Drew ${named} as ${as}. It is on the user's screen, so it is not repeated here.`;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
function outputsFor(rendering: DiagramRendering): Readonly<Record<string, string>> | undefined {
|
|
335
|
+
if (rendering.saved.length === 0) {
|
|
336
|
+
return undefined;
|
|
337
|
+
}
|
|
338
|
+
const keys = {
|
|
339
|
+
source: "sourcePath",
|
|
340
|
+
svg: "svgPath",
|
|
341
|
+
png: "pngPath",
|
|
342
|
+
txt: "textPath",
|
|
343
|
+
} as const;
|
|
344
|
+
return {
|
|
345
|
+
location: rendering.saved[0]?.location ?? "temp",
|
|
346
|
+
...Object.fromEntries(
|
|
347
|
+
rendering.saved.map((artifact) => [keys[artifact.format], artifact.path]),
|
|
348
|
+
),
|
|
349
|
+
};
|
|
350
|
+
}
|
|
351
|
+
|
|
352
|
+
function detailsFor(
|
|
353
|
+
parameters: DiagramParameters,
|
|
354
|
+
rendering: DiagramRendering,
|
|
355
|
+
): DiagramToolDetails {
|
|
356
|
+
const outputs = outputsFor(rendering);
|
|
357
|
+
return {
|
|
358
|
+
language: "d2",
|
|
359
|
+
...(rendering.title === undefined ? {} : { title: rendering.title }),
|
|
360
|
+
profile: rendering.profile,
|
|
361
|
+
requested: parameters.render ?? "auto",
|
|
362
|
+
renderedAs: rendering.image === undefined ? rendering.renderedAs : "image",
|
|
363
|
+
...(rendering.image === undefined ? {} : { image: rendering.image }),
|
|
364
|
+
textPreview: rendering.text,
|
|
365
|
+
source: rendering.source,
|
|
366
|
+
...(rendering.diagnostics.length === 0 ? {} : { diagnostics: rendering.diagnostics }),
|
|
367
|
+
sourceHash: rendering.sourceHash,
|
|
368
|
+
lineCount: rendering.lineCount,
|
|
369
|
+
widthCells: rendering.widthCells,
|
|
370
|
+
...(rendering.d2Version === undefined ? {} : { d2Version: rendering.d2Version }),
|
|
371
|
+
...(outputs === undefined ? {} : { outputs }),
|
|
372
|
+
...(rendering.notes.length === 0 ? {} : { notes: rendering.notes }),
|
|
373
|
+
};
|
|
374
|
+
}
|
|
375
|
+
|
|
376
|
+
/** Saved and temporary file paths, for the expanded view. */
|
|
377
|
+
function pathsFor(details: DiagramToolDetails): readonly string[] {
|
|
378
|
+
return Object.entries(details.outputs ?? {})
|
|
379
|
+
.filter(([key]) => key !== "location")
|
|
380
|
+
.map(([, path]) => path);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** The expanded row: how it was drawn, where the files are, what went wrong, and the source. */
|
|
384
|
+
function expandedLines(details: DiagramToolDetails): readonly string[] {
|
|
385
|
+
const version = details.d2Version === undefined ? "" : `, D2 ${details.d2Version}`;
|
|
386
|
+
const lines = [
|
|
387
|
+
`Drawn as ${DRAWN[details.renderedAs]}, profile ${details.profile}${version}`,
|
|
388
|
+
...pathsFor(details),
|
|
389
|
+
...(details.diagnostics ?? []).map(formatDiagnostic),
|
|
390
|
+
];
|
|
391
|
+
// The collapsed row already shows the source when that is what was drawn.
|
|
392
|
+
return details.renderedAs === "source" ? lines : [...lines, "", details.source];
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
export function registerDiagramTools(
|
|
396
|
+
pi: DiagramExtensionApi,
|
|
397
|
+
dependencies: DiagramExtensionDependencies = {},
|
|
398
|
+
): void {
|
|
399
|
+
const renderer = dependencies.renderer ?? new D2Cli();
|
|
400
|
+
const rasterizer = dependencies.rasterizer ?? new ResvgRasterizer();
|
|
401
|
+
void primeDisplay();
|
|
402
|
+
|
|
403
|
+
pi.registerTool<typeof DiagramParameters, DiagramToolDetails>({
|
|
404
|
+
name: "diagram",
|
|
405
|
+
label: "Diagram",
|
|
406
|
+
description: DIAGRAM_DESCRIPTION,
|
|
407
|
+
parameters: DiagramParameters,
|
|
408
|
+
approval: approvalFor,
|
|
409
|
+
formatApprovalDetails: approvalDetails,
|
|
410
|
+
renderCall(args, theme) {
|
|
411
|
+
return renderDiagramCall(callView(args), theme);
|
|
412
|
+
},
|
|
413
|
+
loadMode: "discoverable",
|
|
414
|
+
concurrency: "shared",
|
|
415
|
+
executionMode: "parallel",
|
|
416
|
+
async execute(_toolCallId, parameters, signal, _onUpdate, context) {
|
|
417
|
+
// Anywhere else the host prints `content`, so the diagram has to travel in it.
|
|
418
|
+
const drawnHere = context.mode === "tui" && displayLoaded();
|
|
419
|
+
const rendering = await renderDiagram(
|
|
420
|
+
{
|
|
421
|
+
source: parameters.source,
|
|
422
|
+
title: parameters.title,
|
|
423
|
+
profile: parameters.profile,
|
|
424
|
+
render: parameters.render,
|
|
425
|
+
formats: parameters.formats,
|
|
426
|
+
save: parameters.save,
|
|
427
|
+
cwd: context.cwd,
|
|
428
|
+
// Drawing an image nothing can show would cost an SVG render for nothing.
|
|
429
|
+
images: drawnHere && imagesSupported() !== false,
|
|
430
|
+
signal,
|
|
431
|
+
},
|
|
432
|
+
renderer,
|
|
433
|
+
rasterizer,
|
|
434
|
+
);
|
|
435
|
+
return {
|
|
436
|
+
content: [{ type: "text", text: contentFor(rendering, drawnHere) }],
|
|
437
|
+
details: detailsFor(parameters, rendering),
|
|
438
|
+
};
|
|
439
|
+
},
|
|
440
|
+
renderResult(result, options, theme, context) {
|
|
441
|
+
const details = result.details;
|
|
442
|
+
return renderDiagramResult(
|
|
443
|
+
{
|
|
444
|
+
image: details.image,
|
|
445
|
+
title: details.title,
|
|
446
|
+
text: details.textPreview,
|
|
447
|
+
notes: details.notes ?? [],
|
|
448
|
+
details: expandedLines(details),
|
|
449
|
+
},
|
|
450
|
+
theme,
|
|
451
|
+
{ showImages: context.showImages, expanded: options.expanded, state: context.state },
|
|
452
|
+
);
|
|
453
|
+
},
|
|
454
|
+
});
|
|
455
|
+
}
|