best-email-builder 0.6.0 → 0.7.0

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.
Files changed (42) hide show
  1. package/CHANGELOG.md +37 -0
  2. package/README.md +41 -4
  3. package/dist/ai-server.js +114 -1
  4. package/dist/ai-server.js.map +1 -1
  5. package/dist/ai.js +504 -169
  6. package/dist/ai.js.map +1 -1
  7. package/dist/builder.js +4 -4
  8. package/dist/{chunk-5W4TUUPD.js → chunk-3RPWH5PC.js} +3 -3
  9. package/dist/{chunk-QPAZLCPO.js → chunk-C6WHIBGH.js} +2 -2
  10. package/dist/{chunk-QPAZLCPO.js.map → chunk-C6WHIBGH.js.map} +1 -1
  11. package/dist/{chunk-NZIQF7IW.js → chunk-CTZ56YJG.js} +192 -23
  12. package/dist/chunk-CTZ56YJG.js.map +1 -0
  13. package/dist/{chunk-NC2YDZSP.js → chunk-QH5OI43L.js} +4 -4
  14. package/dist/{chunk-JU3OUO6X.js → chunk-QRUMXCGW.js} +2 -2
  15. package/dist/{chunk-XEFBPP5Y.js → chunk-UMVBT7NW.js} +3 -3
  16. package/dist/index.js +5 -5
  17. package/dist/library.js +4 -4
  18. package/dist/server.js +4 -4
  19. package/dist/server.js.map +1 -1
  20. package/dist/styles.css +1 -1
  21. package/dist/types/ai/ConnectAi.d.ts +2 -1
  22. package/dist/types/ai/ModelPicker.d.ts +34 -0
  23. package/dist/types/ai/agent.d.ts +31 -0
  24. package/dist/types/ai/agentEndpoint.d.ts +69 -0
  25. package/dist/types/ai/claude.d.ts +2 -1
  26. package/dist/types/ai/draft.d.ts +10 -4
  27. package/dist/types/ai/generator.d.ts +10 -4
  28. package/dist/types/ai/index.d.ts +2 -1
  29. package/dist/types/ai/keys.d.ts +14 -1
  30. package/dist/types/ai/providers.d.ts +48 -5
  31. package/dist/types/ai/rewrite.d.ts +2 -1
  32. package/dist/types/ai/server.d.ts +3 -1
  33. package/dist/types/ai/subjects.d.ts +2 -1
  34. package/dist/types/core/constants.d.ts +1 -1
  35. package/dist/types/engine/NativeAdapter.d.ts +1 -1
  36. package/dist/types/server/handler.d.ts +3 -0
  37. package/package.json +1 -1
  38. package/dist/chunk-NZIQF7IW.js.map +0 -1
  39. /package/dist/{chunk-5W4TUUPD.js.map → chunk-3RPWH5PC.js.map} +0 -0
  40. /package/dist/{chunk-NC2YDZSP.js.map → chunk-QH5OI43L.js.map} +0 -0
  41. /package/dist/{chunk-JU3OUO6X.js.map → chunk-QRUMXCGW.js.map} +0 -0
  42. /package/dist/{chunk-XEFBPP5Y.js.map → chunk-UMVBT7NW.js.map} +0 -0
package/dist/ai.js CHANGED
@@ -23,9 +23,11 @@ import {
23
23
  createHttpCaller,
24
24
  useSearchShortcut,
25
25
  useUi
26
- } from "./chunk-JU3OUO6X.js";
27
- import "./chunk-QPAZLCPO.js";
26
+ } from "./chunk-QRUMXCGW.js";
27
+ import "./chunk-C6WHIBGH.js";
28
28
  import {
29
+ AGENT_ERROR_CODES,
30
+ AGENT_PROTOCOL,
29
31
  AI_MESSAGES,
30
32
  AI_PROVIDERS,
31
33
  DEFAULT_CLAUDE_MODEL,
@@ -37,15 +39,17 @@ import {
37
39
  PURPOSE_CATEGORIES,
38
40
  REWRITE_ACTIONS,
39
41
  REWRITE_LANGUAGES,
42
+ agentEmailModel,
40
43
  claudeEmailModel,
41
44
  claudeProvider,
42
45
  createEmailGenerator,
43
46
  emailWords,
44
47
  failureCode,
48
+ listProviderModels,
45
49
  providerEmailModel,
46
50
  purposesByCategory,
47
51
  resolveProvider
48
- } from "./chunk-NZIQF7IW.js";
52
+ } from "./chunk-CTZ56YJG.js";
49
53
  import "./chunk-OWVXKMLI.js";
50
54
  import "./chunk-W424KPCR.js";
51
55
  import {
@@ -56,6 +60,7 @@ import {
56
60
  // src/ai/keys.js
57
61
  var STORAGE_PREFIX = "best-email-builder:ai-key:";
58
62
  var CHOICE_SLOT = "best-email-builder:ai-provider";
63
+ var MODEL_PREFIX = "best-email-builder:ai-model:";
59
64
  function browserStorage() {
60
65
  try {
61
66
  const storage = globalThis.localStorage;
@@ -70,10 +75,12 @@ var choiceOf = (provider) => Object.freeze({
70
75
  model: provider.model ?? null,
71
76
  keyUrl: provider.keyUrl,
72
77
  keyHint: provider.keyHint,
73
- free: provider.free
78
+ free: provider.free,
79
+ publicModels: Boolean(provider.publicModels)
74
80
  });
75
- function createKeyStore(providerOrList) {
76
- const providers = Object.freeze((Array.isArray(providerOrList) ? providerOrList : [providerOrList]).map(choiceOf));
81
+ function createKeyStore(providerOrList, { fetch: fetchImpl } = {}) {
82
+ const originals = Array.isArray(providerOrList) ? providerOrList : [providerOrList];
83
+ const providers = Object.freeze(originals.map(choiceOf));
77
84
  if (!providers.length) throw new TypeError("createKeyStore: at least one provider");
78
85
  const known = (id) => providers.some((provider) => provider.id === id);
79
86
  const memory = /* @__PURE__ */ new Map();
@@ -104,6 +111,12 @@ function createKeyStore(providerOrList) {
104
111
  const kept = read(CHOICE_SLOT);
105
112
  let current = known(kept) ? kept : (providers.find((provider) => keyOf(provider.id)) || providers[0]).id;
106
113
  const get = (id) => keyOf(id === void 0 ? current : String(id));
114
+ const providerId = (id) => {
115
+ if (id === void 0) return current;
116
+ if (!known(id)) throw new TypeError(`ai.key: no provider "${id}" \u2014 one of ${providers.map((provider) => provider.id).join(", ")}`);
117
+ return id;
118
+ };
119
+ const lists = /* @__PURE__ */ new Map();
107
120
  const changed = () => {
108
121
  snapshot = Object.freeze({ asking: Boolean(pending), refused, connected: Boolean(get()) });
109
122
  for (const listener of [...listeners]) listener();
@@ -145,6 +158,33 @@ function createKeyStore(providerOrList) {
145
158
  write(CHOICE_SLOT, id);
146
159
  changed();
147
160
  },
161
+ model: (id) => {
162
+ const choice = (
163
+ /** @type {ProviderChoice} */
164
+ providers.find((provider) => provider.id === providerId(id))
165
+ );
166
+ const kept2 = memory.get(MODEL_PREFIX + choice.id) ?? read(MODEL_PREFIX + choice.id);
167
+ return kept2 || choice.model || "";
168
+ },
169
+ chooseModel: (model, id) => {
170
+ if (typeof model !== "string" || !model.trim()) throw new TypeError("ai.key.chooseModel: a model id");
171
+ const choice = (
172
+ /** @type {ProviderChoice} */
173
+ providers.find((provider) => provider.id === providerId(id))
174
+ );
175
+ const value = model.trim() === choice.model ? "" : model.trim();
176
+ memory.set(MODEL_PREFIX + choice.id, value);
177
+ write(MODEL_PREFIX + choice.id, value);
178
+ changed();
179
+ },
180
+ listModels: async (id, { apiKey, signal } = {}) => {
181
+ const at = providers.findIndex((provider) => provider.id === providerId(id));
182
+ const key = String(apiKey ?? keyOf(providers[at].id)).trim();
183
+ const slot = `${providers[at].id}
184
+ ${key}`;
185
+ if (!lists.has(slot)) lists.set(slot, await listProviderModels({ provider: originals[at], apiKey: key, fetch: fetchImpl, signal }));
186
+ return lists.get(slot);
187
+ },
148
188
  request: () => {
149
189
  if (!pending) {
150
190
  let resolve = () => {
@@ -170,48 +210,305 @@ function createKeyStore(providerOrList) {
170
210
  }
171
211
 
172
212
  // src/ai/ConnectAi.jsx
173
- import { useEffect, useId, useRef, useState, useSyncExternalStore } from "react";
174
- import { ArrowUpRight, Eye, EyeOff, Gift, KeyRound, ShieldCheck, TriangleAlert } from "lucide-react";
175
- import { jsx, jsxs } from "react/jsx-runtime";
213
+ import { useEffect as useEffect2, useId as useId2, useRef as useRef2, useState as useState2, useSyncExternalStore } from "react";
214
+ import { ArrowUpRight, Eye, EyeOff, Gift as Gift2, KeyRound, ShieldCheck, TriangleAlert } from "lucide-react";
215
+
216
+ // src/ai/ModelPicker.jsx
217
+ import { useEffect, useId, useMemo, useRef, useState } from "react";
218
+ import { Check, ChevronDown, Gift, LoaderCircle, Search } from "lucide-react";
219
+ import { Fragment, jsx, jsxs } from "react/jsx-runtime";
220
+ function ModelPicker({ aiKey, provider, apiKey, delay = 0, value, onChange, open, onOpenChange }) {
221
+ const { t } = useUi();
222
+ const [list, setList] = useState(
223
+ /** @type {{status: 'loading'|'ready'|'needs-key'|'refused'|'failed', models: import('./providers.js').ModelChoice[]}} */
224
+ { status: "loading", models: [] }
225
+ );
226
+ const [query, setQuery] = useState("");
227
+ const [freeOnly, setFreeOnly] = useState(false);
228
+ const [active, setActive] = useState(0);
229
+ const triggerRef = useRef(
230
+ /** @type {HTMLButtonElement|null} */
231
+ null
232
+ );
233
+ const listRef = useRef(
234
+ /** @type {HTMLDivElement|null} */
235
+ null
236
+ );
237
+ const labelId = useId();
238
+ const valueId = useId();
239
+ const listId = useId();
240
+ const hintId = useId();
241
+ useEffect(() => {
242
+ if (!apiKey && !provider.publicModels) {
243
+ setList({ status: "needs-key", models: [] });
244
+ return void 0;
245
+ }
246
+ const controller = new AbortController();
247
+ setList({ status: "loading", models: [] });
248
+ const timer = setTimeout(() => {
249
+ aiKey.listModels(provider.id, { apiKey, signal: controller.signal }).then(
250
+ (models2) => !controller.signal.aborted && setList({ status: "ready", models: models2 }),
251
+ (error) => !controller.signal.aborted && setList({ status: error?.code === "KEY_REFUSED" ? "refused" : "failed", models: [] })
252
+ );
253
+ }, delay);
254
+ return () => {
255
+ clearTimeout(timer);
256
+ controller.abort();
257
+ };
258
+ }, [aiKey, provider.id, provider.publicModels, apiKey, delay]);
259
+ useEffect(() => {
260
+ if (!open) return;
261
+ setQuery("");
262
+ setFreeOnly(false);
263
+ }, [open]);
264
+ const models = useMemo(() => {
265
+ const listed = new Map(list.models.map((model) => [model.id, model]));
266
+ const seen = /* @__PURE__ */ new Set();
267
+ const all = [];
268
+ const add = (model) => {
269
+ if (model.id && !seen.has(model.id)) seen.add(model.id) && all.push(model);
270
+ };
271
+ for (const id of [provider.model, value]) if (id) add(listed.get(id) || { id, name: id, free: false });
272
+ list.models.forEach(add);
273
+ return all;
274
+ }, [list.models, provider.model, value]);
275
+ const hasFree = list.models.some((model) => model.free);
276
+ const typed = query.trim();
277
+ const matches = useMemo(() => {
278
+ const words = typed.toLowerCase().split(/\s+/).filter(Boolean);
279
+ return models.filter((model) => (!freeOnly || model.free) && words.every((word) => `${model.id} ${model.name}`.toLowerCase().includes(word)));
280
+ }, [models, typed, freeOnly]);
281
+ const custom = typed && !/\s/.test(typed) && !models.some((model) => model.id === typed) ? typed : "";
282
+ const count = matches.length + (custom ? 1 : 0);
283
+ const current = models.find((model) => model.id === value);
284
+ useEffect(() => setActive(Math.max(0, matches.findIndex((model) => model.id === value))), [matches, value]);
285
+ useEffect(() => {
286
+ if (open) listRef.current?.querySelector('[data-active="true"]')?.scrollIntoView({ block: "nearest" });
287
+ }, [active, open]);
288
+ const close = () => {
289
+ triggerRef.current?.focus();
290
+ onOpenChange(false);
291
+ };
292
+ const pick = (id) => {
293
+ onChange(id);
294
+ close();
295
+ };
296
+ const onKeyDown = (event) => {
297
+ if (event.key === "ArrowDown" || event.key === "ArrowUp") {
298
+ event.preventDefault();
299
+ setActive((at) => count ? (at + (event.key === "ArrowDown" ? 1 : -1) + count) % count : 0);
300
+ } else if (event.key === "Enter") {
301
+ event.preventDefault();
302
+ const id = active < matches.length ? matches[active]?.id : custom;
303
+ if (id) pick(id);
304
+ }
305
+ };
306
+ const optionId = (at) => `${listId}-${at}`;
307
+ const badges = (model) => /* @__PURE__ */ jsxs(Fragment, { children: [
308
+ model.free && /* @__PURE__ */ jsxs("span", { className: "eb:inline-flex eb:shrink-0 eb:items-center eb:gap-1 eb:rounded-full eb:bg-success/10 eb:px-1.5 eb:py-px eb:text-[10.5px] eb:font-medium eb:text-success", children: [
309
+ /* @__PURE__ */ jsx(Gift, { className: "eb:size-3", "aria-hidden": "true" }),
310
+ t("Free")
311
+ ] }),
312
+ model.id === provider.model && /* @__PURE__ */ jsx("span", { className: "eb:shrink-0 eb:rounded-full eb:bg-muted eb:px-1.5 eb:py-px eb:text-[10.5px] eb:font-medium eb:text-foreground-lighter", children: t("Default") })
313
+ ] });
314
+ const status = {
315
+ loading: /* @__PURE__ */ jsxs(Fragment, { children: [
316
+ /* @__PURE__ */ jsx(LoaderCircle, { className: "eb:size-3 eb:shrink-0 eb:animate-spin", "aria-hidden": "true" }),
317
+ t("Loading the models of {provider}\u2026", { provider: provider.name })
318
+ ] }),
319
+ "needs-key": t("Paste the key to see all the models of {provider}.", { provider: provider.name }),
320
+ refused: /* @__PURE__ */ jsx("span", { className: "eb:text-warning", children: t("{provider} refused this key: its models could not be listed.", { provider: provider.name }) }),
321
+ failed: t("The list of models could not be loaded. The default model works, or type the id of another."),
322
+ ready: list.models.length ? hasFree ? t("{count} models, the free ones first.", { count: list.models.length }) : t("{count} models.", { count: list.models.length }) : t("{provider} lists no models here: type the id of the one you want.", { provider: provider.name })
323
+ }[list.status];
324
+ return /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", "data-testid": "eb-ai-connect-model-field", children: [
325
+ /* @__PURE__ */ jsx("span", { id: labelId, className: "eb:text-[12.5px] eb:leading-none eb:font-medium eb:text-foreground", children: t("Model") }),
326
+ /* @__PURE__ */ jsxs(
327
+ "button",
328
+ {
329
+ ref: triggerRef,
330
+ type: "button",
331
+ "aria-haspopup": "listbox",
332
+ "aria-expanded": open,
333
+ "aria-labelledby": `${labelId} ${valueId}`,
334
+ "aria-describedby": hintId,
335
+ onClick: () => onOpenChange(!open),
336
+ className: cn(
337
+ "eb:flex eb:h-9 eb:w-full eb:min-w-0 eb:items-center eb:gap-2 eb:rounded-md eb:border eb:border-input eb:bg-transparent eb:px-3 eb:text-left eb:shadow-xs eb:transition-[color,box-shadow] eb:outline-none eb:focus-visible:border-ring eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
338
+ open && "eb:border-ring"
339
+ ),
340
+ "data-testid": "eb-ai-connect-model",
341
+ children: [
342
+ /* @__PURE__ */ jsx("span", { id: valueId, className: "eb:min-w-0 eb:truncate eb:font-mono eb:text-[12.5px] eb:text-foreground", children: value || t("Choose a model") }),
343
+ current && badges(current),
344
+ /* @__PURE__ */ jsx(ChevronDown, { className: cn("eb:ml-auto eb:size-4 eb:shrink-0 eb:text-foreground-muted eb:transition-transform", open && "eb:rotate-180"), "aria-hidden": "true" })
345
+ ]
346
+ }
347
+ ),
348
+ open && /* @__PURE__ */ jsxs(
349
+ "div",
350
+ {
351
+ className: "eb:flex eb:flex-col eb:overflow-hidden eb:rounded-md eb:border eb:border-border eb:bg-background",
352
+ onKeyDown: (event) => event.key === "Escape" && close(),
353
+ "data-eb-models": "",
354
+ "data-testid": "eb-ai-connect-models",
355
+ children: [
356
+ /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:items-center eb:gap-2 eb:border-b eb:border-border eb:p-2", children: [
357
+ /* @__PURE__ */ jsxs("div", { className: "eb:relative eb:min-w-0 eb:flex-1", children: [
358
+ /* @__PURE__ */ jsx(Search, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted", "aria-hidden": "true" }),
359
+ /* @__PURE__ */ jsx(
360
+ "input",
361
+ {
362
+ autoFocus: true,
363
+ role: "combobox",
364
+ "aria-expanded": "true",
365
+ "aria-controls": listId,
366
+ "aria-autocomplete": "list",
367
+ "aria-activedescendant": count ? optionId(active) : void 0,
368
+ "aria-label": t("Search the models"),
369
+ value: query,
370
+ onChange: (event) => setQuery(event.target.value),
371
+ onKeyDown,
372
+ placeholder: t("Search the models\u2026"),
373
+ autoComplete: "off",
374
+ autoCapitalize: "off",
375
+ autoCorrect: "off",
376
+ spellCheck: false,
377
+ className: "eb:h-8 eb:w-full eb:rounded-md eb:bg-surface-75 eb:pr-2 eb:pl-7 eb:text-[13px] eb:text-foreground eb:outline-none eb:placeholder:text-foreground-muted eb:focus-visible:ring-2 eb:focus-visible:ring-ring/40",
378
+ "data-testid": "eb-ai-connect-model-search"
379
+ }
380
+ )
381
+ ] }),
382
+ hasFree && /* @__PURE__ */ jsxs(
383
+ "button",
384
+ {
385
+ type: "button",
386
+ "aria-pressed": freeOnly,
387
+ onClick: () => setFreeOnly((on) => !on),
388
+ className: cn(
389
+ "eb:inline-flex eb:h-8 eb:shrink-0 eb:items-center eb:gap-1.5 eb:rounded-md eb:border eb:px-2.5 eb:text-[12px] eb:font-medium eb:transition-colors eb:outline-none eb:focus-visible:ring-[3px] eb:focus-visible:ring-ring/50",
390
+ freeOnly ? "eb:border-success/40 eb:bg-success/10 eb:text-success" : "eb:border-border eb:text-foreground-lighter eb:hover:bg-muted"
391
+ ),
392
+ children: [
393
+ /* @__PURE__ */ jsx(Gift, { className: "eb:size-3.5", "aria-hidden": "true" }),
394
+ t("Free only")
395
+ ]
396
+ }
397
+ )
398
+ ] }),
399
+ /* @__PURE__ */ jsxs("div", { ref: listRef, id: listId, role: "listbox", "aria-labelledby": labelId, className: "eb:max-h-56 eb:overflow-y-auto eb:p-1", children: [
400
+ matches.map((model, at) => /* @__PURE__ */ jsxs(
401
+ "button",
402
+ {
403
+ id: optionId(at),
404
+ type: "button",
405
+ role: "option",
406
+ tabIndex: -1,
407
+ "aria-selected": model.id === value,
408
+ "data-active": at === active,
409
+ "data-model": model.id,
410
+ onClick: () => pick(model.id),
411
+ onMouseMove: () => at !== active && setActive(at),
412
+ className: cn(
413
+ "eb:flex eb:w-full eb:min-w-0 eb:items-start eb:gap-2 eb:rounded-md eb:px-2 eb:py-1.5 eb:text-left",
414
+ at === active ? "eb:bg-accent eb:text-accent-foreground" : ""
415
+ ),
416
+ children: [
417
+ /* @__PURE__ */ jsx(Check, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-primary", model.id !== value && "eb:invisible"), "aria-hidden": "true" }),
418
+ /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:min-w-0 eb:flex-1 eb:flex-col eb:gap-0.5", children: [
419
+ /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:min-w-0 eb:items-center eb:gap-1.5", children: [
420
+ /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:text-[13px] eb:text-foreground", children: model.name }),
421
+ badges(model)
422
+ ] }),
423
+ model.name !== model.id && /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", children: model.id })
424
+ ] })
425
+ ]
426
+ },
427
+ model.id
428
+ )),
429
+ custom && /* @__PURE__ */ jsxs(
430
+ "button",
431
+ {
432
+ id: optionId(matches.length),
433
+ type: "button",
434
+ role: "option",
435
+ tabIndex: -1,
436
+ "aria-selected": false,
437
+ "data-active": active === matches.length,
438
+ onClick: () => pick(custom),
439
+ onMouseMove: () => active !== matches.length && setActive(matches.length),
440
+ className: cn(
441
+ "eb:flex eb:w-full eb:min-w-0 eb:items-center eb:gap-2 eb:rounded-md eb:px-2 eb:py-1.5 eb:text-left eb:text-[13px] eb:text-primary",
442
+ active === matches.length ? "eb:bg-accent" : ""
443
+ ),
444
+ "data-testid": "eb-ai-connect-model-custom",
445
+ children: [
446
+ /* @__PURE__ */ jsx("span", { className: "eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
447
+ /* @__PURE__ */ jsx("span", { className: "eb:min-w-0 eb:truncate", children: t("Use \u201C{model}\u201D", { model: custom }) })
448
+ ]
449
+ }
450
+ ),
451
+ !count && /* @__PURE__ */ jsx("p", { className: "eb:px-2 eb:py-3 eb:text-center eb:text-[12px] eb:text-foreground-muted", children: t("No model matches.") })
452
+ ] })
453
+ ]
454
+ }
455
+ ),
456
+ /* @__PURE__ */ jsx("p", { id: hintId, role: "status", className: "eb:flex eb:min-h-4 eb:items-center eb:gap-1.5 eb:text-[11.5px] eb:text-foreground-muted", "data-testid": "eb-ai-connect-models-status", children: status })
457
+ ] });
458
+ }
459
+
460
+ // src/ai/ConnectAi.jsx
461
+ import { jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
462
+ var LISTABLE_KEY = 20;
176
463
  function ConnectAi({ aiKey }) {
177
464
  const { t } = useUi();
178
465
  const state = useSyncExternalStore(aiKey.subscribe, aiKey.state, aiKey.state);
179
- const [value, setValue] = useState("");
180
- const [shown, setShown] = useState(false);
181
- const [chosenId, setChosenId] = useState(aiKey.provider.id);
182
- const inputRef = useRef(
466
+ const [value, setValue] = useState2("");
467
+ const [shown, setShown] = useState2(false);
468
+ const [chosenId, setChosenId] = useState2(aiKey.provider.id);
469
+ const [picks, setPicks] = useState2(
470
+ /** @type {Record<string, string>} */
471
+ {}
472
+ );
473
+ const [modelsOpen, setModelsOpen] = useState2(false);
474
+ const inputRef = useRef2(
183
475
  /** @type {HTMLInputElement|null} */
184
476
  null
185
477
  );
186
- const choicesRef = useRef(
478
+ const choicesRef = useRef2(
187
479
  /** @type {HTMLDivElement|null} */
188
480
  null
189
481
  );
190
- const inputId = useId();
191
- const hintId = useId();
482
+ const inputId = useId2();
483
+ const hintId = useId2();
192
484
  const choices = aiKey.providers;
193
485
  const chosen = choices.find((provider) => provider.id === chosenId) || aiKey.provider;
194
486
  const { name, keyUrl, keyHint, free } = chosen;
195
- useEffect(() => {
487
+ useEffect2(() => {
196
488
  if (!state.asking) return;
197
489
  setChosenId(aiKey.provider.id);
198
490
  setValue("");
199
491
  setShown(false);
492
+ setPicks({});
493
+ setModelsOpen(false);
200
494
  }, [state.asking, aiKey]);
201
495
  const key = value.trim();
202
496
  const saved = aiKey.get(chosen.id);
203
497
  const unusual = Boolean(key && keyHint && !key.startsWith(keyHint));
204
498
  const refused = state.refused && chosen.id === aiKey.provider.id;
499
+ const modelFor = (id) => picks[id] ?? aiKey.model(id);
205
500
  const submit = (event) => {
206
501
  event.preventDefault();
207
502
  const answer = key || saved;
208
503
  if (!answer) return;
209
504
  if (chosen.id !== aiKey.provider.id) aiKey.choose(chosen.id);
505
+ if (picks[chosen.id] && picks[chosen.id] !== aiKey.model(chosen.id)) aiKey.chooseModel(picks[chosen.id], chosen.id);
210
506
  aiKey.answer(answer);
211
507
  };
212
508
  const choose = (id) => {
213
509
  setChosenId(id);
214
510
  setValue("");
511
+ setModelsOpen(false);
215
512
  };
216
513
  const onChoicesKeyDown = (event) => {
217
514
  const at = choices.findIndex((provider) => provider.id === chosen.id);
@@ -222,25 +519,30 @@ function ConnectAi({ aiKey }) {
222
519
  choose(choices[to].id);
223
520
  choicesRef.current?.querySelectorAll('[role="radio"]')[to]?.focus();
224
521
  };
225
- return /* @__PURE__ */ jsx(Dialog, { open: state.asking, onOpenChange: (open) => !open && aiKey.answer(null), children: /* @__PURE__ */ jsx(
522
+ return /* @__PURE__ */ jsx2(Dialog, { open: state.asking, onOpenChange: (open) => !open && aiKey.answer(null), children: /* @__PURE__ */ jsx2(
226
523
  DialogContent,
227
524
  {
228
- className: cn("eb:gap-0 eb:overflow-hidden eb:p-0", choices.length > 1 ? "eb:sm:max-w-xl" : "eb:sm:max-w-md"),
525
+ className: cn("eb:max-h-[calc(100dvh-2rem)] eb:gap-0 eb:overflow-y-auto eb:p-0", choices.length > 1 ? "eb:sm:max-w-xl" : "eb:sm:max-w-md"),
229
526
  "data-testid": "eb-ai-connect",
230
527
  onOpenAutoFocus: (event) => {
231
528
  event.preventDefault();
232
529
  inputRef.current?.focus();
233
530
  },
234
- children: /* @__PURE__ */ jsxs("form", { onSubmit: submit, children: [
235
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-5 eb:px-6 eb:pt-6 eb:pb-5", children: [
236
- /* @__PURE__ */ jsxs(DialogHeader, { className: "eb:flex-row eb:items-start eb:gap-3.5 eb:pr-6", children: [
237
- /* @__PURE__ */ jsx("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-full eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx(KeyRound, { className: "eb:size-5", "aria-hidden": "true" }) }),
238
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
239
- /* @__PURE__ */ jsx(DialogTitle, { children: t("Connect {provider}", { provider: name }) }),
240
- /* @__PURE__ */ jsx(DialogDescription, { className: "eb:text-[13px] eb:leading-relaxed eb:text-foreground-lighter", children: t("The AI writes with your own {provider} key. Connect it once: this browser remembers it.", { provider: name }) })
531
+ onEscapeKeyDown: (event) => {
532
+ if (!modelsOpen) return;
533
+ event.preventDefault();
534
+ if (!(event.target instanceof Element && event.target.closest("[data-eb-models]"))) setModelsOpen(false);
535
+ },
536
+ children: /* @__PURE__ */ jsxs2("form", { onSubmit: submit, className: "eb:min-w-0", children: [
537
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-5 eb:px-6 eb:pt-6 eb:pb-5", children: [
538
+ /* @__PURE__ */ jsxs2(DialogHeader, { className: "eb:flex-row eb:items-start eb:gap-3.5 eb:pr-6", children: [
539
+ /* @__PURE__ */ jsx2("span", { className: "eb:flex eb:size-10 eb:shrink-0 eb:items-center eb:justify-center eb:rounded-full eb:bg-primary/10 eb:text-primary", children: /* @__PURE__ */ jsx2(KeyRound, { className: "eb:size-5", "aria-hidden": "true" }) }),
540
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
541
+ /* @__PURE__ */ jsx2(DialogTitle, { children: t("Connect {provider}", { provider: name }) }),
542
+ /* @__PURE__ */ jsx2(DialogDescription, { className: "eb:text-[13px] eb:leading-relaxed eb:text-foreground-lighter", children: t("The AI writes with your own {provider} key. Connect it once: this browser remembers it.", { provider: name }) })
241
543
  ] })
242
544
  ] }),
243
- choices.length > 1 && /* @__PURE__ */ jsx(
545
+ choices.length > 1 && /* @__PURE__ */ jsx2(
244
546
  "div",
245
547
  {
246
548
  ref: choicesRef,
@@ -251,7 +553,7 @@ function ConnectAi({ aiKey }) {
251
553
  "data-testid": "eb-ai-connect-providers",
252
554
  children: choices.map((provider) => {
253
555
  const checked = provider.id === chosen.id;
254
- return /* @__PURE__ */ jsxs(
556
+ return /* @__PURE__ */ jsxs2(
255
557
  "button",
256
558
  {
257
559
  type: "button",
@@ -264,12 +566,12 @@ function ConnectAi({ aiKey }) {
264
566
  checked ? "eb:border-primary eb:bg-primary/10" : "eb:border-border eb:hover:bg-muted"
265
567
  ),
266
568
  children: [
267
- /* @__PURE__ */ jsxs("span", { className: "eb:flex eb:items-center eb:gap-1.5 eb:text-[13px] eb:leading-tight eb:font-medium eb:text-foreground", children: [
268
- /* @__PURE__ */ jsx("span", { className: "eb:truncate", children: provider.name }),
269
- provider.free && /* @__PURE__ */ jsx(Gift, { className: "eb:size-3 eb:shrink-0 eb:text-success", "aria-label": t(provider.free) }),
270
- aiKey.get(provider.id) && /* @__PURE__ */ jsx("span", { className: "eb:ml-auto eb:size-1.5 eb:shrink-0 eb:rounded-full eb:bg-success", title: t("Key saved"), "aria-label": t("Key saved") })
569
+ /* @__PURE__ */ jsxs2("span", { className: "eb:flex eb:items-center eb:gap-1.5 eb:text-[13px] eb:leading-tight eb:font-medium eb:text-foreground", children: [
570
+ /* @__PURE__ */ jsx2("span", { className: "eb:truncate", children: provider.name }),
571
+ provider.free && /* @__PURE__ */ jsx2(Gift2, { className: "eb:size-3 eb:shrink-0 eb:text-success", "aria-label": t(provider.free) }),
572
+ aiKey.get(provider.id) && /* @__PURE__ */ jsx2("span", { className: "eb:ml-auto eb:size-1.5 eb:shrink-0 eb:rounded-full eb:bg-success", title: t("Key saved"), "aria-label": t("Key saved") })
271
573
  ] }),
272
- provider.model && /* @__PURE__ */ jsx("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", title: provider.model, children: provider.model })
574
+ modelFor(provider.id) && /* @__PURE__ */ jsx2("span", { className: "eb:truncate eb:font-mono eb:text-[10.5px] eb:text-foreground-muted", title: modelFor(provider.id), children: modelFor(provider.id) })
273
575
  ]
274
576
  },
275
577
  provider.id
@@ -277,24 +579,37 @@ function ConnectAi({ aiKey }) {
277
579
  })
278
580
  }
279
581
  ),
280
- refused && /* @__PURE__ */ jsxs("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-2 eb:rounded-md eb:border eb:border-destructive/30 eb:bg-destructive/10 eb:px-3 eb:py-2 eb:text-[12.5px] eb:text-destructive", children: [
281
- /* @__PURE__ */ jsx(TriangleAlert, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
582
+ refused && /* @__PURE__ */ jsxs2("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-2 eb:rounded-md eb:border eb:border-destructive/30 eb:bg-destructive/10 eb:px-3 eb:py-2 eb:text-[12.5px] eb:text-destructive", children: [
583
+ /* @__PURE__ */ jsx2(TriangleAlert, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
282
584
  t("{provider} refused the last key. Paste another one.", { provider: name })
283
585
  ] }),
284
- (keyUrl || free) && /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-wrap eb:items-center eb:gap-x-3 eb:gap-y-2", children: [
285
- keyUrl && /* @__PURE__ */ jsx(Button, { asChild: true, variant: "outline", size: "sm-compact", children: /* @__PURE__ */ jsxs("a", { href: keyUrl, target: "_blank", rel: "noopener noreferrer", "data-testid": "eb-ai-connect-get-key", children: [
286
- t("Get a key"),
287
- /* @__PURE__ */ jsx(ArrowUpRight, { "aria-hidden": "true" })
288
- ] }) }),
289
- free && /* @__PURE__ */ jsxs("span", { className: "eb:inline-flex eb:items-center eb:gap-1.5 eb:rounded-full eb:bg-success/10 eb:px-2.5 eb:py-1 eb:text-[11.5px] eb:font-medium eb:text-success", children: [
290
- /* @__PURE__ */ jsx(Gift, { className: "eb:size-3.5", "aria-hidden": "true" }),
586
+ (keyUrl || free) && /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-wrap eb:items-center eb:gap-x-3 eb:gap-y-2", children: [
587
+ keyUrl && /* @__PURE__ */ jsx2(Button, { asChild: true, variant: "outline", size: "sm-compact", children: /* @__PURE__ */ jsxs2(
588
+ "a",
589
+ {
590
+ href: keyUrl,
591
+ target: "_blank",
592
+ rel: "noopener noreferrer",
593
+ onClick: (event) => {
594
+ event.preventDefault();
595
+ window.open(keyUrl, "_blank", "noopener,noreferrer");
596
+ },
597
+ "data-testid": "eb-ai-connect-get-key",
598
+ children: [
599
+ t("Get a key"),
600
+ /* @__PURE__ */ jsx2(ArrowUpRight, { "aria-hidden": "true" })
601
+ ]
602
+ }
603
+ ) }),
604
+ free && /* @__PURE__ */ jsxs2("span", { className: "eb:inline-flex eb:items-center eb:gap-1.5 eb:rounded-full eb:bg-success/10 eb:px-2.5 eb:py-1 eb:text-[11.5px] eb:font-medium eb:text-success", children: [
605
+ /* @__PURE__ */ jsx2(Gift2, { className: "eb:size-3.5", "aria-hidden": "true" }),
291
606
  t(free)
292
607
  ] })
293
608
  ] }),
294
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
295
- /* @__PURE__ */ jsx(Label, { htmlFor: inputId, children: t("API key") }),
296
- /* @__PURE__ */ jsxs("div", { className: "eb:relative", children: [
297
- /* @__PURE__ */ jsx(
609
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-1.5", children: [
610
+ /* @__PURE__ */ jsx2(Label, { htmlFor: inputId, children: t("API key") }),
611
+ /* @__PURE__ */ jsxs2("div", { className: "eb:relative", children: [
612
+ /* @__PURE__ */ jsx2(
298
613
  Input,
299
614
  {
300
615
  ref: inputRef,
@@ -312,7 +627,7 @@ function ConnectAi({ aiKey }) {
312
627
  "data-testid": "eb-ai-connect-key"
313
628
  }
314
629
  ),
315
- /* @__PURE__ */ jsx(
630
+ /* @__PURE__ */ jsx2(
316
631
  "button",
317
632
  {
318
633
  type: "button",
@@ -320,20 +635,33 @@ function ConnectAi({ aiKey }) {
320
635
  onClick: () => setShown((current) => !current),
321
636
  "aria-label": shown ? t("Hide the key") : t("Show the key"),
322
637
  "aria-pressed": shown,
323
- children: shown ? /* @__PURE__ */ jsx(EyeOff, { className: "eb:size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx(Eye, { className: "eb:size-4", "aria-hidden": "true" })
638
+ children: shown ? /* @__PURE__ */ jsx2(EyeOff, { className: "eb:size-4", "aria-hidden": "true" }) : /* @__PURE__ */ jsx2(Eye, { className: "eb:size-4", "aria-hidden": "true" })
324
639
  }
325
640
  )
326
641
  ] }),
327
- /* @__PURE__ */ jsx("p", { id: hintId, className: "eb:min-h-4 eb:text-[11.5px] eb:text-foreground-muted", children: unusual ? /* @__PURE__ */ jsx("span", { className: "eb:text-warning", children: t("{provider} keys usually start with {prefix}.", { provider: name, prefix: keyHint }) }) : saved && !key ? t("A key for {provider} is saved in this browser: Connect uses it, or paste another one.", { provider: name }) : keyHint ? t("{provider} keys start with {prefix}.", { provider: name, prefix: keyHint }) : null })
328
- ] })
642
+ /* @__PURE__ */ jsx2("p", { id: hintId, className: "eb:min-h-4 eb:text-[11.5px] eb:text-foreground-muted", children: unusual ? /* @__PURE__ */ jsx2("span", { className: "eb:text-warning", children: t("{provider} keys usually start with {prefix}.", { provider: name, prefix: keyHint }) }) : saved && !key ? t("A key for {provider} is saved in this browser: Connect uses it, or paste another one.", { provider: name }) : keyHint ? t("{provider} keys start with {prefix}.", { provider: name, prefix: keyHint }) : null })
643
+ ] }),
644
+ /* @__PURE__ */ jsx2(
645
+ ModelPicker,
646
+ {
647
+ aiKey,
648
+ provider: chosen,
649
+ apiKey: key.length >= LISTABLE_KEY ? key : saved,
650
+ delay: key.length >= LISTABLE_KEY ? 350 : 0,
651
+ value: modelFor(chosen.id),
652
+ onChange: (model) => setPicks((current) => ({ ...current, [chosen.id]: model })),
653
+ open: modelsOpen,
654
+ onOpenChange: setModelsOpen
655
+ }
656
+ )
329
657
  ] }),
330
- /* @__PURE__ */ jsxs("div", { className: "eb:flex eb:items-start eb:gap-2 eb:border-t eb:border-border eb:bg-surface-75 eb:px-6 eb:py-3 eb:text-[11.5px] eb:leading-relaxed eb:text-foreground-lighter", children: [
331
- /* @__PURE__ */ jsx(ShieldCheck, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted", "aria-hidden": "true" }),
332
- /* @__PURE__ */ jsx("span", { children: t("The key stays in this browser and is sent only to {provider}.", { provider: name }) })
658
+ /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-start eb:gap-2 eb:border-t eb:border-border eb:bg-surface-75 eb:px-6 eb:py-3 eb:text-[11.5px] eb:leading-relaxed eb:text-foreground-lighter", children: [
659
+ /* @__PURE__ */ jsx2(ShieldCheck, { className: "eb:mt-0.5 eb:size-3.5 eb:shrink-0 eb:text-foreground-muted", "aria-hidden": "true" }),
660
+ /* @__PURE__ */ jsx2("span", { children: t("The key stays in this browser and is sent only to {provider}.", { provider: name }) })
333
661
  ] }),
334
- /* @__PURE__ */ jsxs(DialogFooter, { className: "eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
335
- /* @__PURE__ */ jsx(Button, { type: "button", variant: "ghost", onClick: () => aiKey.answer(null), children: t("Cancel") }),
336
- /* @__PURE__ */ jsx(Button, { type: "submit", disabled: !key && !saved, "data-testid": "eb-ai-connect-submit", children: t("Connect") })
662
+ /* @__PURE__ */ jsxs2(DialogFooter, { className: "eb:border-t eb:border-border eb:px-6 eb:py-4", children: [
663
+ /* @__PURE__ */ jsx2(Button, { type: "button", variant: "ghost", onClick: () => aiKey.answer(null), children: t("Cancel") }),
664
+ /* @__PURE__ */ jsx2(Button, { type: "submit", disabled: !key && !saved, "data-testid": "eb-ai-connect-submit", children: t("Connect") })
337
665
  ] })
338
666
  ] })
339
667
  }
@@ -341,8 +669,8 @@ function ConnectAi({ aiKey }) {
341
669
  }
342
670
 
343
671
  // src/ai/GenerateButton.jsx
344
- import { useEffect as useEffect2, useMemo, useRef as useRef2, useState as useState2 } from "react";
345
- import { Check, Loader2, Search, Sparkles, TriangleAlert as TriangleAlert2 } from "lucide-react";
672
+ import { useEffect as useEffect3, useMemo as useMemo2, useRef as useRef3, useState as useState3 } from "react";
673
+ import { Check as Check2, Loader2, Search as Search2, Sparkles, TriangleAlert as TriangleAlert2 } from "lucide-react";
346
674
 
347
675
  // src/ai/chip.js
348
676
  var chipClassName = (selected = false) => cn(
@@ -351,39 +679,39 @@ var chipClassName = (selected = false) => cn(
351
679
  );
352
680
 
353
681
  // src/ai/GenerateButton.jsx
354
- import { Fragment, jsx as jsx2, jsxs as jsxs2 } from "react/jsx-runtime";
682
+ import { Fragment as Fragment2, jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
355
683
  var fold = (text) => text.normalize("NFD").replace(/[̀-ͯ]/g, "").toLowerCase();
356
684
  function GenerateButton({ context, generate: generateEmail }) {
357
685
  const { editor, ready, isDirty, markDirty, template, setMeta, variables, sender, brand } = context;
358
686
  const { t, locale, notify } = useUi();
359
- const [open, setOpen] = useState2(false);
360
- const [purpose, setPurpose] = useState2(
687
+ const [open, setOpen] = useState3(false);
688
+ const [purpose, setPurpose] = useState3(
361
689
  /** @type {string|null} */
362
690
  null
363
691
  );
364
- const [details, setDetails] = useState2("");
365
- const [query, setQuery] = useState2("");
366
- const [busy, setBusy] = useState2(false);
367
- const [error, setError] = useState2("");
368
- const [seconds, setSeconds] = useState2(0);
369
- const [dirty, setDirty] = useState2(false);
370
- const abortRef = useRef2(
692
+ const [details, setDetails] = useState3("");
693
+ const [query, setQuery] = useState3("");
694
+ const [busy, setBusy] = useState3(false);
695
+ const [error, setError] = useState3("");
696
+ const [seconds, setSeconds] = useState3(0);
697
+ const [dirty, setDirty] = useState3(false);
698
+ const abortRef = useRef3(
371
699
  /** @type {AbortController|null} */
372
700
  null
373
701
  );
374
- const searchRef = useRef2(
702
+ const searchRef = useRef3(
375
703
  /** @type {HTMLInputElement|null} */
376
704
  null
377
705
  );
378
706
  useSearchShortcut(searchRef);
379
- useEffect2(() => () => abortRef.current?.abort(), []);
380
- useEffect2(() => {
707
+ useEffect3(() => () => abortRef.current?.abort(), []);
708
+ useEffect3(() => {
381
709
  if (!busy) return void 0;
382
710
  setSeconds(0);
383
711
  const timer = setInterval(() => setSeconds((s) => s + 1), 1e3);
384
712
  return () => clearInterval(timer);
385
713
  }, [busy]);
386
- const groups = useMemo(() => {
714
+ const groups = useMemo2(() => {
387
715
  const words = fold(query.trim()).split(/\s+/).filter(Boolean);
388
716
  return purposesByCategory().map((group) => ({
389
717
  ...group,
@@ -444,12 +772,12 @@ function GenerateButton({ context, generate: generateEmail }) {
444
772
  setBusy(false);
445
773
  };
446
774
  const canGenerate = Boolean(purpose || details.trim());
447
- return /* @__PURE__ */ jsxs2(Fragment, { children: [
448
- /* @__PURE__ */ jsxs2(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: openDialog, children: [
449
- /* @__PURE__ */ jsx2(Sparkles, {}),
775
+ return /* @__PURE__ */ jsxs3(Fragment2, { children: [
776
+ /* @__PURE__ */ jsxs3(Button, { variant: "outline", size: "sm-compact", disabled: !ready, onClick: openDialog, children: [
777
+ /* @__PURE__ */ jsx3(Sparkles, {}),
450
778
  t("Generate")
451
779
  ] }),
452
- /* @__PURE__ */ jsx2(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs2(
780
+ /* @__PURE__ */ jsx3(Dialog, { open, onOpenChange: close, children: /* @__PURE__ */ jsxs3(
453
781
  DialogContent,
454
782
  {
455
783
  className: "eb:flex eb:max-h-[calc(100dvh-2rem)] eb:flex-col eb:gap-0 eb:p-0 eb:sm:max-w-2xl",
@@ -459,13 +787,13 @@ function GenerateButton({ context, generate: generateEmail }) {
459
787
  searchRef.current?.focus();
460
788
  },
461
789
  children: [
462
- /* @__PURE__ */ jsxs2(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3", children: [
463
- /* @__PURE__ */ jsx2(DialogTitle, { children: t("Generate an email") }),
464
- /* @__PURE__ */ jsx2(DialogDescription, { children: t("Pick what it is for. AI writes a complete email that opens here, ready to edit.") })
790
+ /* @__PURE__ */ jsxs3(DialogHeader, { className: "eb:gap-1.5 eb:px-6 eb:pt-6 eb:pb-3", children: [
791
+ /* @__PURE__ */ jsx3(DialogTitle, { children: t("Generate an email") }),
792
+ /* @__PURE__ */ jsx3(DialogDescription, { children: t("Pick what it is for. AI writes a complete email that opens here, ready to edit.") })
465
793
  ] }),
466
- /* @__PURE__ */ jsx2("div", { className: "eb:px-6 eb:pb-3", children: /* @__PURE__ */ jsxs2("div", { className: "eb:relative", children: [
467
- /* @__PURE__ */ jsx2(Search, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2.5 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted", "aria-hidden": "true" }),
468
- /* @__PURE__ */ jsx2(
794
+ /* @__PURE__ */ jsx3("div", { className: "eb:px-6 eb:pb-3", children: /* @__PURE__ */ jsxs3("div", { className: "eb:relative", children: [
795
+ /* @__PURE__ */ jsx3(Search2, { className: "eb:pointer-events-none eb:absolute eb:top-1/2 eb:left-2.5 eb:size-3.5 eb:-translate-y-1/2 eb:text-foreground-muted", "aria-hidden": "true" }),
796
+ /* @__PURE__ */ jsx3(
469
797
  Input,
470
798
  {
471
799
  ref: searchRef,
@@ -479,16 +807,16 @@ function GenerateButton({ context, generate: generateEmail }) {
479
807
  disabled: busy
480
808
  }
481
809
  ),
482
- /* @__PURE__ */ jsx2(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
810
+ /* @__PURE__ */ jsx3(SearchKeyHint, { className: "eb:absolute eb:top-1/2 eb:right-2 eb:-translate-y-1/2" })
483
811
  ] }) }),
484
- /* @__PURE__ */ jsx2("div", { className: "eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:border-y eb:border-border eb:px-6 eb:py-4", "data-testid": "eb-generate-purposes", children: /* @__PURE__ */ jsxs2("fieldset", { disabled: busy, className: "eb:m-0 eb:min-w-0 eb:border-0 eb:p-0", children: [
485
- /* @__PURE__ */ jsx2("legend", { className: "eb:sr-only", children: t("What is the email for?") }),
486
- groups.length === 0 && /* @__PURE__ */ jsx2("p", { className: "eb:py-6 eb:text-center eb:text-[13px] eb:text-foreground-muted", children: t("Nothing matches. Describe your email in the details below.") }),
487
- /* @__PURE__ */ jsx2("div", { className: "eb:flex eb:flex-col eb:gap-4", children: groups.map((group) => /* @__PURE__ */ jsxs2("section", { className: "eb:flex eb:flex-col eb:gap-2", "aria-label": t(group.label), children: [
488
- /* @__PURE__ */ jsx2(SectionLabel, { children: t(group.label) }),
489
- /* @__PURE__ */ jsx2("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: group.purposes.map((entry) => {
812
+ /* @__PURE__ */ jsx3("div", { className: "eb:min-h-0 eb:flex-1 eb:overflow-y-auto eb:border-y eb:border-border eb:px-6 eb:py-4", "data-testid": "eb-generate-purposes", children: /* @__PURE__ */ jsxs3("fieldset", { disabled: busy, className: "eb:m-0 eb:min-w-0 eb:border-0 eb:p-0", children: [
813
+ /* @__PURE__ */ jsx3("legend", { className: "eb:sr-only", children: t("What is the email for?") }),
814
+ groups.length === 0 && /* @__PURE__ */ jsx3("p", { className: "eb:py-6 eb:text-center eb:text-[13px] eb:text-foreground-muted", children: t("Nothing matches. Describe your email in the details below.") }),
815
+ /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:flex-col eb:gap-4", children: groups.map((group) => /* @__PURE__ */ jsxs3("section", { className: "eb:flex eb:flex-col eb:gap-2", "aria-label": t(group.label), children: [
816
+ /* @__PURE__ */ jsx3(SectionLabel, { children: t(group.label) }),
817
+ /* @__PURE__ */ jsx3("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: group.purposes.map((entry) => {
490
818
  const selected = purpose === entry.id;
491
- return /* @__PURE__ */ jsxs2(
819
+ return /* @__PURE__ */ jsxs3(
492
820
  "button",
493
821
  {
494
822
  type: "button",
@@ -497,7 +825,7 @@ function GenerateButton({ context, generate: generateEmail }) {
497
825
  onClick: () => setPurpose(selected ? null : entry.id),
498
826
  className: chipClassName(selected),
499
827
  children: [
500
- selected && /* @__PURE__ */ jsx2(Check, { className: "eb:size-3", "aria-hidden": "true" }),
828
+ selected && /* @__PURE__ */ jsx3(Check2, { className: "eb:size-3", "aria-hidden": "true" }),
501
829
  t(entry.label)
502
830
  ]
503
831
  },
@@ -506,8 +834,8 @@ function GenerateButton({ context, generate: generateEmail }) {
506
834
  }) })
507
835
  ] }, group.id)) })
508
836
  ] }) }),
509
- /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6", children: [
510
- /* @__PURE__ */ jsx2(
837
+ /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-col eb:gap-3 eb:px-6 eb:pt-3 eb:pb-6", children: [
838
+ /* @__PURE__ */ jsx3(
511
839
  Textarea,
512
840
  {
513
841
  value: details,
@@ -522,15 +850,15 @@ function GenerateButton({ context, generate: generateEmail }) {
522
850
  }
523
851
  }
524
852
  ),
525
- error ? /* @__PURE__ */ jsxs2("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
526
- /* @__PURE__ */ jsx2(TriangleAlert2, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
853
+ error ? /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
854
+ /* @__PURE__ */ jsx3(TriangleAlert2, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
527
855
  error
528
- ] }) : dirty && !busy && /* @__PURE__ */ jsx2("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("The email being edited will be replaced, unsaved changes included.") }),
529
- /* @__PURE__ */ jsxs2("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2", children: [
530
- busy && /* @__PURE__ */ jsx2("p", { className: "eb:mr-auto eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: t("Writing your email\u2026 {seconds} s", { seconds }) }),
531
- busy && /* @__PURE__ */ jsx2(Button, { variant: "outline", size: "sm", onClick: cancel, children: t("Cancel") }),
532
- /* @__PURE__ */ jsxs2(Button, { size: "sm", onClick: generate, disabled: !canGenerate || busy, "data-testid": "eb-generate-submit", children: [
533
- busy ? /* @__PURE__ */ jsx2(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx2(Sparkles, {}),
856
+ ] }) : dirty && !busy && /* @__PURE__ */ jsx3("p", { className: "eb:text-[12px] eb:text-foreground-muted", children: t("The email being edited will be replaced, unsaved changes included.") }),
857
+ /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:items-center eb:justify-end eb:gap-2", children: [
858
+ busy && /* @__PURE__ */ jsx3("p", { className: "eb:mr-auto eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: t("Writing your email\u2026 {seconds} s", { seconds }) }),
859
+ busy && /* @__PURE__ */ jsx3(Button, { variant: "outline", size: "sm", onClick: cancel, children: t("Cancel") }),
860
+ /* @__PURE__ */ jsxs3(Button, { size: "sm", onClick: generate, disabled: !canGenerate || busy, "data-testid": "eb-generate-submit", children: [
861
+ busy ? /* @__PURE__ */ jsx3(Loader2, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles, {}),
534
862
  t("Generate")
535
863
  ] })
536
864
  ] })
@@ -542,9 +870,9 @@ function GenerateButton({ context, generate: generateEmail }) {
542
870
  }
543
871
 
544
872
  // src/ai/RewriteText.jsx
545
- import { useEffect as useEffect3, useRef as useRef3, useState as useState3 } from "react";
546
- import { ChevronDown, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
547
- import { jsx as jsx3, jsxs as jsxs3 } from "react/jsx-runtime";
873
+ import { useEffect as useEffect4, useRef as useRef4, useState as useState4 } from "react";
874
+ import { ChevronDown as ChevronDown2, Languages, Loader2 as Loader22, Sparkles as Sparkles2, TriangleAlert as TriangleAlert3 } from "lucide-react";
875
+ import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
548
876
  function nativeName(code) {
549
877
  try {
550
878
  const name = new Intl.DisplayNames([code], { type: "language" }).of(code) || code;
@@ -555,18 +883,18 @@ function nativeName(code) {
555
883
  }
556
884
  function RewriteText({ editor, selection, rewrite }) {
557
885
  const { t, notify } = useUi();
558
- const [running, setRunning] = useState3(
886
+ const [running, setRunning] = useState4(
559
887
  /** @type {string|null} */
560
888
  null
561
889
  );
562
- const [error, setError] = useState3("");
563
- const [instruction, setInstruction] = useState3("");
564
- const abortRef = useRef3(
890
+ const [error, setError] = useState4("");
891
+ const [instruction, setInstruction] = useState4("");
892
+ const abortRef = useRef4(
565
893
  /** @type {AbortController|null} */
566
894
  null
567
895
  );
568
896
  const label = selection.tagName === "mj-button";
569
- useEffect3(() => () => abortRef.current?.abort(), []);
897
+ useEffect4(() => () => abortRef.current?.abort(), []);
570
898
  const currentText = () => {
571
899
  const element = editor.getElement(selection.id);
572
900
  return (label ? element?.content : element?.html) ?? "";
@@ -600,27 +928,27 @@ function RewriteText({ editor, selection, rewrite }) {
600
928
  setRunning(null);
601
929
  };
602
930
  const busy = Boolean(running);
603
- const spinner = /* @__PURE__ */ jsx3(Loader22, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
604
- return /* @__PURE__ */ jsxs3("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
605
- /* @__PURE__ */ jsxs3(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
606
- /* @__PURE__ */ jsx3(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
931
+ const spinner = /* @__PURE__ */ jsx4(Loader22, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
932
+ return /* @__PURE__ */ jsxs4("section", { className: "eb:space-y-2.5", "aria-label": t("Rewrite with AI"), "data-testid": "eb-rewrite", children: [
933
+ /* @__PURE__ */ jsxs4(SectionLabel, { className: "eb:flex eb:items-center eb:gap-1.5", children: [
934
+ /* @__PURE__ */ jsx4(Sparkles2, { className: "eb:size-3", "aria-hidden": "true" }),
607
935
  t("Rewrite with AI")
608
936
  ] }),
609
- /* @__PURE__ */ jsxs3("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
610
- REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs3("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
937
+ /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:flex-wrap eb:gap-1.5", children: [
938
+ REWRITE_ACTIONS.map((action) => /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: busy, onClick: () => run(action.id), children: [
611
939
  running === action.id && spinner,
612
940
  t(action.label)
613
941
  ] }, action.id)),
614
- /* @__PURE__ */ jsxs3(DropdownMenu, { children: [
615
- /* @__PURE__ */ jsx3(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs3("button", { type: "button", className: chipClassName(), children: [
616
- running === "translate" ? spinner : /* @__PURE__ */ jsx3(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
942
+ /* @__PURE__ */ jsxs4(DropdownMenu, { children: [
943
+ /* @__PURE__ */ jsx4(DropdownMenuTrigger, { asChild: true, disabled: busy, children: /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), children: [
944
+ running === "translate" ? spinner : /* @__PURE__ */ jsx4(Languages, { className: "eb:size-3", "aria-hidden": "true" }),
617
945
  t("Translate"),
618
- /* @__PURE__ */ jsx3(ChevronDown, { className: "eb:size-3", "aria-hidden": "true" })
946
+ /* @__PURE__ */ jsx4(ChevronDown2, { className: "eb:size-3", "aria-hidden": "true" })
619
947
  ] }) }),
620
- /* @__PURE__ */ jsx3(DropdownMenuContent, { align: "start", "aria-label": t("Translate into"), children: REWRITE_LANGUAGES.map((code) => /* @__PURE__ */ jsx3(DropdownMenuItem, { lang: code, onSelect: () => run("translate", { targetLanguage: code }), children: nativeName(code) }, code)) })
948
+ /* @__PURE__ */ jsx4(DropdownMenuContent, { align: "start", "aria-label": t("Translate into"), children: REWRITE_LANGUAGES.map((code) => /* @__PURE__ */ jsx4(DropdownMenuItem, { lang: code, onSelect: () => run("translate", { targetLanguage: code }), children: nativeName(code) }, code)) })
621
949
  ] })
622
950
  ] }),
623
- /* @__PURE__ */ jsxs3(
951
+ /* @__PURE__ */ jsxs4(
624
952
  "form",
625
953
  {
626
954
  className: "eb:flex eb:gap-1.5",
@@ -629,7 +957,7 @@ function RewriteText({ editor, selection, rewrite }) {
629
957
  if (instruction.trim()) run("custom", { instruction: instruction.trim() });
630
958
  },
631
959
  children: [
632
- /* @__PURE__ */ jsx3(
960
+ /* @__PURE__ */ jsx4(
633
961
  Input,
634
962
  {
635
963
  value: instruction,
@@ -640,55 +968,55 @@ function RewriteText({ editor, selection, rewrite }) {
640
968
  disabled: busy
641
969
  }
642
970
  ),
643
- /* @__PURE__ */ jsx3(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Rewrite"), disabled: busy || !instruction.trim(), children: running === "custom" ? /* @__PURE__ */ jsx3(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx3(Sparkles2, {}) })
971
+ /* @__PURE__ */ jsx4(Button, { type: "submit", variant: "outline", size: "icon-sm", "aria-label": t("Rewrite"), disabled: busy || !instruction.trim(), children: running === "custom" ? /* @__PURE__ */ jsx4(Loader22, { className: "eb:animate-spin" }) : /* @__PURE__ */ jsx4(Sparkles2, {}) })
644
972
  ]
645
973
  }
646
974
  ),
647
- running && /* @__PURE__ */ jsxs3("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
975
+ running && /* @__PURE__ */ jsxs4("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
648
976
  t("Rewriting\u2026"),
649
- /* @__PURE__ */ jsx3(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
977
+ /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
650
978
  ] }),
651
- error && /* @__PURE__ */ jsxs3("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
652
- /* @__PURE__ */ jsx3(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
979
+ error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
980
+ /* @__PURE__ */ jsx4(TriangleAlert3, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
653
981
  error
654
982
  ] })
655
983
  ] });
656
984
  }
657
985
 
658
986
  // src/ai/SubjectIdeas.jsx
659
- import { useEffect as useEffect4, useLayoutEffect, useRef as useRef4, useState as useState4 } from "react";
660
- import { Check as Check2, Loader2 as Loader23, Sparkles as Sparkles3, TriangleAlert as TriangleAlert4 } from "lucide-react";
661
- import { jsx as jsx4, jsxs as jsxs4 } from "react/jsx-runtime";
987
+ import { useEffect as useEffect5, useLayoutEffect, useRef as useRef5, useState as useState5 } from "react";
988
+ import { Check as Check3, Loader2 as Loader23, Sparkles as Sparkles3, TriangleAlert as TriangleAlert4 } from "lucide-react";
989
+ import { jsx as jsx5, jsxs as jsxs5 } from "react/jsx-runtime";
662
990
  var MAX_AVOID = 12;
663
991
  function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
664
992
  const { t, locale } = useUi();
665
- const [running, setRunning] = useState4(false);
666
- const [error, setError] = useState4("");
667
- const [ideas, setIdeas] = useState4(
993
+ const [running, setRunning] = useState5(false);
994
+ const [error, setError] = useState5("");
995
+ const [ideas, setIdeas] = useState5(
668
996
  /** @type {string[]|null} */
669
997
  null
670
998
  );
671
- const [original, setOriginal] = useState4("");
672
- const shownRef = useRef4(
999
+ const [original, setOriginal] = useState5("");
1000
+ const shownRef = useRef5(
673
1001
  /** @type {string[]} */
674
1002
  []
675
1003
  );
676
- const abortRef = useRef4(
1004
+ const abortRef = useRef5(
677
1005
  /** @type {AbortController|null} */
678
1006
  null
679
1007
  );
680
- const subjectRef = useRef4(meta.subject);
1008
+ const subjectRef = useRef5(meta.subject);
681
1009
  subjectRef.current = meta.subject;
682
- const rootRef = useRef4(
1010
+ const rootRef = useRef5(
683
1011
  /** @type {HTMLDivElement|null} */
684
1012
  null
685
1013
  );
686
- const focusRef = useRef4(
1014
+ const focusRef = useRef5(
687
1015
  /** @type {'list'|'button'|null} */
688
1016
  null
689
1017
  );
690
- useEffect4(() => () => abortRef.current?.abort(), []);
691
- useEffect4(
1018
+ useEffect5(() => () => abortRef.current?.abort(), []);
1019
+ useEffect5(
692
1020
  () => editor.on("load", () => {
693
1021
  abortRef.current?.abort();
694
1022
  abortRef.current = null;
@@ -701,7 +1029,7 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
701
1029
  [editor]
702
1030
  );
703
1031
  const typed = meta.subject.trim();
704
- useEffect4(() => {
1032
+ useEffect5(() => {
705
1033
  if (ideas && typed && !ideas.includes(meta.subject) && typed !== original) setOriginal(typed);
706
1034
  }, [ideas, meta.subject, typed, original]);
707
1035
  useLayoutEffect(() => {
@@ -773,16 +1101,16 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
773
1101
  onSubject(options[to].text);
774
1102
  rootRef.current?.querySelectorAll('[role="radio"]')[to]?.focus();
775
1103
  };
776
- const spinner = /* @__PURE__ */ jsx4(Loader23, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
777
- return /* @__PURE__ */ jsxs4("div", { className: "eb:space-y-2", "data-testid": "eb-subject-ideas", ref: rootRef, children: [
778
- !ideas && /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
779
- running ? spinner : /* @__PURE__ */ jsx4(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1104
+ const spinner = /* @__PURE__ */ jsx5(Loader23, { className: "eb:size-3 eb:animate-spin", "aria-hidden": "true" });
1105
+ return /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2", "data-testid": "eb-subject-ideas", ref: rootRef, children: [
1106
+ !ideas && /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1107
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
780
1108
  t("Suggest subjects")
781
1109
  ] }),
782
- ideas && /* @__PURE__ */ jsxs4("div", { className: "eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5", children: [
783
- /* @__PURE__ */ jsx4("div", { role: "radiogroup", "aria-label": t("Subject ideas"), className: "eb:space-y-0.5", onKeyDown, children: options.map((option, index) => {
1110
+ ideas && /* @__PURE__ */ jsxs5("div", { className: "eb:space-y-2 eb:rounded-md eb:border eb:border-border eb:p-1.5", children: [
1111
+ /* @__PURE__ */ jsx5("div", { role: "radiogroup", "aria-label": t("Subject ideas"), className: "eb:space-y-0.5", onKeyDown, children: options.map((option, index) => {
784
1112
  const chosen = index === checked;
785
- return /* @__PURE__ */ jsxs4(
1113
+ return /* @__PURE__ */ jsxs5(
786
1114
  "button",
787
1115
  {
788
1116
  type: "button",
@@ -795,28 +1123,28 @@ function SubjectIdeas({ editor, meta, onSubject, suggest, variables = null }) {
795
1123
  ),
796
1124
  onClick: () => onSubject(option.text),
797
1125
  children: [
798
- /* @__PURE__ */ jsx4(Check2, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0", chosen ? "eb:text-primary" : "eb:invisible"), "aria-hidden": "true" }),
799
- /* @__PURE__ */ jsx4("span", { className: "eb:min-w-0 eb:flex-1 eb:break-words", children: option.text }),
800
- option.yours && /* @__PURE__ */ jsx4("span", { className: "eb:shrink-0 eb:text-[11px] eb:text-foreground-muted", children: t("yours") })
1126
+ /* @__PURE__ */ jsx5(Check3, { className: cn("eb:mt-0.5 eb:size-3.5 eb:shrink-0", chosen ? "eb:text-primary" : "eb:invisible"), "aria-hidden": "true" }),
1127
+ /* @__PURE__ */ jsx5("span", { className: "eb:min-w-0 eb:flex-1 eb:break-words", children: option.text }),
1128
+ option.yours && /* @__PURE__ */ jsx5("span", { className: "eb:shrink-0 eb:text-[11px] eb:text-foreground-muted", children: t("yours") })
801
1129
  ]
802
1130
  },
803
1131
  `${option.yours ? "yours" : "idea"}:${option.text}`
804
1132
  );
805
1133
  }) }),
806
- /* @__PURE__ */ jsxs4("div", { className: "eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5", children: [
807
- /* @__PURE__ */ jsxs4("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
808
- running ? spinner : /* @__PURE__ */ jsx4(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
1134
+ /* @__PURE__ */ jsxs5("div", { className: "eb:flex eb:items-center eb:gap-1.5 eb:px-1 eb:pb-0.5", children: [
1135
+ /* @__PURE__ */ jsxs5("button", { type: "button", className: chipClassName(), disabled: running || !ready, onClick: run, "data-subject-ideas-button": true, children: [
1136
+ running ? spinner : /* @__PURE__ */ jsx5(Sparkles3, { className: "eb:size-3", "aria-hidden": "true" }),
809
1137
  t("More ideas")
810
1138
  ] }),
811
- /* @__PURE__ */ jsx4(Button, { type: "button", variant: "ghost", size: "xs", className: "eb:ml-auto", disabled: running, onClick: done, children: t("Done") })
1139
+ /* @__PURE__ */ jsx5(Button, { type: "button", variant: "ghost", size: "xs", className: "eb:ml-auto", disabled: running, onClick: done, children: t("Done") })
812
1140
  ] })
813
1141
  ] }),
814
- running && /* @__PURE__ */ jsxs4("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
1142
+ running && /* @__PURE__ */ jsxs5("p", { className: "eb:flex eb:items-center eb:gap-2 eb:text-[12px] eb:text-foreground-muted", role: "status", "aria-live": "polite", children: [
815
1143
  t("Reading the email\u2026"),
816
- /* @__PURE__ */ jsx4(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
1144
+ /* @__PURE__ */ jsx5(Button, { type: "button", variant: "link", size: "xs", className: "eb:h-auto eb:px-0", onClick: cancel, children: t("Cancel") })
817
1145
  ] }),
818
- error && /* @__PURE__ */ jsxs4("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
819
- /* @__PURE__ */ jsx4(TriangleAlert4, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
1146
+ error && /* @__PURE__ */ jsxs5("p", { role: "alert", className: "eb:flex eb:items-start eb:gap-1.5 eb:text-[12px] eb:text-destructive", children: [
1147
+ /* @__PURE__ */ jsx5(TriangleAlert4, { className: "eb:mt-px eb:size-3.5 eb:shrink-0", "aria-hidden": "true" }),
820
1148
  error
821
1149
  ] })
822
1150
  ] });
@@ -865,24 +1193,27 @@ function createAi(options = {}) {
865
1193
  }
866
1194
  const ownOnly = !model && !http && PARTS.some((part) => typeof options[part] === "function");
867
1195
  let transport = {};
868
- const key = provider && askKey ? createKeyStore(providers) : null;
1196
+ const key = provider && askKey ? createKeyStore(providers, { fetch: fetchImpl }) : null;
869
1197
  if (provider) {
870
1198
  const models = /* @__PURE__ */ new Map();
871
1199
  const keyFor = () => key ? key.get() : apiKey || "";
872
1200
  const modelOf = (id) => {
873
- if (!models.has(id)) {
874
- const chosen = (
875
- /** @type {import('./providers.js').AiProvider} */
876
- providers.find((item) => item.id === id)
877
- );
1201
+ const chosen = (
1202
+ /** @type {import('./providers.js').AiProvider} */
1203
+ providers.find((item) => item.id === id)
1204
+ );
1205
+ const name = key ? key.model(id) : chosen.model;
1206
+ const slot = `${id}
1207
+ ${name}`;
1208
+ if (!models.has(slot)) {
878
1209
  models.set(
879
- id,
880
- chosen.createModel ? chosen.createModel({ apiKey: keyFor, fetch: fetchImpl }) : providerEmailModel({ provider: chosen, fetch: fetchImpl, apiKey: keyFor })
1210
+ slot,
1211
+ chosen.createModel ? chosen.createModel({ apiKey: keyFor, fetch: fetchImpl, ...name ? { model: name } : {} }) : providerEmailModel({ provider: chosen, fetch: fetchImpl, apiKey: keyFor, ...name ? { model: name } : {} })
881
1212
  );
882
1213
  }
883
1214
  return (
884
1215
  /** @type {import('./generator.js').EmailModel} */
885
- models.get(id)
1216
+ models.get(slot)
886
1217
  );
887
1218
  };
888
1219
  const model2 = async (request) => {
@@ -959,6 +1290,8 @@ function askedAgain(run, key) {
959
1290
  };
960
1291
  }
961
1292
  export {
1293
+ AGENT_ERROR_CODES,
1294
+ AGENT_PROTOCOL,
962
1295
  AI_MESSAGES,
963
1296
  AI_PROVIDERS,
964
1297
  DEFAULT_CLAUDE_MODEL,
@@ -968,12 +1301,14 @@ export {
968
1301
  PURPOSE_CATEGORIES,
969
1302
  REWRITE_ACTIONS,
970
1303
  REWRITE_LANGUAGES,
1304
+ agentEmailModel,
971
1305
  claudeEmailModel,
972
1306
  claudeProvider,
973
1307
  createAi,
974
1308
  createEmailGenerator,
975
1309
  emailWords,
976
1310
  failureCode,
1311
+ listProviderModels,
977
1312
  providerEmailModel,
978
1313
  resolveProvider
979
1314
  };