@frockbot/plugin-settings 0.3.5 → 0.3.7

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-settings",
3
- "version": "0.3.5",
3
+ "version": "0.3.7",
4
4
  "private": false,
5
5
  "type": "module",
6
6
  "exports": {
@@ -21,15 +21,15 @@
21
21
  "typecheck": "vue-tsc --noEmit -p tsconfig.json"
22
22
  },
23
23
  "dependencies": {
24
- "@frockbot/catalog-core": "0.3.5",
25
- "@frockbot/client-core": "0.3.5",
26
- "@frockbot/client-ui": "0.3.5",
27
- "@frockbot/configuration-core": "0.3.5",
28
- "@frockbot/connection-core": "0.3.5",
29
- "@frockbot/kernel-composition": "0.3.5",
30
- "@frockbot/kernel-contracts": "0.3.5",
31
- "@frockbot/plugin-auth": "0.3.5",
32
- "@frockbot/plugin-shell": "0.3.5",
24
+ "@frockbot/catalog-core": "0.3.7",
25
+ "@frockbot/client-core": "0.3.7",
26
+ "@frockbot/client-ui": "0.3.7",
27
+ "@frockbot/configuration-core": "0.3.7",
28
+ "@frockbot/connection-core": "0.3.7",
29
+ "@frockbot/kernel-composition": "0.3.7",
30
+ "@frockbot/kernel-contracts": "0.3.7",
31
+ "@frockbot/plugin-auth": "0.3.7",
32
+ "@frockbot/plugin-shell": "0.3.7",
33
33
  "cordis": "4.0.0-rc.8",
34
34
  "vue": "3.5.41"
35
35
  },
@@ -266,9 +266,13 @@ async function save(): Promise<void> {
266
266
  >
267
267
  <div>
268
268
  <strong>Identity</strong>
269
- <p>{{ name || "This Bot" }} · Bot {{ web.activeBotId }}</p>
269
+ <p>{{ name || "This Bot" }}</p>
270
270
  </div>
271
- <span>Named by {{ web.botSettings?.profile.namedBy ?? "user" }}</span>
271
+ <span>{{
272
+ web.botSettings?.profile.namedBy === "bot"
273
+ ? "Named by this Bot"
274
+ : "Named by you"
275
+ }}</span>
272
276
  </UiAnchor>
273
277
  <UiAnchor
274
278
  anchor="bot-info-members"
@@ -280,7 +284,7 @@ async function save(): Promise<void> {
280
284
  <strong>Members</strong>
281
285
  <p>This Bot uses what you enable for all of your Bots.</p>
282
286
  </div>
283
- <span>Packages and Connections are shared</span>
287
+ <span>Plugins and connected accounts are shared</span>
284
288
  </UiAnchor>
285
289
  <k-slot name="frockbot.bot-settings-sections" />
286
290
  </div>
@@ -28,6 +28,38 @@ const web = providedWeb;
28
28
  // Connections are User-scoped, so the row's link names no Bot.
29
29
  const connectionsLink = settingsLinkV1({ anchor: "user-connections" });
30
30
 
31
+ /**
32
+ * What the browser came back from an external grant with. Read at boot from the
33
+ * return URL; without this the User lands back in the app with no confirmation
34
+ * and a cancelled grant is silently discarded.
35
+ */
36
+ const connectionReturn = computed(() => web.value.connectionReturn);
37
+
38
+ const connectionReturnMessage = computed(() => {
39
+ const result = connectionReturn.value;
40
+ if (!result) return "";
41
+ const name = connectorDisplayName(result.packageId);
42
+ if (result.status === "ready") return `${name} is connected.`;
43
+ if (result.status === "pending") {
44
+ return `${name} is finishing connecting. This page will show it when it is ready.`;
45
+ }
46
+ return result.reason
47
+ ? `${name} did not connect: ${result.reason}`
48
+ : `${name} did not connect.`;
49
+ });
50
+
51
+ /** The Package's display name when the catalog knows it, its id otherwise. */
52
+ function connectorDisplayName(packageId: string): string {
53
+ return (
54
+ web.value.pluginCatalog.find((item) => item.packageId === packageId)
55
+ ?.displayName ?? packageId
56
+ );
57
+ }
58
+
59
+ function dismissConnectionReturn(): void {
60
+ web.value.connectionReturn = undefined;
61
+ }
62
+
31
63
  const connectors = computed(() =>
32
64
  configurablePackages({
33
65
  catalog: web.value.pluginCatalog,
@@ -254,7 +286,7 @@ async function addMcpServer(): Promise<void> {
254
286
  } catch (error) {
255
287
  mcpApiKey.value = "";
256
288
  web.value.settingsError =
257
- error instanceof Error ? error.message : "Could not add the MCP server";
289
+ error instanceof Error ? error.message : "Couldn't add that server.";
258
290
  }
259
291
  }
260
292
 
@@ -331,6 +363,18 @@ async function connect(
331
363
  Connect an account or service once and every Bot you own can use it.
332
364
  </p>
333
365
 
366
+ <p
367
+ v-if="connectionReturn"
368
+ class="connection-return"
369
+ :class="{
370
+ 'connection-return--failed': connectionReturn.status === 'failed',
371
+ }"
372
+ role="status"
373
+ >
374
+ {{ connectionReturnMessage }}
375
+ <UiButton @click="dismissConnectionReturn">Dismiss</UiButton>
376
+ </p>
377
+
334
378
  <section
335
379
  v-if="pendingAuthorizations.length"
336
380
  class="connect-cards"
@@ -343,11 +387,7 @@ async function connect(
343
387
  >
344
388
  <div class="connect-card-copy">
345
389
  <strong>{{ connection.pendingAuthorization?.label }}</strong>
346
- <span>
347
- Needs your authorization. Only you can complete it — FrockBot
348
- creates the sign-in link when you press Reconnect, and nothing is
349
- stored waiting for it.
350
- </span>
390
+ <span> Needs you to sign in again. </span>
351
391
  </div>
352
392
  <UiButton
353
393
  variant="primary"
@@ -376,11 +416,7 @@ async function connect(
376
416
  <span class="connector-copy">
377
417
  <strong>{{ item.displayName }}</strong>
378
418
  <small>
379
- {{
380
- item.connectionTypes
381
- .map((connection) => connection.displayName)
382
- .join(", ")
383
- }}
419
+ {{ item.connectionTypes[0]?.displayName }}
384
420
  </small>
385
421
  </span>
386
422
  <UiButton
@@ -548,12 +584,8 @@ async function connect(
548
584
  />
549
585
  </label>
550
586
  <p class="field-hint">
551
- Leave both empty for a server that registers clients itself,
552
- which is what most do. FrockBot registers as a public client; a
553
- server that issues a client secret is refused, because a secret
554
- would have to live in this Connection's settings. You will be
555
- sent to the server to sign in, and no token ever reaches this
556
- page.
587
+ Leave both empty unless the server gave you specific values.
588
+ You'll sign in on the server's own site.
557
589
  </p>
558
590
  </template>
559
591
  <div class="api-key-actions">
@@ -734,6 +766,21 @@ async function connect(
734
766
  font-size: var(--frock-text-sm);
735
767
  }
736
768
 
769
+ .connection-return {
770
+ display: flex;
771
+ align-items: center;
772
+ justify-content: space-between;
773
+ gap: 12px;
774
+ margin: 8px 0;
775
+ padding: 8px 12px;
776
+ border-radius: var(--frock-radius-card);
777
+ background: var(--frock-surface-accent);
778
+ }
779
+
780
+ .connection-return--failed {
781
+ background: var(--frock-surface-danger, var(--frock-surface-accent));
782
+ }
783
+
737
784
  .connections-empty {
738
785
  display: flex;
739
786
  flex-direction: column;
@@ -32,8 +32,8 @@ onMounted(() => void web.value.loadPluginCatalog());
32
32
  class="models-anchor"
33
33
  />
34
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.
35
+ FrockBot picks a model for you. Turn on Custom models in Plugins to choose
36
+ your own.
37
37
  </p>
38
38
  <p v-if="web.settingsError" class="settings-error" role="alert">
39
39
  {{ web.settingsError }}
@@ -152,20 +152,17 @@ function isPackageInstalled(packageId: string): boolean {
152
152
  <template>
153
153
  <div class="package-catalog-surface">
154
154
  <p class="catalog-intro">
155
- Browse published Packages. Installing one makes it available to every Bot
156
- you own when the Package is enabled.
155
+ Browse published plugins. Installing one makes it available to every Bot
156
+ you own, once it's turned on.
157
157
  </p>
158
158
  <label class="catalog-search">
159
159
  <UiIcon name="search" />
160
160
  <input
161
161
  v-model="search"
162
- placeholder="Search the Catalog"
163
- aria-label="Search the Package Catalog"
162
+ placeholder="Search plugins"
163
+ aria-label="Search plugins"
164
164
  />
165
165
  </label>
166
- <p v-if="web.packageCatalogGeneration" class="catalog-generation">
167
- Generation {{ web.packageCatalogGeneration }}
168
- </p>
169
166
  <div class="catalog-grid">
170
167
  <article
171
168
  v-for="entry in filteredCatalog"
@@ -211,7 +208,7 @@ function isPackageInstalled(packageId: string): boolean {
211
208
  <p class="catalog-note">{{ openCatalogEntry.description }}</p>
212
209
  <dl class="catalog-facts">
213
210
  <div>
214
- <dt>Package</dt>
211
+ <dt>Plugin</dt>
215
212
  <dd>{{ openCatalogEntry.packageId }}</dd>
216
213
  </div>
217
214
  <div>
@@ -239,8 +236,8 @@ function isPackageInstalled(packageId: string): boolean {
239
236
  >
240
237
  <p>
241
238
  Uninstalling {{ entry.displayName }} removes it from every
242
- Bot. Connections and credentials are kept, and become usable
243
- again if you reinstall and enable the Package.
239
+ Bot. Your connected accounts stay, so reinstalling picks up
240
+ where you left off.
244
241
  </p>
245
242
  <div class="catalog-actions">
246
243
  <UiButton @click="uninstallingPackageId = undefined">
@@ -330,10 +327,13 @@ function isPackageInstalled(packageId: string): boolean {
330
327
  v-if="web.packageCatalog.length === 0 && !web.settingsError"
331
328
  class="catalog-empty"
332
329
  >
333
- The Catalog has nothing to offer yet.
330
+ No plugins are published yet.
334
331
  </p>
335
- <p v-else-if="filteredCatalog.length === 0" class="catalog-empty">
336
- No Catalog entry matches that search.
332
+ <p
333
+ v-else-if="filteredCatalog.length === 0 && !web.settingsError"
334
+ class="catalog-empty"
335
+ >
336
+ No plugin matches that search.
337
337
  </p>
338
338
  <p v-if="web.settingsError" class="settings-error" role="alert">
339
339
  {{ web.settingsError }}
@@ -54,9 +54,7 @@ async function save(): Promise<void> {
54
54
  await web.value.savePackageSettings(props.item.packageId, values);
55
55
  } catch (error) {
56
56
  web.value.settingsError =
57
- error instanceof Error
58
- ? error.message
59
- : "Could not save the Package settings";
57
+ error instanceof Error ? error.message : "Couldn't save those settings.";
60
58
  }
61
59
  }
62
60
  </script>
@@ -73,7 +71,7 @@ async function save(): Promise<void> {
73
71
  v-if="settingFieldKind(definition.schema) === 'enum'"
74
72
  v-model="draft[definition.id]"
75
73
  >
76
- <option value="">Package default</option>
74
+ <option value="">Default</option>
77
75
  <option
78
76
  v-for="choice in definition.schema.enum ?? []"
79
77
  :key="String(choice)"
@@ -35,7 +35,7 @@ const packages = computed(() =>
35
35
  </article>
36
36
  </div>
37
37
  <p v-else class="package-settings-empty">
38
- No enabled Package has settings of its own.
38
+ None of your plugins have settings.
39
39
  </p>
40
40
  </template>
41
41
 
@@ -60,11 +60,29 @@ onMounted(() => {
60
60
  void web.value.loadPluginCatalog();
61
61
  });
62
62
 
63
- /** What a Package offers, in the words its manifest uses. */
63
+ /** Plain nouns for the manifest's capability kinds. */
64
+ const CAPABILITY_NOUNS: Record<string, string> = {
65
+ tool: "Tools",
66
+ model: "Models",
67
+ memory: "Memory",
68
+ notification: "Notifications",
69
+ computer: "Computer",
70
+ ui: "Pages",
71
+ storage: "Storage",
72
+ };
73
+
74
+ function capabilityNoun(kind: string): string {
75
+ return CAPABILITY_NOUNS[kind] ?? kind;
76
+ }
77
+
78
+ /** What a plugin offers, in plain words. */
64
79
  function capabilitySummary(item: PluginCatalogItem): string {
65
80
  const kinds = [...new Set(item.capabilities.map((entry) => entry.kind))];
66
- if (kinds.length === 0) return "No Capabilities";
67
- return kinds.join(", ");
81
+ // A plugin can be worth turning on without contributing a capability of its
82
+ // own — Custom models is exactly that — so saying "no features" tells the
83
+ // User the one plugin they must enable does nothing.
84
+ if (kinds.length === 0) return "Adds settings";
85
+ return kinds.map(capabilityNoun).join(", ");
68
86
  }
69
87
 
70
88
  function installed(packageId: string): boolean {
@@ -111,13 +129,71 @@ function openPackageCatalog(): void {
111
129
  surfaces.open("package-catalog");
112
130
  }
113
131
 
132
+ /**
133
+ * Refusals live on the row that was clicked, not in one panel-wide slot.
134
+ *
135
+ * The grid scrolls, so a message rendered above the list is off-screen for
136
+ * every row past the fold: the User sees the row unchanged and no explanation
137
+ * anywhere.
138
+ */
139
+ const rowErrors = ref<Record<string, string>>({});
140
+
141
+ function rowError(packageId: string): string | undefined {
142
+ return rowErrors.value[packageId];
143
+ }
144
+
145
+ function setRowError(packageId: string, message: string): void {
146
+ rowErrors.value = { ...rowErrors.value, [packageId]: message };
147
+ }
148
+
149
+ function clearRowError(packageId: string): void {
150
+ const { [packageId]: _cleared, ...rest } = rowErrors.value;
151
+ rowErrors.value = rest;
152
+ }
153
+
154
+ /**
155
+ * Say what a Package is called, not what its id is.
156
+ *
157
+ * The backend refuses in terms of ids because that is what it holds; the
158
+ * surface knows the display names, so it substitutes them before the User
159
+ * reads the message.
160
+ */
161
+ function inPlainNames(message: string): string {
162
+ const dependency = message.match(
163
+ /^Package "([^"]+)" requires Package "([^"]+)" to be installed and enabled/,
164
+ );
165
+ if (dependency) {
166
+ return `${displayName(dependency[1]!)} needs ${displayName(
167
+ dependency[2]!,
168
+ )} turned on first.`;
169
+ }
170
+ return message.replace(/"([a-z0-9][a-z0-9-]*)"/g, (quoted, packageId) =>
171
+ displayName(packageId) === packageId
172
+ ? quoted
173
+ : `"${displayName(packageId)}"`,
174
+ );
175
+ }
176
+
177
+ function displayName(packageId: string): string {
178
+ return (
179
+ web.value.pluginCatalog.find(
180
+ (candidate) => candidate.packageId === packageId,
181
+ )?.displayName ?? packageId
182
+ );
183
+ }
184
+
114
185
  async function install(item: PluginCatalogItem): Promise<void> {
115
186
  busyPackageId.value = item.packageId;
187
+ clearRowError(item.packageId);
116
188
  try {
117
189
  await web.value.installPackage(item.packageId, item.version);
118
190
  } catch (error) {
119
- web.value.settingsError =
120
- error instanceof Error ? error.message : "Could not add the Package";
191
+ setRowError(
192
+ item.packageId,
193
+ inPlainNames(
194
+ error instanceof Error ? error.message : "Couldn't add that plugin.",
195
+ ),
196
+ );
121
197
  } finally {
122
198
  busyPackageId.value = undefined;
123
199
  }
@@ -125,13 +201,18 @@ async function install(item: PluginCatalogItem): Promise<void> {
125
201
 
126
202
  async function setEnabled(packageId: string, next: boolean): Promise<void> {
127
203
  busyPackageId.value = packageId;
204
+ clearRowError(packageId);
128
205
  try {
129
206
  await web.value.setPackageEnabled(packageId, next);
130
207
  } catch (error) {
131
- web.value.settingsError =
132
- error instanceof Error
133
- ? error.message
134
- : "Could not change the Package state";
208
+ setRowError(
209
+ packageId,
210
+ inPlainNames(
211
+ error instanceof Error
212
+ ? error.message
213
+ : "Couldn't turn that plugin on or off.",
214
+ ),
215
+ );
135
216
  } finally {
136
217
  busyPackageId.value = undefined;
137
218
  }
@@ -146,14 +227,6 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
146
227
  </span>
147
228
  {{ installedPluginCount }} installed
148
229
  </div>
149
- <!--
150
- A refused command is the answer to the click that caused it, so it is
151
- shown above the list where the click happened, not below a long grid
152
- where it scrolls out of view.
153
- -->
154
- <p v-if="web.settingsError" class="settings-error" role="alert">
155
- {{ web.settingsError }}
156
- </p>
157
230
  <label class="plugin-search">
158
231
  <UiIcon name="search" />
159
232
  <input
@@ -164,17 +237,17 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
164
237
  </label>
165
238
  <UiAnchor
166
239
  anchor="user-packages"
167
- label="Packages"
240
+ label="Plugins"
168
241
  :href="packagesLink"
169
242
  class="plugin-anchor"
170
243
  >
171
244
  <p class="plugin-intro">
172
- Turn Packages on and off for your Bots. Set one up where it belongs:
245
+ Turn plugins on and off for your Bots. Set one up where it belongs:
173
246
  model providers in Models, accounts in Connectors.
174
247
  </p>
175
248
  <div class="plugin-catalog-link">
176
249
  <UiButton type="button" @click="openPackageCatalog">
177
- Browse the Package Catalog
250
+ Browse all plugins
178
251
  </UiButton>
179
252
  </div>
180
253
  </UiAnchor>
@@ -227,6 +300,9 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
227
300
  </UiButton>
228
301
  </span>
229
302
  </div>
303
+ <p v-if="rowError(item.packageId)" class="plugin-failure" role="alert">
304
+ {{ rowError(item.packageId) }}
305
+ </p>
230
306
  <p v-if="failure(item.packageId)" class="plugin-failure" role="alert">
231
307
  {{ failure(item.packageId) }}
232
308
  </p>
@@ -237,10 +313,10 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
237
313
  v-if="userPackages.length === 0 && !web.settingsError"
238
314
  class="plugin-empty"
239
315
  >
240
- This application ships no Packages.
316
+ This deployment ships no plugins.
241
317
  </p>
242
318
  <p v-else-if="filteredCatalog.length === 0" class="plugin-empty">
243
- No Package matches that search.
319
+ No plugin matches that search.
244
320
  </p>
245
321
  </div>
246
322
  </template>
@@ -120,7 +120,7 @@ onBeforeUnmount(() => window.removeEventListener("pointerdown", closeMenu));
120
120
  {{ auth.signingOut.value ? "Signing out…" : "Sign out" }}
121
121
  </button>
122
122
  <p v-if="developmentIdentity" class="profile-menu-hint">
123
- Local development identity is selected by the development login URL.
123
+ You're signed in as a local developer.
124
124
  </p>
125
125
  <p v-if="signOutError" class="profile-menu-error" role="alert">
126
126
  {{ signOutError }}
@@ -70,7 +70,7 @@ async function save(): Promise<void> {
70
70
  <span class="profile-face" aria-hidden="true" />
71
71
  <div>
72
72
  <strong>Your profile</strong>
73
- <p>This identity is shared across your Bots.</p>
73
+ <p>How your Bots refer to you.</p>
74
74
  </div>
75
75
  </div>
76
76
  <UiField label="Name">
@@ -82,13 +82,13 @@ async function save(): Promise<void> {
82
82
  </UiAnchor>
83
83
  <UiAnchor
84
84
  anchor="user-package-settings"
85
- label="Package settings"
85
+ label="Plugin settings"
86
86
  :href="packageSettingsLink"
87
87
  class="settings-row"
88
88
  >
89
89
  <p class="field-hint">
90
- Settings of the Packages you have enabled. Model providers are
91
- configured in Models and accounts in Connectors.
90
+ Settings for the plugins you've turned on. Model providers are set up in
91
+ Models, and accounts in Connectors.
92
92
  </p>
93
93
  <PackageSettingsSection />
94
94
  <UiButton type="button" @click="surfaces.open('plugins')">
@@ -45,12 +45,12 @@ export const settingsClientPlugin: ClientPlugin = (ctx) => {
45
45
  }),
46
46
  surfaces.register({
47
47
  id: "package-catalog",
48
- title: "Package Catalog",
48
+ title: "All plugins",
49
49
  component: PackageCatalogSurface,
50
50
  }),
51
51
  surfaces.register({
52
52
  id: "user-settings",
53
- title: "Application settings",
53
+ title: "Settings",
54
54
  component: UserSettingsSurface,
55
55
  }),
56
56
  ctx.slot({
@@ -165,9 +165,7 @@ describe("Package configuration homes", () => {
165
165
  test("names the surface a Plugins row points at", () => {
166
166
  expect(configurationHomeLabel("models")).toBe("Models");
167
167
  expect(configurationHomeLabel("connections")).toBe("Connectors");
168
- expect(configurationHomeLabel("user-settings")).toBe(
169
- "Application settings",
170
- );
168
+ expect(configurationHomeLabel("user-settings")).toBe("Settings");
171
169
  expect(configurationHomeLabel("none")).toBeUndefined();
172
170
  });
173
171
  });
@@ -97,6 +97,6 @@ export function configurationHomeLabel(
97
97
  ): string | undefined {
98
98
  if (home === "models") return "Models";
99
99
  if (home === "connections") return "Connectors";
100
- if (home === "user-settings") return "Application settings";
100
+ if (home === "user-settings") return "Settings";
101
101
  return undefined;
102
102
  }