@get-bb/plugin-sdk 0.5.31 → 0.6.5

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"
@@ -1079,6 +1605,34 @@ function collectPluginAppRegistrations(definition, onComposerCustomizationReject
1079
1605
 
1080
1606
  // src/testing/app.tsx
1081
1607
  import { jsx, jsxs } from "react/jsx-runtime";
1608
+ function testComposerKey(scope) {
1609
+ switch (scope.kind) {
1610
+ case "thread":
1611
+ return `test-composer:thread:${scope.threadId}`;
1612
+ case "queued-message":
1613
+ return `test-composer:queued-message:${scope.threadId}:${scope.queuedMessageId}`;
1614
+ case "new-thread":
1615
+ return `test-composer:new-thread:${scope.projectId ?? ""}`;
1616
+ }
1617
+ }
1618
+ function testComposerMentionText(mention) {
1619
+ switch (mention.kind) {
1620
+ case "thread":
1621
+ return `@thread:${mention.threadId}`;
1622
+ case "project":
1623
+ return `@project:${mention.projectId}`;
1624
+ case "section":
1625
+ return `@section:${mention.sectionId}`;
1626
+ case "command":
1627
+ return `${mention.trigger}${mention.name}`;
1628
+ case "plugin":
1629
+ return `@${mention.label}`;
1630
+ case "path": {
1631
+ const path = mention.source === "thread-storage" ? `thread-storage:${mention.path}` : mention.path;
1632
+ return `@${path}${mention.entryKind === "directory" && !path.endsWith("/") ? "/" : ""}`;
1633
+ }
1634
+ }
1635
+ }
1082
1636
  function createSdkFakeNode(provided, path, calls) {
1083
1637
  const callable = function sdkFakeNode() {
1084
1638
  };
@@ -1610,19 +2164,21 @@ var testPluginSdkApp = {
1610
2164
  },
1611
2165
  useComposer() {
1612
2166
  const composer = useSlotEnv("useComposer").composer;
1613
- const version = useSyncExternalStore(
2167
+ useSyncExternalStore(
1614
2168
  composer.subscribe,
1615
2169
  composer.getVersionSnapshot,
1616
2170
  composer.getVersionSnapshot
1617
2171
  );
1618
- return useMemo(
1619
- () => ({
1620
- ...composer.api,
1621
- scope: composer.getScope(),
1622
- text: composer.getText()
1623
- }),
1624
- [composer, version]
2172
+ return composer.api;
2173
+ },
2174
+ useComposers() {
2175
+ const composer = useSlotEnv("useComposers").composer;
2176
+ useSyncExternalStore(
2177
+ composer.subscribe,
2178
+ composer.getVersionSnapshot,
2179
+ composer.getVersionSnapshot
1625
2180
  );
2181
+ return composer.apiList;
1626
2182
  },
1627
2183
  ThreadChat: TestThreadChat,
1628
2184
  Markdown: TestMarkdown,
@@ -1760,13 +2316,13 @@ var testPluginSdkApp = {
1760
2316
  const attachmentCount = composer.getAttachmentCount();
1761
2317
  return {
1762
2318
  scope: composer.getScope(),
1763
- layout: "expanded",
2319
+ layout: composer.getLayout(),
1764
2320
  draft: {
1765
2321
  text,
1766
2322
  isEmpty: isComposerDraftEmpty(text, attachmentCount),
1767
2323
  attachmentCount
1768
2324
  },
1769
- run: { isRunning: false, isSubmitting: false }
2325
+ run: composer.getRun()
1770
2326
  };
1771
2327
  }, [composer, version]);
1772
2328
  }
@@ -2171,26 +2727,76 @@ function renderSlot(registration, props, options = {}) {
2171
2727
  const projectId = options.context?.projectId ?? null;
2172
2728
  const threadId = options.context?.threadId ?? null;
2173
2729
  let composerScope = options.composer?.scope ?? (threadId !== null ? { kind: "thread", threadId } : { kind: "new-thread", projectId });
2730
+ let composerSelection = composerScope.kind === "queued-message" ? null : {
2731
+ ...composerScope.kind === "new-thread" && projectId !== null ? { projectId } : {},
2732
+ ...options.composer?.selection
2733
+ };
2174
2734
  let composerText = options.composer?.text ?? "";
2175
- const composerAttachmentCount = options.composer?.attachmentCount ?? 0;
2735
+ let composerMentions = [
2736
+ ...options.composer?.mentions ?? []
2737
+ ];
2738
+ let composerAttachments = [...options.composer?.attachments ?? []];
2739
+ let composerAttachmentCount = options.composer?.attachmentCount ?? composerAttachments.length;
2740
+ const composerLayout = options.composer?.layout ?? "expanded";
2741
+ const composerIsRunning = options.composer?.isRunning ?? false;
2742
+ const composerIsSubmitting = options.composer?.isSubmitting ?? false;
2743
+ const composerPluginId = options.pluginId ?? "test-plugin";
2176
2744
  let composerVersion = 0;
2177
2745
  const composerListeners = /* @__PURE__ */ new Set();
2178
2746
  const notifyComposerListeners = () => {
2179
2747
  composerVersion += 1;
2180
2748
  for (const listener of composerListeners) listener();
2181
2749
  };
2750
+ const commitComposerDraft = (nextText, nextMentions) => {
2751
+ if (nextText === composerText && nextMentions === composerMentions) return;
2752
+ composerText = nextText;
2753
+ composerMentions = nextMentions;
2754
+ notifyComposerListeners();
2755
+ };
2182
2756
  const commitComposerText = (next) => {
2183
2757
  if (next === composerText) return;
2184
- composerText = next;
2185
- notifyComposerListeners();
2758
+ commitComposerDraft(
2759
+ next,
2760
+ reconcileComposerMentions(composerText, next, composerMentions)
2761
+ );
2762
+ };
2763
+ const composerBlockedReason = () => {
2764
+ if (options.composer?.submittingBlockedReason !== void 0) {
2765
+ return options.composer.submittingBlockedReason;
2766
+ }
2767
+ if (composerIsSubmitting) return "Submitting...";
2768
+ return composerText.trim() === "" && composerAttachmentCount === 0 ? "Type a message first." : null;
2769
+ };
2770
+ let composerDraftCache = null;
2771
+ const composerDraft = () => {
2772
+ if (composerDraftCache?.version !== composerVersion) {
2773
+ composerDraftCache = {
2774
+ version: composerVersion,
2775
+ draft: {
2776
+ text: composerText,
2777
+ mentions: composerMentions,
2778
+ attachments: composerAttachments
2779
+ }
2780
+ };
2781
+ }
2782
+ return composerDraftCache.draft;
2186
2783
  };
2187
2784
  const composerLog = {
2188
2785
  get text() {
2189
2786
  return composerText;
2190
2787
  },
2788
+ get draft() {
2789
+ return composerHandle.draft;
2790
+ },
2791
+ get key() {
2792
+ return testComposerKey(composerScope);
2793
+ },
2191
2794
  get scope() {
2192
2795
  return composerScope;
2193
2796
  },
2797
+ get selection() {
2798
+ return composerSelection;
2799
+ },
2194
2800
  get attachmentCount() {
2195
2801
  return composerAttachmentCount;
2196
2802
  },
@@ -2205,26 +2811,88 @@ function renderSlot(registration, props, options = {}) {
2205
2811
  selections: []
2206
2812
  };
2207
2813
  const composerOwnership = { active: true };
2814
+ const composerIsAvailable = () => composerOwnership.active || composerScope.kind !== "queued-message";
2208
2815
  const submissionListeners = /* @__PURE__ */ new Set();
2209
- const composer = {
2816
+ const composerTarget = {
2817
+ get key() {
2818
+ return testComposerKey(composerScope);
2819
+ },
2820
+ get scope() {
2821
+ return composerScope;
2822
+ },
2823
+ getDraft: composerDraft,
2210
2824
  getAttachmentCount: () => composerAttachmentCount,
2211
- getScope: () => composerScope,
2212
- getText: () => composerText,
2213
- getVersionSnapshot: () => composerVersion,
2214
- subscribe(listener) {
2215
- composerListeners.add(listener);
2216
- return () => composerListeners.delete(listener);
2825
+ getSelection: () => composerSelection,
2826
+ setDraft: (next) => {
2827
+ if (next.attachments !== void 0) {
2828
+ composerAttachments = [...next.attachments];
2829
+ composerAttachmentCount = composerAttachments.length;
2830
+ }
2831
+ commitComposerDraft(next.text, [...next.mentions]);
2217
2832
  },
2218
- api: {
2219
- setText(next) {
2220
- commitComposerText(next);
2221
- },
2222
- updateText(updater) {
2223
- commitComposerText(updater(composerText));
2224
- },
2225
- clear() {
2226
- commitComposerText("");
2227
- },
2833
+ addQuote(text) {
2834
+ const trimmed = text.replace(/\r\n|\r/gu, "\n").trim();
2835
+ if (trimmed === "") return;
2836
+ const block = trimmed.split("\n").map((line) => line.length > 0 ? `> ${line}` : ">").join("\n");
2837
+ commitComposerText(
2838
+ composerText === "" ? `${block}
2839
+ ` : `${composerText}
2840
+ ${block}
2841
+ `
2842
+ );
2843
+ composerLog.quotes.push(text);
2844
+ },
2845
+ getEditorState: () => {
2846
+ const reason = composerBlockedReason();
2847
+ return {
2848
+ layout: composerLayout,
2849
+ isRunning: composerIsRunning,
2850
+ isSubmitting: composerIsSubmitting,
2851
+ isSubmittingBlocked: reason !== null,
2852
+ submittingBlockedReason: reason,
2853
+ isAttaching: false,
2854
+ attachmentError: null
2855
+ };
2856
+ },
2857
+ subscribeEditorState: () => () => {
2858
+ },
2859
+ insertAtCursor(value, block) {
2860
+ composerTarget.setDraft(
2861
+ appendComposerDraft(composerDraft(), value, block)
2862
+ );
2863
+ return true;
2864
+ },
2865
+ isAvailable: composerIsAvailable,
2866
+ focus() {
2867
+ composerLog.focusCount += 1;
2868
+ },
2869
+ async submit(submitOptions) {
2870
+ composerLog.submits.push(submitOptions);
2871
+ commitComposerDraft("", []);
2872
+ for (const listener of submissionListeners) listener();
2873
+ },
2874
+ async setSelection(selection) {
2875
+ if (composerScope.kind === "queued-message") {
2876
+ throw new Error("This composer has no pickers to set.");
2877
+ }
2878
+ const {
2879
+ projectId: _projectId,
2880
+ environment: _environment,
2881
+ ...rest
2882
+ } = selection;
2883
+ const accepted = composerScope.kind === "thread" ? rest : { ...selection };
2884
+ composerLog.selections.push(accepted);
2885
+ composerSelection = { ...composerSelection, ...accepted };
2886
+ notifyComposerListeners();
2887
+ return composerSelection;
2888
+ }
2889
+ };
2890
+ const composerHandle = createComposerHandleBinding(
2891
+ testComposerKey(composerScope),
2892
+ {
2893
+ pluginId: composerPluginId,
2894
+ target: composerTarget,
2895
+ mentionText: testComposerMentionText,
2228
2896
  setTextEffect(effect) {
2229
2897
  if (!composerOwnership.active) return;
2230
2898
  composerLog.textEffect = effect;
@@ -2235,76 +2903,56 @@ function renderSlot(registration, props, options = {}) {
2235
2903
  composerLog.inputLocked = locked;
2236
2904
  composerLog.inputLockCalls.push(locked);
2237
2905
  },
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) {
2906
+ onSubmitted(listener) {
2253
2907
  submissionListeners.add(listener);
2254
2908
  return () => {
2255
2909
  submissionListeners.delete(listener);
2256
2910
  };
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
2911
  }
2307
2912
  }
2913
+ ).handle;
2914
+ const forgetLoggedMention = ({
2915
+ provider,
2916
+ id
2917
+ }) => {
2918
+ for (let index = composerLog.mentions.length - 1; index >= 0; index -= 1) {
2919
+ const mention = composerLog.mentions[index];
2920
+ if (mention?.provider === provider && mention.id === id) {
2921
+ composerLog.mentions.splice(index, 1);
2922
+ }
2923
+ }
2924
+ };
2925
+ const { insertMention, removeMention, experimental_removeMention } = composerHandle;
2926
+ Object.assign(composerHandle, {
2927
+ insertMention(mention) {
2928
+ insertMention(mention);
2929
+ composerLog.mentions.push(mention);
2930
+ },
2931
+ removeMention(mention) {
2932
+ removeMention(mention);
2933
+ forgetLoggedMention(mention);
2934
+ },
2935
+ experimental_removeMention(mention) {
2936
+ experimental_removeMention(mention);
2937
+ forgetLoggedMention(mention);
2938
+ }
2939
+ });
2940
+ const composer = {
2941
+ api: composerHandle,
2942
+ apiList: [composerHandle],
2943
+ getAttachmentCount: () => composerAttachmentCount,
2944
+ getLayout: () => composerLayout,
2945
+ getRun: () => ({
2946
+ isRunning: composerIsRunning,
2947
+ isSubmitting: composerIsSubmitting
2948
+ }),
2949
+ getScope: () => composerScope,
2950
+ getText: () => composerText,
2951
+ getVersionSnapshot: () => composerVersion,
2952
+ subscribe(listener) {
2953
+ composerListeners.add(listener);
2954
+ return () => composerListeners.delete(listener);
2955
+ }
2308
2956
  };
2309
2957
  const env = {
2310
2958
  rpcClient,
@@ -2382,7 +3030,7 @@ ${block}
2382
3030
  const setRealtimeConnectionState = async (state) => {
2383
3031
  await act(async () => realtimeConnection.setState(state));
2384
3032
  };
2385
- const setComposerText = async (text) => {
3033
+ const setComposerText2 = async (text) => {
2386
3034
  await act(async () => commitComposerText(text));
2387
3035
  };
2388
3036
  const setComposerScope = async (scope) => {
@@ -2402,7 +3050,7 @@ ${block}
2402
3050
  rpcCalls,
2403
3051
  emitRealtime,
2404
3052
  setRealtimeConnectionState,
2405
- setComposerText,
3053
+ setComposerText: setComposerText2,
2406
3054
  setComposerScope,
2407
3055
  navigateCalls,
2408
3056
  experimental_fixedTabOpenCalls,
@@ -2413,7 +3061,7 @@ ${block}
2413
3061
  behavior: {
2414
3062
  emitRealtime,
2415
3063
  setRealtimeConnectionState,
2416
- setComposerText,
3064
+ setComposerText: setComposerText2,
2417
3065
  setComposerScope
2418
3066
  },
2419
3067
  inspection: {