@frockbot/plugin-settings 0.3.11 → 0.3.13

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.11",
3
+ "version": "0.3.13",
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.11",
25
- "@frockbot/client-core": "0.3.11",
26
- "@frockbot/client-ui": "0.3.11",
27
- "@frockbot/configuration-core": "0.3.11",
28
- "@frockbot/connection-core": "0.3.11",
29
- "@frockbot/kernel-composition": "0.3.11",
30
- "@frockbot/kernel-contracts": "0.3.11",
31
- "@frockbot/plugin-auth": "0.3.11",
32
- "@frockbot/plugin-shell": "0.3.11",
24
+ "@frockbot/catalog-core": "0.3.13",
25
+ "@frockbot/client-core": "0.3.13",
26
+ "@frockbot/client-ui": "0.3.13",
27
+ "@frockbot/configuration-core": "0.3.13",
28
+ "@frockbot/connection-core": "0.3.13",
29
+ "@frockbot/kernel-composition": "0.3.13",
30
+ "@frockbot/kernel-contracts": "0.3.13",
31
+ "@frockbot/plugin-auth": "0.3.13",
32
+ "@frockbot/plugin-shell": "0.3.13",
33
33
  "cordis": "4.0.0-rc.8",
34
34
  "vue": "3.5.41"
35
35
  },
@@ -37,7 +37,7 @@
37
37
  "@types/bun": "1.4.0",
38
38
  "@types/node": "26.2.0",
39
39
  "@vitejs/plugin-vue": "6.0.8",
40
- "typescript": "5.9.3",
40
+ "typescript": "npm:typescript-native-bridge@6.0.3-bridge.16.tsgo.7.0.2",
41
41
  "vite": "8.2.2",
42
42
  "vue-tsc": "3.3.10"
43
43
  },
@@ -365,12 +365,18 @@ async function save(): Promise<void> {
365
365
 
366
366
  .bot-members {
367
367
  display: flex;
368
+ flex-wrap: wrap;
368
369
  align-items: center;
369
370
  justify-content: space-between;
370
371
  gap: 12px;
371
372
  padding: 12px;
372
373
  }
373
374
 
375
+ .bot-members > div {
376
+ min-width: 0;
377
+ flex: 1 1 14rem;
378
+ }
379
+
374
380
  .bot-members strong,
375
381
  .bot-members p {
376
382
  display: block;
@@ -389,10 +395,19 @@ async function save(): Promise<void> {
389
395
  font-size: var(--frock-text-sm);
390
396
  }
391
397
 
398
+ /*
399
+ * A status, not a button. It sat in the slot every other section fills with an
400
+ * action, ragged-right over three lines at 120px, and read as a broken
401
+ * control; a single-line pill is what it is.
402
+ */
392
403
  .bot-members > span {
393
- max-width: 120px;
394
404
  flex: 0 0 auto;
395
- text-align: right;
405
+ align-self: center;
406
+ border: 1px solid var(--frock-border);
407
+ border-radius: 999px;
408
+ padding: 2px 10px;
409
+ margin-top: 0;
410
+ white-space: nowrap;
396
411
  }
397
412
 
398
413
  .notification-setting {
@@ -65,7 +65,13 @@ async function save(): Promise<void> {
65
65
  class="package-settings-form"
66
66
  @submit.prevent="save"
67
67
  >
68
- <label v-for="definition in definitions" :key="definition.id">
68
+ <label
69
+ v-for="definition in definitions"
70
+ :key="definition.id"
71
+ :class="{
72
+ 'field-toggle': settingFieldKind(definition.schema) === 'boolean',
73
+ }"
74
+ >
69
75
  <span>{{ settingLabel(definition) }}</span>
70
76
  <select
71
77
  v-if="settingFieldKind(definition.schema) === 'enum'"
@@ -140,6 +146,34 @@ async function save(): Promise<void> {
140
146
  font-size: var(--frock-text-base);
141
147
  }
142
148
 
149
+ /*
150
+ * A checkbox reads beside its label, not under it. Sized and padded like a
151
+ * text field it drew as an empty box centred in the card with nothing next to
152
+ * it, and nothing on screen said what ticking it would do.
153
+ */
154
+ .package-settings-form label.field-toggle {
155
+ grid-template-columns: auto minmax(0, 1fr);
156
+ align-items: center;
157
+ gap: 4px 8px;
158
+ }
159
+
160
+ .package-settings-form label.field-toggle span {
161
+ order: 2;
162
+ color: var(--frock-text);
163
+ }
164
+
165
+ .package-settings-form label.field-toggle input[type="checkbox"] {
166
+ order: 1;
167
+ width: 16px;
168
+ height: 16px;
169
+ padding: 0;
170
+ }
171
+
172
+ .package-settings-form label.field-toggle .field-hint {
173
+ order: 3;
174
+ grid-column: 1 / -1;
175
+ }
176
+
143
177
  .field-hint {
144
178
  color: var(--frock-text-muted);
145
179
  font-size: var(--frock-text-sm);
@@ -8,7 +8,11 @@
8
8
  * all of it and simply stops being available to any Bot. Installing something
9
9
  * new is the Package Catalog, one surface over.
10
10
  */
11
- import { clientSurfaceRegistryKey } from "@frockbot/client-core";
11
+ import {
12
+ classifyClientFailureV1,
13
+ clientSurfaceRegistryKey,
14
+ presentClientFailureV1,
15
+ } from "@frockbot/client-core";
12
16
  import { UiAnchor, UiButton, UiIcon } from "@frockbot/client-ui";
13
17
  import {
14
18
  frockBotWebDataKey,
@@ -129,6 +133,11 @@ function openPackageCatalog(): void {
129
133
  surfaces.open("package-catalog");
130
134
  }
131
135
 
136
+ /** The read the error banner offers again. */
137
+ async function reload(): Promise<void> {
138
+ await web.value.loadPluginCatalog();
139
+ }
140
+
132
141
  /**
133
142
  * Refusals live on the row that was clicked, not in one panel-wide slot.
134
143
  *
@@ -182,18 +191,30 @@ function displayName(packageId: string): string {
182
191
  );
183
192
  }
184
193
 
194
+ /**
195
+ * What the row says about a click that did not work.
196
+ *
197
+ * A refusal is the deployment explaining an invariant it holds, in terms this
198
+ * surface can turn into names — "X needs Y turned on first" — so its text is
199
+ * worth reading. Everything else is a transport failure, whose text is about
200
+ * this client's own plumbing and is never the User's to read: it becomes the
201
+ * shared sentence and the detail goes to the console.
202
+ */
203
+ function rowFailure(error: unknown, action: string): string {
204
+ const classified = classifyClientFailureV1(error);
205
+ console.debug("plugin action failed", classified.detail);
206
+ return classified.serverMessage
207
+ ? inPlainNames(classified.serverMessage)
208
+ : presentClientFailureV1(error, action);
209
+ }
210
+
185
211
  async function install(item: PluginCatalogItem): Promise<void> {
186
212
  busyPackageId.value = item.packageId;
187
213
  clearRowError(item.packageId);
188
214
  try {
189
215
  await web.value.installPackage(item.packageId, item.version);
190
216
  } catch (error) {
191
- setRowError(
192
- item.packageId,
193
- inPlainNames(
194
- error instanceof Error ? error.message : "Couldn't add that plugin.",
195
- ),
196
- );
217
+ setRowError(item.packageId, rowFailure(error, `add ${item.displayName}`));
197
218
  } finally {
198
219
  busyPackageId.value = undefined;
199
220
  }
@@ -207,11 +228,7 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
207
228
  } catch (error) {
208
229
  setRowError(
209
230
  packageId,
210
- inPlainNames(
211
- error instanceof Error
212
- ? error.message
213
- : "Couldn't turn that plugin on or off.",
214
- ),
231
+ rowFailure(error, next ? "turn that plugin on" : "turn that plugin off"),
215
232
  );
216
233
  } finally {
217
234
  busyPackageId.value = undefined;
@@ -227,6 +244,15 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
227
244
  </span>
228
245
  {{ installedPluginCount }} installed
229
246
  </div>
247
+ <!--
248
+ A refused click belongs on its own row; this slot is for the read that
249
+ draws the whole list. A failed read leaves the last-known list on screen
250
+ — dimmed, because it is no longer confirmed — and offers the read again.
251
+ -->
252
+ <p v-if="web.settingsError" class="settings-error" role="alert">
253
+ {{ web.settingsError }}
254
+ <UiButton type="button" @click="reload">Retry</UiButton>
255
+ </p>
230
256
  <label class="plugin-search">
231
257
  <UiIcon name="search" />
232
258
  <input
@@ -251,7 +277,10 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
251
277
  </UiButton>
252
278
  </div>
253
279
  </UiAnchor>
254
- <div class="plugin-grid">
280
+ <div
281
+ class="plugin-grid"
282
+ :class="{ 'plugin-grid--stale': Boolean(web.settingsError) }"
283
+ >
255
284
  <article
256
285
  v-for="item in filteredCatalog"
257
286
  :key="item.packageId"
@@ -409,11 +438,19 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
409
438
  box-shadow: var(--frock-shadow-control);
410
439
  }
411
440
 
441
+ /*
442
+ * The name is the only thing in the row that identifies what a click will
443
+ * change, so it is the one thing that may not be the first to shrink. A grid
444
+ * gave the actions a min-content column and let the title collapse to an
445
+ * ellipsis — on a phone, to nothing at all — so the row wraps instead: the
446
+ * copy keeps a readable basis and the buttons drop to their own line when
447
+ * they no longer fit beside it.
448
+ */
412
449
  .plugin-summary {
413
- display: grid;
450
+ display: flex;
414
451
  width: 100%;
415
452
  min-width: 0;
416
- grid-template-columns: 44px minmax(0, 1fr) auto auto;
453
+ flex-wrap: wrap;
417
454
  align-items: center;
418
455
  gap: 12px;
419
456
  padding: 8px;
@@ -426,6 +463,7 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
426
463
 
427
464
  .plugin-logo {
428
465
  display: grid;
466
+ flex: 0 0 auto;
429
467
  width: 44px;
430
468
  height: 44px;
431
469
  place-items: center;
@@ -437,6 +475,7 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
437
475
 
438
476
  .plugin-card-copy {
439
477
  min-width: 0;
478
+ flex: 1 1 12rem;
440
479
  }
441
480
 
442
481
  .plugin-card-copy strong,
@@ -460,6 +499,8 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
460
499
  }
461
500
 
462
501
  .plugin-state {
502
+ flex: 0 0 auto;
503
+ margin-left: auto;
463
504
  color: var(--frock-text-muted);
464
505
  font-size: var(--frock-text-sm);
465
506
  font-weight: 700;
@@ -472,7 +513,9 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
472
513
 
473
514
  .plugin-actions {
474
515
  display: flex;
516
+ flex: 0 0 auto;
475
517
  flex-wrap: wrap;
518
+ margin-left: auto;
476
519
  justify-content: flex-end;
477
520
  gap: 8px;
478
521
  }
@@ -490,7 +533,20 @@ async function setEnabled(packageId: string, next: boolean): Promise<void> {
490
533
  }
491
534
 
492
535
  .settings-error {
536
+ display: flex;
537
+ align-items: center;
538
+ flex-wrap: wrap;
539
+ gap: 8px;
493
540
  color: var(--frock-danger-text);
494
541
  font-size: var(--frock-text-sm);
495
542
  }
543
+
544
+ /*
545
+ * What is on screen after a failed read is the last thing known, not the
546
+ * current thing. Dimming it says so, so a toggle is not made against a list
547
+ * the deployment has just refused to confirm.
548
+ */
549
+ .plugin-grid--stale {
550
+ opacity: 0.55;
551
+ }
496
552
  </style>
@@ -11,6 +11,7 @@ const web = inject(frockBotWebDataKey);
11
11
  if (!providedAuth || !surfaces || !web)
12
12
  throw new Error("settings client services were not provided");
13
13
  const auth = providedAuth;
14
+ const shell = web;
14
15
  const menuOpen = ref(false);
15
16
  const signOutError = ref<string>();
16
17
  const sessionUser = computed(() =>
@@ -54,6 +55,9 @@ async function signOut(): Promise<void> {
54
55
  signOutError.value = undefined;
55
56
  try {
56
57
  await auth.signOut();
58
+ // Held conversations belong to the User who was signed in. Nothing of
59
+ // theirs is carried into whoever signs in next.
60
+ shell.value.transcripts.forget();
57
61
  closeMenu();
58
62
  } catch (error) {
59
63
  signOutError.value =
@@ -61,13 +65,36 @@ async function signOut(): Promise<void> {
61
65
  }
62
66
  }
63
67
 
64
- onMounted(() => window.addEventListener("pointerdown", closeMenu));
65
- onBeforeUnmount(() => window.removeEventListener("pointerdown", closeMenu));
68
+ /*
69
+ * Escape closes this menu, as it closes every other overlay in the product.
70
+ * It is bound on the window rather than on the element because the menu can
71
+ * be open while focus is still on the trigger, and because a menu that only
72
+ * an outside click dismisses is a trap for anyone not using a mouse. Focus
73
+ * goes back to the trigger, which is where it came from.
74
+ */
75
+ const trigger = ref<HTMLButtonElement>();
76
+
77
+ function onWindowKeydown(event: KeyboardEvent): void {
78
+ if (event.key !== "Escape" || !menuOpen.value) return;
79
+ event.preventDefault();
80
+ closeMenu();
81
+ trigger.value?.focus();
82
+ }
83
+
84
+ onMounted(() => {
85
+ window.addEventListener("pointerdown", closeMenu);
86
+ window.addEventListener("keydown", onWindowKeydown);
87
+ });
88
+ onBeforeUnmount(() => {
89
+ window.removeEventListener("pointerdown", closeMenu);
90
+ window.removeEventListener("keydown", onWindowKeydown);
91
+ });
66
92
  </script>
67
93
 
68
94
  <template>
69
95
  <div class="profile-area" @pointerdown.stop>
70
96
  <button
97
+ ref="trigger"
71
98
  class="profile-trigger"
72
99
  type="button"
73
100
  :aria-expanded="menuOpen"
package/src/user.test.ts CHANGED
@@ -833,7 +833,7 @@ describe("User settings backend Contribution", () => {
833
833
  expectedRevision: 2,
834
834
  model: {
835
835
  connectionId: "flock-default",
836
- providerModelId: "@flock/auto",
836
+ providerModelId: "@frock/auto",
837
837
  },
838
838
  },
839
839
  storage,
@@ -841,7 +841,7 @@ describe("User settings backend Contribution", () => {
841
841
  ).resolves.toMatchObject({ status: "applied", revision: 3 });
842
842
  expect((await settings.read("user-1")).platformModel).toEqual({
843
843
  connectionId: "flock-default",
844
- providerModelId: "@flock/auto",
844
+ providerModelId: "@frock/auto",
845
845
  });
846
846
  });
847
847