@frockbot/plugin-settings 0.0.0 → 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (36) hide show
  1. package/frockbot.json +55 -0
  2. package/package.json +43 -6
  3. package/src/backend.test.ts +397 -0
  4. package/src/backend.ts +262 -0
  5. package/src/client/BotPanel.vue +57 -0
  6. package/src/client/BotSettingsSurface.vue +1076 -0
  7. package/src/client/BotSettingsTrigger.vue +26 -0
  8. package/src/client/ConnectionsSurface.vue +813 -0
  9. package/src/client/ModelsSurface.vue +419 -0
  10. package/src/client/PackageAccounts.vue +330 -0
  11. package/src/client/PackageCatalogSurface.vue +584 -0
  12. package/src/client/PackageSettingsForm.vue +150 -0
  13. package/src/client/PackageSettingsSection.vue +66 -0
  14. package/src/client/PluginsSurface.vue +412 -0
  15. package/src/client/PluginsTrigger.vue +62 -0
  16. package/src/client/UserProfileTrigger.vue +223 -0
  17. package/src/client/UserSettingsSurface.vue +242 -0
  18. package/src/client/assignment-operations.ts +22 -0
  19. package/src/client/bot-settings.test.ts +218 -0
  20. package/src/client/bot-settings.ts +150 -0
  21. package/src/client/index.test.ts +113 -0
  22. package/src/client/index.ts +78 -0
  23. package/src/client/package-settings.test.ts +63 -0
  24. package/src/client/package-settings.ts +77 -0
  25. package/src/client/package-surfaces.test.ts +147 -0
  26. package/src/client/package-surfaces.ts +93 -0
  27. package/src/client/user-display-name.test.ts +44 -0
  28. package/src/client/user-display-name.ts +16 -0
  29. package/src/env.d.ts +6 -0
  30. package/src/index.ts +1 -0
  31. package/src/manifest.ts +3 -0
  32. package/src/user.test.ts +1277 -0
  33. package/src/user.ts +1221 -0
  34. package/tsconfig.json +15 -0
  35. package/vite.config.ts +30 -0
  36. package/README.md +0 -3
@@ -0,0 +1,330 @@
1
+ <script setup lang="ts">
2
+ /**
3
+ * The accounts of one Package: every Connection it owns, with the durable
4
+ * actions each one offers.
5
+ *
6
+ * Shared by Models and Connections, which differ in what they add around it —
7
+ * a provider catalog and a model choice on one, an authorization handoff on
8
+ * the other — and not in how an account is renamed, rotated, disabled, or
9
+ * disconnected.
10
+ */
11
+ import { UiButton } from "@frockbot/client-ui";
12
+ import type { ConnectionView } from "@frockbot/configuration-core";
13
+ import {
14
+ frockBotWebDataKey,
15
+ type PluginCatalogItem,
16
+ } from "@frockbot/plugin-shell/shared";
17
+ import { computed, inject, ref } from "vue";
18
+
19
+ const props = defineProps<{ item: PluginCatalogItem }>();
20
+
21
+ const providedWeb = inject(frockBotWebDataKey);
22
+ if (!providedWeb) throw new Error("shell client data was not provided");
23
+ const web = providedWeb;
24
+
25
+ const rotatingConnectionId = ref<string>();
26
+ const rotationKey = ref("");
27
+ const labelingConnectionId = ref<string>();
28
+ const connectionLabel = ref("");
29
+
30
+ /**
31
+ * The accounts this card speaks for. A revoked Connection is a tombstone the
32
+ * User has already dismissed, so it is not listed.
33
+ */
34
+ const connections = computed<ConnectionView[]>(() =>
35
+ (web.value.userSettings?.connections ?? []).filter(
36
+ (connection) =>
37
+ connection.packageId === props.item.packageId &&
38
+ connection.state !== "revoked",
39
+ ),
40
+ );
41
+
42
+ type StatusTone = "ready" | "muted" | "attention";
43
+
44
+ /**
45
+ * The User's default model, when this account is the one serving it. Shown on
46
+ * the account it belongs to; changing it is the row at the top of Models.
47
+ */
48
+ function defaultModelName(connection: ConnectionView): string | undefined {
49
+ const selected = web.value.userSettings?.newBotModelTemplate;
50
+ if (selected?.connectionId !== connection.connectionId) return undefined;
51
+ return (
52
+ connection.modelCatalog?.models.find(
53
+ (model) => model.providerModelId === selected.providerModelId,
54
+ )?.displayName ?? selected.providerModelId
55
+ );
56
+ }
57
+
58
+ function connectionTone(connection: ConnectionView): StatusTone {
59
+ if (connection.state === "ready") return "ready";
60
+ if (connection.state === "failed") return "attention";
61
+ if (connection.state === "reconciliation-required") return "attention";
62
+ return "muted";
63
+ }
64
+
65
+ function beginLabeling(connection: ConnectionView): void {
66
+ labelingConnectionId.value = connection.connectionId;
67
+ connectionLabel.value = connection.displayName;
68
+ }
69
+
70
+ async function saveLabel(connectionId: string): Promise<void> {
71
+ try {
72
+ await web.value.updateConnectionLabel(connectionId, connectionLabel.value);
73
+ labelingConnectionId.value = undefined;
74
+ connectionLabel.value = "";
75
+ } catch (error) {
76
+ web.value.settingsError =
77
+ error instanceof Error ? error.message : "Could not rename Connection";
78
+ }
79
+ }
80
+
81
+ async function rotateApiKey(connectionId: string): Promise<void> {
82
+ try {
83
+ await web.value.rotateApiKeyConnection(connectionId, rotationKey.value);
84
+ rotationKey.value = "";
85
+ rotatingConnectionId.value = undefined;
86
+ } catch (error) {
87
+ rotationKey.value = "";
88
+ web.value.settingsError =
89
+ error instanceof Error ? error.message : "Could not rotate credential";
90
+ }
91
+ }
92
+
93
+ async function refreshModels(connectionId: string): Promise<void> {
94
+ try {
95
+ await web.value.refreshConnectionModels(connectionId);
96
+ } catch (error) {
97
+ web.value.settingsError =
98
+ error instanceof Error ? error.message : "Could not refresh models";
99
+ }
100
+ }
101
+
102
+ async function setEnabled(
103
+ connectionId: string,
104
+ enabled: boolean,
105
+ ): Promise<void> {
106
+ try {
107
+ await web.value.setConnectionEnabled(connectionId, enabled);
108
+ } catch (error) {
109
+ web.value.settingsError =
110
+ error instanceof Error ? error.message : "Could not update Connection";
111
+ }
112
+ }
113
+
114
+ async function disconnect(connectionId: string): Promise<void> {
115
+ try {
116
+ await web.value.disconnectConnection(connectionId);
117
+ } catch (error) {
118
+ web.value.settingsError =
119
+ error instanceof Error ? error.message : "Could not disconnect";
120
+ }
121
+ }
122
+
123
+ async function revoke(packageId: string, connectionId: string): Promise<void> {
124
+ try {
125
+ await web.value.revokeConnection(packageId, connectionId);
126
+ } catch (error) {
127
+ web.value.settingsError =
128
+ error instanceof Error ? error.message : "Could not revoke Connection";
129
+ }
130
+ }
131
+ </script>
132
+
133
+ <template>
134
+ <div class="package-accounts">
135
+ <div
136
+ v-for="connection in connections"
137
+ :key="connection.connectionId"
138
+ class="package-account"
139
+ >
140
+ <div class="account-identity">
141
+ <span
142
+ class="account-dot"
143
+ :class="`account-dot--${connectionTone(connection)}`"
144
+ aria-hidden="true"
145
+ />
146
+ <strong>{{ connection.displayName }}</strong>
147
+ <small>{{ connection.state }}</small>
148
+ <small v-if="connection.modelCatalog">
149
+ · models {{ connection.modelCatalog.state }}
150
+ </small>
151
+ </div>
152
+ <div class="account-actions">
153
+ <UiButton @click="beginLabeling(connection)">Rename</UiButton>
154
+ <template v-if="connection.authorization?.kind === 'api-key'">
155
+ <UiButton
156
+ v-if="connection.modelCatalog && connection.state === 'ready'"
157
+ @click="refreshModels(connection.connectionId)"
158
+ >
159
+ Refresh models
160
+ </UiButton>
161
+ <UiButton
162
+ v-if="connection.state === 'ready'"
163
+ @click="setEnabled(connection.connectionId, false)"
164
+ >
165
+ Disable
166
+ </UiButton>
167
+ <UiButton
168
+ v-if="connection.state === 'disabled'"
169
+ @click="setEnabled(connection.connectionId, true)"
170
+ >
171
+ Enable
172
+ </UiButton>
173
+ <UiButton
174
+ v-if="
175
+ connection.state === 'ready' || connection.state === 'disabled'
176
+ "
177
+ @click="rotatingConnectionId = connection.connectionId"
178
+ >
179
+ Rotate key
180
+ </UiButton>
181
+ <UiButton
182
+ v-if="connection.state !== 'revoking'"
183
+ variant="danger"
184
+ @click="disconnect(connection.connectionId)"
185
+ >
186
+ Disconnect
187
+ </UiButton>
188
+ </template>
189
+ <UiButton
190
+ v-else-if="connection.state !== 'revoking'"
191
+ variant="danger"
192
+ @click="revoke(connection.packageId, connection.connectionId)"
193
+ >
194
+ Revoke
195
+ </UiButton>
196
+ </div>
197
+ <p v-if="defaultModelName(connection)" class="account-default">
198
+ Default model: {{ defaultModelName(connection) }}
199
+ </p>
200
+ <p v-if="connection.failure" class="connection-failure" role="alert">
201
+ {{ connection.failure }}
202
+ </p>
203
+ <p
204
+ v-if="connection.modelCatalog?.failure"
205
+ class="connection-failure"
206
+ role="alert"
207
+ >
208
+ {{ connection.modelCatalog.failure }}
209
+ </p>
210
+ <form
211
+ v-if="labelingConnectionId === connection.connectionId"
212
+ class="inline-form"
213
+ @submit.prevent="saveLabel(connection.connectionId)"
214
+ >
215
+ <input
216
+ v-model="connectionLabel"
217
+ maxlength="120"
218
+ aria-label="Connection label"
219
+ required
220
+ />
221
+ <UiButton type="submit">Save label</UiButton>
222
+ </form>
223
+ <form
224
+ v-if="rotatingConnectionId === connection.connectionId"
225
+ class="inline-form"
226
+ @submit.prevent="rotateApiKey(connection.connectionId)"
227
+ >
228
+ <input
229
+ v-model="rotationKey"
230
+ type="password"
231
+ autocomplete="new-password"
232
+ aria-label="New API key"
233
+ required
234
+ />
235
+ <UiButton type="submit">Save new key</UiButton>
236
+ </form>
237
+ </div>
238
+ </div>
239
+ </template>
240
+
241
+ <style scoped>
242
+ .package-account {
243
+ display: grid;
244
+ gap: 8px;
245
+ padding: 12px 8px;
246
+ border-top: 1px solid var(--frock-border);
247
+ }
248
+
249
+ .account-identity {
250
+ display: flex;
251
+ min-width: 0;
252
+ flex-wrap: wrap;
253
+ align-items: baseline;
254
+ gap: 8px;
255
+ }
256
+
257
+ .account-identity strong {
258
+ overflow: hidden;
259
+ font-size: var(--frock-text-base);
260
+ white-space: nowrap;
261
+ text-overflow: ellipsis;
262
+ }
263
+
264
+ .account-identity small {
265
+ color: var(--frock-text-muted);
266
+ font-size: var(--frock-text-sm);
267
+ text-transform: capitalize;
268
+ }
269
+
270
+ .account-dot {
271
+ width: 8px;
272
+ height: 8px;
273
+ align-self: center;
274
+ border-radius: 999px;
275
+ }
276
+
277
+ .account-dot--ready {
278
+ background: var(--frock-success);
279
+ }
280
+
281
+ .account-dot--muted {
282
+ background: var(--frock-text-subtle);
283
+ }
284
+
285
+ .account-dot--attention {
286
+ background: var(--frock-danger-text);
287
+ }
288
+
289
+ .account-actions {
290
+ display: flex;
291
+ flex-wrap: wrap;
292
+ gap: 8px;
293
+ }
294
+
295
+ .account-actions :deep(.ui-button),
296
+ .inline-form :deep(.ui-button) {
297
+ min-height: 28px;
298
+ padding: 0 10px;
299
+ font-size: var(--frock-text-sm);
300
+ }
301
+
302
+ .inline-form {
303
+ display: grid;
304
+ grid-template-columns: minmax(0, 1fr) auto;
305
+ gap: 8px;
306
+ animation: frock-rise-in var(--frock-motion-panel) both;
307
+ }
308
+
309
+ .inline-form input {
310
+ min-width: 0;
311
+ padding: 8px 11px;
312
+ border: 1px solid var(--frock-border);
313
+ border-radius: 9px;
314
+ background: var(--frock-surface-raised);
315
+ color: var(--frock-text);
316
+ font-size: var(--frock-text-base);
317
+ }
318
+
319
+ .account-default {
320
+ margin: 0;
321
+ color: var(--frock-text-muted);
322
+ font-size: var(--frock-text-sm);
323
+ }
324
+
325
+ .connection-failure {
326
+ margin: 0;
327
+ color: var(--frock-danger-text);
328
+ font-size: var(--frock-text-sm);
329
+ }
330
+ </style>