@get-bb/plugin-sdk 0.5.31 → 0.6.9

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.
@@ -16,6 +16,522 @@ function isComposerDraftEmpty(text, attachmentCount) {
16
16
  return text.trim().length === 0 && attachmentCount === 0;
17
17
  }
18
18
 
19
+ // src/internal/composer-draft-transforms.ts
20
+ function removeComposerMentions(draft, matches) {
21
+ const removed = draft.mentions.filter(matches).sort((a, b) => a.from - b.from);
22
+ if (removed.length === 0) return draft;
23
+ let text = "";
24
+ let cursor = 0;
25
+ for (const mention of removed) {
26
+ text += draft.text.slice(cursor, mention.from);
27
+ cursor = mention.to;
28
+ }
29
+ const removedSet = new Set(removed);
30
+ const mentions = draft.mentions.filter((mention) => !removedSet.has(mention)).map((mention) => {
31
+ const offset = removed.reduce(
32
+ (sum, item) => sum + (item.to <= mention.from ? item.to - item.from : 0),
33
+ 0
34
+ );
35
+ return offset === 0 ? mention : { ...mention, from: mention.from - offset, to: mention.to - offset };
36
+ });
37
+ return { ...draft, text: text + draft.text.slice(cursor), mentions };
38
+ }
39
+ function setComposerText(draft, text) {
40
+ if (text === draft.text) return draft;
41
+ return {
42
+ ...draft,
43
+ text,
44
+ mentions: reconcileComposerMentions(draft.text, text, draft.mentions)
45
+ };
46
+ }
47
+ function reconcileComposerMentions(currentText, nextText, mentions) {
48
+ if (currentText === nextText) return [...mentions];
49
+ let unchangedPrefixLength = 0;
50
+ const maximumPrefixLength = Math.min(currentText.length, nextText.length);
51
+ while (unchangedPrefixLength < maximumPrefixLength && currentText[unchangedPrefixLength] === nextText[unchangedPrefixLength]) {
52
+ unchangedPrefixLength += 1;
53
+ }
54
+ let unchangedSuffixLength = 0;
55
+ while (unchangedSuffixLength < currentText.length - unchangedPrefixLength && unchangedSuffixLength < nextText.length - unchangedPrefixLength && currentText[currentText.length - unchangedSuffixLength - 1] === nextText[nextText.length - unchangedSuffixLength - 1]) {
56
+ unchangedSuffixLength += 1;
57
+ }
58
+ const replacedCurrentEnd = currentText.length - unchangedSuffixLength;
59
+ const replacementDelta = nextText.length - currentText.length;
60
+ return mentions.flatMap((mention) => {
61
+ if (mention.to <= unchangedPrefixLength) return [mention];
62
+ if (mention.from >= replacedCurrentEnd) {
63
+ return [
64
+ {
65
+ ...mention,
66
+ from: mention.from + replacementDelta,
67
+ to: mention.to + replacementDelta
68
+ }
69
+ ];
70
+ }
71
+ return [];
72
+ });
73
+ }
74
+
75
+ // src/internal/composer-draft-actions.ts
76
+ import { z } from "zod";
77
+ var range = {
78
+ from: z.number().int().nonnegative(),
79
+ to: z.number().int().nonnegative(),
80
+ label: z.string()
81
+ };
82
+ var mentionSchema = z.discriminatedUnion("kind", [
83
+ z.object({
84
+ ...range,
85
+ kind: z.literal("thread"),
86
+ threadId: z.string(),
87
+ projectId: z.string().optional()
88
+ }),
89
+ z.object({ ...range, kind: z.literal("project"), projectId: z.string() }),
90
+ z.object({ ...range, kind: z.literal("section"), sectionId: z.string() }),
91
+ z.object({
92
+ ...range,
93
+ kind: z.literal("path"),
94
+ path: z.string(),
95
+ source: z.enum(["workspace", "thread-storage"]),
96
+ entryKind: z.enum(["file", "directory"])
97
+ }),
98
+ z.object({
99
+ ...range,
100
+ kind: z.literal("command"),
101
+ trigger: z.enum(["/", "$"]),
102
+ name: z.string(),
103
+ source: z.enum(["skill", "command"]),
104
+ origin: z.enum(["builtin", "project", "user"]),
105
+ argumentHint: z.string().nullable()
106
+ }),
107
+ z.object({
108
+ ...range,
109
+ kind: z.literal("plugin"),
110
+ pluginId: z.string(),
111
+ provider: z.string().min(1).refine((value) => !value.includes(":")),
112
+ id: z.string(),
113
+ icon: z.string().nullable().optional()
114
+ })
115
+ ]);
116
+ var attachmentsSchema = z.array(
117
+ z.object({
118
+ type: z.enum(["localImage", "localFile"]),
119
+ path: z.string().min(1),
120
+ name: z.string(),
121
+ mimeType: z.string().optional(),
122
+ sizeBytes: z.number().nonnegative()
123
+ })
124
+ );
125
+ var replacementSchema = z.object({
126
+ text: z.string(),
127
+ mentions: z.array(mentionSchema),
128
+ attachments: attachmentsSchema.optional()
129
+ }).superRefine((draft, context) => {
130
+ let end = 0;
131
+ for (const mention of [...draft.mentions].sort((a, b) => a.from - b.from)) {
132
+ if (mention.from < end || mention.to <= mention.from || mention.to > draft.text.length) {
133
+ context.addIssue({
134
+ code: "custom",
135
+ message: "Mention ranges must be non-overlapping and inside the draft text."
136
+ });
137
+ }
138
+ end = mention.to;
139
+ }
140
+ });
141
+ function createComposerDraftActions(target) {
142
+ let cached = null;
143
+ const snapshotOf = (current) => {
144
+ const content = current.getDraft();
145
+ if (cached?.content === content) return cached.snapshot;
146
+ const snapshot = Object.freeze({
147
+ text: content.text,
148
+ mentions: Object.freeze(
149
+ content.mentions.map((mention) => Object.freeze({ ...mention }))
150
+ ),
151
+ attachments: Object.freeze(
152
+ content.attachments.map(
153
+ (attachment) => Object.freeze({ ...attachment })
154
+ )
155
+ )
156
+ });
157
+ cached = { content, snapshot };
158
+ return snapshot;
159
+ };
160
+ const requireAvailable = (current) => {
161
+ if (!current.isAvailable())
162
+ throw new Error("This composer is no longer available.");
163
+ };
164
+ return {
165
+ get draft() {
166
+ return snapshotOf(target());
167
+ },
168
+ replace(next) {
169
+ const current = target();
170
+ requireAvailable(current);
171
+ const snapshot = snapshotOf(current);
172
+ const result = typeof next === "function" ? next(snapshot) : next;
173
+ requireAvailable(current);
174
+ if (result === snapshot) return;
175
+ const parsed = replacementSchema.safeParse(result);
176
+ if (!parsed.success)
177
+ throw new Error(
178
+ "Invalid composer draft: text, mention ranges, or attachments are invalid."
179
+ );
180
+ current.setDraft(parsed.data);
181
+ },
182
+ focus() {
183
+ const current = target();
184
+ requireAvailable(current);
185
+ current.focus();
186
+ }
187
+ };
188
+ }
189
+
190
+ // src/internal/composer-handle.ts
191
+ var UNAVAILABLE_MESSAGE = "This composer is no longer available.";
192
+ var OFF_SCREEN_MESSAGE = "This composer isn't on screen.";
193
+ var warnedAliases = /* @__PURE__ */ new Set();
194
+ function warnDeprecatedComposerMember(oldName, newName) {
195
+ if (warnedAliases.has(oldName)) return;
196
+ warnedAliases.add(oldName);
197
+ console.warn(
198
+ `useComposer().${oldName} is deprecated; use useComposer().${newName}.`
199
+ );
200
+ }
201
+ function appendComposerDraft(current, value, block) {
202
+ const base = block ? current.text.replace(/\s+$/u, "") : current.text;
203
+ const separator = block && base.length > 0 ? "\n\n" : "";
204
+ const offset = base.length + separator.length;
205
+ return {
206
+ text: `${base}${separator}${value.text}`,
207
+ mentions: [
208
+ ...current.mentions.filter((mention) => mention.to <= base.length),
209
+ ...value.mentions.map((mention) => ({
210
+ ...mention,
211
+ from: mention.from + offset,
212
+ to: mention.to + offset
213
+ }))
214
+ ]
215
+ };
216
+ }
217
+ function validProviderId(provider) {
218
+ const trimmed = provider.trim();
219
+ return trimmed.length === 0 || trimmed.includes(":") ? null : trimmed;
220
+ }
221
+ function insertValue(parts, controller) {
222
+ const list = Array.isArray(parts) ? parts : [parts];
223
+ let text = "";
224
+ const mentions = [];
225
+ for (const part of list) {
226
+ if (typeof part === "string") {
227
+ text += part;
228
+ continue;
229
+ }
230
+ let mention;
231
+ if ("kind" in part) {
232
+ mention = part;
233
+ } else {
234
+ const provider = validProviderId(part.provider);
235
+ if (provider === null) {
236
+ throw new Error(`Invalid mention provider id "${part.provider}".`);
237
+ }
238
+ mention = {
239
+ from: 0,
240
+ to: 0,
241
+ label: part.label.trim() || part.id,
242
+ kind: "plugin",
243
+ pluginId: controller.pluginId,
244
+ provider,
245
+ id: part.id,
246
+ icon: null
247
+ };
248
+ }
249
+ const serialized = controller.mentionText(mention);
250
+ mentions.push({
251
+ ...mention,
252
+ from: text.length,
253
+ to: text.length + serialized.length
254
+ });
255
+ text += serialized;
256
+ }
257
+ return { text, mentions };
258
+ }
259
+ function isDraftEmpty(draft, attachmentCount) {
260
+ return draft.text.trim().length === 0 && draft.mentions.length === 0 && attachmentCount === 0;
261
+ }
262
+ var legacyDrafts = /* @__PURE__ */ new WeakMap();
263
+ function withLegacyDraftFields(draft, attachmentCount) {
264
+ const cached = legacyDrafts.get(draft);
265
+ if (cached) return cached;
266
+ const snapshot = { ...draft };
267
+ Object.defineProperties(snapshot, {
268
+ isEmpty: { value: isDraftEmpty(draft, attachmentCount), enumerable: false },
269
+ attachmentCount: { value: attachmentCount, enumerable: false }
270
+ });
271
+ Object.freeze(snapshot);
272
+ legacyDrafts.set(draft, snapshot);
273
+ return snapshot;
274
+ }
275
+ function waitForUploads(target) {
276
+ return new Promise((resolve) => {
277
+ if (!target.getEditorState().isAttaching) {
278
+ resolve();
279
+ return;
280
+ }
281
+ const unsubscribe = target.subscribeEditorState(() => {
282
+ if (target.getEditorState().isAttaching) return;
283
+ unsubscribe();
284
+ resolve();
285
+ });
286
+ });
287
+ }
288
+ function createComposerHandleBinding(key, initial) {
289
+ let controller = initial;
290
+ const target = () => controller.target;
291
+ const editorState = () => target().getEditorState();
292
+ const requireAvailable = () => {
293
+ if (!target().isAvailable()) throw new Error(UNAVAILABLE_MESSAGE);
294
+ };
295
+ const legacyAvailable = (method) => {
296
+ if (target().isAvailable()) return true;
297
+ console.warn(
298
+ `[plugin:${controller.pluginId}] useComposer().${method}: ${UNAVAILABLE_MESSAGE}`
299
+ );
300
+ return false;
301
+ };
302
+ const replaceText = (nextText) => {
303
+ const current = target().getDraft();
304
+ if (nextText === current.text) return;
305
+ target().setDraft(setComposerText(current, nextText));
306
+ };
307
+ const insertMention = (mention) => {
308
+ warnDeprecatedComposerMember("insertMention", "insert");
309
+ const provider = validProviderId(mention.provider);
310
+ if (provider === null) {
311
+ console.warn(
312
+ `[plugin:${controller.pluginId}] useComposer().insertMention: invalid provider id "${mention.provider}"`
313
+ );
314
+ return;
315
+ }
316
+ if (!legacyAvailable("insertMention")) return;
317
+ const label = mention.label.trim() || mention.id;
318
+ const current = target().getDraft();
319
+ const separator = current.text.length === 0 || /\s$/u.test(current.text) ? "" : " ";
320
+ const from = current.text.length + separator.length;
321
+ target().setDraft({
322
+ text: `${current.text}${separator}${label} `,
323
+ mentions: [
324
+ ...current.mentions,
325
+ {
326
+ from,
327
+ to: from + label.length,
328
+ label,
329
+ kind: "plugin",
330
+ pluginId: controller.pluginId,
331
+ provider,
332
+ id: mention.id,
333
+ icon: null
334
+ }
335
+ ]
336
+ });
337
+ target().focus();
338
+ };
339
+ const removeMentionFrom = (mention) => {
340
+ const current = target().getDraft();
341
+ const next = removeComposerMentions(
342
+ current,
343
+ (item) => item.kind === "plugin" && item.pluginId === controller.pluginId && item.provider === mention.provider && item.id === mention.id
344
+ );
345
+ if (next !== current) target().setDraft(next);
346
+ };
347
+ const removeMention = (mention) => {
348
+ warnDeprecatedComposerMember("removeMention", "replace");
349
+ requireAvailable();
350
+ removeMentionFrom(mention);
351
+ };
352
+ const insert = (parts, options = {}) => {
353
+ requireAvailable();
354
+ const value = insertValue(parts, controller);
355
+ const block = options.block === true;
356
+ if ((options.at ?? "cursor") === "cursor") {
357
+ if (!target().insertAtCursor(value, block)) {
358
+ throw new Error(OFF_SCREEN_MESSAGE);
359
+ }
360
+ return;
361
+ }
362
+ target().setDraft(appendComposerDraft(target().getDraft(), value, block));
363
+ };
364
+ const submit = async (options) => {
365
+ requireAvailable();
366
+ const hostSubmit = target().submit;
367
+ if (hostSubmit === void 0) {
368
+ throw new Error("This composer cannot submit programmatically.");
369
+ }
370
+ if (options.sendAt !== void 0 && (!Number.isFinite(options.sendAt) || options.sendAt <= Date.now())) {
371
+ throw new Error("Pick a time in the future.");
372
+ }
373
+ const errorBefore = editorState().attachmentError;
374
+ await waitForUploads(target());
375
+ const state = editorState();
376
+ if (state.attachmentError !== null && state.attachmentError !== errorBefore) {
377
+ throw new Error(state.attachmentError);
378
+ }
379
+ if (state.isSubmittingBlocked) {
380
+ throw new Error(
381
+ state.submittingBlockedReason ?? "This composer can't submit right now."
382
+ );
383
+ }
384
+ await hostSubmit(
385
+ options,
386
+ options.experimental_data === void 0 ? void 0 : { pluginId: controller.pluginId, data: options.experimental_data }
387
+ );
388
+ };
389
+ const setSelection = async (selection) => {
390
+ requireAvailable();
391
+ const hostSetSelection = target().setSelection;
392
+ if (hostSetSelection === void 0) {
393
+ throw new Error("This composer has no pickers to set.");
394
+ }
395
+ return hostSetSelection(selection);
396
+ };
397
+ const submittedListeners = /* @__PURE__ */ new Set();
398
+ let submittedSubscription = null;
399
+ const syncSubmittedSubscription = () => {
400
+ const scope = submittedListeners.size === 0 ? null : JSON.stringify(target().scope);
401
+ if (submittedSubscription?.scope === scope) return;
402
+ submittedSubscription?.unsubscribe();
403
+ submittedSubscription = scope === null ? null : {
404
+ scope,
405
+ unsubscribe: controller.onSubmitted(() => {
406
+ for (const listener of [...submittedListeners]) {
407
+ try {
408
+ listener();
409
+ } catch (error) {
410
+ console.error("Composer submission listener failed", error);
411
+ }
412
+ }
413
+ })
414
+ };
415
+ };
416
+ const onSubmitted = (listener) => {
417
+ const entry = () => listener();
418
+ submittedListeners.add(entry);
419
+ syncSubmittedSubscription();
420
+ return () => {
421
+ submittedListeners.delete(entry);
422
+ syncSubmittedSubscription();
423
+ };
424
+ };
425
+ const draftActions = createComposerDraftActions(target);
426
+ const handle = {
427
+ replace: draftActions.replace,
428
+ get scope() {
429
+ return target().scope;
430
+ },
431
+ get key() {
432
+ return target().key;
433
+ },
434
+ get layout() {
435
+ return editorState().layout;
436
+ },
437
+ get isRunning() {
438
+ return editorState().isRunning;
439
+ },
440
+ get isSubmitting() {
441
+ return editorState().isSubmitting;
442
+ },
443
+ get isSubmittingBlocked() {
444
+ return editorState().isSubmittingBlocked;
445
+ },
446
+ get submittingBlockedReason() {
447
+ return editorState().submittingBlockedReason;
448
+ },
449
+ get isEmpty() {
450
+ return isDraftEmpty(target().getDraft(), target().getAttachmentCount());
451
+ },
452
+ get attachmentCount() {
453
+ return target().getAttachmentCount();
454
+ },
455
+ get text() {
456
+ return target().getDraft().text;
457
+ },
458
+ get draft() {
459
+ return withLegacyDraftFields(
460
+ draftActions.draft,
461
+ target().getAttachmentCount()
462
+ );
463
+ },
464
+ get selection() {
465
+ return target().getSelection();
466
+ },
467
+ setText: (next) => {
468
+ warnDeprecatedComposerMember("setText", "replace");
469
+ if (legacyAvailable("setText")) replaceText(next);
470
+ },
471
+ updateText: (updater) => {
472
+ warnDeprecatedComposerMember("updateText", "replace");
473
+ if (legacyAvailable("updateText")) {
474
+ replaceText(updater(target().getDraft().text));
475
+ }
476
+ },
477
+ clear: () => {
478
+ warnDeprecatedComposerMember("clear", "replace");
479
+ if (legacyAvailable("clear")) replaceText("");
480
+ },
481
+ insert,
482
+ setTextEffect: (effect) => controller.setTextEffect(effect),
483
+ setInputLock: (locked) => controller.setInputLock(locked),
484
+ addQuote: (text) => {
485
+ warnDeprecatedComposerMember("addQuote", "replace");
486
+ if (!legacyAvailable("addQuote")) return;
487
+ target().addQuote(text);
488
+ target().focus();
489
+ },
490
+ insertMention,
491
+ removeMention,
492
+ onSubmitted,
493
+ focus: () => target().focus(),
494
+ submit,
495
+ setSelection,
496
+ experimental_removeMention: (mention) => {
497
+ warnDeprecatedComposerMember("experimental_removeMention", "replace");
498
+ if (!legacyAvailable("experimental_removeMention")) return;
499
+ removeMentionFrom(mention);
500
+ },
501
+ experimental_onSubmitted: (listener) => {
502
+ warnDeprecatedComposerMember("experimental_onSubmitted", "onSubmitted");
503
+ return onSubmitted(listener);
504
+ },
505
+ experimental_submit: (options) => {
506
+ warnDeprecatedComposerMember("experimental_submit", "submit");
507
+ return submit(options);
508
+ },
509
+ experimental_setSelection: (selection) => {
510
+ warnDeprecatedComposerMember("experimental_setSelection", "setSelection");
511
+ return setSelection(selection);
512
+ }
513
+ };
514
+ Object.defineProperties(handle, {
515
+ run: {
516
+ enumerable: false,
517
+ get: () => {
518
+ const state = editorState();
519
+ return { isRunning: state.isRunning, isSubmitting: state.isSubmitting };
520
+ }
521
+ },
522
+ setThreadRowStatus: { enumerable: false, value: () => {
523
+ } }
524
+ });
525
+ return {
526
+ key,
527
+ handle,
528
+ update(next) {
529
+ controller = next;
530
+ syncSubmittedSubscription();
531
+ }
532
+ };
533
+ }
534
+
19
535
  // src/internal/composer-customization-validation.ts
20
536
  var PLUGIN_SLOT_ID_PATTERN = /^[a-zA-Z0-9_-]+$/;
21
537
  var PLUGIN_MESSAGE_DIRECTIVE_ID_PATTERN = /^[a-z][a-z0-9]*(-[a-z0-9]+)*$/;
@@ -141,6 +657,30 @@ function parseContributionArray(kind, value, onRejected, parse) {
141
657
  }
142
658
  return parsed;
143
659
  }
660
+ function parseMenuItem(entryKind, value) {
661
+ const entry = value;
662
+ const id = requireSlotId(entryKind, entry?.id);
663
+ const icon = requireOptionalString(entryKind, "icon", entry?.icon);
664
+ const description = requireOptionalString(
665
+ entryKind,
666
+ "description",
667
+ entry?.description
668
+ );
669
+ const disabled = entry?.disabled;
670
+ if (disabled !== void 0 && typeof disabled !== "boolean" && typeof disabled !== "function") {
671
+ throw new Error(
672
+ `${entryKind}: "disabled" must be a boolean or function when set`
673
+ );
674
+ }
675
+ return {
676
+ id,
677
+ label: requireNonEmptyString(entryKind, "label", entry?.label),
678
+ ...icon !== void 0 ? { icon } : {},
679
+ ...description !== void 0 ? { description } : {},
680
+ ...disabled !== void 0 ? { disabled } : {},
681
+ run: requireFunction(entryKind, "run", entry?.run)
682
+ };
683
+ }
144
684
  function parseRegions(kind, registration, onRejected) {
145
685
  const actions = parseContributionArray(`${kind}.actions`, registration.actions, onRejected, (entryKind, value) => {
146
686
  const entry = value;
@@ -168,32 +708,13 @@ function parseRegions(kind, registration, onRejected) {
168
708
  `${kind}.plusMenu`,
169
709
  registration.plusMenu,
170
710
  onRejected,
171
- (entryKind, value) => {
172
- const entry = value;
173
- const id = requireSlotId(entryKind, entry?.id);
174
- const icon = requireOptionalString(entryKind, "icon", entry?.icon);
175
- const description = requireOptionalString(
176
- entryKind,
177
- "description",
178
- entry?.description
179
- );
180
- const disabled = entry?.disabled;
181
- if (disabled !== void 0 && typeof disabled !== "boolean" && typeof disabled !== "function") {
182
- throw new Error(
183
- `${entryKind}: "disabled" must be a boolean or function when set`
184
- );
185
- }
186
- return {
187
- id,
188
- label: requireNonEmptyString(entryKind, "label", entry?.label),
189
- ...icon !== void 0 ? { icon } : {},
190
- ...description !== void 0 ? { description } : {},
191
- ...disabled !== void 0 ? {
192
- disabled
193
- } : {},
194
- run: requireFunction(entryKind, "run", entry?.run)
195
- };
196
- }
711
+ parseMenuItem
712
+ );
713
+ const sendMenu = parseContributionArray(
714
+ `${kind}.sendMenu`,
715
+ registration.sendMenu,
716
+ onRejected,
717
+ parseMenuItem
197
718
  );
198
719
  let richText;
199
720
  if (registration.richText !== void 0) {
@@ -236,6 +757,7 @@ function parseRegions(kind, registration, onRejected) {
236
757
  ...actions !== void 0 ? { actions } : {},
237
758
  ...banners !== void 0 ? { banners } : {},
238
759
  ...plusMenu !== void 0 ? { plusMenu } : {},
760
+ ...sendMenu !== void 0 ? { sendMenu } : {},
239
761
  ...richText !== void 0 ? { richText } : {}
240
762
  };
241
763
  }
@@ -260,7 +782,11 @@ function collectComposerCustomization(registration, seenIds, onRejected) {
260
782
  requireUniqueId(kind, seenIds, id);
261
783
  return {
262
784
  id,
263
- ...scopes !== void 0 ? { scopes: [...scopes] } : {},
785
+ ...scopes !== void 0 ? {
786
+ scopes: scopes.filter(
787
+ (scope) => scope !== "side-chat"
788
+ )
789
+ } : {},
264
790
  ...parseRegions(`${kind}(${id})`, raw ?? {}, onRejected)
265
791
  };
266
792
  } catch (error) {
@@ -401,7 +927,7 @@ function normalizeExperimentalFileOpenOptions(value) {
401
927
  }
402
928
 
403
929
  // src/internal/plugin-app-collector.ts
404
- import { z } from "zod";
930
+ import { z as z2 } from "zod";
405
931
  var sidebarFooterCommandSequence = 0;
406
932
  var SIDEBAR_FOOTER_ACTION_KEYS = /* @__PURE__ */ new Set([
407
933
  "id",
@@ -552,8 +1078,8 @@ function rejectStaleNavPanelKeys(kind, registration) {
552
1078
  }
553
1079
  }
554
1080
  }
555
- var commandShortcutSchema = z.object({
556
- key: z.string().min(1).max(32).refine(
1081
+ var commandShortcutSchema = z2.object({
1082
+ key: z2.string().min(1).max(32).refine(
557
1083
  (key) => ![
558
1084
  "Alt",
559
1085
  "Control",
@@ -564,11 +1090,11 @@ var commandShortcutSchema = z.object({
564
1090
  "Unidentified"
565
1091
  ].includes(key)
566
1092
  ),
567
- mod: z.boolean().default(false),
568
- meta: z.boolean().default(false),
569
- control: z.boolean().default(false),
570
- alt: z.boolean().default(false),
571
- shift: z.boolean().default(false)
1093
+ mod: z2.boolean().default(false),
1094
+ meta: z2.boolean().default(false),
1095
+ control: z2.boolean().default(false),
1096
+ alt: z2.boolean().default(false),
1097
+ shift: z2.boolean().default(false)
572
1098
  }).strict().refine(
573
1099
  (shortcut) => shortcut.mod || shortcut.meta || shortcut.control || shortcut.alt || /^F(?:[1-9]|1[0-9]|2[0-4])$/u.test(shortcut.key),
574
1100
  "Use Command, Control, or Alt with a key, or a function key"
@@ -707,7 +1233,12 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
707
1233
  "description",
708
1234
  registration.description
709
1235
  );
1236
+ const page = registration.experimental_page;
1237
+ if (page !== void 0 && page !== "mobile") {
1238
+ throw new Error(`${kind}: experimental_page must be "mobile"`);
1239
+ }
710
1240
  collected.settingsSections.push({
1241
+ ...page !== void 0 ? { experimental_page: page } : {},
711
1242
  id,
712
1243
  ...title !== void 0 ? { title } : {},
713
1244
  ...description !== void 0 ? { description } : {},
@@ -1079,6 +1610,34 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
1079
1610
 
1080
1611
  // src/testing/app.tsx
1081
1612
  import { jsx, jsxs } from "react/jsx-runtime";
1613
+ function testComposerKey(scope) {
1614
+ switch (scope.kind) {
1615
+ case "thread":
1616
+ return `test-composer:thread:${scope.threadId}`;
1617
+ case "queued-message":
1618
+ return `test-composer:queued-message:${scope.threadId}:${scope.queuedMessageId}`;
1619
+ case "new-thread":
1620
+ return `test-composer:new-thread:${scope.projectId ?? ""}`;
1621
+ }
1622
+ }
1623
+ function testComposerMentionText(mention) {
1624
+ switch (mention.kind) {
1625
+ case "thread":
1626
+ return `@thread:${mention.threadId}`;
1627
+ case "project":
1628
+ return `@project:${mention.projectId}`;
1629
+ case "section":
1630
+ return `@section:${mention.sectionId}`;
1631
+ case "command":
1632
+ return `${mention.trigger}${mention.name}`;
1633
+ case "plugin":
1634
+ return `@${mention.label}`;
1635
+ case "path": {
1636
+ const path = mention.source === "thread-storage" ? `thread-storage:${mention.path}` : mention.path;
1637
+ return `@${path}${mention.entryKind === "directory" && !path.endsWith("/") ? "/" : ""}`;
1638
+ }
1639
+ }
1640
+ }
1082
1641
  function createSdkFakeNode(provided, path, calls) {
1083
1642
  const callable = function sdkFakeNode() {
1084
1643
  };
@@ -1339,7 +1898,16 @@ function TestProviderModelPicker({
1339
1898
  className
1340
1899
  }) {
1341
1900
  const [draft, setDraft] = useState(value);
1342
- useEffect(() => setDraft(value), [value]);
1901
+ const { providerId, model, reasoningLevel, serviceTier } = value;
1902
+ useEffect(
1903
+ () => setDraft({
1904
+ providerId,
1905
+ model,
1906
+ reasoningLevel,
1907
+ ...serviceTier === void 0 ? {} : { serviceTier }
1908
+ }),
1909
+ [providerId, model, reasoningLevel, serviceTier]
1910
+ );
1343
1911
  const reasoningLevels = [
1344
1912
  "none",
1345
1913
  "low",
@@ -1387,11 +1955,11 @@ function TestProviderModelPicker({
1387
1955
  "aria-label": "Reasoning level",
1388
1956
  value: draft.reasoningLevel,
1389
1957
  onChange: (event) => {
1390
- const reasoningLevel = reasoningLevels.find(
1958
+ const reasoningLevel2 = reasoningLevels.find(
1391
1959
  (candidate) => candidate === event.target.value
1392
1960
  );
1393
- if (reasoningLevel === void 0) return;
1394
- setDraft((current) => ({ ...current, reasoningLevel }));
1961
+ if (reasoningLevel2 === void 0) return;
1962
+ setDraft((current) => ({ ...current, reasoningLevel: reasoningLevel2 }));
1395
1963
  }
1396
1964
  }
1397
1965
  ),
@@ -1401,13 +1969,13 @@ function TestProviderModelPicker({
1401
1969
  "aria-label": "Service tier",
1402
1970
  value: draft.serviceTier ?? "",
1403
1971
  onChange: (event) => setDraft((current) => {
1404
- const serviceTier = event.target.value;
1405
- if (serviceTier !== "fast" && serviceTier !== "default") {
1972
+ const serviceTier2 = event.target.value;
1973
+ if (serviceTier2 !== "fast" && serviceTier2 !== "default") {
1406
1974
  const next = { ...current };
1407
1975
  delete next.serviceTier;
1408
1976
  return next;
1409
1977
  }
1410
- return { ...current, serviceTier };
1978
+ return { ...current, serviceTier: serviceTier2 };
1411
1979
  }),
1412
1980
  children: [
1413
1981
  /* @__PURE__ */ jsx("option", { value: "", children: "Unsupported" }),
@@ -1610,19 +2178,21 @@ var testPluginSdkApp = {
1610
2178
  },
1611
2179
  useComposer() {
1612
2180
  const composer = useSlotEnv("useComposer").composer;
1613
- const version = useSyncExternalStore(
2181
+ useSyncExternalStore(
1614
2182
  composer.subscribe,
1615
2183
  composer.getVersionSnapshot,
1616
2184
  composer.getVersionSnapshot
1617
2185
  );
1618
- return useMemo(
1619
- () => ({
1620
- ...composer.api,
1621
- scope: composer.getScope(),
1622
- text: composer.getText()
1623
- }),
1624
- [composer, version]
2186
+ return composer.api;
2187
+ },
2188
+ useComposers() {
2189
+ const composer = useSlotEnv("useComposers").composer;
2190
+ useSyncExternalStore(
2191
+ composer.subscribe,
2192
+ composer.getVersionSnapshot,
2193
+ composer.getVersionSnapshot
1625
2194
  );
2195
+ return composer.apiList;
1626
2196
  },
1627
2197
  ThreadChat: TestThreadChat,
1628
2198
  Markdown: TestMarkdown,
@@ -1760,13 +2330,13 @@ var testPluginSdkApp = {
1760
2330
  const attachmentCount = composer.getAttachmentCount();
1761
2331
  return {
1762
2332
  scope: composer.getScope(),
1763
- layout: "expanded",
2333
+ layout: composer.getLayout(),
1764
2334
  draft: {
1765
2335
  text,
1766
2336
  isEmpty: isComposerDraftEmpty(text, attachmentCount),
1767
2337
  attachmentCount
1768
2338
  },
1769
- run: { isRunning: false, isSubmitting: false }
2339
+ run: composer.getRun()
1770
2340
  };
1771
2341
  }, [composer, version]);
1772
2342
  }
@@ -2171,26 +2741,76 @@ function renderSlot(registration, props, options = {}) {
2171
2741
  const projectId = options.context?.projectId ?? null;
2172
2742
  const threadId = options.context?.threadId ?? null;
2173
2743
  let composerScope = options.composer?.scope ?? (threadId !== null ? { kind: "thread", threadId } : { kind: "new-thread", projectId });
2744
+ let composerSelection = composerScope.kind === "queued-message" ? null : {
2745
+ ...composerScope.kind === "new-thread" && projectId !== null ? { projectId } : {},
2746
+ ...options.composer?.selection
2747
+ };
2174
2748
  let composerText = options.composer?.text ?? "";
2175
- const composerAttachmentCount = options.composer?.attachmentCount ?? 0;
2749
+ let composerMentions = [
2750
+ ...options.composer?.mentions ?? []
2751
+ ];
2752
+ let composerAttachments = [...options.composer?.attachments ?? []];
2753
+ let composerAttachmentCount = options.composer?.attachmentCount ?? composerAttachments.length;
2754
+ const composerLayout = options.composer?.layout ?? "expanded";
2755
+ const composerIsRunning = options.composer?.isRunning ?? false;
2756
+ const composerIsSubmitting = options.composer?.isSubmitting ?? false;
2757
+ const composerPluginId = options.pluginId ?? "test-plugin";
2176
2758
  let composerVersion = 0;
2177
2759
  const composerListeners = /* @__PURE__ */ new Set();
2178
2760
  const notifyComposerListeners = () => {
2179
2761
  composerVersion += 1;
2180
2762
  for (const listener of composerListeners) listener();
2181
2763
  };
2764
+ const commitComposerDraft = (nextText, nextMentions) => {
2765
+ if (nextText === composerText && nextMentions === composerMentions) return;
2766
+ composerText = nextText;
2767
+ composerMentions = nextMentions;
2768
+ notifyComposerListeners();
2769
+ };
2182
2770
  const commitComposerText = (next) => {
2183
2771
  if (next === composerText) return;
2184
- composerText = next;
2185
- notifyComposerListeners();
2772
+ commitComposerDraft(
2773
+ next,
2774
+ reconcileComposerMentions(composerText, next, composerMentions)
2775
+ );
2776
+ };
2777
+ const composerBlockedReason = () => {
2778
+ if (options.composer?.submittingBlockedReason !== void 0) {
2779
+ return options.composer.submittingBlockedReason;
2780
+ }
2781
+ if (composerIsSubmitting) return "Submitting...";
2782
+ return composerText.trim() === "" && composerAttachmentCount === 0 ? "Type a message first." : null;
2783
+ };
2784
+ let composerDraftCache = null;
2785
+ const composerDraft = () => {
2786
+ if (composerDraftCache?.version !== composerVersion) {
2787
+ composerDraftCache = {
2788
+ version: composerVersion,
2789
+ draft: {
2790
+ text: composerText,
2791
+ mentions: composerMentions,
2792
+ attachments: composerAttachments
2793
+ }
2794
+ };
2795
+ }
2796
+ return composerDraftCache.draft;
2186
2797
  };
2187
2798
  const composerLog = {
2188
2799
  get text() {
2189
2800
  return composerText;
2190
2801
  },
2802
+ get draft() {
2803
+ return composerHandle.draft;
2804
+ },
2805
+ get key() {
2806
+ return testComposerKey(composerScope);
2807
+ },
2191
2808
  get scope() {
2192
2809
  return composerScope;
2193
2810
  },
2811
+ get selection() {
2812
+ return composerSelection;
2813
+ },
2194
2814
  get attachmentCount() {
2195
2815
  return composerAttachmentCount;
2196
2816
  },
@@ -2205,26 +2825,88 @@ function renderSlot(registration, props, options = {}) {
2205
2825
  selections: []
2206
2826
  };
2207
2827
  const composerOwnership = { active: true };
2828
+ const composerIsAvailable = () => composerOwnership.active || composerScope.kind !== "queued-message";
2208
2829
  const submissionListeners = /* @__PURE__ */ new Set();
2209
- const composer = {
2830
+ const composerTarget = {
2831
+ get key() {
2832
+ return testComposerKey(composerScope);
2833
+ },
2834
+ get scope() {
2835
+ return composerScope;
2836
+ },
2837
+ getDraft: composerDraft,
2210
2838
  getAttachmentCount: () => composerAttachmentCount,
2211
- getScope: () => composerScope,
2212
- getText: () => composerText,
2213
- getVersionSnapshot: () => composerVersion,
2214
- subscribe(listener) {
2215
- composerListeners.add(listener);
2216
- return () => composerListeners.delete(listener);
2839
+ getSelection: () => composerSelection,
2840
+ setDraft: (next) => {
2841
+ if (next.attachments !== void 0) {
2842
+ composerAttachments = [...next.attachments];
2843
+ composerAttachmentCount = composerAttachments.length;
2844
+ }
2845
+ commitComposerDraft(next.text, [...next.mentions]);
2217
2846
  },
2218
- api: {
2219
- setText(next) {
2220
- commitComposerText(next);
2221
- },
2222
- updateText(updater) {
2223
- commitComposerText(updater(composerText));
2224
- },
2225
- clear() {
2226
- commitComposerText("");
2227
- },
2847
+ addQuote(text) {
2848
+ const trimmed = text.replace(/\r\n|\r/gu, "\n").trim();
2849
+ if (trimmed === "") return;
2850
+ const block = trimmed.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
2851
+ commitComposerText(
2852
+ composerText === "" ? `${block}
2853
+ ` : `${composerText}
2854
+ ${block}
2855
+ `
2856
+ );
2857
+ composerLog.quotes.push(text);
2858
+ },
2859
+ getEditorState: () => {
2860
+ const reason = composerBlockedReason();
2861
+ return {
2862
+ layout: composerLayout,
2863
+ isRunning: composerIsRunning,
2864
+ isSubmitting: composerIsSubmitting,
2865
+ isSubmittingBlocked: reason !== null,
2866
+ submittingBlockedReason: reason,
2867
+ isAttaching: false,
2868
+ attachmentError: null
2869
+ };
2870
+ },
2871
+ subscribeEditorState: () => () => {
2872
+ },
2873
+ insertAtCursor(value, block) {
2874
+ composerTarget.setDraft(
2875
+ appendComposerDraft(composerDraft(), value, block)
2876
+ );
2877
+ return true;
2878
+ },
2879
+ isAvailable: composerIsAvailable,
2880
+ focus() {
2881
+ composerLog.focusCount += 1;
2882
+ },
2883
+ async submit(submitOptions) {
2884
+ composerLog.submits.push(submitOptions);
2885
+ commitComposerDraft("", []);
2886
+ for (const listener of submissionListeners) listener();
2887
+ },
2888
+ async setSelection(selection) {
2889
+ if (composerScope.kind === "queued-message") {
2890
+ throw new Error("This composer has no pickers to set.");
2891
+ }
2892
+ const {
2893
+ projectId: _projectId,
2894
+ environment: _environment,
2895
+ ...rest
2896
+ } = selection;
2897
+ const accepted = composerScope.kind === "thread" ? rest : { ...selection };
2898
+ composerLog.selections.push(accepted);
2899
+ composerSelection = { ...composerSelection, ...accepted };
2900
+ notifyComposerListeners();
2901
+ return composerSelection;
2902
+ }
2903
+ };
2904
+ const composerHandle = createComposerHandleBinding(
2905
+ testComposerKey(composerScope),
2906
+ {
2907
+ pluginId: composerPluginId,
2908
+ target: composerTarget,
2909
+ mentionText: testComposerMentionText,
2228
2910
  setTextEffect(effect) {
2229
2911
  if (!composerOwnership.active) return;
2230
2912
  composerLog.textEffect = effect;
@@ -2235,76 +2917,56 @@ function renderSlot(registration, props, options = {}) {
2235
2917
  composerLog.inputLocked = locked;
2236
2918
  composerLog.inputLockCalls.push(locked);
2237
2919
  },
2238
- addQuote(text) {
2239
- const trimmed = text.replace(/\r\n|\r/gu, "\n").trim();
2240
- if (trimmed !== "") {
2241
- const block = trimmed.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
2242
- commitComposerText(
2243
- composerText === "" ? `${block}
2244
- ` : `${composerText}
2245
- ${block}
2246
- `
2247
- );
2248
- composerLog.quotes.push(text);
2249
- }
2250
- composerLog.focusCount += 1;
2251
- },
2252
- experimental_onSubmitted(listener) {
2920
+ onSubmitted(listener) {
2253
2921
  submissionListeners.add(listener);
2254
2922
  return () => {
2255
2923
  submissionListeners.delete(listener);
2256
2924
  };
2257
- },
2258
- experimental_removeMention({ provider, id }) {
2259
- for (let index = composerLog.mentions.length - 1; index >= 0; index -= 1) {
2260
- const mention = composerLog.mentions[index];
2261
- if (mention?.provider === provider && mention.id === id) {
2262
- commitComposerText(composerText.replace(mention.label, ""));
2263
- composerLog.mentions.splice(index, 1);
2264
- }
2265
- }
2266
- },
2267
- insertMention(mention) {
2268
- const label = mention.label.trim() || mention.id;
2269
- const separator = composerText.length === 0 || /\s$/u.test(composerText) ? "" : " ";
2270
- commitComposerText(`${composerText}${separator}${label} `);
2271
- composerLog.mentions.push(mention);
2272
- composerLog.focusCount += 1;
2273
- },
2274
- focus() {
2275
- composerLog.focusCount += 1;
2276
- },
2277
- async experimental_submit(options2) {
2278
- if (!composerOwnership.active) {
2279
- throw new Error("This composer is no longer active.");
2280
- }
2281
- if (composerText.trim() === "") {
2282
- throw new Error("Type a message before scheduling it.");
2283
- }
2284
- if (options2.sendAt !== void 0 && (!Number.isFinite(options2.sendAt) || options2.sendAt <= Date.now())) {
2285
- throw new Error("Pick a time in the future.");
2286
- }
2287
- composerLog.submits.push(options2);
2288
- commitComposerText("");
2289
- for (const listener of submissionListeners) listener();
2290
- },
2291
- async experimental_setSelection(selection) {
2292
- if (!composerOwnership.active) {
2293
- throw new Error("This composer is no longer active.");
2294
- }
2295
- if (composerScope.kind === "queued-message" || composerScope.kind === "side-chat") {
2296
- throw new Error("This composer has no pickers to set.");
2297
- }
2298
- const {
2299
- projectId: _projectId,
2300
- environment: _environment,
2301
- ...rest
2302
- } = selection;
2303
- const accepted = composerScope.kind === "thread" ? rest : { ...selection };
2304
- composerLog.selections.push(accepted);
2305
- return accepted;
2306
2925
  }
2307
2926
  }
2927
+ ).handle;
2928
+ const forgetLoggedMention = ({
2929
+ provider,
2930
+ id
2931
+ }) => {
2932
+ for (let index = composerLog.mentions.length - 1; index >= 0; index -= 1) {
2933
+ const mention = composerLog.mentions[index];
2934
+ if (mention?.provider === provider && mention.id === id) {
2935
+ composerLog.mentions.splice(index, 1);
2936
+ }
2937
+ }
2938
+ };
2939
+ const { insertMention, removeMention, experimental_removeMention } = composerHandle;
2940
+ Object.assign(composerHandle, {
2941
+ insertMention(mention) {
2942
+ insertMention(mention);
2943
+ composerLog.mentions.push(mention);
2944
+ },
2945
+ removeMention(mention) {
2946
+ removeMention(mention);
2947
+ forgetLoggedMention(mention);
2948
+ },
2949
+ experimental_removeMention(mention) {
2950
+ experimental_removeMention(mention);
2951
+ forgetLoggedMention(mention);
2952
+ }
2953
+ });
2954
+ const composer = {
2955
+ api: composerHandle,
2956
+ apiList: [composerHandle],
2957
+ getAttachmentCount: () => composerAttachmentCount,
2958
+ getLayout: () => composerLayout,
2959
+ getRun: () => ({
2960
+ isRunning: composerIsRunning,
2961
+ isSubmitting: composerIsSubmitting
2962
+ }),
2963
+ getScope: () => composerScope,
2964
+ getText: () => composerText,
2965
+ getVersionSnapshot: () => composerVersion,
2966
+ subscribe(listener) {
2967
+ composerListeners.add(listener);
2968
+ return () => composerListeners.delete(listener);
2969
+ }
2308
2970
  };
2309
2971
  const env = {
2310
2972
  rpcClient,
@@ -2382,7 +3044,7 @@ ${block}
2382
3044
  const setRealtimeConnectionState = async (state) => {
2383
3045
  await act(async () => realtimeConnection.setState(state));
2384
3046
  };
2385
- const setComposerText = async (text) => {
3047
+ const setComposerText2 = async (text) => {
2386
3048
  await act(async () => commitComposerText(text));
2387
3049
  };
2388
3050
  const setComposerScope = async (scope) => {
@@ -2402,7 +3064,7 @@ ${block}
2402
3064
  rpcCalls,
2403
3065
  emitRealtime,
2404
3066
  setRealtimeConnectionState,
2405
- setComposerText,
3067
+ setComposerText: setComposerText2,
2406
3068
  setComposerScope,
2407
3069
  navigateCalls,
2408
3070
  experimental_fixedTabOpenCalls,
@@ -2413,7 +3075,7 @@ ${block}
2413
3075
  behavior: {
2414
3076
  emitRealtime,
2415
3077
  setRealtimeConnectionState,
2416
- setComposerText,
3078
+ setComposerText: setComposerText2,
2417
3079
  setComposerScope
2418
3080
  },
2419
3081
  inspection: {