@frockbot/plugin-settings 0.1.3 → 0.2.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.
@@ -13,24 +13,12 @@ import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
13
13
  import {
14
14
  computed,
15
15
  inject,
16
+ nextTick,
16
17
  onBeforeUnmount,
17
18
  onMounted,
18
- reactive,
19
19
  ref,
20
- nextTick,
21
20
  watch,
22
21
  } from "vue";
23
- import {
24
- assignmentHasPendingOperation,
25
- projectAssignmentOperations,
26
- } from "./assignment-operations.js";
27
- import {
28
- describeModelAssignment,
29
- eligibleModelConnections,
30
- encodeModelSelection,
31
- modelSelectOptions,
32
- resolveBotSettingsModel,
33
- } from "./bot-settings.js";
34
22
 
35
23
  const providedSurfaces = inject(clientSurfaceRegistryKey);
36
24
  const providedWeb = inject(frockBotWebDataKey);
@@ -40,26 +28,13 @@ if (!providedSurfaces || !providedWeb) {
40
28
  const surfaces = providedSurfaces;
41
29
  const web = providedWeb;
42
30
 
43
- /*
44
- * Every row below is deep-linkable. The scheme and the anchor table live in
45
- * `@frockbot/plugin-shell/settings-links`, so a Bot citing a row and the panel
46
- * rendering it read the same list — a link nobody registered does not resolve,
47
- * and a row nobody linked has no link to copy.
48
- */
49
31
  function link(anchor: string): string {
50
32
  return settingsLinkV1({ anchor, botId: web.value.activeBotId });
51
33
  }
52
34
 
53
- /*
54
- * Model and Assignments live under the Advanced disclosure, and a collapsed
55
- * `details` cannot be scrolled to. A link into either opens it first; the User
56
- * can still close it, and the `toggle` handler keeps their choice.
57
- */
58
35
  const ADVANCED_ANCHORS = new Set([
59
36
  "bot-title",
60
37
  "bot-hidden-from-sidebar",
61
- "bot-model",
62
- "bot-capabilities",
63
38
  "bot-routines",
64
39
  "bot-audit",
65
40
  "bot-info-identity",
@@ -85,15 +60,11 @@ const description = ref("");
85
60
  const title = ref("");
86
61
  const hiddenFromSidebar = ref(false);
87
62
  const notifications = ref(false);
88
- /**
89
- * The Bot's undecided approval cards. Read from the same backend state the
90
- * conversation renders, so the two surfaces cannot disagree about what is
91
- * still waiting.
92
- */
93
63
  const pendingApprovals = computed(() =>
94
64
  web.value.approvals.filter((approval) => approval.decision === "pending"),
95
65
  );
96
66
  const decidingApproval = ref<string>();
67
+ const saving = ref(false);
97
68
 
98
69
  async function decideApproval(
99
70
  approvalId: string,
@@ -106,91 +77,6 @@ async function decideApproval(
106
77
  decidingApproval.value = undefined;
107
78
  }
108
79
  }
109
- const saving = ref(false);
110
- const modelMode = ref<"default" | "custom">("default");
111
- const assignmentBusy = ref<string>();
112
- const selectedConnections = reactive<Record<string, string>>({});
113
-
114
- const capabilityItems = computed(() =>
115
- web.value.pluginCatalog.flatMap((pkg) =>
116
- web.value.userSettings?.packages.some(
117
- (installation) =>
118
- installation.packageId === pkg.packageId &&
119
- installation.version === pkg.version &&
120
- installation.state === "installed",
121
- )
122
- ? pkg.capabilities.map((capability) => {
123
- const existing = web.value.botSettings?.assignments.find(
124
- (assignment) =>
125
- assignment.packageId === pkg.packageId &&
126
- assignment.capabilityId === capability.id,
127
- );
128
- const pending = web.value.botSettings?.assignmentOperations.find(
129
- (operation) =>
130
- operation.assignmentId === existing?.assignmentId ||
131
- (operation.target?.packageId === pkg.packageId &&
132
- operation.target.capabilityId === capability.id),
133
- );
134
- const connections =
135
- web.value.userSettings?.connections.filter(
136
- (connection) =>
137
- connection.packageId === pkg.packageId &&
138
- connection.state === "ready" &&
139
- capability.connectionTypes.includes(
140
- connection.connectionTypeId,
141
- ),
142
- ) ?? [];
143
- const key = `${pkg.packageId}:${capability.id}`;
144
- if (!(key in selectedConnections) && existing?.connectionId) {
145
- selectedConnections[key] = existing.connectionId;
146
- }
147
- return { key, pkg, capability, existing, pending, connections };
148
- })
149
- : [],
150
- ),
151
- );
152
-
153
- const assignmentOperations = computed(() =>
154
- projectAssignmentOperations(web.value.botSettings),
155
- );
156
-
157
- const orphanAssignments = computed(
158
- () =>
159
- web.value.botSettings?.assignments.filter(
160
- (assignment) =>
161
- !capabilityItems.value.some(
162
- (item) => item.existing?.assignmentId === assignment.assignmentId,
163
- ),
164
- ) ?? [],
165
- );
166
-
167
- function assignmentOperationPending(assignmentId: string): boolean {
168
- return assignmentHasPendingOperation(
169
- assignmentOperations.value,
170
- assignmentId,
171
- );
172
- }
173
-
174
- const selectedModel = ref("");
175
- const useExactModel = ref(false);
176
- const exactConnectionId = ref("");
177
- const exactProviderModelId = ref("");
178
- const readyConnections = computed(() =>
179
- eligibleModelConnections({
180
- connections: web.value.userSettings?.connections ?? [],
181
- packages: web.value.userSettings?.packages ?? [],
182
- catalog: web.value.pluginCatalog,
183
- }),
184
- );
185
- const modelOptions = computed(() => modelSelectOptions(readyConnections.value));
186
- const defaultModelName = computed(
187
- () =>
188
- describeModelAssignment(
189
- web.value.userSettings?.newBotModelTemplate,
190
- web.value.userSettings?.connections ?? [],
191
- ) ?? "none set",
192
- );
193
- const overriding = computed(() => Boolean(web.value.botSettings?.model));
194
80
 
195
81
  onMounted(() => {
196
82
  window.addEventListener(UI_ANCHOR_EVENT, onAnchorAnnounced);
@@ -200,13 +86,6 @@ onMounted(() => {
200
86
  void web.value.loadUserSettings();
201
87
  });
202
88
 
203
- /*
204
- * The form fills itself from whichever Bot's durable settings arrive, rather
205
- * than from whatever had loaded by the time this panel mounted. A deep link
206
- * opens the panel before the Flock has selected a Bot, and a User can switch
207
- * Bots with the panel open; both used to leave the fields on screen belonging
208
- * to nobody.
209
- */
210
89
  const hydratedBotId = ref<string>();
211
90
 
212
91
  watch(
@@ -220,53 +99,17 @@ watch(
220
99
  title.value = settings.profile.title ?? "";
221
100
  hiddenFromSidebar.value = settings.profile.hiddenFromSidebar === true;
222
101
  notifications.value = settings.notifications.enabled;
223
- modelMode.value = settings.model ? "custom" : "default";
224
- selectedModel.value = encodeModelSelection(settings.model);
225
- exactConnectionId.value =
226
- settings.model?.connectionId ??
227
- readyConnections.value[0]?.connectionId ??
228
- "";
229
- exactProviderModelId.value = settings.model?.providerModelId ?? "";
230
- useExactModel.value = Boolean(
231
- settings.model &&
232
- !modelOptions.value.some((model) => model.value === selectedModel.value),
233
- );
234
102
  },
235
103
  { immediate: true },
236
104
  );
237
105
 
238
- // The Connections may land after the Bot did; an empty exact-model Connection
239
- // takes the first ready one the moment there is one.
240
- watch(readyConnections, (connections) => {
241
- if (!exactConnectionId.value && connections[0]) {
242
- exactConnectionId.value = connections[0].connectionId;
243
- }
244
- });
245
-
246
106
  onBeforeUnmount(() =>
247
107
  window.removeEventListener(UI_ANCHOR_EVENT, onAnchorAnnounced),
248
108
  );
249
109
 
250
- async function saveModel(): Promise<void> {
251
- if (modelMode.value === "default") {
252
- // Following the default means holding no Bot binding at all.
253
- if (web.value.botSettings?.model) await web.value.clearBotModel();
254
- return;
255
- }
256
- const selected = resolveBotSettingsModel({
257
- current: web.value.botSettings?.model,
258
- useExactModel: useExactModel.value,
259
- selectedModel: selectedModel.value,
260
- exactConnectionId: exactConnectionId.value,
261
- exactProviderModelId: exactProviderModelId.value,
262
- });
263
- if (selected) await web.value.saveBotModel(selected);
264
- }
265
-
266
110
  async function save(): Promise<void> {
267
111
  saving.value = true;
268
112
  try {
269
- // A partial update: the empty string clears an optional field.
270
113
  await web.value.setBotProfile({
271
114
  name: name.value,
272
115
  label: label.value,
@@ -274,7 +117,6 @@ async function save(): Promise<void> {
274
117
  title: title.value,
275
118
  hiddenFromSidebar: hiddenFromSidebar.value,
276
119
  });
277
- await saveModel();
278
120
  await web.value.saveBotNotifications({ enabled: notifications.value });
279
121
  surfaces.close();
280
122
  } catch (error) {
@@ -284,70 +126,6 @@ async function save(): Promise<void> {
284
126
  saving.value = false;
285
127
  }
286
128
  }
287
-
288
- async function assign(
289
- item: (typeof capabilityItems.value)[number],
290
- ): Promise<void> {
291
- assignmentBusy.value = item.key;
292
- try {
293
- await web.value.assignCapability({
294
- assignmentId: crypto.randomUUID(),
295
- packageId: item.pkg.packageId,
296
- capabilityId: item.capability.id,
297
- connectionId: selectedConnections[item.key] || undefined,
298
- });
299
- } catch (error) {
300
- web.value.settingsError =
301
- error instanceof Error ? error.message : "Could not assign Capability";
302
- await web.value.loadBotSettings();
303
- } finally {
304
- assignmentBusy.value = undefined;
305
- }
306
- }
307
-
308
- async function replace(
309
- item: (typeof capabilityItems.value)[number],
310
- ): Promise<void> {
311
- if (!item.existing) return;
312
- assignmentBusy.value = item.key;
313
- try {
314
- await web.value.replaceCapability({
315
- assignmentId: item.existing.assignmentId,
316
- packageId: item.pkg.packageId,
317
- capabilityId: item.capability.id,
318
- connectionId: selectedConnections[item.key] || undefined,
319
- });
320
- } catch (error) {
321
- web.value.settingsError =
322
- error instanceof Error ? error.message : "Could not replace Assignment";
323
- await web.value.loadBotSettings();
324
- } finally {
325
- assignmentBusy.value = undefined;
326
- }
327
- }
328
-
329
- async function unassignAssignment(
330
- assignmentId: string,
331
- key: string,
332
- ): Promise<void> {
333
- assignmentBusy.value = key;
334
- try {
335
- await web.value.unassignCapability(assignmentId);
336
- } catch (error) {
337
- web.value.settingsError =
338
- error instanceof Error ? error.message : "Could not unassign Capability";
339
- await web.value.loadBotSettings();
340
- } finally {
341
- assignmentBusy.value = undefined;
342
- }
343
- }
344
-
345
- async function unassign(
346
- item: (typeof capabilityItems.value)[number],
347
- ): Promise<void> {
348
- if (!item.existing) return;
349
- await unassignAssignment(item.existing.assignmentId, item.key);
350
- }
351
129
  </script>
352
130
 
353
131
  <template>
@@ -410,12 +188,6 @@ async function unassign(
410
188
  </label>
411
189
  </UiAnchor>
412
190
  </div>
413
- <!--
414
- Pending decisions. The card in the conversation is where a decision is
415
- normally answered; this is where the ones nobody scrolled back to are
416
- still findable, because "a request for more authority becomes a durable
417
- pending decision for the User" is only true if the User can find it.
418
- -->
419
191
  <UiAnchor
420
192
  v-if="pendingApprovals.length > 0"
421
193
  anchor="bot-approvals"
@@ -496,9 +268,7 @@ async function unassign(
496
268
  <strong>Identity</strong>
497
269
  <p>{{ name || "This Bot" }} · Bot {{ web.activeBotId }}</p>
498
270
  </div>
499
- <span>
500
- Named by {{ web.botSettings?.profile.namedBy ?? "user" }}
501
- </span>
271
+ <span>Named by {{ web.botSettings?.profile.namedBy ?? "user" }}</span>
502
272
  </UiAnchor>
503
273
  <UiAnchor
504
274
  anchor="bot-info-members"
@@ -508,214 +278,9 @@ async function unassign(
508
278
  >
509
279
  <div>
510
280
  <strong>Members</strong>
511
- <p>This Bot and the authority explicitly assigned to it.</p>
281
+ <p>This Bot uses what you enable for all of your Bots.</p>
512
282
  </div>
513
- <span>
514
- {{ web.botSettings?.assignments.length ?? 0 }} Capability
515
- Assignment(s)
516
- </span>
517
- </UiAnchor>
518
- <p v-if="overriding" class="model-note">Overrides default model</p>
519
- <UiAnchor
520
- anchor="bot-model"
521
- label="Model"
522
- :href="link('bot-model')"
523
- class="settings-row"
524
- >
525
- <fieldset class="model-mode">
526
- <legend>Model</legend>
527
- <label>
528
- <input v-model="modelMode" type="radio" value="default" />
529
- <span>Use default model ({{ defaultModelName }})</span>
530
- </label>
531
- <label>
532
- <input v-model="modelMode" type="radio" value="custom" />
533
- <span>Custom model</span>
534
- </label>
535
- </fieldset>
536
- </UiAnchor>
537
- <template v-if="modelMode === 'custom'">
538
- <label class="exact-model-setting">
539
- <span>
540
- <strong>Use exact model ID</strong>
541
- <small>Choose a model not listed in the advisory catalog.</small>
542
- </span>
543
- <input v-model="useExactModel" type="checkbox" />
544
- </label>
545
- <template v-if="useExactModel">
546
- <UiField label="Connection">
547
- <select v-model="exactConnectionId">
548
- <option disabled value="">Select a Connection</option>
549
- <option
550
- v-for="connection in readyConnections"
551
- :key="connection.connectionId"
552
- :value="connection.connectionId"
553
- >
554
- {{ connection.displayName }}
555
- </option>
556
- </select>
557
- </UiField>
558
- <UiField label="Exact provider model ID">
559
- <input
560
- v-model="exactProviderModelId"
561
- maxlength="256"
562
- placeholder="model-name:cloud"
563
- />
564
- </UiField>
565
- </template>
566
- <UiField v-else label="Model">
567
- <select v-model="selectedModel">
568
- <option disabled value="">Select a connected model</option>
569
- <option
570
- v-for="model in modelOptions"
571
- :key="model.value"
572
- :value="model.value"
573
- >
574
- {{ model.label }}
575
- </option>
576
- </select>
577
- </UiField>
578
- </template>
579
- <UiAnchor
580
- anchor="bot-capabilities"
581
- label="Capability Assignments"
582
- :href="link('bot-capabilities')"
583
- class="settings-row"
584
- >
585
- <section class="assignment-settings">
586
- <div>
587
- <strong>Capability Assignments</strong>
588
- <p>
589
- Grant this Bot an installed Capability and required Connection.
590
- </p>
591
- </div>
592
- <article
593
- v-for="item in capabilityItems"
594
- :key="item.key"
595
- class="assignment-card"
596
- >
597
- <div>
598
- <strong
599
- >{{ item.pkg.displayName }} · {{ item.capability.id }}</strong
600
- >
601
- <small v-if="item.pending">
602
- {{ item.pending.kind }} · {{ item.pending.state }}
603
- </small>
604
- <small v-else-if="item.existing">
605
- {{ item.existing.state }}
606
- </small>
607
- <small v-else>Not assigned</small>
608
- </div>
609
- <select
610
- v-if="item.capability.connectionTypes.length > 0"
611
- v-model="selectedConnections[item.key]"
612
- :disabled="Boolean(item.pending)"
613
- :aria-label="`Connection for ${item.capability.id}`"
614
- >
615
- <option value="">Choose a ready Connection</option>
616
- <option
617
- v-for="connection in item.connections"
618
- :key="connection.connectionId"
619
- :value="connection.connectionId"
620
- >
621
- {{ connection.displayName }}
622
- </option>
623
- </select>
624
- <div class="assignment-actions">
625
- <UiButton
626
- v-if="!item.existing"
627
- type="button"
628
- :disabled="
629
- Boolean(item.pending) ||
630
- assignmentBusy === item.key ||
631
- (item.capability.connectionTypes.length > 0 &&
632
- !selectedConnections[item.key])
633
- "
634
- @click="assign(item)"
635
- >
636
- Assign
637
- </UiButton>
638
- <template v-else>
639
- <UiButton
640
- type="button"
641
- :disabled="
642
- Boolean(item.pending) || assignmentBusy === item.key
643
- "
644
- @click="replace(item)"
645
- >
646
- Replace
647
- </UiButton>
648
- <UiButton
649
- type="button"
650
- variant="danger"
651
- :disabled="
652
- Boolean(item.pending) || assignmentBusy === item.key
653
- "
654
- @click="unassign(item)"
655
- >
656
- Unassign
657
- </UiButton>
658
- </template>
659
- </div>
660
- </article>
661
- <article
662
- v-for="operation in assignmentOperations"
663
- :key="`operation:${operation.commandId}`"
664
- class="assignment-card"
665
- data-assignment-operation
666
- >
667
- <div>
668
- <strong>
669
- {{ operation.target?.packageId ?? "Unavailable Package" }} ·
670
- {{ operation.target?.capabilityId ?? operation.assignmentId }}
671
- </strong>
672
- <small>{{ operation.kind }} · {{ operation.state }}</small>
673
- </div>
674
- </article>
675
- <article
676
- v-for="assignment in orphanAssignments"
677
- :key="assignment.assignmentId"
678
- class="assignment-card"
679
- >
680
- <div>
681
- <strong
682
- >{{ assignment.packageId }} ·
683
- {{ assignment.capabilityId }}</strong
684
- >
685
- <small
686
- >{{ assignment.state }} · no longer available in the
687
- catalog</small
688
- >
689
- </div>
690
- <div class="assignment-actions">
691
- <UiButton
692
- type="button"
693
- variant="danger"
694
- :disabled="
695
- assignmentBusy === assignment.assignmentId ||
696
- assignmentOperationPending(assignment.assignmentId)
697
- "
698
- @click="
699
- unassignAssignment(
700
- assignment.assignmentId,
701
- assignment.assignmentId,
702
- )
703
- "
704
- >
705
- Unassign
706
- </UiButton>
707
- </div>
708
- </article>
709
- <p
710
- v-if="
711
- capabilityItems.length === 0 && orphanAssignments.length === 0
712
- "
713
- class="assignment-empty"
714
- >
715
- No assignable Capabilities are available in the production
716
- catalog.
717
- </p>
718
- </section>
283
+ <span>Packages and Connections are shared</span>
719
284
  </UiAnchor>
720
285
  <k-slot name="frockbot.bot-settings-sections" />
721
286
  </div>
@@ -735,10 +300,6 @@ async function unassign(
735
300
  </template>
736
301
 
737
302
  <style scoped>
738
- /*
739
- * A deep-linkable row. The anchor floats its copy control in the top-right
740
- * corner, so every row keeps that corner clear.
741
- */
742
303
  .settings-row {
743
304
  display: flex;
744
305
  flex-direction: column;
@@ -753,7 +314,6 @@ async function unassign(
753
314
  padding: 16px;
754
315
  }
755
316
 
756
- .assignment-card,
757
317
  .notification-setting,
758
318
  .bot-members {
759
319
  border: 1px solid var(--frock-border);
@@ -769,19 +329,6 @@ async function unassign(
769
329
  padding-right: 0;
770
330
  }
771
331
 
772
- .assignment-settings p,
773
- .assignment-card small {
774
- margin-top: 4px;
775
- color: var(--frock-text-muted);
776
- font-size: var(--frock-text-sm);
777
- line-height: var(--frock-leading-normal);
778
- }
779
-
780
- .assignment-settings {
781
- display: grid;
782
- gap: 10px;
783
- }
784
-
785
332
  .bot-members {
786
333
  display: flex;
787
334
  align-items: center;
@@ -809,91 +356,35 @@ async function unassign(
809
356
  }
810
357
 
811
358
  .bot-members > span {
812
- max-width: 110px;
359
+ max-width: 120px;
813
360
  flex: 0 0 auto;
814
361
  text-align: right;
815
362
  }
816
363
 
817
- .assignment-settings strong {
818
- font-size: var(--frock-text-md);
819
- font-weight: 600;
820
- }
821
-
822
- .assignment-card {
823
- display: grid;
824
- gap: 8px;
825
- padding: 12px;
826
- }
827
-
828
- .assignment-card strong {
829
- font-size: var(--frock-text-md);
830
- font-weight: 600;
831
- overflow-wrap: anywhere;
832
- }
833
-
834
- .assignment-card small {
835
- display: block;
836
- }
837
-
838
- .assignment-card select {
839
- width: 100%;
840
- }
841
-
842
- .assignment-actions {
843
- display: flex;
844
- flex-wrap: wrap;
845
- justify-content: flex-end;
846
- gap: 8px;
847
- }
848
-
849
- /* The right panel is ~360px wide, so Assignment controls stay compact. */
850
- .assignment-actions :deep(.ui-button) {
851
- min-height: 30px;
852
- padding: 0 10px;
853
- font-size: var(--frock-text-sm);
854
- }
855
-
856
- .assignment-empty {
857
- padding: 12px;
858
- color: var(--frock-text-muted);
859
- font-size: var(--frock-text-sm);
860
- border: 1px dashed var(--frock-border);
861
- border-radius: var(--frock-radius-card);
862
- }
863
-
864
- .exact-model-setting,
865
364
  .notification-setting {
866
365
  display: flex;
867
366
  align-items: center;
868
367
  justify-content: space-between;
869
368
  gap: 12px;
870
369
  padding: 12px;
871
- border: 1px solid var(--frock-border);
872
- border-radius: var(--frock-radius-card);
873
- background: var(--frock-surface-subtle);
874
370
  }
875
371
 
876
- .exact-model-setting strong,
877
- .exact-model-setting small,
878
372
  .notification-setting strong,
879
373
  .notification-setting small {
880
374
  display: block;
881
375
  }
882
376
 
883
- .notification-setting strong,
884
- .exact-model-setting strong {
377
+ .notification-setting strong {
885
378
  font-size: var(--frock-text-md);
886
379
  font-weight: 600;
887
380
  }
888
381
 
889
- .exact-model-setting small,
890
382
  .notification-setting small {
891
383
  margin-top: 4px;
892
384
  color: var(--frock-text-muted);
893
385
  font-size: var(--frock-text-sm);
894
386
  }
895
387
 
896
- .exact-model-setting input[type="checkbox"],
897
388
  .notification-setting input[type="checkbox"] {
898
389
  position: relative;
899
390
  width: 38px;
@@ -907,7 +398,6 @@ async function unassign(
907
398
  transition: background-color var(--frock-motion-fast);
908
399
  }
909
400
 
910
- .exact-model-setting input[type="checkbox"]::before,
911
401
  .notification-setting input[type="checkbox"]::before {
912
402
  position: absolute;
913
403
  top: 2px;
@@ -921,18 +411,15 @@ async function unassign(
921
411
  transition: transform var(--frock-motion-fast);
922
412
  }
923
413
 
924
- .exact-model-setting input[type="checkbox"]:checked,
925
414
  .notification-setting input[type="checkbox"]:checked {
926
415
  border-color: var(--frock-action-primary);
927
416
  background: var(--frock-action-primary);
928
417
  }
929
418
 
930
- .exact-model-setting input[type="checkbox"]:checked::before,
931
419
  .notification-setting input[type="checkbox"]:checked::before {
932
420
  transform: translateX(16px);
933
421
  }
934
422
 
935
- .exact-model-setting input[type="checkbox"]:focus-visible,
936
423
  .notification-setting input[type="checkbox"]:focus-visible {
937
424
  outline: 2px solid var(--frock-focus-ring);
938
425
  outline-offset: 2px;
@@ -978,12 +465,24 @@ async function unassign(
978
465
  gap: 0.5rem;
979
466
  }
980
467
 
981
- .model-note {
468
+ .primary-contributions {
469
+ display: flex;
470
+ flex-direction: column;
471
+ gap: 8px;
472
+ }
473
+
474
+ .settings-error {
982
475
  margin: 0;
983
- color: var(--frock-text-muted);
476
+ color: var(--frock-danger-text);
984
477
  font-size: var(--frock-text-sm);
985
478
  }
986
479
 
480
+ .settings-actions {
481
+ display: flex;
482
+ justify-content: flex-end;
483
+ gap: 8px;
484
+ }
485
+
987
486
  .advanced {
988
487
  border-top: 1px solid var(--frock-border);
989
488
  padding-top: 12px;
@@ -1018,59 +517,7 @@ async function unassign(
1018
517
  .advanced__body {
1019
518
  display: flex;
1020
519
  flex-direction: column;
1021
- gap: 12px;
520
+ gap: 16px;
1022
521
  padding-top: 12px;
1023
522
  }
1024
-
1025
- .model-mode {
1026
- display: flex;
1027
- flex-direction: column;
1028
- gap: 10px;
1029
- margin: 0;
1030
- border: 0;
1031
- padding: 0;
1032
- }
1033
-
1034
- .model-mode legend {
1035
- float: left;
1036
- width: 100%;
1037
- margin-bottom: 4px;
1038
- padding: 0;
1039
- color: var(--frock-text);
1040
- font-size: var(--frock-text-md);
1041
- font-weight: 600;
1042
- }
1043
-
1044
- .model-mode label {
1045
- display: flex;
1046
- align-items: center;
1047
- gap: 8px;
1048
- color: var(--frock-text);
1049
- font-size: var(--frock-text-md);
1050
- cursor: pointer;
1051
- }
1052
-
1053
- .model-mode input {
1054
- width: 17px;
1055
- height: 17px;
1056
- flex: 0 0 auto;
1057
- accent-color: var(--frock-action-primary);
1058
- }
1059
-
1060
- .settings-error {
1061
- margin: 0;
1062
- color: var(--frock-danger-text);
1063
- font-size: var(--frock-text-sm);
1064
- }
1065
-
1066
- .settings-actions {
1067
- display: flex;
1068
- justify-content: flex-end;
1069
- }
1070
-
1071
- .primary-contributions {
1072
- display: flex;
1073
- flex-direction: column;
1074
- gap: 12px;
1075
- }
1076
523
  </style>