@get-bb/plugin-sdk 0.4.3
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/README.md +141 -0
- package/bundled-types/bb-plugin-sdk-app.d.ts +1519 -0
- package/bundled-types/bb-plugin-sdk-internal-composer-customization-validation.d.ts +30 -0
- package/bundled-types/bb-plugin-sdk-internal-composer-view.d.ts +10 -0
- package/bundled-types/bb-plugin-sdk-internal-host-policy.d.ts +141 -0
- package/bundled-types/bb-plugin-sdk-testing-app.d.ts +238 -0
- package/bundled-types/bb-plugin-sdk-testing.d.ts +309 -0
- package/bundled-types/bb-plugin-sdk.d.ts +13635 -0
- package/dist/app.js +36 -0
- package/dist/index.js +11 -0
- package/dist/internal/composer-customization-validation.js +238 -0
- package/dist/internal/composer-view.js +7 -0
- package/dist/internal/host-policy.js +261 -0
- package/dist/testing/app.js +1190 -0
- package/dist/testing/index.js +1625 -0
- package/package.json +137 -0
|
@@ -0,0 +1,1190 @@
|
|
|
1
|
+
// src/testing/app.tsx
|
|
2
|
+
import {
|
|
3
|
+
createContext,
|
|
4
|
+
useContext,
|
|
5
|
+
useEffect,
|
|
6
|
+
useMemo,
|
|
7
|
+
useRef,
|
|
8
|
+
useState,
|
|
9
|
+
useSyncExternalStore
|
|
10
|
+
} from "react";
|
|
11
|
+
import { act, render } from "@testing-library/react";
|
|
12
|
+
|
|
13
|
+
// src/internal/composer-view.ts
|
|
14
|
+
function isComposerDraftEmpty(text, attachmentCount) {
|
|
15
|
+
return text.trim().length === 0 && attachmentCount === 0;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
// src/internal/composer-customization-validation.ts
|
|
19
|
+
var PLUGIN_SLOT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
|
|
20
|
+
var PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
|
|
21
|
+
function normalizePluginThreadRowStatus(value, onRejected) {
|
|
22
|
+
const kind = "contentScript.experimental_setThreadRowStatus";
|
|
23
|
+
if (value === null) return null;
|
|
24
|
+
if (typeof value !== "object" || Array.isArray(value)) {
|
|
25
|
+
onRejected(`${kind}: status must be null or a non-array object`);
|
|
26
|
+
return void 0;
|
|
27
|
+
}
|
|
28
|
+
const status = value;
|
|
29
|
+
const icon = status.icon;
|
|
30
|
+
if (typeof icon !== "string" || icon.trim() === "") {
|
|
31
|
+
onRejected(`${kind}: "icon" must be a non-blank string`);
|
|
32
|
+
return void 0;
|
|
33
|
+
}
|
|
34
|
+
const label = status.label;
|
|
35
|
+
if (typeof label !== "string" || label.trim() === "") {
|
|
36
|
+
onRejected(`${kind}: "label" must be a non-blank string`);
|
|
37
|
+
return void 0;
|
|
38
|
+
}
|
|
39
|
+
const tone = status.tone;
|
|
40
|
+
if (tone !== void 0 && tone !== "default" && tone !== "running" && tone !== "success" && tone !== "error") {
|
|
41
|
+
onRejected(
|
|
42
|
+
`${kind}: "tone" must be "default", "running", "success", or "error" when set`
|
|
43
|
+
);
|
|
44
|
+
return void 0;
|
|
45
|
+
}
|
|
46
|
+
return {
|
|
47
|
+
icon: icon.trim(),
|
|
48
|
+
label: label.trim(),
|
|
49
|
+
...tone !== void 0 ? { tone } : {}
|
|
50
|
+
};
|
|
51
|
+
}
|
|
52
|
+
function requireSlotId(kind, value) {
|
|
53
|
+
if (typeof value !== "string" || !PLUGIN_SLOT_ID_PATTERN.test(value)) {
|
|
54
|
+
throw new Error(
|
|
55
|
+
`${kind}: "id" must match ${String(PLUGIN_SLOT_ID_PATTERN)}, got ${JSON.stringify(value)}`
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
return value;
|
|
59
|
+
}
|
|
60
|
+
function requireMessageDirectiveId(kind, value) {
|
|
61
|
+
if (typeof value !== "string" || !PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN.test(value)) {
|
|
62
|
+
throw new Error(
|
|
63
|
+
`${kind}: "id" must match ${String(PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN)}, got ${JSON.stringify(value)}`
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
return value;
|
|
67
|
+
}
|
|
68
|
+
function requireNonEmptyString(kind, field, value) {
|
|
69
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
70
|
+
throw new Error(`${kind}: "${field}" must be a non-empty string`);
|
|
71
|
+
}
|
|
72
|
+
return value;
|
|
73
|
+
}
|
|
74
|
+
function requireOptionalString(kind, field, value) {
|
|
75
|
+
if (value !== void 0 && typeof value !== "string") {
|
|
76
|
+
throw new Error(`${kind}: "${field}" must be a string when set`);
|
|
77
|
+
}
|
|
78
|
+
return value;
|
|
79
|
+
}
|
|
80
|
+
function requireComponent(kind, value) {
|
|
81
|
+
if (typeof value !== "function") {
|
|
82
|
+
throw new Error(`${kind}: "component" must be a React component function`);
|
|
83
|
+
}
|
|
84
|
+
return value;
|
|
85
|
+
}
|
|
86
|
+
function requireFunction(kind, field, value) {
|
|
87
|
+
if (typeof value !== "function") {
|
|
88
|
+
throw new Error(`${kind}: "${field}" must be a function`);
|
|
89
|
+
}
|
|
90
|
+
return value;
|
|
91
|
+
}
|
|
92
|
+
function requireUniqueId(kind, seen, id) {
|
|
93
|
+
if (seen.has(id)) {
|
|
94
|
+
throw new Error(`${kind}: duplicate id "${id}"`);
|
|
95
|
+
}
|
|
96
|
+
seen.add(id);
|
|
97
|
+
}
|
|
98
|
+
function parseContributionArray(kind, value, onRejected, parse) {
|
|
99
|
+
if (value === void 0) return void 0;
|
|
100
|
+
if (!Array.isArray(value)) {
|
|
101
|
+
onRejected(`${kind}: must be an array when set`);
|
|
102
|
+
return void 0;
|
|
103
|
+
}
|
|
104
|
+
const seenIds = /* @__PURE__ */ new Set();
|
|
105
|
+
const parsed = [];
|
|
106
|
+
for (const [index, entry] of value.entries()) {
|
|
107
|
+
const entryKind = `${kind}[${index}]`;
|
|
108
|
+
try {
|
|
109
|
+
const parsedEntry = parse(entryKind, entry);
|
|
110
|
+
requireUniqueId(entryKind, seenIds, parsedEntry.id);
|
|
111
|
+
parsed.push(parsedEntry);
|
|
112
|
+
} catch (error) {
|
|
113
|
+
onRejected(error instanceof Error ? error.message : String(error));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
return parsed;
|
|
117
|
+
}
|
|
118
|
+
function parseRegions(kind, registration, onRejected) {
|
|
119
|
+
const actions = parseContributionArray(`${kind}.actions`, registration.actions, onRejected, (entryKind, value) => {
|
|
120
|
+
const entry = value;
|
|
121
|
+
return {
|
|
122
|
+
id: requireSlotId(entryKind, entry?.id),
|
|
123
|
+
component: requireComponent(entryKind, entry?.component)
|
|
124
|
+
};
|
|
125
|
+
});
|
|
126
|
+
const banners = parseContributionArray(`${kind}.banners`, registration.banners, onRejected, (entryKind, value) => {
|
|
127
|
+
const entry = value;
|
|
128
|
+
const id = requireSlotId(entryKind, entry?.id);
|
|
129
|
+
const chrome = entry?.chrome;
|
|
130
|
+
if (chrome !== void 0 && chrome !== "card" && chrome !== "bare") {
|
|
131
|
+
throw new Error(
|
|
132
|
+
`${entryKind}: "chrome" must be "card" or "bare" when set`
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
...chrome !== void 0 ? { chrome } : {},
|
|
138
|
+
component: requireComponent(entryKind, entry?.component)
|
|
139
|
+
};
|
|
140
|
+
});
|
|
141
|
+
const plusMenu = parseContributionArray(
|
|
142
|
+
`${kind}.plusMenu`,
|
|
143
|
+
registration.plusMenu,
|
|
144
|
+
onRejected,
|
|
145
|
+
(entryKind, value) => {
|
|
146
|
+
const entry = value;
|
|
147
|
+
const id = requireSlotId(entryKind, entry?.id);
|
|
148
|
+
const icon = requireOptionalString(entryKind, "icon", entry?.icon);
|
|
149
|
+
const description = requireOptionalString(
|
|
150
|
+
entryKind,
|
|
151
|
+
"description",
|
|
152
|
+
entry?.description
|
|
153
|
+
);
|
|
154
|
+
const disabled = entry?.disabled;
|
|
155
|
+
if (disabled !== void 0 && typeof disabled !== "boolean" && typeof disabled !== "function") {
|
|
156
|
+
throw new Error(
|
|
157
|
+
`${entryKind}: "disabled" must be a boolean or function when set`
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
return {
|
|
161
|
+
id,
|
|
162
|
+
label: requireNonEmptyString(entryKind, "label", entry?.label),
|
|
163
|
+
...icon !== void 0 ? { icon } : {},
|
|
164
|
+
...description !== void 0 ? { description } : {},
|
|
165
|
+
...disabled !== void 0 ? {
|
|
166
|
+
disabled
|
|
167
|
+
} : {},
|
|
168
|
+
run: requireFunction(entryKind, "run", entry?.run)
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
);
|
|
172
|
+
let richText;
|
|
173
|
+
if (registration.richText !== void 0) {
|
|
174
|
+
const raw = registration.richText;
|
|
175
|
+
if (typeof raw !== "object" || raw === null || Array.isArray(raw)) {
|
|
176
|
+
onRejected(`${kind}.richText: must be an object when set`);
|
|
177
|
+
} else {
|
|
178
|
+
const effects = parseContributionArray(
|
|
179
|
+
`${kind}.richText.effects`,
|
|
180
|
+
raw.effects,
|
|
181
|
+
onRejected,
|
|
182
|
+
(entryKind, value) => {
|
|
183
|
+
const entry = value;
|
|
184
|
+
return {
|
|
185
|
+
id: requireSlotId(entryKind, entry?.id),
|
|
186
|
+
match: requireFunction(entryKind, "match", entry?.match),
|
|
187
|
+
className: requireNonEmptyString(
|
|
188
|
+
entryKind,
|
|
189
|
+
"className",
|
|
190
|
+
entry?.className
|
|
191
|
+
)
|
|
192
|
+
};
|
|
193
|
+
}
|
|
194
|
+
);
|
|
195
|
+
const onDraftChange = raw.onDraftChange;
|
|
196
|
+
if (onDraftChange !== void 0 && typeof onDraftChange !== "function") {
|
|
197
|
+
onRejected(
|
|
198
|
+
`${kind}.richText: "onDraftChange" must be a function when set`
|
|
199
|
+
);
|
|
200
|
+
}
|
|
201
|
+
richText = {
|
|
202
|
+
...effects !== void 0 ? { effects } : {},
|
|
203
|
+
...typeof onDraftChange === "function" ? {
|
|
204
|
+
onDraftChange
|
|
205
|
+
} : {}
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
return {
|
|
210
|
+
...actions !== void 0 ? { actions } : {},
|
|
211
|
+
...banners !== void 0 ? { banners } : {},
|
|
212
|
+
...plusMenu !== void 0 ? { plusMenu } : {},
|
|
213
|
+
...richText !== void 0 ? { richText } : {}
|
|
214
|
+
};
|
|
215
|
+
}
|
|
216
|
+
function collectComposerCustomization(registration, seenIds, onRejected) {
|
|
217
|
+
const kind = "composer.customize";
|
|
218
|
+
try {
|
|
219
|
+
const raw = registration;
|
|
220
|
+
const id = requireSlotId(kind, raw?.id);
|
|
221
|
+
const scopes = raw?.scopes;
|
|
222
|
+
if (scopes !== void 0) {
|
|
223
|
+
if (!Array.isArray(scopes)) {
|
|
224
|
+
throw new Error(`${kind}: "scopes" must be an array when set`);
|
|
225
|
+
}
|
|
226
|
+
for (const scope of scopes) {
|
|
227
|
+
if (scope !== "thread" && scope !== "queued-message" && scope !== "side-chat" && scope !== "new-thread") {
|
|
228
|
+
throw new Error(
|
|
229
|
+
`${kind}: invalid scope kind ${JSON.stringify(scope)}`
|
|
230
|
+
);
|
|
231
|
+
}
|
|
232
|
+
}
|
|
233
|
+
}
|
|
234
|
+
requireUniqueId(kind, seenIds, id);
|
|
235
|
+
return {
|
|
236
|
+
id,
|
|
237
|
+
...scopes !== void 0 ? { scopes: [...scopes] } : {},
|
|
238
|
+
...parseRegions(`${kind}(${id})`, raw ?? {}, onRejected)
|
|
239
|
+
};
|
|
240
|
+
} catch (error) {
|
|
241
|
+
onRejected(error instanceof Error ? error.message : String(error));
|
|
242
|
+
return null;
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// src/testing/app.tsx
|
|
247
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
248
|
+
function SlotLifecycleGuard({
|
|
249
|
+
children,
|
|
250
|
+
onUnmount
|
|
251
|
+
}) {
|
|
252
|
+
useEffect(() => () => onUnmount(), [onUnmount]);
|
|
253
|
+
return children;
|
|
254
|
+
}
|
|
255
|
+
var SlotEnvContext = createContext(null);
|
|
256
|
+
function useSlotEnv(hook) {
|
|
257
|
+
const env = useContext(SlotEnvContext);
|
|
258
|
+
if (!env) {
|
|
259
|
+
throw new Error(
|
|
260
|
+
`${hook}() needs the test slot environment \u2014 mount the component via renderSlot(...) from @get-bb/plugin-sdk/testing/app`
|
|
261
|
+
);
|
|
262
|
+
}
|
|
263
|
+
return env;
|
|
264
|
+
}
|
|
265
|
+
function definePluginApp(setup) {
|
|
266
|
+
if (typeof setup !== "function") {
|
|
267
|
+
throw new Error("definePluginApp expects a setup function");
|
|
268
|
+
}
|
|
269
|
+
return Object.freeze({ __bbPluginApp: true, setup });
|
|
270
|
+
}
|
|
271
|
+
function isPluginAppDefinition(value) {
|
|
272
|
+
return typeof value === "object" && value !== null && value.__bbPluginApp === true && typeof value.setup === "function";
|
|
273
|
+
}
|
|
274
|
+
function TestThreadChat({
|
|
275
|
+
threadId,
|
|
276
|
+
variant = "full",
|
|
277
|
+
layout = "contained",
|
|
278
|
+
focusRequest,
|
|
279
|
+
permissionPolicy = "inherit",
|
|
280
|
+
className,
|
|
281
|
+
leadingContent,
|
|
282
|
+
messageActions
|
|
283
|
+
}) {
|
|
284
|
+
return /* @__PURE__ */ jsxs(
|
|
285
|
+
"div",
|
|
286
|
+
{
|
|
287
|
+
"data-testid": "bb-thread-chat",
|
|
288
|
+
"data-thread-id": threadId,
|
|
289
|
+
"data-variant": variant,
|
|
290
|
+
"data-layout": layout,
|
|
291
|
+
"data-focus-request": focusRequest ?? 0,
|
|
292
|
+
"data-permission-policy": permissionPolicy,
|
|
293
|
+
"data-message-actions": (messageActions ?? []).map((action) => action.id).join(" "),
|
|
294
|
+
className,
|
|
295
|
+
children: [
|
|
296
|
+
leadingContent === void 0 ? null : /* @__PURE__ */ jsx("div", { "data-testid": "bb-thread-chat-leading-content", children: leadingContent }),
|
|
297
|
+
"ThreadChat stub (",
|
|
298
|
+
threadId,
|
|
299
|
+
")",
|
|
300
|
+
(messageActions ?? []).map((action) => /* @__PURE__ */ jsx(
|
|
301
|
+
"button",
|
|
302
|
+
{
|
|
303
|
+
type: "button",
|
|
304
|
+
"data-testid": `bb-thread-chat-action-${action.id}`,
|
|
305
|
+
"data-roles": action.roles === void 0 ? "" : action.roles.join(" "),
|
|
306
|
+
onClick: () => {
|
|
307
|
+
void action.run({
|
|
308
|
+
id: "test-message",
|
|
309
|
+
threadId,
|
|
310
|
+
role: action.roles?.[0] ?? "assistant",
|
|
311
|
+
text: "test message text",
|
|
312
|
+
sourceSeqEnd: 1
|
|
313
|
+
});
|
|
314
|
+
},
|
|
315
|
+
children: action.title
|
|
316
|
+
},
|
|
317
|
+
action.id
|
|
318
|
+
))
|
|
319
|
+
]
|
|
320
|
+
}
|
|
321
|
+
);
|
|
322
|
+
}
|
|
323
|
+
function TestMarkdown({ content, className }) {
|
|
324
|
+
return /* @__PURE__ */ jsx("div", { "data-testid": "bb-markdown", className, children: content });
|
|
325
|
+
}
|
|
326
|
+
function TestNewThreadComposer({
|
|
327
|
+
defaultProjectId,
|
|
328
|
+
defaultProviderId,
|
|
329
|
+
defaultModel,
|
|
330
|
+
defaultReasoningLevel,
|
|
331
|
+
defaultServiceTier,
|
|
332
|
+
defaultPermissionMode,
|
|
333
|
+
defaultEnvironment,
|
|
334
|
+
initialPrompt,
|
|
335
|
+
placeholder,
|
|
336
|
+
layout = "contained",
|
|
337
|
+
focusRequest,
|
|
338
|
+
className,
|
|
339
|
+
draftKey,
|
|
340
|
+
onSubmit
|
|
341
|
+
}) {
|
|
342
|
+
const [text, setText] = useState(initialPrompt ?? "");
|
|
343
|
+
return /* @__PURE__ */ jsxs(
|
|
344
|
+
"div",
|
|
345
|
+
{
|
|
346
|
+
"data-testid": "bb-new-thread-composer",
|
|
347
|
+
"data-default-project-id": defaultProjectId ?? "",
|
|
348
|
+
"data-default-provider-id": defaultProviderId ?? "",
|
|
349
|
+
"data-default-model": defaultModel ?? "",
|
|
350
|
+
"data-default-reasoning-level": defaultReasoningLevel ?? "",
|
|
351
|
+
"data-default-service-tier": defaultServiceTier ?? "",
|
|
352
|
+
"data-default-permission-mode": defaultPermissionMode ?? "",
|
|
353
|
+
"data-default-environment": defaultEnvironment === void 0 ? "" : JSON.stringify(defaultEnvironment),
|
|
354
|
+
"data-layout": layout,
|
|
355
|
+
"data-focus-request": focusRequest ?? 0,
|
|
356
|
+
"data-draft-key": draftKey ?? "",
|
|
357
|
+
className,
|
|
358
|
+
children: [
|
|
359
|
+
/* @__PURE__ */ jsx(
|
|
360
|
+
"textarea",
|
|
361
|
+
{
|
|
362
|
+
"data-testid": "bb-new-thread-composer-input",
|
|
363
|
+
placeholder,
|
|
364
|
+
value: text,
|
|
365
|
+
onChange: (event) => setText(event.target.value)
|
|
366
|
+
}
|
|
367
|
+
),
|
|
368
|
+
/* @__PURE__ */ jsx(
|
|
369
|
+
"button",
|
|
370
|
+
{
|
|
371
|
+
type: "button",
|
|
372
|
+
"data-testid": "bb-new-thread-composer-submit",
|
|
373
|
+
onClick: () => {
|
|
374
|
+
void onSubmit({
|
|
375
|
+
projectId: defaultProjectId ?? "project-test",
|
|
376
|
+
providerId: defaultProviderId ?? "codex",
|
|
377
|
+
model: defaultModel ?? "gpt-5",
|
|
378
|
+
reasoningLevel: defaultReasoningLevel ?? "medium",
|
|
379
|
+
permissionMode: defaultPermissionMode ?? "auto",
|
|
380
|
+
...defaultServiceTier !== void 0 ? { serviceTier: defaultServiceTier } : {},
|
|
381
|
+
executionInputSources: {},
|
|
382
|
+
environment: defaultEnvironment ?? { type: "project-default" },
|
|
383
|
+
input: [{ type: "text", text, mentions: [] }]
|
|
384
|
+
});
|
|
385
|
+
},
|
|
386
|
+
children: "Start thread"
|
|
387
|
+
}
|
|
388
|
+
)
|
|
389
|
+
]
|
|
390
|
+
}
|
|
391
|
+
);
|
|
392
|
+
}
|
|
393
|
+
var testPluginSdkApp = {
|
|
394
|
+
definePluginApp,
|
|
395
|
+
useRpc() {
|
|
396
|
+
return useSlotEnv("useRpc").rpcClient;
|
|
397
|
+
},
|
|
398
|
+
useRealtime(channel, handler) {
|
|
399
|
+
const env = useSlotEnv("useRealtime");
|
|
400
|
+
const handlerRef = useRef(handler);
|
|
401
|
+
useEffect(() => {
|
|
402
|
+
handlerRef.current = handler;
|
|
403
|
+
});
|
|
404
|
+
useEffect(() => {
|
|
405
|
+
const listener = (payload) => handlerRef.current(payload);
|
|
406
|
+
let listeners = env.realtimeHandlers.get(channel);
|
|
407
|
+
if (!listeners) {
|
|
408
|
+
listeners = /* @__PURE__ */ new Set();
|
|
409
|
+
env.realtimeHandlers.set(channel, listeners);
|
|
410
|
+
}
|
|
411
|
+
listeners.add(listener);
|
|
412
|
+
return () => {
|
|
413
|
+
listeners.delete(listener);
|
|
414
|
+
};
|
|
415
|
+
}, [env, channel]);
|
|
416
|
+
},
|
|
417
|
+
useRealtimeConnectionState() {
|
|
418
|
+
const connection = useSlotEnv(
|
|
419
|
+
"useRealtimeConnectionState"
|
|
420
|
+
).realtimeConnection;
|
|
421
|
+
return useSyncExternalStore(
|
|
422
|
+
connection.subscribe,
|
|
423
|
+
connection.getSnapshot,
|
|
424
|
+
connection.getSnapshot
|
|
425
|
+
);
|
|
426
|
+
},
|
|
427
|
+
useSettings() {
|
|
428
|
+
return useSlotEnv("useSettings").settingsState;
|
|
429
|
+
},
|
|
430
|
+
useBbContext() {
|
|
431
|
+
return useSlotEnv("useBbContext").bbContext;
|
|
432
|
+
},
|
|
433
|
+
useBbNavigate() {
|
|
434
|
+
return useSlotEnv("useBbNavigate").navigate;
|
|
435
|
+
},
|
|
436
|
+
useComposer() {
|
|
437
|
+
const composer = useSlotEnv("useComposer").composer;
|
|
438
|
+
const version = useSyncExternalStore(
|
|
439
|
+
composer.subscribe,
|
|
440
|
+
composer.getVersionSnapshot,
|
|
441
|
+
composer.getVersionSnapshot
|
|
442
|
+
);
|
|
443
|
+
return useMemo(
|
|
444
|
+
() => ({
|
|
445
|
+
...composer.api,
|
|
446
|
+
scope: composer.getScope(),
|
|
447
|
+
text: composer.getText()
|
|
448
|
+
}),
|
|
449
|
+
[composer, version]
|
|
450
|
+
);
|
|
451
|
+
},
|
|
452
|
+
ThreadChat: TestThreadChat,
|
|
453
|
+
Markdown: TestMarkdown,
|
|
454
|
+
experimental_NewThreadComposer: TestNewThreadComposer,
|
|
455
|
+
experimental_useSidebarThreads() {
|
|
456
|
+
return useSlotEnv("experimental_useSidebarThreads").sidebarThreads;
|
|
457
|
+
},
|
|
458
|
+
experimental_useSidebarThreadActions() {
|
|
459
|
+
return useSlotEnv("experimental_useSidebarThreadActions").sidebarActions;
|
|
460
|
+
},
|
|
461
|
+
experimental_useSidebarThreadSplit(threadId) {
|
|
462
|
+
const env = useSlotEnv("experimental_useSidebarThreadSplit");
|
|
463
|
+
return useMemo(
|
|
464
|
+
() => ({
|
|
465
|
+
splitProps: {
|
|
466
|
+
onPointerDown: () => {
|
|
467
|
+
env.sidebarActionCalls.push({ method: "open", threadId });
|
|
468
|
+
}
|
|
469
|
+
},
|
|
470
|
+
isAvailable: true,
|
|
471
|
+
layout: null
|
|
472
|
+
}),
|
|
473
|
+
[env, threadId]
|
|
474
|
+
);
|
|
475
|
+
},
|
|
476
|
+
experimental_useSidebarThreadPullRequest(threadId) {
|
|
477
|
+
const env = useSlotEnv("experimental_useSidebarThreadPullRequest");
|
|
478
|
+
return useMemo(
|
|
479
|
+
() => ({
|
|
480
|
+
isLoading: false,
|
|
481
|
+
pullRequest: env.sidebarPullRequests.get(threadId) ?? null
|
|
482
|
+
}),
|
|
483
|
+
[env, threadId]
|
|
484
|
+
);
|
|
485
|
+
},
|
|
486
|
+
useComposerView() {
|
|
487
|
+
const composer = useSlotEnv("useComposerView").composer;
|
|
488
|
+
const version = useSyncExternalStore(
|
|
489
|
+
composer.subscribe,
|
|
490
|
+
composer.getVersionSnapshot,
|
|
491
|
+
composer.getVersionSnapshot
|
|
492
|
+
);
|
|
493
|
+
return useMemo(() => {
|
|
494
|
+
const text = composer.getText();
|
|
495
|
+
const attachmentCount = composer.getAttachmentCount();
|
|
496
|
+
return {
|
|
497
|
+
scope: composer.getScope(),
|
|
498
|
+
layout: "expanded",
|
|
499
|
+
draft: {
|
|
500
|
+
text,
|
|
501
|
+
isEmpty: isComposerDraftEmpty(text, attachmentCount),
|
|
502
|
+
attachmentCount
|
|
503
|
+
},
|
|
504
|
+
run: { isRunning: false, isSubmitting: false }
|
|
505
|
+
};
|
|
506
|
+
}, [composer, version]);
|
|
507
|
+
}
|
|
508
|
+
};
|
|
509
|
+
function installTestPluginRuntime() {
|
|
510
|
+
const host = globalThis;
|
|
511
|
+
host.__bbPluginRuntime = {
|
|
512
|
+
...host.__bbPluginRuntime,
|
|
513
|
+
pluginSdkApp: testPluginSdkApp
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
function collectRegistrations(definition) {
|
|
517
|
+
const captured = {
|
|
518
|
+
homepageSections: [],
|
|
519
|
+
settingsSections: [],
|
|
520
|
+
navPanels: [],
|
|
521
|
+
threadPanelActions: [],
|
|
522
|
+
newThreadPanelActions: [],
|
|
523
|
+
composerCustomizations: [],
|
|
524
|
+
pendingInteractions: [],
|
|
525
|
+
sidebarFooterActions: [],
|
|
526
|
+
threadLists: [],
|
|
527
|
+
threadHeaderActions: [],
|
|
528
|
+
fileOpeners: [],
|
|
529
|
+
messageDirectives: [],
|
|
530
|
+
messageActions: [],
|
|
531
|
+
contentScripts: []
|
|
532
|
+
};
|
|
533
|
+
const seenIds = {
|
|
534
|
+
homepageSection: /* @__PURE__ */ new Set(),
|
|
535
|
+
settingsSection: /* @__PURE__ */ new Set(),
|
|
536
|
+
navPanel: /* @__PURE__ */ new Set(),
|
|
537
|
+
threadPanelAction: /* @__PURE__ */ new Set(),
|
|
538
|
+
newThreadPanelAction: /* @__PURE__ */ new Set(),
|
|
539
|
+
composerCustomization: /* @__PURE__ */ new Set(),
|
|
540
|
+
pendingInteraction: /* @__PURE__ */ new Set(),
|
|
541
|
+
sidebarFooterAction: /* @__PURE__ */ new Set(),
|
|
542
|
+
threadList: /* @__PURE__ */ new Set(),
|
|
543
|
+
threadHeaderAction: /* @__PURE__ */ new Set(),
|
|
544
|
+
fileOpener: /* @__PURE__ */ new Set(),
|
|
545
|
+
messageDirective: /* @__PURE__ */ new Set(),
|
|
546
|
+
messageAction: /* @__PURE__ */ new Set(),
|
|
547
|
+
contentScript: /* @__PURE__ */ new Set()
|
|
548
|
+
};
|
|
549
|
+
definition.setup({
|
|
550
|
+
slots: {
|
|
551
|
+
homepageSection(registration) {
|
|
552
|
+
const kind = "slots.homepageSection";
|
|
553
|
+
const id = requireSlotId(kind, registration?.id);
|
|
554
|
+
requireUniqueId(kind, seenIds.homepageSection, id);
|
|
555
|
+
captured.homepageSections.push({
|
|
556
|
+
id,
|
|
557
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
558
|
+
component: requireComponent(kind, registration.component)
|
|
559
|
+
});
|
|
560
|
+
},
|
|
561
|
+
settingsSection(registration) {
|
|
562
|
+
const kind = "slots.settingsSection";
|
|
563
|
+
const id = requireSlotId(kind, registration?.id);
|
|
564
|
+
requireUniqueId(kind, seenIds.settingsSection, id);
|
|
565
|
+
const title = requireOptionalString(kind, "title", registration.title);
|
|
566
|
+
const description = requireOptionalString(
|
|
567
|
+
kind,
|
|
568
|
+
"description",
|
|
569
|
+
registration.description
|
|
570
|
+
);
|
|
571
|
+
captured.settingsSections.push({
|
|
572
|
+
id,
|
|
573
|
+
...title !== void 0 ? { title } : {},
|
|
574
|
+
...description !== void 0 ? { description } : {},
|
|
575
|
+
component: requireComponent(kind, registration.component)
|
|
576
|
+
});
|
|
577
|
+
},
|
|
578
|
+
navPanel(registration) {
|
|
579
|
+
const kind = "slots.navPanel";
|
|
580
|
+
const id = requireSlotId(kind, registration?.id);
|
|
581
|
+
requireUniqueId(kind, seenIds.navPanel, id);
|
|
582
|
+
const path = requireNonEmptyString(kind, "path", registration.path);
|
|
583
|
+
if (!PLUGIN_SLOT_ID_PATTERN.test(path)) {
|
|
584
|
+
throw new Error(
|
|
585
|
+
`${kind}: "path" must match ${String(PLUGIN_SLOT_ID_PATTERN)} (it becomes a URL segment), got ${JSON.stringify(path)}`
|
|
586
|
+
);
|
|
587
|
+
}
|
|
588
|
+
if (registration.headerContent !== void 0 && typeof registration.headerContent !== "function") {
|
|
589
|
+
throw new Error(
|
|
590
|
+
`${kind}: "headerContent" must be a React component function when set`
|
|
591
|
+
);
|
|
592
|
+
}
|
|
593
|
+
if (registration.experimental_sidebarAccessory !== void 0 && typeof registration.experimental_sidebarAccessory !== "function") {
|
|
594
|
+
throw new Error(
|
|
595
|
+
`${kind}: "experimental_sidebarAccessory" must be a React component function when set`
|
|
596
|
+
);
|
|
597
|
+
}
|
|
598
|
+
captured.navPanels.push({
|
|
599
|
+
id,
|
|
600
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
601
|
+
icon: requireNonEmptyString(kind, "icon", registration.icon),
|
|
602
|
+
path,
|
|
603
|
+
component: requireComponent(kind, registration.component),
|
|
604
|
+
...registration.experimental_sidebarAccessory !== void 0 ? {
|
|
605
|
+
experimental_sidebarAccessory: registration.experimental_sidebarAccessory
|
|
606
|
+
} : {},
|
|
607
|
+
...registration.headerContent !== void 0 ? { headerContent: registration.headerContent } : {}
|
|
608
|
+
});
|
|
609
|
+
},
|
|
610
|
+
threadPanelAction(registration) {
|
|
611
|
+
const kind = "slots.threadPanelAction";
|
|
612
|
+
const id = requireSlotId(kind, registration?.id);
|
|
613
|
+
requireUniqueId(kind, seenIds.threadPanelAction, id);
|
|
614
|
+
if (registration.run !== void 0 && typeof registration.run !== "function") {
|
|
615
|
+
throw new Error(`${kind}: "run" must be a function when set`);
|
|
616
|
+
}
|
|
617
|
+
if (registration.layout !== void 0 && registration.layout !== "padded" && registration.layout !== "flush") {
|
|
618
|
+
throw new Error(`${kind}: "layout" must be "padded" or "flush"`);
|
|
619
|
+
}
|
|
620
|
+
captured.threadPanelActions.push({
|
|
621
|
+
id,
|
|
622
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
623
|
+
...registration.icon !== void 0 ? { icon: requireNonEmptyString(kind, "icon", registration.icon) } : {},
|
|
624
|
+
component: requireComponent(kind, registration.component),
|
|
625
|
+
...registration.layout !== void 0 ? { layout: registration.layout } : {},
|
|
626
|
+
...registration.run !== void 0 ? { run: registration.run } : {}
|
|
627
|
+
});
|
|
628
|
+
},
|
|
629
|
+
experimental_newThreadPanelAction(registration) {
|
|
630
|
+
const kind = "slots.experimental_newThreadPanelAction";
|
|
631
|
+
const id = requireSlotId(kind, registration?.id);
|
|
632
|
+
requireUniqueId(kind, seenIds.newThreadPanelAction, id);
|
|
633
|
+
if (registration.run !== void 0 && typeof registration.run !== "function") {
|
|
634
|
+
throw new Error(`${kind}: "run" must be a function when set`);
|
|
635
|
+
}
|
|
636
|
+
if (registration.layout !== void 0 && registration.layout !== "padded" && registration.layout !== "flush") {
|
|
637
|
+
throw new Error(`${kind}: "layout" must be "padded" or "flush"`);
|
|
638
|
+
}
|
|
639
|
+
captured.newThreadPanelActions.push({
|
|
640
|
+
id,
|
|
641
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
642
|
+
...registration.icon !== void 0 ? { icon: requireNonEmptyString(kind, "icon", registration.icon) } : {},
|
|
643
|
+
component: requireComponent(kind, registration.component),
|
|
644
|
+
...registration.layout !== void 0 ? { layout: registration.layout } : {},
|
|
645
|
+
...registration.run !== void 0 ? { run: registration.run } : {}
|
|
646
|
+
});
|
|
647
|
+
},
|
|
648
|
+
pendingInteraction(registration) {
|
|
649
|
+
const kind = "slots.pendingInteraction";
|
|
650
|
+
const id = requireSlotId(kind, registration?.id);
|
|
651
|
+
requireUniqueId(kind, seenIds.pendingInteraction, id);
|
|
652
|
+
captured.pendingInteractions.push({
|
|
653
|
+
id,
|
|
654
|
+
component: requireComponent(kind, registration.component)
|
|
655
|
+
});
|
|
656
|
+
},
|
|
657
|
+
sidebarFooterAction(registration) {
|
|
658
|
+
const kind = "slots.sidebarFooterAction";
|
|
659
|
+
const id = requireSlotId(kind, registration?.id);
|
|
660
|
+
requireUniqueId(kind, seenIds.sidebarFooterAction, id);
|
|
661
|
+
if (typeof registration.run !== "function") {
|
|
662
|
+
throw new Error(`${kind}: "run" must be a function`);
|
|
663
|
+
}
|
|
664
|
+
captured.sidebarFooterActions.push({
|
|
665
|
+
id,
|
|
666
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
667
|
+
icon: requireNonEmptyString(kind, "icon", registration.icon),
|
|
668
|
+
run: registration.run
|
|
669
|
+
});
|
|
670
|
+
},
|
|
671
|
+
experimental_threadList(registration) {
|
|
672
|
+
const kind = "slots.experimental_threadList";
|
|
673
|
+
const id = requireSlotId(kind, registration?.id);
|
|
674
|
+
requireUniqueId(kind, seenIds.threadList, id);
|
|
675
|
+
const description = requireOptionalString(
|
|
676
|
+
kind,
|
|
677
|
+
"description",
|
|
678
|
+
registration.description
|
|
679
|
+
);
|
|
680
|
+
captured.threadLists.push({
|
|
681
|
+
id,
|
|
682
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
683
|
+
...description !== void 0 ? { description } : {},
|
|
684
|
+
component: requireComponent(kind, registration.component)
|
|
685
|
+
});
|
|
686
|
+
},
|
|
687
|
+
experimental_threadHeaderAction(registration) {
|
|
688
|
+
const kind = "slots.experimental_threadHeaderAction";
|
|
689
|
+
const id = requireSlotId(kind, registration?.id);
|
|
690
|
+
requireUniqueId(kind, seenIds.threadHeaderAction, id);
|
|
691
|
+
captured.threadHeaderActions.push({
|
|
692
|
+
id,
|
|
693
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
694
|
+
component: requireComponent(kind, registration.component)
|
|
695
|
+
});
|
|
696
|
+
},
|
|
697
|
+
fileOpener(registration) {
|
|
698
|
+
const kind = "slots.fileOpener";
|
|
699
|
+
const id = requireSlotId(kind, registration?.id);
|
|
700
|
+
requireUniqueId(kind, seenIds.fileOpener, id);
|
|
701
|
+
const rawExtensions = registration?.extensions;
|
|
702
|
+
if (!Array.isArray(rawExtensions) || rawExtensions.length === 0) {
|
|
703
|
+
throw new Error(
|
|
704
|
+
`${kind}: "extensions" must be a non-empty array of lowercase extensions without the dot`
|
|
705
|
+
);
|
|
706
|
+
}
|
|
707
|
+
const extensions = rawExtensions.map((extension) => {
|
|
708
|
+
if (typeof extension !== "string" || !/^[a-z0-9]+$/.test(extension)) {
|
|
709
|
+
throw new Error(
|
|
710
|
+
`${kind}: extensions must be lowercase alphanumerics without the dot, got ${JSON.stringify(extension)}`
|
|
711
|
+
);
|
|
712
|
+
}
|
|
713
|
+
return extension;
|
|
714
|
+
});
|
|
715
|
+
captured.fileOpeners.push({
|
|
716
|
+
id,
|
|
717
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
718
|
+
extensions,
|
|
719
|
+
component: requireComponent(kind, registration.component)
|
|
720
|
+
});
|
|
721
|
+
},
|
|
722
|
+
messageDirective(registration) {
|
|
723
|
+
const kind = "slots.messageDirective";
|
|
724
|
+
const id = requireMessageDirectiveId(kind, registration?.id);
|
|
725
|
+
requireUniqueId(kind, seenIds.messageDirective, id);
|
|
726
|
+
captured.messageDirectives.push({
|
|
727
|
+
id,
|
|
728
|
+
component: requireComponent(kind, registration.component)
|
|
729
|
+
});
|
|
730
|
+
},
|
|
731
|
+
messageAction(registration) {
|
|
732
|
+
const kind = "slots.messageAction";
|
|
733
|
+
const id = requireSlotId(kind, registration?.id);
|
|
734
|
+
requireUniqueId(kind, seenIds.messageAction, id);
|
|
735
|
+
if (typeof registration.run !== "function") {
|
|
736
|
+
throw new Error(`${kind}: "run" must be a function`);
|
|
737
|
+
}
|
|
738
|
+
captured.messageActions.push({
|
|
739
|
+
id,
|
|
740
|
+
title: requireNonEmptyString(kind, "title", registration.title),
|
|
741
|
+
...registration.icon !== void 0 ? { icon: requireNonEmptyString(kind, "icon", registration.icon) } : {},
|
|
742
|
+
run: registration.run
|
|
743
|
+
});
|
|
744
|
+
}
|
|
745
|
+
},
|
|
746
|
+
composer: {
|
|
747
|
+
customize(registration) {
|
|
748
|
+
const customization = collectComposerCustomization(
|
|
749
|
+
registration,
|
|
750
|
+
seenIds.composerCustomization,
|
|
751
|
+
(reason) => console.warn(reason)
|
|
752
|
+
);
|
|
753
|
+
if (customization !== null) {
|
|
754
|
+
captured.composerCustomizations.push(customization);
|
|
755
|
+
}
|
|
756
|
+
}
|
|
757
|
+
},
|
|
758
|
+
contentScripts: {
|
|
759
|
+
register(registration) {
|
|
760
|
+
const kind = "contentScripts.register";
|
|
761
|
+
const id = requireSlotId(kind, registration?.id);
|
|
762
|
+
requireUniqueId(kind, seenIds.contentScript, id);
|
|
763
|
+
if (typeof registration.mount !== "function") {
|
|
764
|
+
throw new Error(`${kind}: "mount" must be a function`);
|
|
765
|
+
}
|
|
766
|
+
captured.contentScripts.push({ id, mount: registration.mount });
|
|
767
|
+
}
|
|
768
|
+
}
|
|
769
|
+
});
|
|
770
|
+
return captured;
|
|
771
|
+
}
|
|
772
|
+
async function loadPluginApp(source) {
|
|
773
|
+
installTestPluginRuntime();
|
|
774
|
+
const resolved = typeof source === "function" ? await source() : source;
|
|
775
|
+
const definition = isPluginAppDefinition(resolved) ? resolved : resolved.default;
|
|
776
|
+
if (!isPluginAppDefinition(definition)) {
|
|
777
|
+
throw new Error(
|
|
778
|
+
"the bundle's default export is not definePluginApp(...) from @get-bb/plugin-sdk/app"
|
|
779
|
+
);
|
|
780
|
+
}
|
|
781
|
+
return collectRegistrations(definition);
|
|
782
|
+
}
|
|
783
|
+
async function mountPluginContentScripts(app, options) {
|
|
784
|
+
const controller = new AbortController();
|
|
785
|
+
const generation = options.generation ?? 1;
|
|
786
|
+
const mounted = [];
|
|
787
|
+
const threadRowStatuses = /* @__PURE__ */ new Map();
|
|
788
|
+
const threadRowStatusCalls = [];
|
|
789
|
+
let disposed = false;
|
|
790
|
+
const setThreadRowStatus = (threadId, status) => {
|
|
791
|
+
if (controller.signal.aborted) return;
|
|
792
|
+
if (typeof threadId !== "string" || threadId.trim().length === 0) {
|
|
793
|
+
console.warn(
|
|
794
|
+
`bb plugin "${options.pluginId}": contentScript.experimental_setThreadRowStatus: "threadId" must be a non-empty string`
|
|
795
|
+
);
|
|
796
|
+
return;
|
|
797
|
+
}
|
|
798
|
+
const normalizedThreadId = threadId.trim();
|
|
799
|
+
const normalizedStatus = normalizePluginThreadRowStatus(
|
|
800
|
+
status,
|
|
801
|
+
(reason) => console.warn(`bb plugin "${options.pluginId}": ${reason}`)
|
|
802
|
+
);
|
|
803
|
+
if (normalizedStatus === void 0) return;
|
|
804
|
+
const recordedStatus = normalizedStatus === null ? null : { ...normalizedStatus };
|
|
805
|
+
threadRowStatusCalls.push({
|
|
806
|
+
threadId: normalizedThreadId,
|
|
807
|
+
status: recordedStatus
|
|
808
|
+
});
|
|
809
|
+
if (recordedStatus === null) {
|
|
810
|
+
threadRowStatuses.delete(normalizedThreadId);
|
|
811
|
+
} else {
|
|
812
|
+
threadRowStatuses.set(normalizedThreadId, recordedStatus);
|
|
813
|
+
}
|
|
814
|
+
};
|
|
815
|
+
const dispose = async () => {
|
|
816
|
+
if (disposed) return;
|
|
817
|
+
disposed = true;
|
|
818
|
+
controller.abort();
|
|
819
|
+
for (const script of [...mounted].reverse()) {
|
|
820
|
+
if (script.dispose === null) continue;
|
|
821
|
+
try {
|
|
822
|
+
await script.dispose();
|
|
823
|
+
} catch (error) {
|
|
824
|
+
console.warn(
|
|
825
|
+
`[plugin:${options.pluginId}] content script "${script.id}" cleanup failed: ${error instanceof Error ? error.message : String(error)}`
|
|
826
|
+
);
|
|
827
|
+
}
|
|
828
|
+
}
|
|
829
|
+
threadRowStatuses.clear();
|
|
830
|
+
};
|
|
831
|
+
try {
|
|
832
|
+
for (const registration of app.contentScripts) {
|
|
833
|
+
const result = await registration.mount({
|
|
834
|
+
pluginId: options.pluginId,
|
|
835
|
+
generation,
|
|
836
|
+
signal: controller.signal,
|
|
837
|
+
...!options.omitExperimentalThreadRowStatus ? { experimental_setThreadRowStatus: setThreadRowStatus } : {}
|
|
838
|
+
});
|
|
839
|
+
if (result !== void 0 && typeof result !== "function") {
|
|
840
|
+
throw new Error(
|
|
841
|
+
`content script "${registration.id}" mount must return a cleanup function, a promise of one, or nothing`
|
|
842
|
+
);
|
|
843
|
+
}
|
|
844
|
+
mounted.push({ id: registration.id, dispose: result ?? null });
|
|
845
|
+
}
|
|
846
|
+
} catch (error) {
|
|
847
|
+
await dispose();
|
|
848
|
+
throw error;
|
|
849
|
+
}
|
|
850
|
+
return {
|
|
851
|
+
inspection: {
|
|
852
|
+
get mountedIds() {
|
|
853
|
+
return mounted.map(({ id }) => id);
|
|
854
|
+
},
|
|
855
|
+
signal: controller.signal,
|
|
856
|
+
get disposed() {
|
|
857
|
+
return disposed;
|
|
858
|
+
},
|
|
859
|
+
get threadRowStatusCalls() {
|
|
860
|
+
return threadRowStatusCalls.map(({ threadId, status }) => ({
|
|
861
|
+
threadId,
|
|
862
|
+
status: status === null ? null : { ...status }
|
|
863
|
+
}));
|
|
864
|
+
},
|
|
865
|
+
getThreadRowStatus(threadId) {
|
|
866
|
+
const status = threadRowStatuses.get(threadId);
|
|
867
|
+
return status === void 0 ? null : { ...status };
|
|
868
|
+
}
|
|
869
|
+
},
|
|
870
|
+
lifecycle: { dispose }
|
|
871
|
+
};
|
|
872
|
+
}
|
|
873
|
+
function strictJsonRoundTrip(value, label) {
|
|
874
|
+
const ancestors = /* @__PURE__ */ new Set();
|
|
875
|
+
function visit(current, path) {
|
|
876
|
+
if (current === null || typeof current === "string" || typeof current === "boolean") {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (typeof current === "number") {
|
|
880
|
+
if (!Number.isFinite(current)) {
|
|
881
|
+
throw new Error(`${label} at ${path} contains a non-finite number`);
|
|
882
|
+
}
|
|
883
|
+
return;
|
|
884
|
+
}
|
|
885
|
+
if (typeof current !== "object") {
|
|
886
|
+
throw new Error(`${label} at ${path} is not a JSON value`);
|
|
887
|
+
}
|
|
888
|
+
if (ancestors.has(current)) {
|
|
889
|
+
throw new Error(`${label} at ${path} is cyclic`);
|
|
890
|
+
}
|
|
891
|
+
ancestors.add(current);
|
|
892
|
+
try {
|
|
893
|
+
if (Array.isArray(current)) {
|
|
894
|
+
current.forEach((item, index) => visit(item, `${path}[${index}]`));
|
|
895
|
+
return;
|
|
896
|
+
}
|
|
897
|
+
const prototype = Object.getPrototypeOf(current);
|
|
898
|
+
if (prototype !== Object.prototype && prototype !== null) {
|
|
899
|
+
throw new Error(`${label} at ${path} must be a plain JSON object`);
|
|
900
|
+
}
|
|
901
|
+
if (Reflect.ownKeys(current).some((key) => typeof key === "symbol")) {
|
|
902
|
+
throw new Error(`${label} at ${path} contains a symbol key`);
|
|
903
|
+
}
|
|
904
|
+
for (const [key, child] of Object.entries(current)) {
|
|
905
|
+
visit(child, `${path}.${key}`);
|
|
906
|
+
}
|
|
907
|
+
} finally {
|
|
908
|
+
ancestors.delete(current);
|
|
909
|
+
}
|
|
910
|
+
}
|
|
911
|
+
visit(value, "$");
|
|
912
|
+
return JSON.parse(JSON.stringify(value));
|
|
913
|
+
}
|
|
914
|
+
function renderSlot(registration, props, options = {}) {
|
|
915
|
+
const rpcCalls = [];
|
|
916
|
+
const rpcHandlers = options.rpc ?? {};
|
|
917
|
+
const rpcClient = {
|
|
918
|
+
async call(method, input) {
|
|
919
|
+
const normalizedInput = input === void 0 ? null : strictJsonRoundTrip(input, `rpc "${method}" input`);
|
|
920
|
+
rpcCalls.push({ method, input: normalizedInput });
|
|
921
|
+
const handler = rpcHandlers[method];
|
|
922
|
+
if (!handler) {
|
|
923
|
+
throw new Error(
|
|
924
|
+
`no rpc handler for "${method}" \u2014 add it to renderSlot options.rpc`
|
|
925
|
+
);
|
|
926
|
+
}
|
|
927
|
+
const result2 = await handler(normalizedInput);
|
|
928
|
+
return strictJsonRoundTrip(result2, `rpc "${method}" result`);
|
|
929
|
+
}
|
|
930
|
+
};
|
|
931
|
+
const realtimeHandlers = /* @__PURE__ */ new Map();
|
|
932
|
+
let realtimeConnectionState = options.realtimeConnectionState ?? "connected";
|
|
933
|
+
const realtimeConnectionListeners = /* @__PURE__ */ new Set();
|
|
934
|
+
const realtimeConnection = {
|
|
935
|
+
getSnapshot: () => realtimeConnectionState,
|
|
936
|
+
subscribe(listener) {
|
|
937
|
+
realtimeConnectionListeners.add(listener);
|
|
938
|
+
return () => realtimeConnectionListeners.delete(listener);
|
|
939
|
+
},
|
|
940
|
+
setState(state) {
|
|
941
|
+
if (state === realtimeConnectionState) return;
|
|
942
|
+
realtimeConnectionState = state;
|
|
943
|
+
for (const listener of realtimeConnectionListeners) listener();
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
const navigateCalls = [];
|
|
947
|
+
const sidebarActionCalls = [];
|
|
948
|
+
const sidebarPullRequests = new Map(
|
|
949
|
+
Object.entries(options.sidebarPullRequests ?? {})
|
|
950
|
+
);
|
|
951
|
+
const sidebarThreads = {
|
|
952
|
+
status: options.sidebarThreads?.status ?? "ready",
|
|
953
|
+
threads: options.sidebarThreads?.threads ?? [],
|
|
954
|
+
projects: options.sidebarThreads?.projects ?? []
|
|
955
|
+
};
|
|
956
|
+
const sidebarActions = {
|
|
957
|
+
open(threadId2, openOptions) {
|
|
958
|
+
sidebarActionCalls.push({
|
|
959
|
+
method: "open",
|
|
960
|
+
threadId: threadId2,
|
|
961
|
+
...openOptions ? { options: { ...openOptions } } : {}
|
|
962
|
+
});
|
|
963
|
+
},
|
|
964
|
+
openNewThread(newThreadOptions) {
|
|
965
|
+
sidebarActionCalls.push({
|
|
966
|
+
method: "openNewThread",
|
|
967
|
+
...newThreadOptions ? { options: { ...newThreadOptions } } : {}
|
|
968
|
+
});
|
|
969
|
+
},
|
|
970
|
+
async setPinned(threadId2, pinned) {
|
|
971
|
+
sidebarActionCalls.push({ method: "setPinned", threadId: threadId2, pinned });
|
|
972
|
+
},
|
|
973
|
+
async setRead(threadId2, read) {
|
|
974
|
+
sidebarActionCalls.push({ method: "setRead", threadId: threadId2, read });
|
|
975
|
+
},
|
|
976
|
+
async rename(threadId2, title) {
|
|
977
|
+
sidebarActionCalls.push({ method: "rename", threadId: threadId2, title });
|
|
978
|
+
},
|
|
979
|
+
archive(threadId2) {
|
|
980
|
+
sidebarActionCalls.push({ method: "archive", threadId: threadId2 });
|
|
981
|
+
},
|
|
982
|
+
requestDelete(threadId2) {
|
|
983
|
+
sidebarActionCalls.push({ method: "requestDelete", threadId: threadId2 });
|
|
984
|
+
}
|
|
985
|
+
};
|
|
986
|
+
const navigate = {
|
|
987
|
+
toThread(threadId2) {
|
|
988
|
+
navigateCalls.push({ method: "toThread", threadId: threadId2 });
|
|
989
|
+
},
|
|
990
|
+
toProject(projectId2) {
|
|
991
|
+
navigateCalls.push({ method: "toProject", projectId: projectId2 });
|
|
992
|
+
},
|
|
993
|
+
toPluginPanel(path, panelOptions) {
|
|
994
|
+
navigateCalls.push({
|
|
995
|
+
method: "toPluginPanel",
|
|
996
|
+
path,
|
|
997
|
+
...panelOptions !== void 0 ? { options: panelOptions } : {}
|
|
998
|
+
});
|
|
999
|
+
},
|
|
1000
|
+
toCompose(composeOptions) {
|
|
1001
|
+
navigateCalls.push({
|
|
1002
|
+
method: "toCompose",
|
|
1003
|
+
...composeOptions !== void 0 ? { options: composeOptions } : {}
|
|
1004
|
+
});
|
|
1005
|
+
},
|
|
1006
|
+
openThreadPanel(panelOptions) {
|
|
1007
|
+
navigateCalls.push({
|
|
1008
|
+
method: "openThreadPanel",
|
|
1009
|
+
options: panelOptions
|
|
1010
|
+
});
|
|
1011
|
+
return options.openThreadPanel?.(panelOptions) ?? false;
|
|
1012
|
+
}
|
|
1013
|
+
};
|
|
1014
|
+
const projectId = options.context?.projectId ?? null;
|
|
1015
|
+
const threadId = options.context?.threadId ?? null;
|
|
1016
|
+
let composerScope = options.composer?.scope ?? (threadId !== null ? { kind: "thread", threadId } : { kind: "new-thread", projectId });
|
|
1017
|
+
let composerText = options.composer?.text ?? "";
|
|
1018
|
+
const composerAttachmentCount = options.composer?.attachmentCount ?? 0;
|
|
1019
|
+
let composerVersion = 0;
|
|
1020
|
+
const composerListeners = /* @__PURE__ */ new Set();
|
|
1021
|
+
const notifyComposerListeners = () => {
|
|
1022
|
+
composerVersion += 1;
|
|
1023
|
+
for (const listener of composerListeners) listener();
|
|
1024
|
+
};
|
|
1025
|
+
const commitComposerText = (next) => {
|
|
1026
|
+
if (next === composerText) return;
|
|
1027
|
+
composerText = next;
|
|
1028
|
+
notifyComposerListeners();
|
|
1029
|
+
};
|
|
1030
|
+
const composerLog = {
|
|
1031
|
+
get text() {
|
|
1032
|
+
return composerText;
|
|
1033
|
+
},
|
|
1034
|
+
get scope() {
|
|
1035
|
+
return composerScope;
|
|
1036
|
+
},
|
|
1037
|
+
get attachmentCount() {
|
|
1038
|
+
return composerAttachmentCount;
|
|
1039
|
+
},
|
|
1040
|
+
textEffect: null,
|
|
1041
|
+
textEffectCalls: [],
|
|
1042
|
+
inputLocked: false,
|
|
1043
|
+
inputLockCalls: [],
|
|
1044
|
+
quotes: [],
|
|
1045
|
+
mentions: [],
|
|
1046
|
+
focusCount: 0
|
|
1047
|
+
};
|
|
1048
|
+
const composerOwnership = { active: true };
|
|
1049
|
+
const composer = {
|
|
1050
|
+
getAttachmentCount: () => composerAttachmentCount,
|
|
1051
|
+
getScope: () => composerScope,
|
|
1052
|
+
getText: () => composerText,
|
|
1053
|
+
getVersionSnapshot: () => composerVersion,
|
|
1054
|
+
subscribe(listener) {
|
|
1055
|
+
composerListeners.add(listener);
|
|
1056
|
+
return () => composerListeners.delete(listener);
|
|
1057
|
+
},
|
|
1058
|
+
api: {
|
|
1059
|
+
setText(next) {
|
|
1060
|
+
commitComposerText(next);
|
|
1061
|
+
},
|
|
1062
|
+
updateText(updater) {
|
|
1063
|
+
commitComposerText(updater(composerText));
|
|
1064
|
+
},
|
|
1065
|
+
clear() {
|
|
1066
|
+
commitComposerText("");
|
|
1067
|
+
},
|
|
1068
|
+
setTextEffect(effect) {
|
|
1069
|
+
if (!composerOwnership.active) return;
|
|
1070
|
+
composerLog.textEffect = effect;
|
|
1071
|
+
composerLog.textEffectCalls.push(effect);
|
|
1072
|
+
},
|
|
1073
|
+
setInputLock(locked) {
|
|
1074
|
+
if (!composerOwnership.active) return;
|
|
1075
|
+
composerLog.inputLocked = locked;
|
|
1076
|
+
composerLog.inputLockCalls.push(locked);
|
|
1077
|
+
},
|
|
1078
|
+
addQuote(text) {
|
|
1079
|
+
const trimmed = text.replace(/\r\n|\r/gu, "\n").trim();
|
|
1080
|
+
if (trimmed !== "") {
|
|
1081
|
+
const block = trimmed.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
|
|
1082
|
+
commitComposerText(
|
|
1083
|
+
composerText === "" ? `${block}
|
|
1084
|
+
` : `${composerText}
|
|
1085
|
+
${block}
|
|
1086
|
+
`
|
|
1087
|
+
);
|
|
1088
|
+
composerLog.quotes.push(text);
|
|
1089
|
+
}
|
|
1090
|
+
composerLog.focusCount += 1;
|
|
1091
|
+
},
|
|
1092
|
+
insertMention(mention) {
|
|
1093
|
+
const label = mention.label.trim() || mention.id;
|
|
1094
|
+
const separator = composerText.length === 0 || /\s$/u.test(composerText) ? "" : " ";
|
|
1095
|
+
commitComposerText(`${composerText}${separator}${label} `);
|
|
1096
|
+
composerLog.mentions.push(mention);
|
|
1097
|
+
composerLog.focusCount += 1;
|
|
1098
|
+
},
|
|
1099
|
+
focus() {
|
|
1100
|
+
composerLog.focusCount += 1;
|
|
1101
|
+
}
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
const env = {
|
|
1105
|
+
rpcClient,
|
|
1106
|
+
rpcCalls,
|
|
1107
|
+
realtimeHandlers,
|
|
1108
|
+
realtimeConnection,
|
|
1109
|
+
settingsState: { values: options.settings, isLoading: false },
|
|
1110
|
+
bbContext: { projectId, threadId },
|
|
1111
|
+
navigate,
|
|
1112
|
+
navigateCalls,
|
|
1113
|
+
composer,
|
|
1114
|
+
composerLog,
|
|
1115
|
+
sidebarThreads,
|
|
1116
|
+
sidebarActions,
|
|
1117
|
+
sidebarActionCalls,
|
|
1118
|
+
sidebarPullRequests
|
|
1119
|
+
};
|
|
1120
|
+
const releaseComposerOwnership = () => {
|
|
1121
|
+
if (!composerOwnership.active) return;
|
|
1122
|
+
composerOwnership.active = false;
|
|
1123
|
+
composerLog.textEffect = null;
|
|
1124
|
+
composerLog.inputLocked = false;
|
|
1125
|
+
};
|
|
1126
|
+
const renderSlotTree = (ui) => /* @__PURE__ */ jsx(SlotEnvContext.Provider, { value: env, children: /* @__PURE__ */ jsx(SlotLifecycleGuard, { onUnmount: releaseComposerOwnership, children: ui }) });
|
|
1127
|
+
const Component = registration.component;
|
|
1128
|
+
const element = renderSlotTree(/* @__PURE__ */ jsx(Component, { ...props }));
|
|
1129
|
+
const result = render(element);
|
|
1130
|
+
const rerenderSlot = (ui) => {
|
|
1131
|
+
result.rerender(renderSlotTree(ui));
|
|
1132
|
+
};
|
|
1133
|
+
const emitRealtime = async (channel, payload) => {
|
|
1134
|
+
const normalized = payload === void 0 ? null : strictJsonRoundTrip(payload, `realtime "${channel}" payload`);
|
|
1135
|
+
const listeners = realtimeHandlers.get(channel);
|
|
1136
|
+
await act(async () => {
|
|
1137
|
+
for (const listener of [...listeners ?? []]) {
|
|
1138
|
+
listener(normalized);
|
|
1139
|
+
}
|
|
1140
|
+
});
|
|
1141
|
+
};
|
|
1142
|
+
const setRealtimeConnectionState = async (state) => {
|
|
1143
|
+
await act(async () => realtimeConnection.setState(state));
|
|
1144
|
+
};
|
|
1145
|
+
const setComposerText = async (text) => {
|
|
1146
|
+
await act(async () => commitComposerText(text));
|
|
1147
|
+
};
|
|
1148
|
+
const setComposerScope = async (scope) => {
|
|
1149
|
+
await act(async () => {
|
|
1150
|
+
composerScope = scope;
|
|
1151
|
+
notifyComposerListeners();
|
|
1152
|
+
});
|
|
1153
|
+
};
|
|
1154
|
+
const unmountSlot = () => {
|
|
1155
|
+
if (!composerOwnership.active) return;
|
|
1156
|
+
result.unmount();
|
|
1157
|
+
};
|
|
1158
|
+
return {
|
|
1159
|
+
...result,
|
|
1160
|
+
rerender: rerenderSlot,
|
|
1161
|
+
unmount: unmountSlot,
|
|
1162
|
+
rpcCalls,
|
|
1163
|
+
emitRealtime,
|
|
1164
|
+
setRealtimeConnectionState,
|
|
1165
|
+
setComposerText,
|
|
1166
|
+
setComposerScope,
|
|
1167
|
+
navigateCalls,
|
|
1168
|
+
sidebarActionCalls,
|
|
1169
|
+
composer: composerLog,
|
|
1170
|
+
behavior: {
|
|
1171
|
+
emitRealtime,
|
|
1172
|
+
setRealtimeConnectionState,
|
|
1173
|
+
setComposerText,
|
|
1174
|
+
setComposerScope
|
|
1175
|
+
},
|
|
1176
|
+
inspection: {
|
|
1177
|
+
rpcCalls,
|
|
1178
|
+
navigateCalls,
|
|
1179
|
+
sidebarActionCalls,
|
|
1180
|
+
composer: composerLog
|
|
1181
|
+
},
|
|
1182
|
+
lifecycle: { rerender: rerenderSlot, unmount: unmountSlot }
|
|
1183
|
+
};
|
|
1184
|
+
}
|
|
1185
|
+
export {
|
|
1186
|
+
installTestPluginRuntime,
|
|
1187
|
+
loadPluginApp,
|
|
1188
|
+
mountPluginContentScripts,
|
|
1189
|
+
renderSlot
|
|
1190
|
+
};
|