@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.
@@ -1,11 +1,10 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * Connections: the accounts and services a User authorizes and may then give a
4
- * Bot access to — Composio, Gmail, a remote MCP server.
3
+ * Connectors: the surface for accounts and services a User authorizes for all
4
+ * of their Bots — Composio, Gmail, a remote MCP server.
5
5
  *
6
- * It holds authorization and its durable state only. Whether the Package
7
- * providing a connector is enabled at all is Plugins' question, and a model
8
- * provider's account belongs to Models even though it is a Connection too.
6
+ * It holds Connection authorization and state only. Whether the Package
7
+ * providing a connector is enabled at all is Plugins' question.
9
8
  */
10
9
  import { clientSurfaceRegistryKey } from "@frockbot/client-core";
11
10
  import { UiAnchor, UiButton } from "@frockbot/client-ui";
@@ -50,6 +49,7 @@ const pendingAuthorizations = computed(() =>
50
49
  ),
51
50
  );
52
51
  const reconnectingConnectionId = ref<string>();
52
+ const togglingConnectionTypeId = ref<string>();
53
53
 
54
54
  const apiKeyPackageId = ref<string>();
55
55
  const apiKeyConnectionTypeId = ref<string>();
@@ -80,7 +80,7 @@ const mcpScope = ref("");
80
80
  const mcpClientId = ref("");
81
81
 
82
82
  /**
83
- * The MCP lifecycle panel: the durable server records the User Durable Object
83
+ * The MCP lifecycle panel: the server records the User Durable Object
84
84
  * owns, which the Connection rows cannot show — a server's `needs-auth` or
85
85
  * `error` state, when it last handshook, what its instructions are, and the
86
86
  * refusals this build recorded rather than performed.
@@ -157,6 +157,43 @@ function connectionCount(packageId: string): number {
157
157
  ).length;
158
158
  }
159
159
 
160
+ function credentiallessConnection(item: PluginCatalogItem) {
161
+ const connectionType = item.connectionTypes[0];
162
+ if (connectionType?.authorizationKind !== "none") return undefined;
163
+ return (web.value.userSettings?.connections ?? []).find(
164
+ (connection) =>
165
+ connection.packageId === item.packageId &&
166
+ connection.connectionTypeId === connectionType.id &&
167
+ connection.state !== "revoked",
168
+ );
169
+ }
170
+
171
+ async function toggleCredentialless(item: PluginCatalogItem): Promise<void> {
172
+ const connectionType = item.connectionTypes[0];
173
+ if (connectionType?.authorizationKind !== "none") return;
174
+ togglingConnectionTypeId.value = connectionType.id;
175
+ try {
176
+ const connection = credentiallessConnection(item);
177
+ if (!connection) {
178
+ await web.value.createConnection({
179
+ packageId: item.packageId,
180
+ connectionTypeId: connectionType.id,
181
+ label: item.displayName,
182
+ });
183
+ } else {
184
+ await web.value.setConnectionEnabled(
185
+ connection.connectionId,
186
+ connection.state !== "ready",
187
+ );
188
+ }
189
+ } catch (error) {
190
+ web.value.settingsError =
191
+ error instanceof Error ? error.message : "Could not update Connection";
192
+ } finally {
193
+ togglingConnectionTypeId.value = undefined;
194
+ }
195
+ }
196
+
160
197
  function beginMcpConnection(): void {
161
198
  mcpFormOpen.value = true;
162
199
  mcpLabel.value = "";
@@ -286,13 +323,12 @@ async function connect(
286
323
  <div class="connections-surface">
287
324
  <UiAnchor
288
325
  anchor="user-connections"
289
- label="Connections"
326
+ label="Connectors"
290
327
  :href="connectionsLink"
291
328
  class="settings-row"
292
329
  >
293
330
  <p class="field-hint">
294
- Accounts and services you can give your Bots access to. A Bot still
295
- needs an explicit Assignment before it can use one.
331
+ Connect an account or service once and every Bot you own can use it.
296
332
  </p>
297
333
 
298
334
  <section
@@ -348,7 +384,20 @@ async function connect(
348
384
  </small>
349
385
  </span>
350
386
  <UiButton
351
- v-if="
387
+ v-if="item.connectionTypes[0]?.authorizationKind === 'none'"
388
+ :disabled="
389
+ togglingConnectionTypeId === item.connectionTypes[0]?.id
390
+ "
391
+ @click="toggleCredentialless(item)"
392
+ >
393
+ {{
394
+ credentiallessConnection(item)?.state === "ready"
395
+ ? "Disable"
396
+ : "Enable"
397
+ }}
398
+ </UiButton>
399
+ <UiButton
400
+ v-else-if="
352
401
  connectionCount(item.packageId) === 0 ||
353
402
  item.connectionTypes[0]?.allowMultiple
354
403
  "
@@ -15,10 +15,10 @@ if (!surfaces || !web)
15
15
  v-if="web.connectionsAvailable"
16
16
  class="settings-trigger"
17
17
  type="button"
18
- @click="surfaces.open('plugins')"
18
+ @click="surfaces.open('connections')"
19
19
  >
20
20
  <span class="settings-trigger__icon"><UiIcon name="plugins" /></span>
21
- Plugins
21
+ Connectors
22
22
  </button>
23
23
  </template>
24
24
 
@@ -1,134 +1,17 @@
1
1
  <script setup lang="ts">
2
- /**
3
- * Models: where a User configures model provider Packages and picks a model.
4
- *
5
- * Enablement is not here — Plugins turns a provider on and off, and a disabled
6
- * provider disappears from this surface with its accounts and its catalogs
7
- * intact.
8
- */
9
- import { clientSurfaceRegistryKey } from "@frockbot/client-core";
10
- import { UiAnchor, UiButton, UiField } from "@frockbot/client-ui";
11
- import {
12
- frockBotWebDataKey,
13
- type PluginCatalogItem,
14
- } from "@frockbot/plugin-shell/shared";
2
+ import { UiAnchor } from "@frockbot/client-ui";
3
+ import { frockBotWebDataKey } from "@frockbot/plugin-shell/shared";
15
4
  import { settingsLinkV1 } from "@frockbot/plugin-shell/settings-links";
16
- import { computed, inject, onMounted, ref } from "vue";
17
- import {
18
- decodeModelSelection,
19
- eligibleModelConnections,
20
- encodeModelSelection,
21
- modelSelectOptions,
22
- } from "./bot-settings.js";
23
- import { configurablePackages } from "./package-surfaces.js";
24
- import PackageAccounts from "./PackageAccounts.vue";
25
- import PackageSettingsForm from "./PackageSettingsForm.vue";
5
+ import { inject, onMounted } from "vue";
26
6
 
27
- const providedSurfaces = inject(clientSurfaceRegistryKey);
28
7
  const providedWeb = inject(frockBotWebDataKey);
29
- if (!providedSurfaces || !providedWeb) {
30
- throw new Error("settings client services were not provided");
31
- }
32
- const surfaces = providedSurfaces;
8
+ if (!providedWeb) throw new Error("settings client services were not provided");
33
9
  const web = providedWeb;
34
- // Model configuration is User-scoped, so these links name no Bot.
10
+
35
11
  const defaultModelLink = settingsLinkV1({ anchor: "user-default-model" });
36
12
  const providersLink = settingsLinkV1({ anchor: "user-model-providers" });
37
13
 
38
- const defaultModel = ref("");
39
- const savingModel = ref(false);
40
-
41
- /**
42
- * The endpoint root of an Ollama Cloud Connection, declared by that Connection
43
- * Type as `api-base-url`. It is the only Connection Type setting the shared
44
- * API-key form has a field for.
45
- */
46
- const OLLAMA_PACKAGE_ID = "provider-ollama-cloud";
47
- const apiKeyPackageId = ref<string>();
48
- const apiKeyConnectionTypeId = ref<string>();
49
- const apiKeyLabel = ref("");
50
- const apiKey = ref("");
51
- const apiKeyBaseUrl = ref("");
52
-
53
- const providers = computed(() =>
54
- configurablePackages({
55
- catalog: web.value.pluginCatalog,
56
- packages: web.value.userSettings?.packages ?? [],
57
- home: "models",
58
- }),
59
- );
60
- const readyConnections = computed(() =>
61
- eligibleModelConnections({
62
- connections: web.value.userSettings?.connections ?? [],
63
- packages: web.value.userSettings?.packages ?? [],
64
- catalog: web.value.pluginCatalog,
65
- }),
66
- );
67
- const modelOptions = computed(() => modelSelectOptions(readyConnections.value));
68
-
69
- onMounted(async () => {
70
- await web.value.loadPluginCatalog();
71
- defaultModel.value = encodeModelSelection(
72
- web.value.userSettings?.newBotModelTemplate,
73
- );
74
- });
75
-
76
- async function saveDefaultModel(): Promise<void> {
77
- savingModel.value = true;
78
- try {
79
- await web.value.saveDefaultModel(decodeModelSelection(defaultModel.value));
80
- } catch (error) {
81
- web.value.settingsError =
82
- error instanceof Error ? error.message : "Could not save the model";
83
- } finally {
84
- savingModel.value = false;
85
- }
86
- }
87
-
88
- function connectionCount(packageId: string): number {
89
- return (web.value.userSettings?.connections ?? []).filter(
90
- (connection) =>
91
- connection.packageId === packageId && connection.state !== "revoked",
92
- ).length;
93
- }
94
-
95
- function beginConnect(item: PluginCatalogItem): void {
96
- const connectionType = item.connectionTypes[0];
97
- if (!connectionType) return;
98
- apiKeyPackageId.value = item.packageId;
99
- apiKeyConnectionTypeId.value = connectionType.id;
100
- apiKeyLabel.value = item.displayName;
101
- apiKey.value = "";
102
- apiKeyBaseUrl.value = "";
103
- }
104
-
105
- function cancelConnect(): void {
106
- apiKey.value = "";
107
- apiKeyBaseUrl.value = "";
108
- apiKeyPackageId.value = undefined;
109
- apiKeyConnectionTypeId.value = undefined;
110
- }
111
-
112
- async function connectApiKey(): Promise<void> {
113
- if (!apiKeyPackageId.value || !apiKeyConnectionTypeId.value) return;
114
- try {
115
- // An empty endpoint field means the Package default, so the command
116
- // carries no settings at all rather than an empty bag.
117
- const apiBaseUrl = apiKeyBaseUrl.value.trim();
118
- await web.value.createApiKeyConnection({
119
- packageId: apiKeyPackageId.value,
120
- connectionTypeId: apiKeyConnectionTypeId.value,
121
- label: apiKeyLabel.value,
122
- apiKey: apiKey.value,
123
- ...(apiBaseUrl ? { settings: { "api-base-url": apiBaseUrl } } : {}),
124
- });
125
- cancelConnect();
126
- } catch (error) {
127
- apiKey.value = "";
128
- web.value.settingsError =
129
- error instanceof Error ? error.message : "Could not create Connection";
130
- }
131
- }
14
+ onMounted(() => void web.value.loadPluginCatalog());
132
15
  </script>
133
16
 
134
17
  <template>
@@ -137,142 +20,21 @@ async function connectApiKey(): Promise<void> {
137
20
  anchor="user-default-model"
138
21
  label="Default model"
139
22
  :href="defaultModelLink"
140
- class="settings-row"
141
- >
142
- <UiField
143
- v-if="modelOptions.length > 0"
144
- label="Default model"
145
- hint="used by every Bot"
146
- >
147
- <select v-model="defaultModel">
148
- <option value="">No default model</option>
149
- <option
150
- v-for="model in modelOptions"
151
- :key="model.value"
152
- :value="model.value"
153
- >
154
- {{ model.label }}
155
- </option>
156
- </select>
157
- </UiField>
158
- <p v-if="modelOptions.length > 0" class="field-hint">
159
- Used by every Bot unless a Bot overrides it in its own settings.
160
- </p>
161
- <div v-else class="model-empty">
162
- <p>
163
- Connect a model provider below, then choose the model your Bots use.
164
- </p>
165
- </div>
166
- <div v-if="modelOptions.length > 0" class="settings-actions">
167
- <UiButton
168
- type="button"
169
- variant="primary"
170
- :disabled="savingModel"
171
- @click="saveDefaultModel"
172
- >
173
- {{ savingModel ? "Saving…" : "Save model" }}
174
- </UiButton>
175
- </div>
176
- </UiAnchor>
177
-
23
+ class="models-anchor"
24
+ />
25
+ <div class="models-sections">
26
+ <k-slot name="frockbot.models-sections" />
27
+ </div>
178
28
  <UiAnchor
179
29
  anchor="user-model-providers"
180
30
  label="Model providers"
181
31
  :href="providersLink"
182
- class="settings-row"
183
- >
184
- <p class="field-hint">
185
- Accounts and endpoints for the model providers you have enabled.
186
- </p>
187
- <div class="provider-grid">
188
- <article
189
- v-for="item in providers"
190
- :key="item.packageId"
191
- class="provider-card"
192
- >
193
- <div class="provider-summary">
194
- <span class="provider-logo" aria-hidden="true">
195
- {{ item.displayName.slice(0, 1) }}
196
- </span>
197
- <span class="provider-copy">
198
- <strong>{{ item.displayName }}</strong>
199
- <small>
200
- {{
201
- connectionCount(item.packageId) === 0
202
- ? "No account connected"
203
- : `${connectionCount(item.packageId)} account(s)`
204
- }}
205
- </small>
206
- </span>
207
- <UiButton
208
- v-if="
209
- connectionCount(item.packageId) === 0 ||
210
- item.connectionTypes[0]?.allowMultiple
211
- "
212
- @click="beginConnect(item)"
213
- >
214
- {{
215
- connectionCount(item.packageId) === 0
216
- ? "Connect"
217
- : "Add another account"
218
- }}
219
- </UiButton>
220
- </div>
221
-
222
- <PackageAccounts :item="item" />
223
-
224
- <form
225
- v-if="apiKeyPackageId === item.packageId"
226
- class="api-key-form"
227
- @submit.prevent="connectApiKey"
228
- >
229
- <label>
230
- <span>Connection label</span>
231
- <input v-model="apiKeyLabel" maxlength="120" required />
232
- </label>
233
- <label>
234
- <span>API key</span>
235
- <input
236
- v-model="apiKey"
237
- type="password"
238
- autocomplete="new-password"
239
- required
240
- />
241
- </label>
242
- <label v-if="item.packageId === OLLAMA_PACKAGE_ID">
243
- <span>API base URL</span>
244
- <input
245
- v-model="apiKeyBaseUrl"
246
- type="url"
247
- inputmode="url"
248
- autocomplete="off"
249
- maxlength="2048"
250
- placeholder="https://ollama.com"
251
- />
252
- </label>
253
- <p v-if="item.packageId === OLLAMA_PACKAGE_ID" class="field-hint">
254
- Leave empty for Ollama Cloud. Point this at a local Ollama server
255
- (http://127.0.0.1:11434) or any Ollama-compatible endpoint.
256
- </p>
257
- <div class="api-key-actions">
258
- <UiButton @click="cancelConnect">Cancel</UiButton>
259
- <UiButton type="submit" variant="primary">
260
- Connect account
261
- </UiButton>
262
- </div>
263
- </form>
264
-
265
- <PackageSettingsForm :item="item" />
266
- </article>
267
- </div>
268
- <div v-if="providers.length === 0" class="model-empty">
269
- <p>No model provider is enabled.</p>
270
- <UiButton type="button" @click="surfaces.open('plugins')">
271
- Open Plugins
272
- </UiButton>
273
- </div>
274
- </UiAnchor>
275
-
32
+ class="models-anchor"
33
+ />
34
+ <p class="models-empty">
35
+ FrockBot chooses a model for you. Enable Custom models in Plugins to
36
+ choose an account model or a model for one Bot.
37
+ </p>
276
38
  <p v-if="web.settingsError" class="settings-error" role="alert">
277
39
  {{ web.settingsError }}
278
40
  </p>
@@ -287,128 +49,28 @@ async function connectApiKey(): Promise<void> {
287
49
  padding: 16px;
288
50
  }
289
51
 
290
- .settings-row {
291
- display: flex;
292
- flex-direction: column;
293
- gap: 16px;
294
- padding-right: var(--frock-control-sm);
295
- }
296
-
297
- .field-hint {
298
- margin: 0;
299
- color: var(--frock-text-muted);
300
- font-size: var(--frock-text-sm);
52
+ .models-anchor {
53
+ min-height: 1px;
301
54
  }
302
55
 
303
- .model-empty {
56
+ .models-sections {
304
57
  display: flex;
305
58
  flex-direction: column;
306
- align-items: flex-start;
307
- gap: 10px;
308
- padding: 12px;
309
- border: 1px solid var(--frock-border);
310
- border-radius: var(--frock-radius-card);
311
- background: var(--frock-surface-subtle);
59
+ gap: 16px;
312
60
  }
313
61
 
314
- .model-empty p {
62
+ .models-empty {
315
63
  margin: 0;
316
- color: var(--frock-text-muted);
317
- font-size: var(--frock-text-sm);
318
- }
319
-
320
- .provider-grid {
321
- display: grid;
322
- gap: 12px;
323
- }
324
-
325
- .provider-card {
326
- min-width: 0;
327
- padding: 8px;
64
+ padding: 12px;
328
65
  border: 1px solid var(--frock-border);
329
66
  border-radius: var(--frock-radius-card);
330
- background: var(--frock-surface-raised);
331
- box-shadow: var(--frock-shadow-card);
332
- }
333
-
334
- .provider-summary {
335
- display: grid;
336
- width: 100%;
337
- min-width: 0;
338
- grid-template-columns: 44px minmax(0, 1fr) auto;
339
- align-items: center;
340
- gap: 12px;
341
- padding: 8px;
342
- }
343
-
344
- .provider-logo {
345
- display: grid;
346
- width: 44px;
347
- height: 44px;
348
- place-items: center;
349
- border-radius: 12px;
350
- color: var(--frock-action-secondary-text);
351
- background: var(--frock-surface-accent);
352
- font-weight: 800;
353
- }
354
-
355
- .provider-copy {
356
- min-width: 0;
357
- }
358
-
359
- .provider-copy strong,
360
- .provider-copy small {
361
- display: block;
362
- overflow: hidden;
363
- white-space: nowrap;
364
- text-overflow: ellipsis;
365
- }
366
-
367
- .provider-copy strong {
368
- font-size: var(--frock-text-md);
369
- font-weight: 600;
370
- }
371
-
372
- .provider-copy small {
373
- margin-top: 4px;
374
- color: var(--frock-text-muted);
375
- font-size: var(--frock-text-sm);
376
- }
377
-
378
- .api-key-form {
379
- display: grid;
380
- gap: 12px;
381
- margin: 0 8px;
382
- padding: 12px 0 8px;
383
- border-top: 1px solid var(--frock-border);
384
- animation: frock-rise-in var(--frock-motion-enter) both;
385
- }
386
-
387
- .api-key-form label {
388
- display: grid;
389
- gap: 6px;
390
- }
391
-
392
- .api-key-form span {
393
67
  color: var(--frock-text-muted);
68
+ background: var(--frock-surface-subtle);
394
69
  font-size: var(--frock-text-sm);
395
70
  }
396
71
 
397
- .api-key-form input {
398
- min-width: 0;
399
- padding: 8px 11px;
400
- border: 1px solid var(--frock-border);
401
- border-radius: 9px;
402
- background: var(--frock-surface-raised);
403
- color: var(--frock-text);
404
- font-size: var(--frock-text-base);
405
- }
406
-
407
- .api-key-actions,
408
- .settings-actions {
409
- display: flex;
410
- justify-content: flex-end;
411
- gap: 8px;
72
+ .models-sections:not(:empty) ~ .models-empty {
73
+ display: none;
412
74
  }
413
75
 
414
76
  .settings-error {
@@ -1,7 +1,7 @@
1
1
  <script setup lang="ts">
2
2
  /**
3
- * The accounts of one Package: every Connection it owns, with the durable
4
- * actions each one offers.
3
+ * The accounts of one Package: every Connection it owns and the actions each
4
+ * one offers.
5
5
  *
6
6
  * Shared by Models and Connections, which differ in what they add around it —
7
7
  * a provider catalog and a model choice on one, an authorization handoff on
@@ -41,20 +41,6 @@ const connections = computed<ConnectionView[]>(() =>
41
41
 
42
42
  type StatusTone = "ready" | "muted" | "attention";
43
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
44
  function connectionTone(connection: ConnectionView): StatusTone {
59
45
  if (connection.state === "ready") return "ready";
60
46
  if (connection.state === "failed") return "attention";
@@ -194,9 +180,6 @@ async function revoke(packageId: string, connectionId: string): Promise<void> {
194
180
  Revoke
195
181
  </UiButton>
196
182
  </div>
197
- <p v-if="defaultModelName(connection)" class="account-default">
198
- Default model: {{ defaultModelName(connection) }}
199
- </p>
200
183
  <p v-if="connection.failure" class="connection-failure" role="alert">
201
184
  {{ connection.failure }}
202
185
  </p>
@@ -316,12 +299,6 @@ async function revoke(packageId: string, connectionId: string): Promise<void> {
316
299
  font-size: var(--frock-text-base);
317
300
  }
318
301
 
319
- .account-default {
320
- margin: 0;
321
- color: var(--frock-text-muted);
322
- font-size: var(--frock-text-sm);
323
- }
324
-
325
302
  .connection-failure {
326
303
  margin: 0;
327
304
  color: var(--frock-danger-text);
@@ -153,7 +153,7 @@ function isPackageInstalled(packageId: string): boolean {
153
153
  <div class="package-catalog-surface">
154
154
  <p class="catalog-intro">
155
155
  Browse published Packages. Installing one makes it available to every Bot
156
- you own; each Bot still needs an explicit Assignment to use it.
156
+ you own when the Package is enabled.
157
157
  </p>
158
158
  <label class="catalog-search">
159
159
  <UiIcon name="search" />
@@ -239,8 +239,8 @@ function isPackageInstalled(packageId: string): boolean {
239
239
  >
240
240
  <p>
241
241
  Uninstalling {{ entry.displayName }} removes it from every
242
- Bot. Its Assignments stay visible as unavailable so you can
243
- repair or remove them. Connections and credentials are kept.
242
+ Bot. Connections and credentials are kept, and become usable
243
+ again if you reinstall and enable the Package.
244
244
  </p>
245
245
  <div class="catalog-actions">
246
246
  <UiButton @click="uninstallingPackageId = undefined">
@@ -23,7 +23,12 @@ const providedWeb = inject(frockBotWebDataKey);
23
23
  if (!providedWeb) throw new Error("shell client data was not provided");
24
24
  const web = providedWeb;
25
25
 
26
- const definitions = computed(() => props.item.settings ?? []);
26
+ const definitions = computed(() =>
27
+ (props.item.settings ?? []).filter(
28
+ (definition) =>
29
+ definition.role !== "model" && definition.scopes.includes("user"),
30
+ ),
31
+ );
27
32
  const draft = ref<Record<string, string | number | boolean>>({});
28
33
 
29
34
  /** The stored values of this Package, as the User settings hold them. */
@@ -159,7 +159,7 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
159
159
  >
160
160
  <p class="plugin-intro">
161
161
  Turn Packages on and off for your Bots. Set one up where it belongs:
162
- model providers in Models, accounts in Connections.
162
+ model providers in Models, accounts in Connectors.
163
163
  </p>
164
164
  <div class="plugin-catalog-link">
165
165
  <UiButton type="button" @click="openPackageCatalog">
@@ -90,7 +90,7 @@ onBeforeUnmount(() => window.removeEventListener("pointerdown", closeMenu));
90
90
  Settings
91
91
  </button>
92
92
  <!-- The three Package surfaces are reachable where the User already is,
93
- and gated exactly as the sidebar's Plugins trigger is: a shell
93
+ and gated exactly as the sidebar's Connectors trigger is: a shell
94
94
  without the Connection protocol shows none of them. -->
95
95
  <template v-if="web.connectionsAvailable">
96
96
  <button type="button" role="menuitem" @click="openSurface('models')">
@@ -104,7 +104,7 @@ onBeforeUnmount(() => window.removeEventListener("pointerdown", closeMenu));
104
104
  role="menuitem"
105
105
  @click="openSurface('connections')"
106
106
  >
107
- Connections
107
+ Connectors
108
108
  </button>
109
109
  </template>
110
110
  <button v-if="developmentIdentity" type="button" role="menuitem" disabled>