@get-bb/plugin-sdk 0.4.9 → 0.4.11

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.
@@ -251,6 +251,137 @@ function collectComposerCustomization(registration, seenIds, onRejected) {
251
251
  }
252
252
  }
253
253
 
254
+ // src/internal/file-navigation-validation.ts
255
+ var FILE_PATH_MAX_LENGTH = 32768;
256
+ var WINDOWS_DRIVE_ABSOLUTE_PATH = /^[A-Za-z]:[\\/]/u;
257
+ var WINDOWS_UNC_ABSOLUTE_PATH = /^\\\\/u;
258
+ function isJsonObject(value) {
259
+ if (typeof value !== "object" || value === null || Array.isArray(value)) {
260
+ return false;
261
+ }
262
+ const prototype = Object.getPrototypeOf(value);
263
+ return prototype === Object.prototype || prototype === null;
264
+ }
265
+ function hasExactKeys(value, keys) {
266
+ const actualKeys = Object.keys(value);
267
+ return actualKeys.length === keys.length && keys.every((key) => Object.prototype.hasOwnProperty.call(value, key));
268
+ }
269
+ function isNonEmptyIdentity(value) {
270
+ return typeof value === "string" && value.length > 0 && value.length <= FILE_PATH_MAX_LENGTH && value.trim() === value;
271
+ }
272
+ function hasControlCharacter(value) {
273
+ for (const character of value) {
274
+ const codePoint = character.codePointAt(0);
275
+ if (codePoint !== void 0 && codePoint < 32) return true;
276
+ }
277
+ return false;
278
+ }
279
+ function hasUnpairedSurrogate(value) {
280
+ for (let index = 0; index < value.length; index += 1) {
281
+ const codeUnit = value.charCodeAt(index);
282
+ if (codeUnit >= 55296 && codeUnit <= 56319) {
283
+ if (index + 1 >= value.length) return true;
284
+ const nextCodeUnit = value.charCodeAt(index + 1);
285
+ if (nextCodeUnit < 56320 || nextCodeUnit > 57343) return true;
286
+ index += 1;
287
+ } else if (codeUnit >= 56320 && codeUnit <= 57343) {
288
+ return true;
289
+ }
290
+ }
291
+ return false;
292
+ }
293
+ function isValidPathSegment(segment) {
294
+ return segment.length > 0 && segment !== "." && segment !== "..";
295
+ }
296
+ function isPositiveSafeInteger(value) {
297
+ return typeof value === "number" && Number.isSafeInteger(value) && value > 0;
298
+ }
299
+ function isValidRelativeFilePath(value) {
300
+ if (typeof value !== "string" || value.length === 0 || value.length > FILE_PATH_MAX_LENGTH || value.trim() !== value || value.includes("\\") || hasControlCharacter(value) || hasUnpairedSurrogate(value)) {
301
+ return false;
302
+ }
303
+ return value.split("/").every(isValidPathSegment);
304
+ }
305
+ function isValidAbsoluteHostFilePath(value) {
306
+ if (typeof value !== "string" || value.length === 0 || value.length > FILE_PATH_MAX_LENGTH || value.trim() !== value || hasControlCharacter(value) || hasUnpairedSurrogate(value)) {
307
+ return false;
308
+ }
309
+ if (value.startsWith("/") && !value.startsWith("//")) {
310
+ const segments = value.slice(1).split("/");
311
+ return segments.length > 0 && segments.every(isValidPathSegment);
312
+ }
313
+ if (WINDOWS_DRIVE_ABSOLUTE_PATH.test(value)) {
314
+ const segments = value.slice(3).split(/[\\/]/u);
315
+ return segments.length > 0 && segments.every(isValidPathSegment);
316
+ }
317
+ if (WINDOWS_UNC_ABSOLUTE_PATH.test(value)) {
318
+ const segments = value.slice(2).split(/[\\/]/u);
319
+ return segments.length >= 3 && segments.every(isValidPathSegment);
320
+ }
321
+ return false;
322
+ }
323
+ function normalizeExperimentalLiveFileTarget(value) {
324
+ if (!isJsonObject(value) || typeof value.kind !== "string") return null;
325
+ switch (value.kind) {
326
+ case "workspace":
327
+ if (!hasExactKeys(value, ["kind", "environmentId", "path"]) || !isNonEmptyIdentity(value.environmentId) || !isValidRelativeFilePath(value.path)) {
328
+ return null;
329
+ }
330
+ return {
331
+ kind: value.kind,
332
+ environmentId: value.environmentId,
333
+ path: value.path
334
+ };
335
+ case "host":
336
+ if (!hasExactKeys(value, ["kind", "hostId", "path"]) || !isNonEmptyIdentity(value.hostId) || !isValidAbsoluteHostFilePath(value.path)) {
337
+ return null;
338
+ }
339
+ return { kind: value.kind, hostId: value.hostId, path: value.path };
340
+ case "thread-storage":
341
+ if (!hasExactKeys(value, ["kind", "threadId", "path"]) || !isNonEmptyIdentity(value.threadId) || !isValidRelativeFilePath(value.path)) {
342
+ return null;
343
+ }
344
+ return { kind: value.kind, threadId: value.threadId, path: value.path };
345
+ default:
346
+ return null;
347
+ }
348
+ }
349
+ function normalizeExperimentalFileLocation(value) {
350
+ if (value === null) return null;
351
+ if (!isJsonObject(value) || typeof value.kind !== "string") return void 0;
352
+ switch (value.kind) {
353
+ case "line":
354
+ if (!hasExactKeys(value, ["kind", "line", "column"]) || !isPositiveSafeInteger(value.line) || value.column !== null && !isPositiveSafeInteger(value.column)) {
355
+ return void 0;
356
+ }
357
+ return {
358
+ kind: value.kind,
359
+ line: value.line,
360
+ column: value.column
361
+ };
362
+ case "range":
363
+ if (!hasExactKeys(value, ["kind", "startLine", "endLine"]) || !isPositiveSafeInteger(value.startLine) || !isPositiveSafeInteger(value.endLine) || value.endLine < value.startLine) {
364
+ return void 0;
365
+ }
366
+ return {
367
+ kind: value.kind,
368
+ startLine: value.startLine,
369
+ endLine: value.endLine
370
+ };
371
+ default:
372
+ return void 0;
373
+ }
374
+ }
375
+ function normalizeExperimentalFileOpenOptions(value) {
376
+ if (!isJsonObject(value) || !hasExactKeys(value, ["target", "location"])) {
377
+ return null;
378
+ }
379
+ const target = normalizeExperimentalLiveFileTarget(value.target);
380
+ const location = normalizeExperimentalFileLocation(value.location);
381
+ if (target === null || location === void 0) return null;
382
+ return { target, location };
383
+ }
384
+
254
385
  // src/internal/plugin-app-collector.ts
255
386
  function collectPluginAppRegistrations(definition, onComposerCustomizationRejected = (reason) => console.warn(reason)) {
256
387
  const collected = {
@@ -324,6 +455,7 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
324
455
  const kind = "slots.navPanel";
325
456
  const id = requireSlotId(kind, registration?.id);
326
457
  requireUniqueId(kind, seenIds.navPanel, id);
458
+ const panelId = id;
327
459
  const path = requireNonEmptyString(kind, "path", registration.path);
328
460
  if (!PLUGIN_SLOT_ID_PATTERN.test(path)) {
329
461
  throw new Error(
@@ -359,8 +491,25 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
359
491
  `${fixedTabKind}: "layout" must be "padded" or "flush" when set`
360
492
  );
361
493
  }
494
+ const fixedTabPanelId = requireNonEmptyString(
495
+ fixedTabKind,
496
+ "panelId",
497
+ fixedTab?.panelId
498
+ );
499
+ if (fixedTabPanelId !== panelId) {
500
+ throw new Error(
501
+ `${fixedTabKind}: "panelId" must match its containing navPanel id ${JSON.stringify(panelId)}`
502
+ );
503
+ }
504
+ const experimentalTarget = fixedTab?.experimental_target;
505
+ if (experimentalTarget !== void 0 && (typeof experimentalTarget !== "object" || experimentalTarget === null || typeof Reflect.get(experimentalTarget, "validate") !== "function")) {
506
+ throw new Error(
507
+ `${fixedTabKind}: "experimental_target.validate" must be a function when set`
508
+ );
509
+ }
362
510
  return {
363
511
  id: id2,
512
+ panelId: fixedTabPanelId,
364
513
  title: requireNonEmptyString(
365
514
  fixedTabKind,
366
515
  "title",
@@ -372,7 +521,10 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
372
521
  fixedTab?.icon
373
522
  ),
374
523
  component: requireComponent(fixedTabKind, fixedTab?.component),
375
- ...layout === void 0 ? {} : { layout }
524
+ ...layout === void 0 ? {} : { layout },
525
+ ...experimentalTarget === void 0 ? {} : {
526
+ experimental_target: experimentalTarget
527
+ }
376
528
  };
377
529
  });
378
530
  })();
@@ -679,6 +831,65 @@ function TestThreadChat({
679
831
  function TestMarkdown({ content, className }) {
680
832
  return /* @__PURE__ */ jsx("div", { "data-testid": "bb-markdown", className, children: content });
681
833
  }
834
+ function TestUrlLink({
835
+ href,
836
+ onClick,
837
+ rel,
838
+ target,
839
+ ...anchorProps
840
+ }) {
841
+ const navigate = useSlotEnv("experimental_UrlLink").navigate;
842
+ const normalizedTarget = target?.toLowerCase();
843
+ const opensNewBrowsingContext = normalizedTarget !== void 0 && normalizedTarget !== "" && normalizedTarget !== "_self" && normalizedTarget !== "_parent" && normalizedTarget !== "_top" && normalizedTarget !== "_unfencedtop";
844
+ const relTokens = rel?.split(/\s+/u).filter(Boolean) ?? [];
845
+ const normalizedRelTokens = relTokens.map((token) => token.toLowerCase());
846
+ const resolvedRel = opensNewBrowsingContext && !normalizedRelTokens.includes("opener") ? [
847
+ ...relTokens,
848
+ ...normalizedRelTokens.includes("noopener") ? [] : ["noopener"],
849
+ ...normalizedRelTokens.includes("noreferrer") ? [] : ["noreferrer"]
850
+ ].join(" ") : rel;
851
+ return /* @__PURE__ */ jsx(
852
+ "a",
853
+ {
854
+ ...anchorProps,
855
+ href,
856
+ target,
857
+ rel: resolvedRel,
858
+ onClick: (event) => {
859
+ onClick?.(event);
860
+ if (event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.currentTarget.hasAttribute("download") || event.currentTarget.hasAttribute("target")) {
861
+ return;
862
+ }
863
+ if (navigate.experimental_openUrl(href)) event.preventDefault();
864
+ }
865
+ }
866
+ );
867
+ }
868
+ function TestFileLink({
869
+ target,
870
+ location = null,
871
+ onClick,
872
+ ...anchorProps
873
+ }) {
874
+ const navigate = useSlotEnv("experimental_FileLink").navigate;
875
+ const options = normalizeExperimentalFileOpenOptions({ target, location });
876
+ const href = options === null ? void 0 : `./${encodeURIComponent(options.target.path)}`;
877
+ return /* @__PURE__ */ jsx(
878
+ "a",
879
+ {
880
+ ...anchorProps,
881
+ href,
882
+ onClick: (event) => {
883
+ onClick?.(event);
884
+ if (options === null || event.defaultPrevented || event.button !== 0 || event.altKey || event.ctrlKey || event.metaKey || event.shiftKey || event.currentTarget.hasAttribute("download")) {
885
+ return;
886
+ }
887
+ event.preventDefault();
888
+ navigate.experimental_openFilePreview(options);
889
+ }
890
+ }
891
+ );
892
+ }
682
893
  function TestNewThreadComposer({
683
894
  defaultProjectId,
684
895
  defaultProviderId,
@@ -829,6 +1040,30 @@ var testPluginSdkApp = {
829
1040
  useBbNavigate() {
830
1041
  return useSlotEnv("useBbNavigate").navigate;
831
1042
  },
1043
+ experimental_useAppPanel() {
1044
+ return useSlotEnv("experimental_useAppPanel").appPanel;
1045
+ },
1046
+ experimental_useFixedTabTarget(tab) {
1047
+ const store = useSlotEnv("experimental_useFixedTabTarget").fixedTabTarget;
1048
+ const state = useSyncExternalStore(
1049
+ store.subscribe,
1050
+ store.getSnapshot,
1051
+ store.getSnapshot
1052
+ );
1053
+ if (state === null || state.panelId !== tab.panelId || state.tabId !== tab.id || tab.experimental_target === void 0) {
1054
+ return null;
1055
+ }
1056
+ try {
1057
+ if (!tab.experimental_target.validate(state.target)) return null;
1058
+ } catch {
1059
+ return null;
1060
+ }
1061
+ return {
1062
+ clear: () => store.clear(state.sequence),
1063
+ sequence: state.sequence,
1064
+ target: state.target
1065
+ };
1066
+ },
832
1067
  useComposer() {
833
1068
  const composer = useSlotEnv("useComposer").composer;
834
1069
  const version = useSyncExternalStore(
@@ -847,12 +1082,17 @@ var testPluginSdkApp = {
847
1082
  },
848
1083
  ThreadChat: TestThreadChat,
849
1084
  Markdown: TestMarkdown,
1085
+ experimental_FileLink: TestFileLink,
1086
+ experimental_UrlLink: TestUrlLink,
850
1087
  experimental_NewThreadComposer: TestNewThreadComposer,
851
1088
  experimental_SourceCode: TestSourceCode,
852
1089
  experimental_Diff: TestDiff,
853
1090
  experimental_useSidebarThreads() {
854
1091
  return useSlotEnv("experimental_useSidebarThreads").sidebarThreads;
855
1092
  },
1093
+ experimental_useProviders() {
1094
+ return useSlotEnv("experimental_useProviders").providers;
1095
+ },
856
1096
  experimental_useSidebarThreadActions() {
857
1097
  return useSlotEnv("experimental_useSidebarThreadActions").sidebarActions;
858
1098
  },
@@ -1086,6 +1326,70 @@ function renderSlot(registration, props, options = {}) {
1086
1326
  }
1087
1327
  };
1088
1328
  const navigateCalls = [];
1329
+ const experimental_fixedTabOpenCalls = [];
1330
+ let fixedTabTargetSnapshot = options.experimental_fixedTabTarget === void 0 ? null : {
1331
+ panelId: options.experimental_fixedTabTarget.panelId,
1332
+ sequence: 1,
1333
+ tabId: options.experimental_fixedTabTarget.tabId,
1334
+ target: strictJsonRoundTrip(
1335
+ options.experimental_fixedTabTarget.target,
1336
+ "fixed tab target"
1337
+ )
1338
+ };
1339
+ const fixedTabTargetListeners = /* @__PURE__ */ new Set();
1340
+ const fixedTabTarget = {
1341
+ getSnapshot: () => fixedTabTargetSnapshot,
1342
+ subscribe(listener) {
1343
+ fixedTabTargetListeners.add(listener);
1344
+ return () => fixedTabTargetListeners.delete(listener);
1345
+ },
1346
+ clear(sequence) {
1347
+ if (fixedTabTargetSnapshot?.sequence !== sequence) return;
1348
+ fixedTabTargetSnapshot = null;
1349
+ for (const listener of fixedTabTargetListeners) listener();
1350
+ }
1351
+ };
1352
+ const appPanel = {
1353
+ openFixedTab(panelOptions) {
1354
+ let target;
1355
+ if (panelOptions.target !== void 0) {
1356
+ try {
1357
+ target = strictJsonRoundTrip(
1358
+ panelOptions.target,
1359
+ "fixed tab open target"
1360
+ );
1361
+ } catch {
1362
+ return false;
1363
+ }
1364
+ if (panelOptions.tab.experimental_target === void 0) return false;
1365
+ try {
1366
+ if (!panelOptions.tab.experimental_target.validate(target)) {
1367
+ return false;
1368
+ }
1369
+ } catch {
1370
+ return false;
1371
+ }
1372
+ }
1373
+ const call = {
1374
+ surface: panelOptions.surface,
1375
+ panelId: panelOptions.tab.panelId,
1376
+ tabId: panelOptions.tab.id,
1377
+ ...target === void 0 ? {} : { target }
1378
+ };
1379
+ experimental_fixedTabOpenCalls.push(call);
1380
+ const accepted = options.experimental_openFixedTab?.(call) ?? false;
1381
+ if (accepted && target !== void 0) {
1382
+ fixedTabTargetSnapshot = {
1383
+ panelId: panelOptions.tab.panelId,
1384
+ sequence: (fixedTabTargetSnapshot?.sequence ?? 0) + 1,
1385
+ tabId: panelOptions.tab.id,
1386
+ target
1387
+ };
1388
+ for (const listener of fixedTabTargetListeners) listener();
1389
+ }
1390
+ return accepted;
1391
+ }
1392
+ };
1089
1393
  const sidebarActionCalls = [];
1090
1394
  const sidebarPullRequests = new Map(
1091
1395
  Object.entries(options.sidebarPullRequests ?? {})
@@ -1095,6 +1399,10 @@ function renderSlot(registration, props, options = {}) {
1095
1399
  threads: options.sidebarThreads?.threads ?? [],
1096
1400
  projects: options.sidebarThreads?.projects ?? []
1097
1401
  };
1402
+ const providers = {
1403
+ status: options.providers?.status ?? "ready",
1404
+ providers: options.providers?.providers ?? []
1405
+ };
1098
1406
  const sidebarActions = {
1099
1407
  open(threadId2, openOptions) {
1100
1408
  sidebarActionCalls.push({
@@ -1151,6 +1459,24 @@ function renderSlot(registration, props, options = {}) {
1151
1459
  options: panelOptions
1152
1460
  });
1153
1461
  return options.openThreadPanel?.(panelOptions) ?? false;
1462
+ },
1463
+ experimental_openUrl(url) {
1464
+ navigateCalls.push({ method: "experimental_openUrl", url });
1465
+ return options.openUrl?.(url) ?? false;
1466
+ },
1467
+ experimental_openFilePreview(fileOptions) {
1468
+ navigateCalls.push({
1469
+ method: "experimental_openFilePreview",
1470
+ options: fileOptions
1471
+ });
1472
+ return options.openFilePreview?.(fileOptions) ?? false;
1473
+ },
1474
+ experimental_openFileExternally(fileOptions) {
1475
+ navigateCalls.push({
1476
+ method: "experimental_openFileExternally",
1477
+ options: fileOptions
1478
+ });
1479
+ return options.openFileExternally?.(fileOptions) ?? false;
1154
1480
  }
1155
1481
  };
1156
1482
  const projectId = options.context?.projectId ?? null;
@@ -1252,12 +1578,16 @@ ${block}
1252
1578
  bbContext: { projectId, threadId },
1253
1579
  navigate,
1254
1580
  navigateCalls,
1581
+ appPanel,
1582
+ experimental_fixedTabOpenCalls,
1583
+ fixedTabTarget,
1255
1584
  composer,
1256
1585
  composerLog,
1257
1586
  sidebarThreads,
1258
1587
  sidebarActions,
1259
1588
  sidebarActionCalls,
1260
- sidebarPullRequests
1589
+ sidebarPullRequests,
1590
+ providers
1261
1591
  };
1262
1592
  const releaseComposerOwnership = () => {
1263
1593
  if (!composerOwnership.active) return;
@@ -1307,6 +1637,7 @@ ${block}
1307
1637
  setComposerText,
1308
1638
  setComposerScope,
1309
1639
  navigateCalls,
1640
+ experimental_fixedTabOpenCalls,
1310
1641
  sidebarActionCalls,
1311
1642
  composer: composerLog,
1312
1643
  behavior: {
@@ -1318,6 +1649,7 @@ ${block}
1318
1649
  inspection: {
1319
1650
  rpcCalls,
1320
1651
  navigateCalls,
1652
+ experimental_fixedTabOpenCalls,
1321
1653
  sidebarActionCalls,
1322
1654
  composer: composerLog
1323
1655
  },