@aparte/plugin-model-selector 0.19.3 → 0.21.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.
@@ -12,8 +12,11 @@ import { type AparteConfig, type AparteConfigAware } from '@aparte/core';
12
12
  *
13
13
  * @element aparte-model-selector
14
14
  * @attr {boolean} auto-select - Selects the first model as soon as one is available.
15
- * @attr {boolean} persist - Writes the selection back through the config, so it survives a reload.
15
+ * @attr {boolean} persist - Writes the selection into the config, which is what the next message is sent with.
16
+ * Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not
17
+ * outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`.
16
18
  * @attr {boolean} searchable - Adds the dropdown's filter field.
19
+ * @attr {boolean} details - Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too.
17
20
  * @attr {string} placeholder - Overrides the text shown while nothing is selected.
18
21
  * @attr {boolean} disabled - Disables the picker. Inside an `<aparte-composer>` it also follows the
19
22
  * composer's own `disabled`, so a disabled composer does not leave its model picker operable.
@@ -33,6 +36,12 @@ export declare class AparteModelSelector extends HTMLElement implements AparteCo
33
36
  private _aparteSelect;
34
37
  private _isLoading;
35
38
  private _isRendering;
39
+ /** A load was asked for while one ran; the running one goes round again. */
40
+ private _reloadQueued;
41
+ /** Bumped by {@link _restartLoading}: a load of an older generation discards its result. */
42
+ private _loadGeneration;
43
+ /** The providers the current list was built from, as {@link _providersKey} gives them. */
44
+ private _loadedProvidersKey;
36
45
  private _expandedGroups;
37
46
  /**
38
47
  * Config governing THIS element — nearest instance boundary, else the global.
@@ -54,6 +63,17 @@ export declare class AparteModelSelector extends HTMLElement implements AparteCo
54
63
  private _boundComposerChange;
55
64
  static get observedAttributes(): string[];
56
65
  connectedCallback(): Promise<void>;
66
+ /**
67
+ * The config's own selection when this element last read it. A change to IT is someone
68
+ * choosing a model (an onboarding flow, `setModelConfig`); every other config change — a
69
+ * language, a provider, an icon — leaves the person's own pick alone. Compared with the
70
+ * pick instead, a pick made without `persist` (which writes nothing to the config) was
71
+ * put back to the config's default by the next unrelated change.
72
+ */
73
+ private _seenConfigSelection;
74
+ private _configSelectionKey;
75
+ /** The registered providers, as one comparable string: what the list was built from. */
76
+ private _providersKey;
57
77
  /** Listen for configuration changes (e.g. from an onboarding flow). */
58
78
  private _subscribeToConfig;
59
79
  /**
@@ -63,6 +83,10 @@ export declare class AparteModelSelector extends HTMLElement implements AparteCo
63
83
  * time: the app's own copy is not the locale's to overwrite.
64
84
  */
65
85
  private _relabel;
86
+ /** How a token count is written in the locale's language: 128K, 128 k… */
87
+ private _tokenFormat;
88
+ /** The line under a model's name with `details`: what its provider declares about it. */
89
+ private _metaText;
66
90
  disconnectedCallback(): void;
67
91
  /**
68
92
  * The boundary above us appeared or changed: move the subscription and reload,
@@ -81,7 +105,23 @@ export declare class AparteModelSelector extends HTMLElement implements AparteCo
81
105
  get modelId(): string | null;
82
106
  /** Programmatically set the selection. */
83
107
  setSelection(providerId: string, modelId: string): void;
108
+ /**
109
+ * Load every provider's models. A call that arrives while a load runs does not start a
110
+ * second one and does not vanish either: the running load goes round again, reading
111
+ * what changed meanwhile, and answers `false` so its caller leaves the render to the
112
+ * load that is still running.
113
+ */
84
114
  private _loadAllProviderModels;
115
+ /**
116
+ * Start over: a mount, or a new config above us. A load still in flight from before
117
+ * keeps running — its fetches cannot be called back — but it is now of an older
118
+ * generation, so it discards what it brings back instead of writing it over the list
119
+ * the new load builds. Under every wrapper this is the normal order: the element
120
+ * connects on the global config, then `bind()` attaches the chat's own, and a slow
121
+ * global `/models` used to land last and win.
122
+ */
123
+ private _restartLoading;
124
+ private _loadOnce;
85
125
  private _onOptgroupToggle;
86
126
  /** Our own attribute, or the disabled state of the composer we sit in. */
87
127
  private _isDisabled;
@@ -1 +1 @@
1
- {"version":3,"file":"aparte-model-selector.d.ts","sourceRoot":"","sources":["../src/aparte-model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAOzB,MAAM,cAAc,CAAC;AAoBtB;;;;;;;;;;;;;;;;;;;;;;;;;;GA0BG;AACH,qBAAa,mBAAoB,SAAQ,WAAY,YAAW,iBAAiB;IAC7E,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,OAAO,CAAC,eAAe,CAA0B;IAEjD;;;;;;;;;;OAUG;IACH,OAAO,CAAC,IAAI,CAAoC;IAEhD,OAAO,CAAC,kBAAkB,CAA6B;IAGvD,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,OAAO,CAAC,qBAAqB,CAA2D;IACxF,mEAAmE;IACnE,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,oBAAoB,CAAyC;IAErE,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IA4BxC,uEAAuE;IACvE,OAAO,CAAC,kBAAkB;IAgC1B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IAMhB,oBAAoB,IAAI,IAAI;IAQ5B;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAqB7C,wBAAwB,IAAI,IAAI;IAUhC,2BAA2B;IAC3B,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,wBAAwB;IACxB,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,0CAA0C;IAC1C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;YAgBzC,sBAAsB;YAmDtB,iBAAiB;IA2C/B,0EAA0E;IAC1E,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,WAAW;IA+HnB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,iBAAiB;IAiBzB,OAAO,CAAC,uBAAuB;CAWlC;AAOD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,uBAAuB,EAAE,mBAAmB,CAAC;KAChD;CACJ"}
1
+ {"version":3,"file":"aparte-model-selector.d.ts","sourceRoot":"","sources":["../src/aparte-model-selector.ts"],"names":[],"mappings":"AAAA,OAAO,EAIH,KAAK,YAAY,EACjB,KAAK,iBAAiB,EAOzB,MAAM,cAAc,CAAC;AAoBtB;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,mBAAoB,SAAQ,WAAY,YAAW,iBAAiB;IAC7E,OAAO,CAAC,kBAAkB,CAAuB;IACjD,OAAO,CAAC,eAAe,CAAuB;IAC9C,OAAO,CAAC,eAAe,CAAwB;IAC/C,OAAO,CAAC,aAAa,CAA6B;IAClD,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,YAAY,CAAS;IAC7B,4EAA4E;IAC5E,OAAO,CAAC,aAAa,CAAS;IAC9B,4FAA4F;IAC5F,OAAO,CAAC,eAAe,CAAK;IAC5B,0FAA0F;IAC1F,OAAO,CAAC,mBAAmB,CAAM;IACjC,OAAO,CAAC,eAAe,CAA0B;IAEjD;;;;;;;;;;OAUG;IACH,OAAO,CAAC,IAAI,CAAoC;IAEhD,OAAO,CAAC,kBAAkB,CAA6B;IAGvD,OAAO,CAAC,kBAAkB,CAAiC;IAC3D,OAAO,CAAC,qBAAqB,CAA2D;IACxF,mEAAmE;IACnE,OAAO,CAAC,SAAS,CAAwB;IACzC,OAAO,CAAC,oBAAoB,CAAyC;IAErE,MAAM,KAAK,kBAAkB,IAAI,MAAM,EAAE,CAExC;IAEK,iBAAiB,IAAI,OAAO,CAAC,IAAI,CAAC;IA+BxC;;;;;;OAMG;IACH,OAAO,CAAC,oBAAoB,CAAM;IAElC,OAAO,CAAC,mBAAmB;IAK3B,wFAAwF;IACxF,OAAO,CAAC,aAAa;IAIrB,uEAAuE;IACvE,OAAO,CAAC,kBAAkB;IAmC1B;;;;;OAKG;IACH,OAAO,CAAC,QAAQ;IAmBhB,0EAA0E;IAC1E,OAAO,CAAC,YAAY;IAIpB,yFAAyF;IACzF,OAAO,CAAC,SAAS;IAUjB,oBAAoB,IAAI,IAAI;IAQ5B;;;;;;;;OAQG;IACH,mBAAmB,CAAC,IAAI,EAAE,YAAY,GAAG,IAAI;IAqB7C,wBAAwB,IAAI,IAAI;IAUhC,2BAA2B;IAC3B,IAAI,UAAU,IAAI,MAAM,GAAG,IAAI,CAE9B;IAED,wBAAwB;IACxB,IAAI,OAAO,IAAI,MAAM,GAAG,IAAI,CAE3B;IAED,0CAA0C;IAC1C,YAAY,CAAC,UAAU,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,IAAI;IAgBvD;;;;;OAKG;YACW,sBAAsB;IAmBpC;;;;;;;OAOG;IACH,OAAO,CAAC,eAAe;YAMT,SAAS;YAmDT,iBAAiB;IA2C/B,0EAA0E;IAC1E,OAAO,CAAC,WAAW;IAInB,OAAO,CAAC,aAAa;IAIrB,OAAO,CAAC,OAAO;IAWf,OAAO,CAAC,WAAW;IAsInB,OAAO,CAAC,oBAAoB;IAK5B,OAAO,CAAC,aAAa;IAcrB,OAAO,CAAC,WAAW;IAwBnB,OAAO,CAAC,iBAAiB;IAkBzB,OAAO,CAAC,uBAAuB;CAWlC;AAOD,OAAO,CAAC,MAAM,CAAC;IACX,UAAU,qBAAqB;QAC3B,uBAAuB,EAAE,mBAAmB,CAAC;KAChD;CACJ"}
@@ -88,6 +88,36 @@
88
88
  "privacy": "private",
89
89
  "default": "false"
90
90
  },
91
+ {
92
+ "kind": "field",
93
+ "name": "_reloadQueued",
94
+ "type": {
95
+ "text": "boolean"
96
+ },
97
+ "privacy": "private",
98
+ "default": "false",
99
+ "description": "A load was asked for while one ran; the running one goes round again."
100
+ },
101
+ {
102
+ "kind": "field",
103
+ "name": "_loadGeneration",
104
+ "type": {
105
+ "text": "number"
106
+ },
107
+ "privacy": "private",
108
+ "default": "0",
109
+ "description": "Bumped by _restartLoading: a load of an older generation discards its result."
110
+ },
111
+ {
112
+ "kind": "field",
113
+ "name": "_loadedProvidersKey",
114
+ "type": {
115
+ "text": "string"
116
+ },
117
+ "privacy": "private",
118
+ "default": "''",
119
+ "description": "The providers the current list was built from, as _providersKey gives them."
120
+ },
91
121
  {
92
122
  "kind": "field",
93
123
  "name": "_expandedGroups",
@@ -141,6 +171,37 @@
141
171
  "name": "_boundComposerChange",
142
172
  "privacy": "private"
143
173
  },
174
+ {
175
+ "kind": "field",
176
+ "name": "_seenConfigSelection",
177
+ "type": {
178
+ "text": "string"
179
+ },
180
+ "privacy": "private",
181
+ "default": "''",
182
+ "description": "The config's own selection when this element last read it. A change to IT is someone\nchoosing a model (an onboarding flow, `setModelConfig`); every other config change — a\nlanguage, a provider, an icon — leaves the person's own pick alone. Compared with the\npick instead, a pick made without `persist` (which writes nothing to the config) was\nput back to the config's default by the next unrelated change."
183
+ },
184
+ {
185
+ "kind": "method",
186
+ "name": "_configSelectionKey",
187
+ "privacy": "private",
188
+ "return": {
189
+ "type": {
190
+ "text": "string"
191
+ }
192
+ }
193
+ },
194
+ {
195
+ "kind": "method",
196
+ "name": "_providersKey",
197
+ "privacy": "private",
198
+ "return": {
199
+ "type": {
200
+ "text": "string"
201
+ }
202
+ },
203
+ "description": "The registered providers, as one comparable string: what the list was built from."
204
+ },
144
205
  {
145
206
  "kind": "method",
146
207
  "name": "_subscribeToConfig",
@@ -163,6 +224,42 @@
163
224
  },
164
225
  "description": "Re-apply the one string this element takes from the locale, in place.\n\nAn explicit `placeholder` attribute still wins, exactly as it does at render\ntime: the app's own copy is not the locale's to overwrite."
165
226
  },
227
+ {
228
+ "kind": "method",
229
+ "name": "_tokenFormat",
230
+ "privacy": "private",
231
+ "return": {
232
+ "type": {
233
+ "text": "Intl.NumberFormat"
234
+ }
235
+ },
236
+ "description": "How a token count is written in the locale's language: 128K, 128 k…"
237
+ },
238
+ {
239
+ "kind": "method",
240
+ "name": "_metaText",
241
+ "privacy": "private",
242
+ "return": {
243
+ "type": {
244
+ "text": "string"
245
+ }
246
+ },
247
+ "parameters": [
248
+ {
249
+ "name": "m",
250
+ "type": {
251
+ "text": "{ description?: string; contextWindow?: number }"
252
+ }
253
+ },
254
+ {
255
+ "name": "tokens",
256
+ "type": {
257
+ "text": "Intl.NumberFormat"
258
+ }
259
+ }
260
+ ],
261
+ "description": "The line under a model's name with `details`: what its provider declares about it."
262
+ },
166
263
  {
167
264
  "kind": "method",
168
265
  "name": "aparteConfigChanged",
@@ -227,11 +324,41 @@
227
324
  "kind": "method",
228
325
  "name": "_loadAllProviderModels",
229
326
  "privacy": "private",
327
+ "return": {
328
+ "type": {
329
+ "text": "Promise<boolean>"
330
+ }
331
+ },
332
+ "description": "Load every provider's models. A call that arrives while a load runs does not start a\nsecond one and does not vanish either: the running load goes round again, reading\nwhat changed meanwhile, and answers `false` so its caller leaves the render to the\nload that is still running."
333
+ },
334
+ {
335
+ "kind": "method",
336
+ "name": "_restartLoading",
337
+ "privacy": "private",
338
+ "return": {
339
+ "type": {
340
+ "text": "void"
341
+ }
342
+ },
343
+ "description": "Start over: a mount, or a new config above us. A load still in flight from before\nkeeps running — its fetches cannot be called back — but it is now of an older\ngeneration, so it discards what it brings back instead of writing it over the list\nthe new load builds. Under every wrapper this is the normal order: the element\nconnects on the global config, then `bind()` attaches the chat's own, and a slow\nglobal `/models` used to land last and win."
344
+ },
345
+ {
346
+ "kind": "method",
347
+ "name": "_loadOnce",
348
+ "privacy": "private",
230
349
  "return": {
231
350
  "type": {
232
351
  "text": "Promise<void>"
233
352
  }
234
- }
353
+ },
354
+ "parameters": [
355
+ {
356
+ "name": "generation",
357
+ "type": {
358
+ "text": "number"
359
+ }
360
+ }
361
+ ]
235
362
  },
236
363
  {
237
364
  "kind": "method",
@@ -396,7 +523,7 @@
396
523
  "type": {
397
524
  "text": "boolean"
398
525
  },
399
- "description": "Writes the selection back through the config, so it survives a reload."
526
+ "description": "Writes the selection into the config, which is what the next message is sent with. Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`."
400
527
  },
401
528
  {
402
529
  "name": "auto-select",
@@ -425,6 +552,13 @@
425
552
  "text": "boolean"
426
553
  },
427
554
  "description": "Disables the picker. Inside an `<aparte-composer>` it also follows the composer's own `disabled`, so a disabled composer does not leave its model picker operable."
555
+ },
556
+ {
557
+ "name": "details",
558
+ "type": {
559
+ "text": "boolean"
560
+ },
561
+ "description": "Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too."
428
562
  }
429
563
  ],
430
564
  "superclass": {
@@ -578,7 +712,7 @@
578
712
  {
579
713
  "kind": "field",
580
714
  "name": "persist",
581
- "description": "Writes the selection back through the config, so it survives a reload."
715
+ "description": "Writes the selection into the config, which is what the next message is sent with. Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`."
582
716
  },
583
717
  {
584
718
  "kind": "field",
@@ -600,6 +734,11 @@
600
734
  "name": "disabled",
601
735
  "description": "Disables the picker. Inside an `<aparte-composer>` it also follows the composer's own `disabled`, so a disabled composer does not leave its model picker operable."
602
736
  },
737
+ {
738
+ "kind": "field",
739
+ "name": "details",
740
+ "description": "Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too."
741
+ },
603
742
  {
604
743
  "kind": "field",
605
744
  "name": "modelChange",
@@ -1,6 +1,6 @@
1
1
  /** `<aparte-model-selector>` — Selects an AI provider and a model. Renders an `<aparte-select>` with providers grouped into `<aparte-optgroup>`s — a single provider renders a flat list — which is what makes the BYOK pattern possible without core knowing about any vendor. */
2
2
  export interface AparteModelSelectorAttributes {
3
- /** Writes the selection back through the config, so it survives a reload. */
3
+ /** Writes the selection into the config, which is what the next message is sent with. Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`. */
4
4
  persist?: boolean;
5
5
  /** Selects the first model as soon as one is available. */
6
6
  'auto-select'?: boolean;
@@ -10,6 +10,8 @@ export interface AparteModelSelectorAttributes {
10
10
  placeholder?: string;
11
11
  /** Disables the picker. Inside an `<aparte-composer>` it also follows the composer's own `disabled`, so a disabled composer does not leave its model picker operable. */
12
12
  disabled?: boolean;
13
+ /** Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too. */
14
+ details?: boolean;
13
15
  }
14
16
  /**
15
17
  * Tag name to its attribute surface — the registry every wrapper maps over.
@@ -1 +1 @@
1
- {"version":3,"file":"element-attributes.d.ts","sourceRoot":"","sources":["../../src/generated/element-attributes.ts"],"names":[],"mappings":"AAIA,mRAAmR;AACnR,MAAM,WAAW,6BAA6B;IAC1C,6EAA6E;IAC7E,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yKAAyK;IACzK,QAAQ,CAAC,EAAE,OAAO,CAAC;CACtB;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACpC,uBAAuB,EAAE,6BAA6B,CAAC;CAC1D;AAED,2FAA2F;AAC3F,MAAM,MAAM,oBAAoB,GAAG,MAAM,uBAAuB,CAAC"}
1
+ {"version":3,"file":"element-attributes.d.ts","sourceRoot":"","sources":["../../src/generated/element-attributes.ts"],"names":[],"mappings":"AAIA,mRAAmR;AACnR,MAAM,WAAW,6BAA6B;IAC1C,0SAA0S;IAC1S,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,2DAA2D;IAC3D,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB,wCAAwC;IACxC,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0DAA0D;IAC1D,WAAW,CAAC,EAAE,MAAM,CAAC;IACrB,yKAAyK;IACzK,QAAQ,CAAC,EAAE,OAAO,CAAC;IACnB,gMAAgM;IAChM,OAAO,CAAC,EAAE,OAAO,CAAC;CACrB;AAED;;;;;GAKG;AACH,MAAM,WAAW,uBAAuB;IACpC,uBAAuB,EAAE,6BAA6B,CAAC;CAC1D;AAED,2FAA2F;AAC3F,MAAM,MAAM,oBAAoB,GAAG,MAAM,uBAAuB,CAAC"}
@@ -29,7 +29,7 @@ declare abstract class AparteElementBase {
29
29
  }
30
30
  /** `<aparte-model-selector>` — Selects an AI provider and a model. Renders an `<aparte-select>` with providers grouped into `<aparte-optgroup>`s — a single provider renders a flat list — which is what makes the BYOK pattern possible without core knowing about any vendor. */
31
31
  export declare class AparteModelSelectorDirective extends AparteElementBase {
32
- /** Writes the selection back through the config, so it survives a reload. */
32
+ /** Writes the selection into the config, which is what the next message is sent with. Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`. */
33
33
  set persist(v: boolean);
34
34
  /** Selects the first model as soon as one is available. */
35
35
  set autoSelect(v: boolean);
@@ -39,15 +39,18 @@ export declare class AparteModelSelectorDirective extends AparteElementBase {
39
39
  set placeholder(v: string | undefined);
40
40
  /** Disables the picker. Inside an `<aparte-composer>` it also follows the composer's own `disabled`, so a disabled composer does not leave its model picker operable. */
41
41
  set disabled(v: boolean);
42
+ /** Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too. */
43
+ set details(v: boolean);
42
44
  /** The provider or the model changed; carries both ids. */
43
45
  readonly modelChange: EventEmitter<AparteModelChangeEventDetail>;
44
46
  protected onModelChange(e: CustomEvent<AparteModelChangeEventDetail>): void;
45
47
  static ɵfac: i0.ɵɵFactoryDeclaration<AparteModelSelectorDirective, never>;
46
- static ɵdir: i0.ɵɵDirectiveDeclaration<AparteModelSelectorDirective, "aparte-model-selector", never, { "persist": { "alias": "persist"; "required": false; }; "autoSelect": { "alias": "autoSelect"; "required": false; }; "searchable": { "alias": "searchable"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; }, { "modelChange": "modelChange"; }, never, never, true, never>;
48
+ static ɵdir: i0.ɵɵDirectiveDeclaration<AparteModelSelectorDirective, "aparte-model-selector", never, { "persist": { "alias": "persist"; "required": false; }; "autoSelect": { "alias": "autoSelect"; "required": false; }; "searchable": { "alias": "searchable"; "required": false; }; "placeholder": { "alias": "placeholder"; "required": false; }; "disabled": { "alias": "disabled"; "required": false; }; "details": { "alias": "details"; "required": false; }; }, { "modelChange": "modelChange"; }, never, never, true, never>;
47
49
  static ngAcceptInputType_persist: unknown;
48
50
  static ngAcceptInputType_autoSelect: unknown;
49
51
  static ngAcceptInputType_searchable: unknown;
50
52
  static ngAcceptInputType_disabled: unknown;
53
+ static ngAcceptInputType_details: unknown;
51
54
  }
52
55
  /**
53
56
  * Every element directive, for one import in a standalone component.
@@ -1 +1 @@
1
- {"version":3,"file":"element.directives.d.ts","sourceRoot":"","sources":["../../src/generated/element.directives.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,OAAO,EAEH,UAAU,EACV,YAAY,EAMf,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EACR,4BAA4B,EAC/B,MAAM,cAAc,CAAC;;AAEtB;;;;;;;GAOG;AACH,uBACe,iBAAiB;IAC5B,SAAS,CAAC,QAAQ,CAAC,IAAI,kBAAmC;IAE1D,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;yCAHxC,iBAAiB;2CAAjB,iBAAiB;CAM/B;AAED,mRAAmR;AACnR,qBACa,4BAA6B,SAAQ,iBAAiB;IAC/D,6EAA6E;IAC7E,IAA4C,OAAO,CAAC,CAAC,EAAE,OAAO,EAA+B;IAC7F,2DAA2D;IAC3D,IAA4C,UAAU,CAAC,CAAC,EAAE,OAAO,EAAmC;IACpG,wCAAwC;IACxC,IAA4C,UAAU,CAAC,CAAC,EAAE,OAAO,EAAkC;IACnG,0DAA0D;IAC1D,IAAa,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAmC;IACjF,yKAAyK;IACzK,IAA4C,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAgC;IAE/F,2DAA2D;IACjD,QAAQ,CAAC,WAAW,6CAAoD;IAGlF,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,4BAA4B,CAAC,GAAG,IAAI;yCAhBlE,4BAA4B;2CAA5B,4BAA4B;sCA6B047c,OAAQ;yCAAR,OAAQ;yCAAR,OAAQ;uCAAR,OAAQ;CAZ177c;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,gDAE5B,CAAC"}
1
+ {"version":3,"file":"element.directives.d.ts","sourceRoot":"","sources":["../../src/generated/element.directives.ts"],"names":[],"mappings":"AAIA;;;;;;;;;;;GAWG;AACH,OAAO,EAEH,UAAU,EACV,YAAY,EAMf,MAAM,eAAe,CAAC;AAEvB,OAAO,KAAK,EACR,4BAA4B,EAC/B,MAAM,cAAc,CAAC;;AAEtB;;;;;;;GAOG;AACH,uBACe,iBAAiB;IAC5B,SAAS,CAAC,QAAQ,CAAC,IAAI,kBAAmC;IAE1D,SAAS,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,GAAG,IAAI;yCAHxC,iBAAiB;2CAAjB,iBAAiB;CAM/B;AAED,mRAAmR;AACnR,qBACa,4BAA6B,SAAQ,iBAAiB;IAC/D,0SAA0S;IAC1S,IAA4C,OAAO,CAAC,CAAC,EAAE,OAAO,EAA+B;IAC7F,2DAA2D;IAC3D,IAA4C,UAAU,CAAC,CAAC,EAAE,OAAO,EAAmC;IACpG,wCAAwC;IACxC,IAA4C,UAAU,CAAC,CAAC,EAAE,OAAO,EAAkC;IACnG,0DAA0D;IAC1D,IAAa,WAAW,CAAC,CAAC,EAAE,MAAM,GAAG,SAAS,EAAmC;IACjF,yKAAyK;IACzK,IAA4C,QAAQ,CAAC,CAAC,EAAE,OAAO,EAAgC;IAC/F,gMAAgM;IAChM,IAA4C,OAAO,CAAC,CAAC,EAAE,OAAO,EAA+B;IAE7F,2DAA2D;IACjD,QAAQ,CAAC,WAAW,6CAAoD;IAGlF,SAAS,CAAC,aAAa,CAAC,CAAC,EAAE,WAAW,CAAC,4BAA4B,CAAC,GAAG,IAAI;yCAlBlE,4BAA4B;2CAA5B,4BAA4B;sCA+Bs46c,OAAQ;yCAAR,OAAQ;yCAAR,OAAQ;uCAAR,OAAQ;sCAAR,OAAQ;CAZt76c;AAED;;;;;;GAMG;AACH,eAAO,MAAM,yBAAyB,gDAE5B,CAAC"}
@@ -37,7 +37,7 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
37
37
  }] });
38
38
  /** `<aparte-model-selector>` — Selects an AI provider and a model. Renders an `<aparte-select>` with providers grouped into `<aparte-optgroup>`s — a single provider renders a flat list — which is what makes the BYOK pattern possible without core knowing about any vendor. */
39
39
  export class AparteModelSelectorDirective extends AparteElementBase {
40
- /** Writes the selection back through the config, so it survives a reload. */
40
+ /** Writes the selection into the config, which is what the next message is sent with. Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`. */
41
41
  set persist(v) { this.write('persist', v); }
42
42
  /** Selects the first model as soon as one is available. */
43
43
  set autoSelect(v) { this.write('auto-select', v); }
@@ -47,11 +47,13 @@ export class AparteModelSelectorDirective extends AparteElementBase {
47
47
  set placeholder(v) { this.write('placeholder', v); }
48
48
  /** Disables the picker. Inside an `<aparte-composer>` it also follows the composer's own `disabled`, so a disabled composer does not leave its model picker operable. */
49
49
  set disabled(v) { this.write('disabled', v); }
50
+ /** Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too. */
51
+ set details(v) { this.write('details', v); }
50
52
  /** The provider or the model changed; carries both ids. */
51
53
  modelChange = new EventEmitter();
52
54
  onModelChange(e) { this.modelChange.emit(e.detail); }
53
55
  static ɵfac = i0.ɵɵngDeclareFactory({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteModelSelectorDirective, deps: null, target: i0.ɵɵFactoryTarget.Directive });
54
- static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteModelSelectorDirective, isStandalone: true, selector: "aparte-model-selector", inputs: { persist: ["persist", "persist", booleanAttribute], autoSelect: ["autoSelect", "autoSelect", booleanAttribute], searchable: ["searchable", "searchable", booleanAttribute], placeholder: "placeholder", disabled: ["disabled", "disabled", booleanAttribute] }, outputs: { modelChange: "modelChange" }, host: { listeners: { "aparte-model-change": "onModelChange($event)" } }, usesInheritance: true, ngImport: i0 });
56
+ static ɵdir = i0.ɵɵngDeclareDirective({ minVersion: "16.1.0", version: "19.2.25", type: AparteModelSelectorDirective, isStandalone: true, selector: "aparte-model-selector", inputs: { persist: ["persist", "persist", booleanAttribute], autoSelect: ["autoSelect", "autoSelect", booleanAttribute], searchable: ["searchable", "searchable", booleanAttribute], placeholder: "placeholder", disabled: ["disabled", "disabled", booleanAttribute], details: ["details", "details", booleanAttribute] }, outputs: { modelChange: "modelChange" }, host: { listeners: { "aparte-model-change": "onModelChange($event)" } }, usesInheritance: true, ngImport: i0 });
55
57
  }
56
58
  i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImport: i0, type: AparteModelSelectorDirective, decorators: [{
57
59
  type: Directive,
@@ -70,6 +72,9 @@ i0.ɵɵngDeclareClassMetadata({ minVersion: "12.0.0", version: "19.2.25", ngImpo
70
72
  }], disabled: [{
71
73
  type: Input,
72
74
  args: [{ transform: booleanAttribute }]
75
+ }], details: [{
76
+ type: Input,
77
+ args: [{ transform: booleanAttribute }]
73
78
  }], modelChange: [{
74
79
  type: Output
75
80
  }], onModelChange: [{
package/dist/index.js CHANGED
@@ -9,6 +9,12 @@ class AparteModelSelector extends HTMLElement {
9
9
  _aparteSelect = null;
10
10
  _isLoading = false;
11
11
  _isRendering = false;
12
+ /** A load was asked for while one ran; the running one goes round again. */
13
+ _reloadQueued = false;
14
+ /** Bumped by {@link _restartLoading}: a load of an older generation discards its result. */
15
+ _loadGeneration = 0;
16
+ /** The providers the current list was built from, as {@link _providersKey} gives them. */
17
+ _loadedProvidersKey = "";
12
18
  _expandedGroups = /* @__PURE__ */ new Set();
13
19
  /**
14
20
  * Config governing THIS element — nearest instance boundary, else the global.
@@ -34,15 +40,16 @@ class AparteModelSelector extends HTMLElement {
34
40
  this._syncDisabled();
35
41
  };
36
42
  static get observedAttributes() {
37
- return ["persist", "auto-select", "searchable", "placeholder", "disabled"];
43
+ return ["persist", "auto-select", "searchable", "placeholder", "disabled", "details"];
38
44
  }
39
45
  async connectedCallback() {
40
46
  this._cfg = resolveConfig(this);
41
- this._isLoading = false;
47
+ this._restartLoading();
42
48
  this._providerModels = [];
43
49
  this._setupEventListeners();
44
50
  this._composer = this.closest("aparte-composer");
45
51
  this._composer?.addEventListener("aparte-composer-change", this._boundComposerChange);
52
+ this._seenConfigSelection = this._configSelectionKey();
46
53
  const config = this._cfg.getModelConfig();
47
54
  if (config.defaultProvider && config.defaultModel) {
48
55
  this._currentProviderId = config.defaultProvider;
@@ -50,23 +57,40 @@ class AparteModelSelector extends HTMLElement {
50
57
  } else {
51
58
  this._loadPersistedSelection();
52
59
  }
53
- await this._loadAllProviderModels();
54
- this._render();
55
60
  this._subscribeToConfig();
61
+ if (await this._loadAllProviderModels()) this._render();
62
+ }
63
+ /**
64
+ * The config's own selection when this element last read it. A change to IT is someone
65
+ * choosing a model (an onboarding flow, `setModelConfig`); every other config change — a
66
+ * language, a provider, an icon — leaves the person's own pick alone. Compared with the
67
+ * pick instead, a pick made without `persist` (which writes nothing to the config) was
68
+ * put back to the config's default by the next unrelated change.
69
+ */
70
+ _seenConfigSelection = "";
71
+ _configSelectionKey() {
72
+ const c = this._cfg.getModelConfig();
73
+ return `${c.defaultProvider ?? ""}::${c.defaultModel ?? ""}`;
74
+ }
75
+ /** The registered providers, as one comparable string: what the list was built from. */
76
+ _providersKey() {
77
+ return this._cfg.getAIProviders().map((p) => p.id).join("\n");
56
78
  }
57
79
  /** Listen for configuration changes (e.g. from an onboarding flow). */
58
80
  _subscribeToConfig() {
59
81
  this._configUnsubscribe?.();
60
82
  this._configUnsubscribe = this._cfg.subscribe(() => {
61
83
  void (async () => {
62
- const cfg = this._cfg.getModelConfig();
63
- if (cfg.defaultProvider === this._currentProviderId && cfg.defaultModel === this._currentModelId && this._providerModels.length > 0) {
84
+ const selectionMoved = this._configSelectionKey() !== this._seenConfigSelection;
85
+ if (!selectionMoved && this._providerModels.length > 0 && this._providersKey() === this._loadedProvidersKey) {
64
86
  this._relabel();
65
87
  return;
66
88
  }
67
- this._loadPersistedSelection();
68
- await this._loadAllProviderModels();
69
- this._render();
89
+ if (selectionMoved) {
90
+ this._seenConfigSelection = this._configSelectionKey();
91
+ this._loadPersistedSelection();
92
+ }
93
+ if (await this._loadAllProviderModels()) this._render();
70
94
  })();
71
95
  });
72
96
  }
@@ -80,6 +104,29 @@ class AparteModelSelector extends HTMLElement {
80
104
  if (!this._aparteSelect) return;
81
105
  const fromLocale = this._cfg.getLocale()["modelSelectorPlaceholder"] || "Select a model...";
82
106
  this._aparteSelect.setAttribute("placeholder", this.getAttribute("placeholder") || fromLocale);
107
+ if (!this.hasAttribute("details")) return;
108
+ const options = new Map(Array.from(this.querySelectorAll("aparte-option"), (o) => [o.getAttribute("value"), o]));
109
+ const tokens = this._tokenFormat();
110
+ for (const pm of this._providerModels) {
111
+ for (const m of pm.models) {
112
+ const line = options.get(`${pm.provider.id}::${m.id}`)?.querySelector(".aparte-option__meta");
113
+ if (line) line.textContent = this._metaText(m, tokens);
114
+ }
115
+ }
116
+ }
117
+ /** How a token count is written in the locale's language: 128K, 128 k… */
118
+ _tokenFormat() {
119
+ return new Intl.NumberFormat(this._cfg.getLocale().tag || void 0, { notation: "compact", maximumFractionDigits: 1 });
120
+ }
121
+ /** The line under a model's name with `details`: what its provider declares about it. */
122
+ _metaText(m, tokens) {
123
+ const parts = [];
124
+ if (m.description) parts.push(m.description);
125
+ if (m.contextWindow && m.contextWindow > 0) {
126
+ const size = tokens.format(m.contextWindow);
127
+ parts.push((this._cfg.t("modelContextWindow") || "{n} context").replace("{n}", () => size));
128
+ }
129
+ return parts.join(" · ");
83
130
  }
84
131
  disconnectedCallback() {
85
132
  this._aparteSelect?.removeEventListener("aparte-select-change", this._boundHandleChange);
@@ -103,8 +150,9 @@ class AparteModelSelector extends HTMLElement {
103
150
  this._cfg = next;
104
151
  if (!this.isConnected) return;
105
152
  void (async () => {
106
- this._isLoading = false;
153
+ this._restartLoading();
107
154
  this._providerModels = [];
155
+ this._seenConfigSelection = this._configSelectionKey();
108
156
  const model = this._cfg.getModelConfig();
109
157
  if (model.defaultProvider && model.defaultModel) {
110
158
  this._currentProviderId = model.defaultProvider;
@@ -112,9 +160,8 @@ class AparteModelSelector extends HTMLElement {
112
160
  } else {
113
161
  this._loadPersistedSelection();
114
162
  }
115
- await this._loadAllProviderModels();
116
- this._render();
117
163
  this._subscribeToConfig();
164
+ if (await this._loadAllProviderModels()) this._render();
118
165
  })();
119
166
  }
120
167
  attributeChangedCallback() {
@@ -146,30 +193,63 @@ class AparteModelSelector extends HTMLElement {
146
193
  // ─────────────────────────────────────────────────────────────────────────
147
194
  // Data Loading
148
195
  // ─────────────────────────────────────────────────────────────────────────
196
+ /**
197
+ * Load every provider's models. A call that arrives while a load runs does not start a
198
+ * second one and does not vanish either: the running load goes round again, reading
199
+ * what changed meanwhile, and answers `false` so its caller leaves the render to the
200
+ * load that is still running.
201
+ */
149
202
  async _loadAllProviderModels() {
150
- if (this._isLoading) return;
203
+ if (this._isLoading) {
204
+ this._reloadQueued = true;
205
+ return false;
206
+ }
207
+ const generation = this._loadGeneration;
151
208
  this._isLoading = true;
152
209
  try {
153
- const providers = this._cfg.getAIProviders();
154
- const uniqueProviders = Array.from(new Map(providers.map((p) => [p.id, p])).values());
155
- const config = this._cfg.getModelConfig();
156
- const tempList = new Array(uniqueProviders.length);
157
- await Promise.all(uniqueProviders.map(async (provider, i) => {
158
- try {
159
- const models = await this._cfg.refreshProviderModels(provider.id);
160
- const filters = config.modelFilters?.[provider.id];
161
- const filteredModels = filters?.length ? models.filter((m) => filters.includes(m.id)) : models;
162
- if (filteredModels.length > 0) {
163
- tempList[i] = { provider, models: filteredModels };
164
- }
165
- } catch (error) {
166
- console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);
167
- }
168
- }));
169
- this._providerModels = tempList.filter((pm) => pm !== void 0);
210
+ do {
211
+ this._reloadQueued = false;
212
+ await this._loadOnce(generation);
213
+ } while (this._reloadQueued && this.isConnected && generation === this._loadGeneration);
170
214
  } finally {
171
- this._isLoading = false;
215
+ if (generation === this._loadGeneration) this._isLoading = false;
172
216
  }
217
+ return generation === this._loadGeneration;
218
+ }
219
+ /**
220
+ * Start over: a mount, or a new config above us. A load still in flight from before
221
+ * keeps running — its fetches cannot be called back — but it is now of an older
222
+ * generation, so it discards what it brings back instead of writing it over the list
223
+ * the new load builds. Under every wrapper this is the normal order: the element
224
+ * connects on the global config, then `bind()` attaches the chat's own, and a slow
225
+ * global `/models` used to land last and win.
226
+ */
227
+ _restartLoading() {
228
+ this._loadGeneration++;
229
+ this._isLoading = false;
230
+ this._reloadQueued = false;
231
+ }
232
+ async _loadOnce(generation) {
233
+ const providersKey = this._providersKey();
234
+ const providers = this._cfg.getAIProviders();
235
+ const uniqueProviders = Array.from(new Map(providers.map((p) => [p.id, p])).values());
236
+ const config = this._cfg.getModelConfig();
237
+ const tempList = new Array(uniqueProviders.length);
238
+ await Promise.all(uniqueProviders.map(async (provider, i) => {
239
+ try {
240
+ const models = provider.fetchModels ? await this._cfg.refreshProviderModels(provider.id) : provider.getModels();
241
+ const filters = config.modelFilters?.[provider.id];
242
+ const filteredModels = filters?.length ? models.filter((m) => filters.includes(m.id)) : models;
243
+ if (filteredModels.length > 0) {
244
+ tempList[i] = { provider, models: filteredModels };
245
+ }
246
+ } catch (error) {
247
+ console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);
248
+ }
249
+ }));
250
+ if (generation !== this._loadGeneration) return;
251
+ this._providerModels = tempList.filter((pm) => pm !== void 0);
252
+ this._loadedProvidersKey = providersKey;
173
253
  }
174
254
  async _onOptgroupToggle(e) {
175
255
  const detail = e.detail;
@@ -222,6 +302,12 @@ class AparteModelSelector extends HTMLElement {
222
302
  const localePlaceholder = this._cfg.getLocale()["modelSelectorPlaceholder"] || "Select a model...";
223
303
  const placeholder = this.getAttribute("placeholder") || localePlaceholder;
224
304
  const searchable = this.hasAttribute("searchable");
305
+ const details = this.hasAttribute("details");
306
+ const tokens = details ? this._tokenFormat() : null;
307
+ const meta = (m) => {
308
+ const text = tokens ? this._metaText(m, tokens) : "";
309
+ return text ? `<span class="aparte-option__meta">${esc(text)}</span>` : "";
310
+ };
225
311
  const autoSelect = this.hasAttribute("auto-select");
226
312
  const wasOpen = keepOpen || this._aparteSelect?.hasAttribute("open");
227
313
  let currentValue = this._currentModelId ? `${this._currentProviderId}::${this._currentModelId}` : "";
@@ -239,7 +325,7 @@ class AparteModelSelector extends HTMLElement {
239
325
  const singleProvider = this._providerModels.length === 1;
240
326
  const optionsHtml = singleProvider && only ? only.models.map((m) => {
241
327
  const key = `${only.provider.id}::${m.id}`;
242
- return `<aparte-option value="${esc(key)}">${esc(m.name)}</aparte-option>`;
328
+ return `<aparte-option value="${esc(key)}">${esc(m.name)}${meta(m)}</aparte-option>`;
243
329
  }).join("") : this._providerModels.map((pm) => {
244
330
  const label = pm.provider.getMetadata().name;
245
331
  const isCollapsed = !this._expandedGroups.has(label);
@@ -251,7 +337,7 @@ class AparteModelSelector extends HTMLElement {
251
337
  >
252
338
  ${pm.models.map((m) => {
253
339
  const key = `${pm.provider.id}::${m.id}`;
254
- return `<aparte-option value="${esc(key)}">${esc(m.name)}</aparte-option>`;
340
+ return `<aparte-option value="${esc(key)}">${esc(m.name)}${meta(m)}</aparte-option>`;
255
341
  }).join("")}
256
342
  </aparte-optgroup>
257
343
  `;
@@ -346,6 +432,7 @@ class AparteModelSelector extends HTMLElement {
346
432
  if (currentConfig.defaultProvider === this._currentProviderId && currentConfig.defaultModel === this._currentModelId) {
347
433
  return;
348
434
  }
435
+ this._seenConfigSelection = `${this._currentProviderId}::${this._currentModelId}`;
349
436
  this._cfg.setModelConfig({
350
437
  defaultProvider: this._currentProviderId,
351
438
  defaultModel: this._currentModelId
package/dist/index.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sources":["../src/aparte-model-selector.ts"],"sourcesContent":["import {\n aparteGlobalConfig,\n escapeAttr,\n resolveConfig,\n type AparteConfig,\n type AparteConfigAware,\n AparteSelect,\n type AparteOptgroup,\n type AparteAIProvider,\n type AparteAIModel,\n type AparteModelChangeEventDetail,\n type AparteSelectChangeDetail,\n} from '@aparte/core';\n\ninterface ProviderModels {\n provider: AparteAIProvider;\n models: AparteAIModel[];\n}\n\n/**\n * Escape a value before it is interpolated into an HTML string (text node or\n * double-quoted attribute). Model names/ids come from a remote `/models`\n * endpoint and provider labels from consumer config — both hostile-by-default,\n * and both flow through `innerHTML` when the option list is (re)built.\n */\nfunction esc(value: unknown): string {\n // Coerce, then delegate: core owns the escaping. This local copy used to\n // inline it and, like two of its eight siblings across the scope, left the\n // apostrophe through — enough to break out of a single-quoted attribute.\n return escapeAttr(String(value ?? ''));\n}\n\n/**\n * Selects an AI provider and a model. Renders an `<aparte-select>` with providers\n * grouped into `<aparte-optgroup>`s — a single provider renders a flat list — which is\n * what makes the BYOK pattern possible without core knowing about any vendor.\n *\n * These tags sat at the top of the file, above the imports and a helper, so nothing\n * read them: TypeScript associates only the comment adjacent to the declaration. This\n * package ships no manifest of its own today (only `@aparte/core` runs `cem analyze`),\n * so the loss was invisible to tooling — but it is also what an editor shows on hover,\n * and what a typed framework wrapper is written from.\n *\n * @element aparte-model-selector\n * @attr {boolean} auto-select - Selects the first model as soon as one is available.\n * @attr {boolean} persist - Writes the selection back through the config, so it survives a reload.\n * @attr {boolean} searchable - Adds the dropdown's filter field.\n * @attr {string} placeholder - Overrides the text shown while nothing is selected.\n * @attr {boolean} disabled - Disables the picker. Inside an `<aparte-composer>` it also follows the\n * composer's own `disabled`, so a disabled composer does not leave its model picker operable.\n *\n * @fires {CustomEvent<AparteModelChangeEventDetail>} aparte-model-change - The provider or the model changed; carries both ids.\n *\n * @example\n * <!-- Mount it in the composer's toolbar; it reads the models the config already knows. -->\n * <aparte-composer-toolbar>\n * <aparte-model-selector searchable placeholder=\"Pick a model\"></aparte-model-selector>\n * </aparte-composer-toolbar>\n */\nexport class AparteModelSelector extends HTMLElement implements AparteConfigAware {\n private _currentProviderId: string | null = null;\n private _currentModelId: string | null = null;\n private _providerModels: ProviderModels[] = [];\n private _aparteSelect: AparteSelect | null = null;\n private _isLoading = false;\n private _isRendering = false;\n private _expandedGroups: Set<string> = new Set();\n\n /**\n * Config governing THIS element — nearest instance boundary, else the global.\n *\n * Cached rather than resolved per call, because this element does not only\n * READ it: it holds a `subscribe()` registration keyed on the object. That\n * cache is only correct if something tells us when the boundary changes, and\n * under all four wrappers it does change after we connect —\n * `AparteChatHost.bind()` runs `attachConfig` from a post-mount hook. Hence\n * {@link AparteConfigAware}; without it this element served the global config's\n * providers to a chat that was given its own.\n */\n private _cfg: AparteConfig = aparteGlobalConfig;\n\n private _configUnsubscribe: (() => void) | null = null;\n\n // Bound handlers for cleanup\n private _boundHandleChange = this._handleChange.bind(this);\n private _handleOptgroupToggle = (e: Event): void => { void this._onOptgroupToggle(e); };\n /** The composer we sit in, if any — its `disabled` is ours too. */\n private _composer: Element | null = null;\n private _boundComposerChange = (): void => { this._syncDisabled(); };\n\n static get observedAttributes(): string[] {\n return ['persist', 'auto-select', 'searchable', 'placeholder', 'disabled'];\n }\n\n async connectedCallback(): Promise<void> {\n this._cfg = resolveConfig(this);\n // Reset loading flag so a fresh mount always does a full load\n // (prevents a stale _isLoading=true from a previous mount blocking this one)\n this._isLoading = false;\n this._providerModels = [];\n\n this._setupEventListeners();\n // The public event, not the composer's private bus: a disabled composer says so\n // through `aparte-composer-change`, and the picker inside it follows.\n this._composer = this.closest('aparte-composer');\n this._composer?.addEventListener('aparte-composer-change', this._boundComposerChange);\n\n // Seed selection from the resolved config first (handles race conditions)\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n\n await this._loadAllProviderModels();\n this._render();\n\n this._subscribeToConfig();\n }\n\n /** Listen for configuration changes (e.g. from an onboarding flow). */\n private _subscribeToConfig(): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = this._cfg.subscribe(() => {\n void (async () => {\n const cfg = this._cfg.getModelConfig();\n\n // Guard: only react if the model config actually changed relative to our state\n if (cfg.defaultProvider === this._currentProviderId &&\n cfg.defaultModel === this._currentModelId &&\n this._providerModels.length > 0) {\n // …but a config change is not only a MODEL change. A language\n // switch fires this subscription too, hit this guard, and left\n // the placeholder in the previous language — the one string this\n // element takes from the locale, and the only one a user sees\n // before opening the list.\n //\n // The guard stays: a full `_render()` re-loads every provider's\n // models asynchronously and would close an open dropdown and\n // discard a typed search. So the cheap path is a relabel — one\n // attribute, no rebuild — which is the same contract a segment\n // renderer's `relabel` works under.\n this._relabel();\n return;\n }\n\n this._loadPersistedSelection();\n await this._loadAllProviderModels();\n this._render();\n })();\n });\n }\n\n /**\n * Re-apply the one string this element takes from the locale, in place.\n *\n * An explicit `placeholder` attribute still wins, exactly as it does at render\n * time: the app's own copy is not the locale's to overwrite.\n */\n private _relabel(): void {\n if (!this._aparteSelect) return;\n const fromLocale = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n this._aparteSelect.setAttribute('placeholder', this.getAttribute('placeholder') || fromLocale);\n }\n\n disconnectedCallback(): void {\n this._aparteSelect?.removeEventListener('aparte-select-change', this._boundHandleChange);\n this.removeEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n this._composer?.removeEventListener('aparte-composer-change', this._boundComposerChange);\n this._composer = null;\n this._configUnsubscribe?.();\n }\n\n /**\n * The boundary above us appeared or changed: move the subscription and reload,\n * because a different config means different providers and a different\n * selection.\n *\n * See {@link AparteConfigAware}. The element's own per-instance tests passed\n * before this existed only because they mount the boundary FIRST — the one\n * ordering no wrapper produces.\n */\n aparteConfigChanged(next: AparteConfig): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = null;\n this._cfg = next;\n if (!this.isConnected) return;\n void (async () => {\n this._isLoading = false;\n this._providerModels = [];\n const model = this._cfg.getModelConfig();\n if (model.defaultProvider && model.defaultModel) {\n this._currentProviderId = model.defaultProvider;\n this._currentModelId = model.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n await this._loadAllProviderModels();\n this._render();\n this._subscribeToConfig();\n })();\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && !this._isLoading) {\n this._render();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Current provider ID. */\n get providerId(): string | null {\n return this._currentProviderId;\n }\n\n /** Current model ID. */\n get modelId(): string | null {\n return this._currentModelId;\n }\n\n /** Programmatically set the selection. */\n setSelection(providerId: string, modelId: string): void {\n this._currentProviderId = providerId;\n this._currentModelId = modelId;\n\n const compositeValue = `${providerId}::${modelId}`;\n if (this._aparteSelect) {\n this._aparteSelect.value = compositeValue;\n }\n\n this._emitChange();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Data Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n private async _loadAllProviderModels(): Promise<void> {\n if (this._isLoading) return;\n this._isLoading = true;\n\n try {\n const providers = this._cfg.getAIProviders();\n\n // Deduplicate providers by ID just in case\n const uniqueProviders = Array.from(new Map(providers.map(p => [p.id, p])).values());\n\n const config = this._cfg.getModelConfig();\n\n // Store in a temporary list to avoid mid-render state corruption.\n //\n // Indexed by REGISTRATION order, not filled by completion order. The\n // fetches run in parallel, so a `push` here ordered the dropdown — and\n // therefore what `auto-select` picks — by whichever /models endpoint\n // answered first. A cloud provider on a CDN beats a local server that\n // has to wake up, so an app registering `[OLLAMA, LMSTUDIO, OPENROUTER]`\n // could silently land on the paid one, and land on a different one on\n // the next reload. `auto-select` documents itself as \"the first model\";\n // first has to mean first, not fastest.\n const tempList: (ProviderModels | undefined)[] = new Array(uniqueProviders.length);\n\n // Fetch all providers in parallel\n await Promise.all(uniqueProviders.map(async (provider, i) => {\n try {\n const models = await this._cfg.refreshProviderModels(provider.id);\n\n // Apply model filters if configured\n const filters = config.modelFilters?.[provider.id];\n const filteredModels = filters?.length\n ? models.filter(m => filters.includes(m.id))\n : models;\n\n if (filteredModels.length > 0) {\n tempList[i] = { provider, models: filteredModels };\n }\n } catch (error) {\n console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);\n }\n }));\n\n // The holes are the providers that failed or returned nothing: dropped,\n // which keeps the order of the survivors.\n this._providerModels = tempList.filter((pm): pm is ProviderModels => pm !== undefined);\n } finally {\n this._isLoading = false;\n }\n }\n\n private async _onOptgroupToggle(e: Event): Promise<void> {\n const detail = (e as CustomEvent).detail;\n\n // Track expansion state\n if (detail.collapsed) {\n this._expandedGroups.delete(detail.label);\n return;\n }\n\n this._expandedGroups.add(detail.label);\n\n // Find which provider this optgroup belongs to\n const pm = this._providerModels.find(p => p.provider.getMetadata().name === detail.label);\n if (!pm || !pm.provider.fetchModels) return;\n\n // Show a loading state on the optgroup while we lazily fetch its models\n const groupEl = e.target as HTMLElement;\n if (groupEl && groupEl.tagName === 'APARTE-OPTGROUP') {\n (groupEl as AparteOptgroup).loading = true;\n }\n\n try {\n // Retrieve latest models via Config (provider-agnostic)\n const freshModels = await this._cfg.refreshProviderModels(pm.provider.id);\n\n if (freshModels && freshModels.length > 0) {\n pm.models = freshModels;\n this._render(true);\n }\n } catch (error) {\n console.error(`[AparteModelSelector] Lazy fetch failed for ${pm.provider.id}:`, error);\n } finally {\n const el = e.target as HTMLElement;\n if (el && el.tagName === 'APARTE-OPTGROUP') {\n (el as AparteOptgroup).loading = false;\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Our own attribute, or the disabled state of the composer we sit in. */\n private _isDisabled(): boolean {\n return this.hasAttribute('disabled') || (this._composer?.hasAttribute('disabled') ?? false);\n }\n\n private _syncDisabled(): void {\n this._aparteSelect?.toggleAttribute('disabled', this._isDisabled());\n }\n\n private _render(keepOpen = false): void {\n if (this._isRendering) return;\n this._isRendering = true;\n\n try {\n this._renderImpl(keepOpen);\n } finally {\n this._isRendering = false;\n }\n }\n\n private _renderImpl(keepOpen: boolean): void {\n // Attribute overrides locale\n const localePlaceholder = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n const placeholder = this.getAttribute('placeholder') || localePlaceholder;\n const searchable = this.hasAttribute('searchable');\n const autoSelect = this.hasAttribute('auto-select');\n\n // Capture current open state of aparte-select if it exists\n const wasOpen = keepOpen || this._aparteSelect?.hasAttribute('open');\n\n // Determine current value\n let currentValue = this._currentModelId ? `${this._currentProviderId}::${this._currentModelId}` : '';\n\n if (!currentValue && autoSelect) {\n const first = this._providerModels[0];\n const firstModel = first?.models[0];\n if (first && firstModel) {\n this._currentProviderId = first.provider.id;\n this._currentModelId = firstModel.id;\n currentValue = `${this._currentProviderId}::${this._currentModelId}`;\n // Emit the initial change once the element settles\n setTimeout(() => this._emitChange(), 0);\n }\n }\n\n // Build options HTML — single provider: flat list; multiple: grouped optgroups\n const only = this._providerModels[0];\n const singleProvider = this._providerModels.length === 1;\n const optionsHtml = singleProvider && only // safe-text: every value below goes through esc(); the tags are this file's own\n ? only.models.map(m => {\n const key = `${only.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')\n : this._providerModels.map(pm => {\n const label = pm.provider.getMetadata().name;\n const isCollapsed = !this._expandedGroups.has(label);\n return `\n <aparte-optgroup\n label=\"${esc(label)}\"\n collapsible\n ${isCollapsed ? 'collapsed' : ''}\n >\n ${pm.models.map(m => {\n const key = `${pm.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}</aparte-option>`;\n }).join('')}\n </aparte-optgroup>\n `;\n }).join('');\n\n if (this._aparteSelect) {\n // Non-destructive update of attributes\n this._aparteSelect.setAttribute('placeholder', placeholder);\n if (currentValue) this._aparteSelect.setAttribute('value', currentValue);\n else this._aparteSelect.removeAttribute('value');\n\n if (searchable) this._aparteSelect.setAttribute('searchable', '');\n else this._aparteSelect.removeAttribute('searchable');\n\n this._syncDisabled();\n\n if (wasOpen) this._aparteSelect.setAttribute('open', '');\n else this._aparteSelect.removeAttribute('open');\n\n // Update children cleanly: prefer direct container update if available\n const optionsContainer = this._aparteSelect.querySelector('.aparte-select-options');\n if (optionsContainer) {\n // Direct update (faster, skips the observer)\n optionsContainer.innerHTML = optionsHtml;\n\n // Force label re-evaluation: remove then re-set value so aparte-select\n // re-scans the newly injected options (setAttribute guard blocks same-value updates)\n if (currentValue) {\n this._aparteSelect.removeAttribute('value');\n this._aparteSelect.setAttribute('value', currentValue);\n }\n } else {\n // Fallback: self-healing mode\n const temp = document.createElement('div');\n temp.innerHTML = optionsHtml;\n const newChildren = Array.from(temp.children);\n\n const childrenToRemove = Array.from(this._aparteSelect.children).filter(c =>\n c.tagName === 'APARTE-OPTION' || c.tagName === 'APARTE-OPTGROUP'\n );\n childrenToRemove.forEach(c => c.remove());\n\n newChildren.forEach(c => this._aparteSelect?.appendChild(c));\n }\n\n // Critical: re-apply value after a microtask so AparteSelect has processed\n // the new options and can resolve the label.\n if (currentValue) {\n setTimeout(() => {\n if (this._aparteSelect) {\n this._aparteSelect.value = currentValue;\n }\n }, 0);\n }\n } else {\n // Full initial render.\n //\n // The select carries no class of ours: this element is already addressable\n // as `aparte-model-selector aparte-select`, which is what a consumer\n // restyling it writes. The `aparte-model-selector-select` that used to be\n // here carried no CSS and was queried by nothing — it named the element a\n // second time, and a class that neither styles nor selects is not a hook.\n this.innerHTML = `\n <aparte-select\n placeholder=\"${esc(placeholder)}\"\n ${currentValue ? `value=\"${esc(currentValue)}\"` : ''}\n ${searchable ? 'searchable' : ''}\n ${this._isDisabled() ? 'disabled' : ''}\n ${wasOpen ? 'open' : ''}\n >\n ${optionsHtml}\n </aparte-select>\n `;\n this._aparteSelect = this.querySelector('aparte-select');\n this._setupEventListeners();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Event Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _setupEventListeners(): void {\n this._aparteSelect?.addEventListener('aparte-select-change', this._boundHandleChange);\n this.addEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n }\n\n private _handleChange(e: Event): void {\n const detail = (e as CustomEvent<AparteSelectChangeDetail>).detail;\n const previousProviderId = this._currentProviderId;\n const previousModelId = this._currentModelId;\n\n // Parse composite value \"providerId::modelId\"\n const [providerId, modelId] = detail.value.split('::');\n\n this._currentProviderId = providerId || null;\n this._currentModelId = modelId || null;\n\n this._emitChange(previousProviderId, previousModelId);\n }\n\n private _emitChange(previousProviderId?: string | null, previousModelId?: string | null): void {\n const detail: AparteModelChangeEventDetail = {\n providerId: this._currentProviderId || undefined,\n modelId: this._currentModelId || '',\n previousProviderId: previousProviderId || undefined,\n previousModelId: previousModelId || undefined,\n };\n\n this.dispatchEvent(new CustomEvent<AparteModelChangeEventDetail>('aparte-model-change', {\n bubbles: true,\n composed: true,\n detail,\n }));\n\n // Persist if enabled\n if (this.hasAttribute('persist')) {\n this._persistSelection();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Persistence (opt-in)\n // ─────────────────────────────────────────────────────────────────────────\n\n private _persistSelection(): void {\n if (!this._currentProviderId || !this._currentModelId) return;\n\n // Sync with the config — auto-save is handled by its setModelConfig()\n // if a AparteModelPreferenceProvider has been registered by the host app.\n const currentConfig = this._cfg.getModelConfig();\n if (currentConfig.defaultProvider === this._currentProviderId &&\n currentConfig.defaultModel === this._currentModelId) {\n return;\n }\n\n this._cfg.setModelConfig({\n defaultProvider: this._currentProviderId,\n defaultModel: this._currentModelId,\n });\n }\n\n private _loadPersistedSelection(): void {\n // The resolved config is the single source of truth. The host app is\n // responsible for restoring preferences into it (via restoreModelPreference())\n // before this component mounts.\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n }\n // else: no-op — the config is empty\n }\n}\n\n// Register the custom element\nif (!customElements.get('aparte-model-selector')) {\n customElements.define('aparte-model-selector', AparteModelSelector);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-model-selector': AparteModelSelector;\n }\n}\n"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sources":["../src/aparte-model-selector.ts"],"sourcesContent":["import {\n aparteGlobalConfig,\n escapeAttr,\n resolveConfig,\n type AparteConfig,\n type AparteConfigAware,\n AparteSelect,\n type AparteOptgroup,\n type AparteAIProvider,\n type AparteAIModel,\n type AparteModelChangeEventDetail,\n type AparteSelectChangeDetail,\n} from '@aparte/core';\n\ninterface ProviderModels {\n provider: AparteAIProvider;\n models: AparteAIModel[];\n}\n\n/**\n * Escape a value before it is interpolated into an HTML string (text node or\n * double-quoted attribute). Model names/ids come from a remote `/models`\n * endpoint and provider labels from consumer config — both hostile-by-default,\n * and both flow through `innerHTML` when the option list is (re)built.\n */\nfunction esc(value: unknown): string {\n // Coerce, then delegate: core owns the escaping. This local copy used to\n // inline it and, like two of its eight siblings across the scope, left the\n // apostrophe through — enough to break out of a single-quoted attribute.\n return escapeAttr(String(value ?? ''));\n}\n\n/**\n * Selects an AI provider and a model. Renders an `<aparte-select>` with providers\n * grouped into `<aparte-optgroup>`s — a single provider renders a flat list — which is\n * what makes the BYOK pattern possible without core knowing about any vendor.\n *\n * These tags sat at the top of the file, above the imports and a helper, so nothing\n * read them: TypeScript associates only the comment adjacent to the declaration. This\n * package ships no manifest of its own today (only `@aparte/core` runs `cem analyze`),\n * so the loss was invisible to tooling — but it is also what an editor shows on hover,\n * and what a typed framework wrapper is written from.\n *\n * @element aparte-model-selector\n * @attr {boolean} auto-select - Selects the first model as soon as one is available.\n * @attr {boolean} persist - Writes the selection into the config, which is what the next message is sent with.\n * Without it the picker only fires `aparte-model-change`, and applying the choice is the app's. It does not\n * outlive a reload on its own: that takes `setModelPreferenceProvider()` and `restoreModelPreference()`.\n * @attr {boolean} searchable - Adds the dropdown's filter field.\n * @attr {boolean} details - Shows, under each model's name, its `description` and its `contextWindow` — the metadata a provider already declares. The trigger keeps the name alone; the filter searches the line too.\n * @attr {string} placeholder - Overrides the text shown while nothing is selected.\n * @attr {boolean} disabled - Disables the picker. Inside an `<aparte-composer>` it also follows the\n * composer's own `disabled`, so a disabled composer does not leave its model picker operable.\n *\n * @fires {CustomEvent<AparteModelChangeEventDetail>} aparte-model-change - The provider or the model changed; carries both ids.\n *\n * @example\n * <!-- Mount it in the composer's toolbar; it reads the models the config already knows. -->\n * <aparte-composer-toolbar>\n * <aparte-model-selector searchable placeholder=\"Pick a model\"></aparte-model-selector>\n * </aparte-composer-toolbar>\n */\nexport class AparteModelSelector extends HTMLElement implements AparteConfigAware {\n private _currentProviderId: string | null = null;\n private _currentModelId: string | null = null;\n private _providerModels: ProviderModels[] = [];\n private _aparteSelect: AparteSelect | null = null;\n private _isLoading = false;\n private _isRendering = false;\n /** A load was asked for while one ran; the running one goes round again. */\n private _reloadQueued = false;\n /** Bumped by {@link _restartLoading}: a load of an older generation discards its result. */\n private _loadGeneration = 0;\n /** The providers the current list was built from, as {@link _providersKey} gives them. */\n private _loadedProvidersKey = '';\n private _expandedGroups: Set<string> = new Set();\n\n /**\n * Config governing THIS element — nearest instance boundary, else the global.\n *\n * Cached rather than resolved per call, because this element does not only\n * READ it: it holds a `subscribe()` registration keyed on the object. That\n * cache is only correct if something tells us when the boundary changes, and\n * under all four wrappers it does change after we connect —\n * `AparteChatHost.bind()` runs `attachConfig` from a post-mount hook. Hence\n * {@link AparteConfigAware}; without it this element served the global config's\n * providers to a chat that was given its own.\n */\n private _cfg: AparteConfig = aparteGlobalConfig;\n\n private _configUnsubscribe: (() => void) | null = null;\n\n // Bound handlers for cleanup\n private _boundHandleChange = this._handleChange.bind(this);\n private _handleOptgroupToggle = (e: Event): void => { void this._onOptgroupToggle(e); };\n /** The composer we sit in, if any — its `disabled` is ours too. */\n private _composer: Element | null = null;\n private _boundComposerChange = (): void => { this._syncDisabled(); };\n\n static get observedAttributes(): string[] {\n return ['persist', 'auto-select', 'searchable', 'placeholder', 'disabled', 'details'];\n }\n\n async connectedCallback(): Promise<void> {\n this._cfg = resolveConfig(this);\n // A fresh mount always does a full load, whatever a previous mount left running.\n this._restartLoading();\n this._providerModels = [];\n\n this._setupEventListeners();\n // The public event, not the composer's private bus: a disabled composer says so\n // through `aparte-composer-change`, and the picker inside it follows.\n this._composer = this.closest('aparte-composer');\n this._composer?.addEventListener('aparte-composer-change', this._boundComposerChange);\n\n // Seed selection from the resolved config first (handles race conditions)\n this._seenConfigSelection = this._configSelectionKey();\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n\n // Subscribe BEFORE the first load, not after it. Importing this plugin defines the\n // element, and a tag already in the page connects right then — before the module\n // that imported it has run the rest of its body, which is where the providers get\n // registered. Subscribed after the `await`, that registration fired into nothing\n // and the picker stayed empty for the life of the page.\n this._subscribeToConfig();\n if (await this._loadAllProviderModels()) this._render();\n }\n\n /**\n * The config's own selection when this element last read it. A change to IT is someone\n * choosing a model (an onboarding flow, `setModelConfig`); every other config change — a\n * language, a provider, an icon — leaves the person's own pick alone. Compared with the\n * pick instead, a pick made without `persist` (which writes nothing to the config) was\n * put back to the config's default by the next unrelated change.\n */\n private _seenConfigSelection = '';\n\n private _configSelectionKey(): string {\n const c = this._cfg.getModelConfig();\n return `${c.defaultProvider ?? ''}::${c.defaultModel ?? ''}`;\n }\n\n /** The registered providers, as one comparable string: what the list was built from. */\n private _providersKey(): string {\n return this._cfg.getAIProviders().map(p => p.id).join('\\n');\n }\n\n /** Listen for configuration changes (e.g. from an onboarding flow). */\n private _subscribeToConfig(): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = this._cfg.subscribe(() => {\n void (async () => {\n // Guard: only react if the model config actually changed relative to what we\n // read — its selection, or the set of providers the list was built from\n // (an app that registers a provider once a key is typed in its settings).\n const selectionMoved = this._configSelectionKey() !== this._seenConfigSelection;\n if (!selectionMoved &&\n this._providerModels.length > 0 &&\n this._providersKey() === this._loadedProvidersKey) {\n // …but a config change is not only a MODEL change. A language\n // switch fires this subscription too, hit this guard, and left\n // the placeholder in the previous language — the one string this\n // element takes from the locale, and the only one a user sees\n // before opening the list.\n //\n // The guard stays: a full `_render()` re-loads every provider's\n // models asynchronously and would close an open dropdown and\n // discard a typed search. So the cheap path is a relabel — one\n // attribute, no rebuild — which is the same contract a segment\n // renderer's `relabel` works under.\n this._relabel();\n return;\n }\n\n if (selectionMoved) {\n this._seenConfigSelection = this._configSelectionKey();\n this._loadPersistedSelection();\n }\n if (await this._loadAllProviderModels()) this._render();\n })();\n });\n }\n\n /**\n * Re-apply the one string this element takes from the locale, in place.\n *\n * An explicit `placeholder` attribute still wins, exactly as it does at render\n * time: the app's own copy is not the locale's to overwrite.\n */\n private _relabel(): void {\n if (!this._aparteSelect) return;\n const fromLocale = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n this._aparteSelect.setAttribute('placeholder', this.getAttribute('placeholder') || fromLocale);\n // The `details` lines are locale text too (\"{n} context\", the number's format): written\n // in place, so an open list and a typed search survive the language switch.\n if (!this.hasAttribute('details')) return;\n // One lookup of the options and one number format for the whole list: a catalogue\n // of hundreds of models is relabelled on every config change.\n const options = new Map(Array.from(this.querySelectorAll('aparte-option'), (o) => [o.getAttribute('value'), o]));\n const tokens = this._tokenFormat();\n for (const pm of this._providerModels) {\n for (const m of pm.models) {\n const line = options.get(`${pm.provider.id}::${m.id}`)?.querySelector('.aparte-option__meta');\n if (line) line.textContent = this._metaText(m, tokens);\n }\n }\n }\n\n /** How a token count is written in the locale's language: 128K, 128 k… */\n private _tokenFormat(): Intl.NumberFormat {\n return new Intl.NumberFormat(this._cfg.getLocale().tag || undefined, { notation: 'compact', maximumFractionDigits: 1 });\n }\n\n /** The line under a model's name with `details`: what its provider declares about it. */\n private _metaText(m: { description?: string; contextWindow?: number }, tokens: Intl.NumberFormat): string {\n const parts: string[] = [];\n if (m.description) parts.push(m.description);\n if (m.contextWindow && m.contextWindow > 0) {\n const size = tokens.format(m.contextWindow);\n parts.push((this._cfg.t('modelContextWindow') || '{n} context').replace('{n}', () => size));\n }\n return parts.join(' · ');\n }\n\n disconnectedCallback(): void {\n this._aparteSelect?.removeEventListener('aparte-select-change', this._boundHandleChange);\n this.removeEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n this._composer?.removeEventListener('aparte-composer-change', this._boundComposerChange);\n this._composer = null;\n this._configUnsubscribe?.();\n }\n\n /**\n * The boundary above us appeared or changed: move the subscription and reload,\n * because a different config means different providers and a different\n * selection.\n *\n * See {@link AparteConfigAware}. The element's own per-instance tests passed\n * before this existed only because they mount the boundary FIRST — the one\n * ordering no wrapper produces.\n */\n aparteConfigChanged(next: AparteConfig): void {\n this._configUnsubscribe?.();\n this._configUnsubscribe = null;\n this._cfg = next;\n if (!this.isConnected) return;\n void (async () => {\n this._restartLoading();\n this._providerModels = [];\n this._seenConfigSelection = this._configSelectionKey();\n const model = this._cfg.getModelConfig();\n if (model.defaultProvider && model.defaultModel) {\n this._currentProviderId = model.defaultProvider;\n this._currentModelId = model.defaultModel;\n } else {\n this._loadPersistedSelection();\n }\n this._subscribeToConfig();\n if (await this._loadAllProviderModels()) this._render();\n })();\n }\n\n attributeChangedCallback(): void {\n if (this.isConnected && !this._isLoading) {\n this._render();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Public API\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Current provider ID. */\n get providerId(): string | null {\n return this._currentProviderId;\n }\n\n /** Current model ID. */\n get modelId(): string | null {\n return this._currentModelId;\n }\n\n /** Programmatically set the selection. */\n setSelection(providerId: string, modelId: string): void {\n this._currentProviderId = providerId;\n this._currentModelId = modelId;\n\n const compositeValue = `${providerId}::${modelId}`;\n if (this._aparteSelect) {\n this._aparteSelect.value = compositeValue;\n }\n\n this._emitChange();\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Data Loading\n // ─────────────────────────────────────────────────────────────────────────\n\n /**\n * Load every provider's models. A call that arrives while a load runs does not start a\n * second one and does not vanish either: the running load goes round again, reading\n * what changed meanwhile, and answers `false` so its caller leaves the render to the\n * load that is still running.\n */\n private async _loadAllProviderModels(): Promise<boolean> {\n if (this._isLoading) {\n this._reloadQueued = true;\n return false;\n }\n const generation = this._loadGeneration;\n this._isLoading = true;\n try {\n do {\n this._reloadQueued = false;\n await this._loadOnce(generation);\n } while (this._reloadQueued && this.isConnected && generation === this._loadGeneration);\n } finally {\n // A restart already handed the flag to the load that replaced this one.\n if (generation === this._loadGeneration) this._isLoading = false;\n }\n return generation === this._loadGeneration;\n }\n\n /**\n * Start over: a mount, or a new config above us. A load still in flight from before\n * keeps running — its fetches cannot be called back — but it is now of an older\n * generation, so it discards what it brings back instead of writing it over the list\n * the new load builds. Under every wrapper this is the normal order: the element\n * connects on the global config, then `bind()` attaches the chat's own, and a slow\n * global `/models` used to land last and win.\n */\n private _restartLoading(): void {\n this._loadGeneration++;\n this._isLoading = false;\n this._reloadQueued = false;\n }\n\n private async _loadOnce(generation: number): Promise<void> {\n const providersKey = this._providersKey();\n const providers = this._cfg.getAIProviders();\n\n // Deduplicate providers by ID just in case\n const uniqueProviders = Array.from(new Map(providers.map(p => [p.id, p])).values());\n\n const config = this._cfg.getModelConfig();\n\n // Store in a temporary list to avoid mid-render state corruption.\n //\n // Indexed by REGISTRATION order, not filled by completion order. The\n // fetches run in parallel, so a `push` here ordered the dropdown — and\n // therefore what `auto-select` picks — by whichever /models endpoint\n // answered first. A cloud provider on a CDN beats a local server that\n // has to wake up, so an app registering `[OLLAMA, LMSTUDIO, OPENROUTER]`\n // could silently land on the paid one, and land on a different one on\n // the next reload. `auto-select` documents itself as \"the first model\";\n // first has to mean first, not fastest.\n const tempList: (ProviderModels | undefined)[] = new Array(uniqueProviders.length);\n\n // Fetch all providers in parallel\n await Promise.all(uniqueProviders.map(async (provider, i) => {\n try {\n // A provider with a fixed list has no `fetchModels`: its `getModels()` IS the\n // list, and `refreshProviderModels` answers `[]` for it.\n const models = provider.fetchModels\n ? await this._cfg.refreshProviderModels(provider.id)\n : provider.getModels();\n\n // Apply model filters if configured\n const filters = config.modelFilters?.[provider.id];\n const filteredModels = filters?.length\n ? models.filter(m => filters.includes(m.id))\n : models;\n\n if (filteredModels.length > 0) {\n tempList[i] = { provider, models: filteredModels };\n }\n } catch (error) {\n console.warn(`[AparteModelSelector] Failed to load models for ${provider.id}:`, error);\n }\n }));\n\n if (generation !== this._loadGeneration) return;\n // The holes are the providers that failed or returned nothing: dropped,\n // which keeps the order of the survivors.\n this._providerModels = tempList.filter((pm): pm is ProviderModels => pm !== undefined);\n this._loadedProvidersKey = providersKey;\n }\n\n private async _onOptgroupToggle(e: Event): Promise<void> {\n const detail = (e as CustomEvent).detail;\n\n // Track expansion state\n if (detail.collapsed) {\n this._expandedGroups.delete(detail.label);\n return;\n }\n\n this._expandedGroups.add(detail.label);\n\n // Find which provider this optgroup belongs to\n const pm = this._providerModels.find(p => p.provider.getMetadata().name === detail.label);\n if (!pm || !pm.provider.fetchModels) return;\n\n // Show a loading state on the optgroup while we lazily fetch its models\n const groupEl = e.target as HTMLElement;\n if (groupEl && groupEl.tagName === 'APARTE-OPTGROUP') {\n (groupEl as AparteOptgroup).loading = true;\n }\n\n try {\n // Retrieve latest models via Config (provider-agnostic)\n const freshModels = await this._cfg.refreshProviderModels(pm.provider.id);\n\n if (freshModels && freshModels.length > 0) {\n pm.models = freshModels;\n this._render(true);\n }\n } catch (error) {\n console.error(`[AparteModelSelector] Lazy fetch failed for ${pm.provider.id}:`, error);\n } finally {\n const el = e.target as HTMLElement;\n if (el && el.tagName === 'APARTE-OPTGROUP') {\n (el as AparteOptgroup).loading = false;\n }\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Rendering\n // ─────────────────────────────────────────────────────────────────────────\n\n /** Our own attribute, or the disabled state of the composer we sit in. */\n private _isDisabled(): boolean {\n return this.hasAttribute('disabled') || (this._composer?.hasAttribute('disabled') ?? false);\n }\n\n private _syncDisabled(): void {\n this._aparteSelect?.toggleAttribute('disabled', this._isDisabled());\n }\n\n private _render(keepOpen = false): void {\n if (this._isRendering) return;\n this._isRendering = true;\n\n try {\n this._renderImpl(keepOpen);\n } finally {\n this._isRendering = false;\n }\n }\n\n private _renderImpl(keepOpen: boolean): void {\n // Attribute overrides locale\n const localePlaceholder = this._cfg.getLocale()['modelSelectorPlaceholder'] || 'Select a model...';\n const placeholder = this.getAttribute('placeholder') || localePlaceholder;\n const searchable = this.hasAttribute('searchable');\n // The line under a name: what the provider declares about the model, when asked for.\n const details = this.hasAttribute('details');\n const tokens = details ? this._tokenFormat() : null;\n const meta = (m: { description?: string; contextWindow?: number }): string => {\n const text = tokens ? this._metaText(m, tokens) : '';\n return text ? `<span class=\"aparte-option__meta\">${esc(text)}</span>` : '';\n };\n const autoSelect = this.hasAttribute('auto-select');\n\n // Capture current open state of aparte-select if it exists\n const wasOpen = keepOpen || this._aparteSelect?.hasAttribute('open');\n\n // Determine current value\n let currentValue = this._currentModelId ? `${this._currentProviderId}::${this._currentModelId}` : '';\n\n if (!currentValue && autoSelect) {\n const first = this._providerModels[0];\n const firstModel = first?.models[0];\n if (first && firstModel) {\n this._currentProviderId = first.provider.id;\n this._currentModelId = firstModel.id;\n currentValue = `${this._currentProviderId}::${this._currentModelId}`;\n // Emit the initial change once the element settles\n setTimeout(() => this._emitChange(), 0);\n }\n }\n\n // Build options HTML — single provider: flat list; multiple: grouped optgroups\n const only = this._providerModels[0];\n const singleProvider = this._providerModels.length === 1;\n const optionsHtml = singleProvider && only // safe-text: every value below goes through esc(); the tags are this file's own\n ? only.models.map(m => {\n const key = `${only.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}${meta(m)}</aparte-option>`;\n }).join('')\n : this._providerModels.map(pm => {\n const label = pm.provider.getMetadata().name;\n const isCollapsed = !this._expandedGroups.has(label);\n return `\n <aparte-optgroup\n label=\"${esc(label)}\"\n collapsible\n ${isCollapsed ? 'collapsed' : ''}\n >\n ${pm.models.map(m => {\n const key = `${pm.provider.id}::${m.id}`;\n return `<aparte-option value=\"${esc(key)}\">${esc(m.name)}${meta(m)}</aparte-option>`;\n }).join('')}\n </aparte-optgroup>\n `;\n }).join('');\n\n if (this._aparteSelect) {\n // Non-destructive update of attributes\n this._aparteSelect.setAttribute('placeholder', placeholder);\n if (currentValue) this._aparteSelect.setAttribute('value', currentValue);\n else this._aparteSelect.removeAttribute('value');\n\n if (searchable) this._aparteSelect.setAttribute('searchable', '');\n else this._aparteSelect.removeAttribute('searchable');\n\n this._syncDisabled();\n\n if (wasOpen) this._aparteSelect.setAttribute('open', '');\n else this._aparteSelect.removeAttribute('open');\n\n // Update children cleanly: prefer direct container update if available\n const optionsContainer = this._aparteSelect.querySelector('.aparte-select-options');\n if (optionsContainer) {\n // Direct update (faster, skips the observer)\n optionsContainer.innerHTML = optionsHtml;\n\n // Force label re-evaluation: remove then re-set value so aparte-select\n // re-scans the newly injected options (setAttribute guard blocks same-value updates)\n if (currentValue) {\n this._aparteSelect.removeAttribute('value');\n this._aparteSelect.setAttribute('value', currentValue);\n }\n } else {\n // Fallback: self-healing mode\n const temp = document.createElement('div');\n temp.innerHTML = optionsHtml;\n const newChildren = Array.from(temp.children);\n\n const childrenToRemove = Array.from(this._aparteSelect.children).filter(c =>\n c.tagName === 'APARTE-OPTION' || c.tagName === 'APARTE-OPTGROUP'\n );\n childrenToRemove.forEach(c => c.remove());\n\n newChildren.forEach(c => this._aparteSelect?.appendChild(c));\n }\n\n // Critical: re-apply value after a microtask so AparteSelect has processed\n // the new options and can resolve the label.\n if (currentValue) {\n setTimeout(() => {\n if (this._aparteSelect) {\n this._aparteSelect.value = currentValue;\n }\n }, 0);\n }\n } else {\n // Full initial render.\n //\n // The select carries no class of ours: this element is already addressable\n // as `aparte-model-selector aparte-select`, which is what a consumer\n // restyling it writes. The `aparte-model-selector-select` that used to be\n // here carried no CSS and was queried by nothing — it named the element a\n // second time, and a class that neither styles nor selects is not a hook.\n this.innerHTML = `\n <aparte-select\n placeholder=\"${esc(placeholder)}\"\n ${currentValue ? `value=\"${esc(currentValue)}\"` : ''}\n ${searchable ? 'searchable' : ''}\n ${this._isDisabled() ? 'disabled' : ''}\n ${wasOpen ? 'open' : ''}\n >\n ${optionsHtml}\n </aparte-select>\n `;\n this._aparteSelect = this.querySelector('aparte-select');\n this._setupEventListeners();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Event Handling\n // ─────────────────────────────────────────────────────────────────────────\n\n private _setupEventListeners(): void {\n this._aparteSelect?.addEventListener('aparte-select-change', this._boundHandleChange);\n this.addEventListener('aparte-optgroup-toggle', this._handleOptgroupToggle);\n }\n\n private _handleChange(e: Event): void {\n const detail = (e as CustomEvent<AparteSelectChangeDetail>).detail;\n const previousProviderId = this._currentProviderId;\n const previousModelId = this._currentModelId;\n\n // Parse composite value \"providerId::modelId\"\n const [providerId, modelId] = detail.value.split('::');\n\n this._currentProviderId = providerId || null;\n this._currentModelId = modelId || null;\n\n this._emitChange(previousProviderId, previousModelId);\n }\n\n private _emitChange(previousProviderId?: string | null, previousModelId?: string | null): void {\n const detail: AparteModelChangeEventDetail = {\n providerId: this._currentProviderId || undefined,\n modelId: this._currentModelId || '',\n previousProviderId: previousProviderId || undefined,\n previousModelId: previousModelId || undefined,\n };\n\n this.dispatchEvent(new CustomEvent<AparteModelChangeEventDetail>('aparte-model-change', {\n bubbles: true,\n composed: true,\n detail,\n }));\n\n // Persist if enabled\n if (this.hasAttribute('persist')) {\n this._persistSelection();\n }\n }\n\n // ─────────────────────────────────────────────────────────────────────────\n // Persistence (opt-in)\n // ─────────────────────────────────────────────────────────────────────────\n\n private _persistSelection(): void {\n if (!this._currentProviderId || !this._currentModelId) return;\n\n // Sync with the config — auto-save is handled by its setModelConfig()\n // if a AparteModelPreferenceProvider has been registered by the host app.\n const currentConfig = this._cfg.getModelConfig();\n if (currentConfig.defaultProvider === this._currentProviderId &&\n currentConfig.defaultModel === this._currentModelId) {\n return;\n }\n\n this._seenConfigSelection = `${this._currentProviderId}::${this._currentModelId}`;\n this._cfg.setModelConfig({\n defaultProvider: this._currentProviderId,\n defaultModel: this._currentModelId,\n });\n }\n\n private _loadPersistedSelection(): void {\n // The resolved config is the single source of truth. The host app is\n // responsible for restoring preferences into it (via restoreModelPreference())\n // before this component mounts.\n const config = this._cfg.getModelConfig();\n if (config.defaultProvider && config.defaultModel) {\n this._currentProviderId = config.defaultProvider;\n this._currentModelId = config.defaultModel;\n }\n // else: no-op — the config is empty\n }\n}\n\n// Register the custom element\nif (!customElements.get('aparte-model-selector')) {\n customElements.define('aparte-model-selector', AparteModelSelector);\n}\n\ndeclare global {\n interface HTMLElementTagNameMap {\n 'aparte-model-selector': AparteModelSelector;\n }\n}\n"],"names":[],"mappings":"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"}
package/dist/svelte.d.ts CHANGED
@@ -18,6 +18,8 @@ import type { AparteElementAttributes, AparteElementTagName } from './generated/
18
18
  type AparteEventName = Extract<keyof HTMLElementEventMap, `aparte-${string}`>;
19
19
  type PluginHandlers = {
20
20
  [K in AparteEventName as `on:${K}`]?: (event: HTMLElementEventMap[K]) => void;
21
+ } & {
22
+ [K in AparteEventName as `on${K}`]?: (event: HTMLElementEventMap[K]) => void;
21
23
  };
22
24
  type PluginElements = {
23
25
  [K in AparteElementTagName]: HTMLAttributes<HTMLElement> & AparteTemplateAttrs<AparteElementAttributes[K]> & PluginHandlers;
@@ -1 +1 @@
1
- {"version":3,"file":"svelte.d.ts","sourceRoot":"","sources":["../src/svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEvG,KAAK,eAAe,GAAG,OAAO,CAAC,MAAM,mBAAmB,EAAE,UAAU,MAAM,EAAE,CAAC,CAAC;AAE9E,KAAK,cAAc,GAAG;KACjB,CAAC,IAAI,eAAe,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI;CAChF,CAAC;AAEF,KAAK,cAAc,GAAG;KACjB,CAAC,IAAI,oBAAoB,GACtB,cAAc,CAAC,WAAW,CAAC,GAAG,mBAAmB,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc;CACrG,CAAC;AAEF,OAAO,QAAQ,iBAAiB,CAAC;IAC7B,UAAU,kBAAmB,SAAQ,cAAc;KAAG;CACzD;AAED,OAAO,EAAE,CAAC"}
1
+ {"version":3,"file":"svelte.d.ts","sourceRoot":"","sources":["../src/svelte.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;GAaG;AACH,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AACtD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,cAAc,CAAC;AACxD,OAAO,KAAK,EAAE,uBAAuB,EAAE,oBAAoB,EAAE,MAAM,mCAAmC,CAAC;AAEvG,KAAK,eAAe,GAAG,OAAO,CAAC,MAAM,mBAAmB,EAAE,UAAU,MAAM,EAAE,CAAC,CAAC;AAE9E,KAAK,cAAc,GAAG;KACjB,CAAC,IAAI,eAAe,IAAI,MAAM,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI;CAChF,GAAG;KAEC,CAAC,IAAI,eAAe,IAAI,KAAK,CAAC,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,EAAE,mBAAmB,CAAC,CAAC,CAAC,KAAK,IAAI;CAC/E,CAAC;AAEF,KAAK,cAAc,GAAG;KACjB,CAAC,IAAI,oBAAoB,GACtB,cAAc,CAAC,WAAW,CAAC,GAAG,mBAAmB,CAAC,uBAAuB,CAAC,CAAC,CAAC,CAAC,GAAG,cAAc;CACrG,CAAC;AAEF,OAAO,QAAQ,iBAAiB,CAAC;IAC7B,UAAU,kBAAmB,SAAQ,cAAc;KAAG;CACzD;AAED,OAAO,EAAE,CAAC"}
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aparte/plugin-model-selector",
3
- "version": "0.19.3",
3
+ "version": "0.21.0",
4
4
  "homepage": "https://apartejs.dev/plugins/model-selector/",
5
5
  "description": "Provider + model selector web component (<aparte-model-selector>) for aparté — grouped dropdown, BYOK.",
6
6
  "type": "module",
@@ -15,6 +15,26 @@
15
15
  "types": "./dist/index.node.d.ts",
16
16
  "default": "./dist/index.node.js"
17
17
  },
18
+ "worker": {
19
+ "types": "./dist/index.node.d.ts",
20
+ "default": "./dist/index.node.js"
21
+ },
22
+ "workerd": {
23
+ "types": "./dist/index.node.d.ts",
24
+ "default": "./dist/index.node.js"
25
+ },
26
+ "edge-light": {
27
+ "types": "./dist/index.node.d.ts",
28
+ "default": "./dist/index.node.js"
29
+ },
30
+ "deno": {
31
+ "types": "./dist/index.node.d.ts",
32
+ "default": "./dist/index.node.js"
33
+ },
34
+ "bun": {
35
+ "types": "./dist/index.node.d.ts",
36
+ "default": "./dist/index.node.js"
37
+ },
18
38
  "types": "./dist/index.d.ts",
19
39
  "import": "./dist/index.js",
20
40
  "default": "./dist/index.js"
@@ -50,7 +70,7 @@
50
70
  },
51
71
  "peerDependencies": {
52
72
  "@angular/core": "^19.2.0",
53
- "@aparte/core": ">=0.19.3 <1.0.0",
73
+ "@aparte/core": ">=0.21.0 <1.0.0",
54
74
  "react": "^18.0.0 || ^19.0.0",
55
75
  "svelte": "^4.0.0 || ^5.0.0",
56
76
  "vue": "^3.5.0"
@@ -72,7 +92,7 @@
72
92
  "vite": "^6.0.0",
73
93
  "vue": "^3.5.0",
74
94
  "zone.js": "~0.15.0",
75
- "@aparte/core": "0.19.3"
95
+ "@aparte/core": "0.21.0"
76
96
  },
77
97
  "keywords": [
78
98
  "aparte",
@@ -108,7 +128,7 @@
108
128
  },
109
129
  "scripts": {
110
130
  "dev": "vite",
111
- "build": "vite build && tsc -b --emitDeclarationOnly --force && ngc -p tsconfig.angular.json && pnpm run analyze",
131
+ "build": "vite build && tsc -b --emitDeclarationOnly --force && ngc -p tsconfig.angular.json && pnpm run analyze && node ../../../scripts/stamp-dist.mjs",
112
132
  "preview": "vite preview",
113
133
  "test": "vitest",
114
134
  "test:run": "vitest run",