@actagent/diffs 2026.6.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.
@@ -0,0 +1,361 @@
1
+ // Diffs plugin module implements viewer client behavior.
2
+ import { FileDiff, preloadHighlighter } from "@pierre/diffs";
3
+ import type {
4
+ FileContents,
5
+ FileDiffMetadata,
6
+ FileDiffOptions,
7
+ SupportedLanguages,
8
+ } from "@pierre/diffs";
9
+ import { normalizeDiffViewerPayloadLanguages } from "./language-hints.js";
10
+ import type { DiffViewerPayload, DiffLayout, DiffTheme } from "./types.js";
11
+ import { parseViewerPayloadJson } from "./viewer-payload.js";
12
+
13
+ type ViewerState = {
14
+ theme: DiffTheme;
15
+ layout: DiffLayout;
16
+ backgroundEnabled: boolean;
17
+ wrapEnabled: boolean;
18
+ };
19
+
20
+ type DiffController = {
21
+ payload: DiffViewerPayload;
22
+ diff: FileDiff;
23
+ };
24
+
25
+ export const controllers: DiffController[] = [];
26
+
27
+ const viewerState: ViewerState = {
28
+ theme: "dark",
29
+ layout: "unified",
30
+ backgroundEnabled: true,
31
+ wrapEnabled: true,
32
+ };
33
+
34
+ function parsePayload(element: HTMLScriptElement): DiffViewerPayload {
35
+ const raw = element.textContent?.trim();
36
+ if (!raw) {
37
+ throw new Error("Diff payload was empty.");
38
+ }
39
+ return parseViewerPayloadJson(raw);
40
+ }
41
+
42
+ function getCards(): Array<{ host: HTMLElement; payload: DiffViewerPayload }> {
43
+ const cards: Array<{ host: HTMLElement; payload: DiffViewerPayload }> = [];
44
+ for (const card of document.querySelectorAll<HTMLElement>(".oc-diff-card")) {
45
+ const host = card.querySelector<HTMLElement>("[data-actagent-diff-host]");
46
+ const payloadNode = card.querySelector<HTMLScriptElement>("[data-actagent-diff-payload]");
47
+ if (!host || !payloadNode) {
48
+ continue;
49
+ }
50
+
51
+ try {
52
+ cards.push({ host, payload: parsePayload(payloadNode) });
53
+ } catch (error) {
54
+ console.warn("Skipping invalid diff payload", error);
55
+ }
56
+ }
57
+ return cards;
58
+ }
59
+
60
+ function ensureShadowRoot(host: HTMLElement): void {
61
+ if (host.shadowRoot) {
62
+ return;
63
+ }
64
+ const template = host.querySelector<HTMLTemplateElement>(
65
+ ":scope > template[shadowrootmode='open']",
66
+ );
67
+ if (!template) {
68
+ return;
69
+ }
70
+ const shadowRoot = host.attachShadow({ mode: "open" });
71
+ shadowRoot.append(template.content.cloneNode(true));
72
+ template.remove();
73
+ }
74
+
75
+ function getHydrateProps(payload: DiffViewerPayload): {
76
+ fileDiff?: FileDiffMetadata;
77
+ oldFile?: FileContents;
78
+ newFile?: FileContents;
79
+ } {
80
+ if (payload.fileDiff) {
81
+ return { fileDiff: payload.fileDiff };
82
+ }
83
+ return {
84
+ oldFile: payload.oldFile,
85
+ newFile: payload.newFile,
86
+ };
87
+ }
88
+
89
+ type ToolbarIconName =
90
+ | "split"
91
+ | "unified"
92
+ | "wrap-on"
93
+ | "wrap-off"
94
+ | "background-on"
95
+ | "background-off"
96
+ | "theme-dark"
97
+ | "theme-light";
98
+
99
+ const toolbarIconSvg: Record<ToolbarIconName, string> = {
100
+ split: `<svg viewBox="0 0 16 16" aria-hidden="true">
101
+ <path fill="currentColor" d="M14 0H8.5v16H14a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2m-1.5 6.5v1h1a.5.5 0 0 1 0 1h-1v1a.5.5 0 0 1-1 0v-1h-1a.5.5 0 0 1 0-1h1v-1a.5.5 0 0 1 1 0"></path>
102
+ <path fill="currentColor" opacity="0.5" d="M2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h5.5V0zm.5 7.5h3a.5.5 0 0 1 0 1h-3a.5.5 0 0 1 0-1"></path>
103
+ </svg>`,
104
+ unified: `<svg viewBox="0 0 16 16" aria-hidden="true">
105
+ <path fill="currentColor" fill-rule="evenodd" d="M16 14a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.5h16zm-8-4a.5.5 0 0 0-.5.5v1h-1a.5.5 0 0 0 0 1h1v1a.5.5 0 0 0 1 0v-1h1a.5.5 0 0 0 0-1h-1v-1A.5.5 0 0 0 8 10" clip-rule="evenodd"></path>
106
+ <path fill="currentColor" fill-rule="evenodd" opacity="0.5" d="M14 0a2 2 0 0 1 2 2v5.5H0V2a2 2 0 0 1 2-2zM6.5 3.5a.5.5 0 0 0 0 1h3a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
107
+ </svg>`,
108
+ "wrap-on": `<svg viewBox="0 0 16 16" aria-hidden="true">
109
+ <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" opacity="1" d="M3.868 3.449a1.21 1.21 0 0 0-.473-.329c-.274-.111-.623-.15-1.055-.076a3.5 3.5 0 0 0-.71.208c-.082.035-.16.077-.235.125l-.043.03v1.056l.168-.139c.15-.124.326-.225.527-.303.196-.074.4-.113.604-.113.188 0 .33.051.431.157.087.095.137.248.147.456l-.962.144c-.219.03-.41.086-.57.166a1.245 1.245 0 0 0-.398.311c-.103.125-.181.27-.229.426-.097.33-.093.68.011 1.008a1.096 1.096 0 0 0 .638.67c.155.063.328.093.528.093a1.25 1.25 0 0 0 .978-.441v.345h1.007V4.65c0-.255-.03-.484-.089-.681a1.423 1.423 0 0 0-.275-.52zm-.636 1.896v.236c0 .119-.018.231-.055.341a.745.745 0 0 1-.377.447.694.694 0 0 1-.512.027.454.454 0 0 1-.156-.094.389.389 0 0 1-.094-.139.474.474 0 0 1-.035-.186c0-.077.01-.147.024-.212a.33.33 0 0 1 .078-.141.436.436 0 0 1 .161-.109 1.3 1.3 0 0 1 .305-.073l.661-.097zm5.051-1.067a2.253 2.253 0 0 0-.244-.656 1.354 1.354 0 0 0-.436-.459 1.165 1.165 0 0 0-.642-.173 1.136 1.136 0 0 0-.69.223 1.33 1.33 0 0 0-.264.266V1H5.09v6.224h.918v-.281c.123.152.287.266.472.328.098.032.208.047.33.047.255 0 .483-.06.677-.177.192-.115.355-.278.486-.486a2.29 2.29 0 0 0 .293-.718 3.87 3.87 0 0 0 .096-.886 3.714 3.714 0 0 0-.078-.773zm-.86.758c0 .232-.02.439-.06.613-.036.172-.09.315-.159.424a.639.639 0 0 1-.233.237.582.582 0 0 1-.565.014.683.683 0 0 1-.21-.183.925.925 0 0 1-.142-.283A1.187 1.187 0 0 1 6 5.5v-.517c0-.164.02-.314.06-.447.036-.132.087-.242.156-.336a.668.668 0 0 1 .228-.208.584.584 0 0 1 .29-.071.554.554 0 0 1 .496.279c.063.099.108.214.143.354.031.143.05.306.05.482zM2.407 9.9a.913.913 0 0 1 .316-.239c.218-.1.547-.105.766-.018.104.042.204.1.32.184l.33.26V8.945l-.097-.062a1.932 1.932 0 0 0-.905-.215c-.308 0-.593.057-.846.168-.25.11-.467.27-.647.475-.18.21-.318.453-.403.717-.09.272-.137.57-.137.895 0 .289.043.561.13.808.086.249.211.471.373.652.161.185.361.333.597.441.232.104.493.155.778.155.233 0 .434-.028.613-.084.165-.05.322-.123.466-.217l.078-.061v-.889l-.2.095a.4.4 0 0 1-.076.026c-.05.017-.099.035-.128.049-.036.023-.227.09-.227.09-.06.024-.14.043-.218.059a.977.977 0 0 1-.599-.057.827.827 0 0 1-.306-.225 1.088 1.088 0 0 1-.205-.376 1.728 1.728 0 0 1-.076-.529c0-.21.028-.399.083-.56.054-.158.13-.294.22-.4zM14 6h-4V5h4.5l.5.5v6l-.5.5H7.879l2.07 2.071-.706.707-2.89-2.889v-.707l2.89-2.89L9.95 9l-2 2H14V6z"></path>
110
+ </svg>`,
111
+ "wrap-off": `<svg viewBox="0 0 16 16" aria-hidden="true">
112
+ <path fill="currentColor" fill-rule="evenodd" clip-rule="evenodd" opacity="0.85" d="M3.868 3.449a1.21 1.21 0 0 0-.473-.329c-.274-.111-.623-.15-1.055-.076a3.5 3.5 0 0 0-.71.208c-.082.035-.16.077-.235.125l-.043.03v1.056l.168-.139c.15-.124.326-.225.527-.303.196-.074.4-.113.604-.113.188 0 .33.051.431.157.087.095.137.248.147.456l-.962.144c-.219.03-.41.086-.57.166a1.245 1.245 0 0 0-.398.311c-.103.125-.181.27-.229.426-.097.33-.093.68.011 1.008a1.096 1.096 0 0 0 .638.67c.155.063.328.093.528.093a1.25 1.25 0 0 0 .978-.441v.345h1.007V4.65c0-.255-.03-.484-.089-.681a1.423 1.423 0 0 0-.275-.52zm-.636 1.896v.236c0 .119-.018.231-.055.341a.745.745 0 0 1-.377.447.694.694 0 0 1-.512.027.454.454 0 0 1-.156-.094.389.389 0 0 1-.094-.139.474.474 0 0 1-.035-.186c0-.077.01-.147.024-.212a.33.33 0 0 1 .078-.141.436.436 0 0 1 .161-.109 1.3 1.3 0 0 1 .305-.073l.661-.097zm5.051-1.067a2.253 2.253 0 0 0-.244-.656 1.354 1.354 0 0 0-.436-.459 1.165 1.165 0 0 0-.642-.173 1.136 1.136 0 0 0-.69.223 1.33 1.33 0 0 0-.264.266V1H5.09v6.224h.918v-.281c.123.152.287.266.472.328.098.032.208.047.33.047.255 0 .483-.06.677-.177.192-.115.355-.278.486-.486a2.29 2.29 0 0 0 .293-.718 3.87 3.87 0 0 0 .096-.886 3.714 3.714 0 0 0-.078-.773zm-.86.758c0 .232-.02.439-.06.613-.036.172-.09.315-.159.424a.639.639 0 0 1-.233.237.582.582 0 0 1-.565.014.683.683 0 0 1-.21-.183.925.925 0 0 1-.142-.283A1.187 1.187 0 0 1 6 5.5v-.517c0-.164.02-.314.06-.447.036-.132.087-.242.156-.336a.668.668 0 0 1 .228-.208.584.584 0 0 1 .29-.071.554.554 0 0 1 .496.279c.063.099.108.214.143.354.031.143.05.306.05.482zM2.407 9.9a.913.913 0 0 1 .316-.239c.218-.1.547-.105.766-.018.104.042.204.1.32.184l.33.26V8.945l-.097-.062a1.932 1.932 0 0 0-.905-.215c-.308 0-.593.057-.846.168-.25.11-.467.27-.647.475-.18.21-.318.453-.403.717-.09.272-.137.57-.137.895 0 .289.043.561.13.808.086.249.211.471.373.652.161.185.361.333.597.441.232.104.493.155.778.155.233 0 .434-.028.613-.084.165-.05.322-.123.466-.217l.078-.061v-.889l-.2.095a.4.4 0 0 1-.076.026c-.05.017-.099.035-.128.049-.036.023-.227.09-.227.09-.06.024-.14.043-.218.059a.977.977 0 0 1-.599-.057.827.827 0 0 1-.306-.225 1.088 1.088 0 0 1-.205-.376 1.728 1.728 0 0 1-.076-.529c0-.21.028-.399.083-.56.054-.158.13-.294.22-.4zM14 6h-4V5h4.5l.5.5v6l-.5.5H7.879l2.07 2.071-.706.707-2.89-2.889v-.707l2.89-2.89L9.95 9l-2 2H14V6z"></path>
113
+ </svg>`,
114
+ "background-on": `<svg viewBox="0 0 16 16" aria-hidden="true">
115
+ <path fill="currentColor" opacity="0.5" d="M0 2.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 2.25"></path>
116
+ <path fill="currentColor" fill-rule="evenodd" d="M15 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2.5 9a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1zm0-2a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
117
+ <path fill="currentColor" opacity="0.5" d="M0 14.75A.75.75 0 0 1 .75 14h5.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75"></path>
118
+ </svg>`,
119
+ "background-off": `<svg viewBox="0 0 16 16" aria-hidden="true">
120
+ <path fill="currentColor" opacity="0.34" d="M0 2.25a.75.75 0 0 1 .75-.75h10.5a.75.75 0 0 1 0 1.5H.75A.75.75 0 0 1 0 2.25"></path>
121
+ <path fill="currentColor" opacity="0.34" fill-rule="evenodd" d="M15 5a1 1 0 0 1 1 1v5a1 1 0 0 1-1 1H1a1 1 0 0 1-1-1V6a1 1 0 0 1 1-1zM2.5 9a.5.5 0 0 0 0 1h8a.5.5 0 0 0 0-1zm0-2a.5.5 0 0 0 0 1h11a.5.5 0 0 0 0-1z" clip-rule="evenodd"></path>
122
+ <path fill="currentColor" opacity="0.34" d="M0 14.75A.75.75 0 0 1 .75 14h5.5a.75.75 0 0 1 0 1.5H.75a.75.75 0 0 1-.75-.75"></path>
123
+ <path d="M2.5 13.5 13.5 2.5" stroke="currentColor" stroke-width="1.35" stroke-linecap="round"></path>
124
+ </svg>`,
125
+ "theme-dark": `<svg viewBox="0 0 16 16" aria-hidden="true">
126
+ <path fill="currentColor" d="M10.794 3.647a.217.217 0 0 1 .412 0l.387 1.162c.173.518.58.923 1.097 1.096l1.162.388a.217.217 0 0 1 0 .412l-1.162.386a1.73 1.73 0 0 0-1.097 1.097l-.387 1.162a.217.217 0 0 1-.412 0l-.387-1.162A1.74 1.74 0 0 0 9.31 7.092l-1.162-.386a.217.217 0 0 1 0-.412l1.162-.388a1.73 1.73 0 0 0 1.097-1.096zM13.863.598a.144.144 0 0 1 .221-.071.14.14 0 0 1 .053.07l.258.775c.115.345.386.616.732.731l.774.258a.145.145 0 0 1 0 .274l-.774.259a1.16 1.16 0 0 0-.732.732l-.258.773a.145.145 0 0 1-.274 0l-.258-.773a1.16 1.16 0 0 0-.732-.732l-.774-.259a.145.145 0 0 1 0-.273l.774-.259c.346-.115.617-.386.732-.732z"></path>
127
+ <path fill="currentColor" d="M6.25 1.742a.67.67 0 0 1 .07.75 6.3 6.3 0 0 0-.768 3.028c0 2.746 1.746 5.084 4.193 5.979H1.774A7.2 7.2 0 0 1 1 8.245c0-3.013 1.85-5.598 4.484-6.694a.66.66 0 0 1 .766.19M.75 12.499a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z"></path>
128
+ </svg>`,
129
+ "theme-light": `<svg viewBox="0 0 16 16" aria-hidden="true">
130
+ <path fill="currentColor" d="M8.21 2.109a.256.256 0 0 0-.42 0L6.534 3.893a.256.256 0 0 1-.316.085l-1.982-.917a.256.256 0 0 0-.362.21l-.196 2.174a.256.256 0 0 1-.232.232l-2.175.196a.256.256 0 0 0-.209.362l.917 1.982a.256.256 0 0 1-.085.316L.11 9.791a.256.256 0 0 0 0 .418L1.23 11H3.1a5 5 0 1 1 9.8 0h1.869l1.123-.79a.256.256 0 0 0 0-.42l-1.785-1.257a.256.256 0 0 1-.085-.316l.917-1.982a.256.256 0 0 0-.21-.362l-2.174-.196a.256.256 0 0 1-.232-.232l-.196-2.175a.256.256 0 0 0-.362-.209l-1.982.917a.256.256 0 0 1-.316-.085z"></path>
131
+ <path fill="currentColor" d="M4 10q.001.519.126 1h7.748A4 4 0 1 0 4 10M.75 12a.75.75 0 0 0 0 1.5h14.5a.75.75 0 0 0 0-1.5z"></path>
132
+ </svg>`,
133
+ };
134
+
135
+ function createToolbarButton(params: {
136
+ title: string;
137
+ active: boolean;
138
+ icon: ToolbarIconName;
139
+ onClick: () => void;
140
+ }): HTMLButtonElement {
141
+ const button = document.createElement("button");
142
+ button.type = "button";
143
+ button.className = "oc-diff-toolbar-button";
144
+ button.dataset.active = String(params.active);
145
+ button.title = params.title;
146
+ button.setAttribute("aria-label", params.title);
147
+ button.innerHTML = toolbarIconSvg[params.icon];
148
+ applyToolbarButtonStyles(button, params.active);
149
+ button.addEventListener("click", (event) => {
150
+ event.preventDefault();
151
+ params.onClick();
152
+ });
153
+ return button;
154
+ }
155
+
156
+ function applyToolbarStyles(toolbar: HTMLElement): void {
157
+ toolbar.style.display = "inline-flex";
158
+ toolbar.style.alignItems = "center";
159
+ toolbar.style.gap = "6px";
160
+ toolbar.style.marginInlineStart = "6px";
161
+ toolbar.style.flex = "0 0 auto";
162
+ }
163
+
164
+ function applyToolbarButtonStyles(button: HTMLButtonElement, active: boolean): void {
165
+ button.style.display = "inline-flex";
166
+ button.style.alignItems = "center";
167
+ button.style.justifyContent = "center";
168
+ button.style.width = "24px";
169
+ button.style.height = "24px";
170
+ button.style.padding = "0";
171
+ button.style.margin = "0";
172
+ button.style.border = "0";
173
+ button.style.borderRadius = "0";
174
+ button.style.background = "transparent";
175
+ button.style.boxShadow = "none";
176
+ button.style.lineHeight = "0";
177
+ button.style.cursor = "pointer";
178
+ button.style.overflow = "visible";
179
+ button.style.flex = "0 0 auto";
180
+ button.style.opacity = active ? "0.92" : "0.6";
181
+ button.style.color =
182
+ viewerState.theme === "dark" ? "rgba(226, 232, 240, 0.74)" : "rgba(15, 23, 42, 0.52)";
183
+ button.dataset.active = String(active);
184
+ const icon = button.querySelector("svg");
185
+ if (!icon) {
186
+ return;
187
+ }
188
+ icon.style.display = "block";
189
+ icon.style.width = "16px";
190
+ icon.style.height = "16px";
191
+ icon.style.minWidth = "16px";
192
+ icon.style.minHeight = "16px";
193
+ icon.style.overflow = "visible";
194
+ icon.style.flex = "0 0 auto";
195
+ icon.style.color = "inherit";
196
+ icon.style.fill = "currentColor";
197
+ icon.style.pointerEvents = "none";
198
+ }
199
+
200
+ function createToolbar(): HTMLElement {
201
+ const toolbar = document.createElement("div");
202
+ toolbar.className = "oc-diff-toolbar";
203
+ applyToolbarStyles(toolbar);
204
+
205
+ toolbar.append(
206
+ createToolbarButton({
207
+ title: viewerState.layout === "unified" ? "Switch to split diff" : "Switch to unified diff",
208
+ active: viewerState.layout === "split",
209
+ icon: viewerState.layout === "split" ? "split" : "unified",
210
+ onClick: () => {
211
+ viewerState.layout = viewerState.layout === "unified" ? "split" : "unified";
212
+ syncAllControllers();
213
+ },
214
+ }),
215
+ );
216
+
217
+ toolbar.append(
218
+ createToolbarButton({
219
+ title: viewerState.wrapEnabled ? "Disable word wrap" : "Enable word wrap",
220
+ active: viewerState.wrapEnabled,
221
+ icon: viewerState.wrapEnabled ? "wrap-on" : "wrap-off",
222
+ onClick: () => {
223
+ viewerState.wrapEnabled = !viewerState.wrapEnabled;
224
+ syncAllControllers();
225
+ },
226
+ }),
227
+ );
228
+
229
+ toolbar.append(
230
+ createToolbarButton({
231
+ title: viewerState.backgroundEnabled
232
+ ? "Hide background highlights"
233
+ : "Show background highlights",
234
+ active: viewerState.backgroundEnabled,
235
+ icon: viewerState.backgroundEnabled ? "background-on" : "background-off",
236
+ onClick: () => {
237
+ viewerState.backgroundEnabled = !viewerState.backgroundEnabled;
238
+ syncAllControllers();
239
+ },
240
+ }),
241
+ );
242
+
243
+ toolbar.append(
244
+ createToolbarButton({
245
+ title: viewerState.theme === "dark" ? "Switch to light theme" : "Switch to dark theme",
246
+ active: viewerState.theme === "dark",
247
+ icon: viewerState.theme === "dark" ? "theme-dark" : "theme-light",
248
+ onClick: () => {
249
+ viewerState.theme = viewerState.theme === "dark" ? "light" : "dark";
250
+ syncAllControllers();
251
+ },
252
+ }),
253
+ );
254
+
255
+ return toolbar;
256
+ }
257
+
258
+ function createRenderOptions(payload: DiffViewerPayload): FileDiffOptions<undefined> {
259
+ return {
260
+ theme: payload.options.theme,
261
+ themeType: viewerState.theme,
262
+ diffStyle: viewerState.layout,
263
+ diffIndicators: payload.options.diffIndicators,
264
+ expandUnchanged: payload.options.expandUnchanged,
265
+ overflow: viewerState.wrapEnabled ? "wrap" : "scroll",
266
+ disableLineNumbers: payload.options.disableLineNumbers,
267
+ disableBackground: !viewerState.backgroundEnabled,
268
+ unsafeCSS: payload.options.unsafeCSS,
269
+ renderHeaderMetadata: () => createToolbar(),
270
+ };
271
+ }
272
+
273
+ function syncDocumentTheme(): void {
274
+ document.body.dataset.theme = viewerState.theme;
275
+ }
276
+
277
+ function applyState(controller: DiffController): void {
278
+ controller.diff.setOptions(createRenderOptions(controller.payload));
279
+ controller.diff.rerender();
280
+ }
281
+
282
+ function syncAllControllers(): void {
283
+ syncDocumentTheme();
284
+ for (const controller of controllers) {
285
+ applyState(controller);
286
+ }
287
+ }
288
+
289
+ export async function hydrateViewer(): Promise<void> {
290
+ const cards = await Promise.all(
291
+ getCards().map(async ({ host, payload }) => ({
292
+ host,
293
+ payload: await normalizeDiffViewerPayloadLanguages(payload),
294
+ })),
295
+ );
296
+ const langs = new Set<SupportedLanguages>();
297
+ const firstPayload = cards[0]?.payload;
298
+
299
+ if (firstPayload) {
300
+ viewerState.theme = firstPayload.options.themeType;
301
+ viewerState.layout = firstPayload.options.diffStyle;
302
+ viewerState.backgroundEnabled = firstPayload.options.backgroundEnabled;
303
+ viewerState.wrapEnabled = firstPayload.options.overflow === "wrap";
304
+ }
305
+
306
+ for (const { payload } of cards) {
307
+ for (const lang of payload.langs) {
308
+ langs.add(lang);
309
+ }
310
+ }
311
+
312
+ await preloadHighlighter({
313
+ themes: ["pierre-light", "pierre-dark"],
314
+ langs: [...langs],
315
+ });
316
+
317
+ syncDocumentTheme();
318
+
319
+ for (const { host, payload } of cards) {
320
+ try {
321
+ ensureShadowRoot(host);
322
+ const diff = new FileDiff(createRenderOptions(payload));
323
+ diff.hydrate({
324
+ fileContainer: host,
325
+ prerenderedHTML: payload.prerenderedHTML,
326
+ ...getHydrateProps(payload),
327
+ });
328
+ const controller = { payload, diff };
329
+ applyState(controller);
330
+ controllers.push(controller);
331
+ } catch (error) {
332
+ console.warn("Skipping diff card that failed to hydrate", error);
333
+ }
334
+ }
335
+ }
336
+
337
+ async function main(): Promise<void> {
338
+ try {
339
+ await hydrateViewer();
340
+ document.documentElement.dataset.actagentDiffsReady = "true";
341
+ } catch (error) {
342
+ document.documentElement.dataset.actagentDiffsError = "true";
343
+ console.error("Failed to hydrate diff viewer", error);
344
+ }
345
+ }
346
+
347
+ export const disableAutoStartKey = Symbol.for("actagent.diffs.disableAutoStart");
348
+
349
+ const autoStartDisabled = Boolean(
350
+ (globalThis as typeof globalThis & Record<symbol, unknown>)[disableAutoStartKey],
351
+ );
352
+
353
+ if (typeof document !== "undefined" && !autoStartDisabled) {
354
+ if (document.readyState === "loading") {
355
+ document.addEventListener("DOMContentLoaded", () => {
356
+ void main();
357
+ });
358
+ } else {
359
+ void main();
360
+ }
361
+ }
@@ -0,0 +1,95 @@
1
+ // Diffs plugin module implements viewer payload behavior.
2
+ import { DIFF_INDICATORS, DIFF_LAYOUTS, DIFF_THEMES } from "./types.js";
3
+ import type { DiffViewerPayload } from "./types.js";
4
+
5
+ const OVERFLOW_VALUES = ["scroll", "wrap"] as const;
6
+
7
+ function isRecord(value: unknown): value is Record<string, unknown> {
8
+ return value !== null && typeof value === "object" && !Array.isArray(value);
9
+ }
10
+
11
+ export function parseViewerPayloadJson(raw: string): DiffViewerPayload {
12
+ let parsed: unknown;
13
+ try {
14
+ parsed = JSON.parse(raw);
15
+ } catch {
16
+ throw new Error("Diff payload is not valid JSON.");
17
+ }
18
+
19
+ if (!isDiffViewerPayload(parsed)) {
20
+ throw new Error("Diff payload has invalid shape.");
21
+ }
22
+
23
+ return parsed;
24
+ }
25
+
26
+ function isDiffViewerPayload(value: unknown): value is DiffViewerPayload {
27
+ if (!isRecord(value)) {
28
+ return false;
29
+ }
30
+
31
+ if (typeof value.prerenderedHTML !== "string") {
32
+ return false;
33
+ }
34
+
35
+ if (!Array.isArray(value.langs) || !value.langs.every((lang) => typeof lang === "string")) {
36
+ return false;
37
+ }
38
+
39
+ if (!isViewerOptions(value.options)) {
40
+ return false;
41
+ }
42
+
43
+ const hasFileDiff = isRecord(value.fileDiff);
44
+ const hasBeforeAfterFiles = isRecord(value.oldFile) && isRecord(value.newFile);
45
+ if (!hasFileDiff && !hasBeforeAfterFiles) {
46
+ return false;
47
+ }
48
+
49
+ return true;
50
+ }
51
+
52
+ function isViewerOptions(value: unknown): boolean {
53
+ if (!isRecord(value)) {
54
+ return false;
55
+ }
56
+
57
+ if (!isRecord(value.theme)) {
58
+ return false;
59
+ }
60
+ if (value.theme.light !== "pierre-light" || value.theme.dark !== "pierre-dark") {
61
+ return false;
62
+ }
63
+
64
+ if (!includesValue(DIFF_LAYOUTS, value.diffStyle)) {
65
+ return false;
66
+ }
67
+ if (!includesValue(DIFF_INDICATORS, value.diffIndicators)) {
68
+ return false;
69
+ }
70
+ if (!includesValue(DIFF_THEMES, value.themeType)) {
71
+ return false;
72
+ }
73
+ if (!includesValue(OVERFLOW_VALUES, value.overflow)) {
74
+ return false;
75
+ }
76
+
77
+ if (typeof value.disableLineNumbers !== "boolean") {
78
+ return false;
79
+ }
80
+ if (typeof value.expandUnchanged !== "boolean") {
81
+ return false;
82
+ }
83
+ if (typeof value.backgroundEnabled !== "boolean") {
84
+ return false;
85
+ }
86
+ if (typeof value.unsafeCSS !== "string") {
87
+ return false;
88
+ }
89
+
90
+ return true;
91
+ }
92
+
93
+ function includesValue<T extends readonly string[]>(values: T, value: unknown): value is T[number] {
94
+ return typeof value === "string" && values.includes(value as T[number]);
95
+ }
package/tsconfig.json ADDED
@@ -0,0 +1,16 @@
1
+ {
2
+ "extends": "../tsconfig.package-boundary.base.json",
3
+ "compilerOptions": {
4
+ "rootDir": "."
5
+ },
6
+ "include": ["./*.ts", "./src/**/*.ts"],
7
+ "exclude": [
8
+ "./**/*.test.ts",
9
+ "./dist/**",
10
+ "./node_modules/**",
11
+ "./src/test-support/**",
12
+ "./src/**/*test-helpers.ts",
13
+ "./src/**/*test-harness.ts",
14
+ "./src/**/*test-support.ts"
15
+ ]
16
+ }