@frockbot/plugin-settings 0.3.10 → 0.3.12
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.
|
|
3
|
+
"version": "0.3.12",
|
|
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.
|
|
25
|
-
"@frockbot/client-core": "0.3.
|
|
26
|
-
"@frockbot/client-ui": "0.3.
|
|
27
|
-
"@frockbot/configuration-core": "0.3.
|
|
28
|
-
"@frockbot/connection-core": "0.3.
|
|
29
|
-
"@frockbot/kernel-composition": "0.3.
|
|
30
|
-
"@frockbot/kernel-contracts": "0.3.
|
|
31
|
-
"@frockbot/plugin-auth": "0.3.
|
|
32
|
-
"@frockbot/plugin-shell": "0.3.
|
|
24
|
+
"@frockbot/catalog-core": "0.3.12",
|
|
25
|
+
"@frockbot/client-core": "0.3.12",
|
|
26
|
+
"@frockbot/client-ui": "0.3.12",
|
|
27
|
+
"@frockbot/configuration-core": "0.3.12",
|
|
28
|
+
"@frockbot/connection-core": "0.3.12",
|
|
29
|
+
"@frockbot/kernel-composition": "0.3.12",
|
|
30
|
+
"@frockbot/kernel-contracts": "0.3.12",
|
|
31
|
+
"@frockbot/plugin-auth": "0.3.12",
|
|
32
|
+
"@frockbot/plugin-shell": "0.3.12",
|
|
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": "
|
|
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
|
},
|
|
@@ -59,6 +59,13 @@ const label = ref("");
|
|
|
59
59
|
const description = ref("");
|
|
60
60
|
const title = ref("");
|
|
61
61
|
const hiddenFromSidebar = ref(false);
|
|
62
|
+
/*
|
|
63
|
+
* Pinning is durable as the instant it happened, so the sidebar can order
|
|
64
|
+
* pinned Bots by pin time. The panel edits the intent; the original instant is
|
|
65
|
+
* kept across an unrelated save so re-saving never reshuffles the tiles.
|
|
66
|
+
*/
|
|
67
|
+
const pinned = ref(false);
|
|
68
|
+
const pinnedAt = ref("");
|
|
62
69
|
const notifications = ref(false);
|
|
63
70
|
const pendingApprovals = computed(() =>
|
|
64
71
|
web.value.approvals.filter((approval) => approval.decision === "pending"),
|
|
@@ -98,6 +105,8 @@ watch(
|
|
|
98
105
|
description.value = settings.profile.description ?? "";
|
|
99
106
|
title.value = settings.profile.title ?? "";
|
|
100
107
|
hiddenFromSidebar.value = settings.profile.hiddenFromSidebar === true;
|
|
108
|
+
pinnedAt.value = settings.profile.pinnedAt ?? "";
|
|
109
|
+
pinned.value = pinnedAt.value !== "";
|
|
101
110
|
notifications.value = settings.notifications.enabled;
|
|
102
111
|
},
|
|
103
112
|
{ immediate: true },
|
|
@@ -116,6 +125,10 @@ async function save(): Promise<void> {
|
|
|
116
125
|
description: description.value,
|
|
117
126
|
title: title.value,
|
|
118
127
|
hiddenFromSidebar: hiddenFromSidebar.value,
|
|
128
|
+
pinnedAt: pinned.value
|
|
129
|
+
? pinnedAt.value || new Date().toISOString()
|
|
130
|
+
: // The empty string unpins, the same way it clears an optional text field.
|
|
131
|
+
"",
|
|
119
132
|
});
|
|
120
133
|
await web.value.saveBotNotifications({ enabled: notifications.value });
|
|
121
134
|
surfaces.close();
|
|
@@ -162,6 +175,23 @@ async function save(): Promise<void> {
|
|
|
162
175
|
/>
|
|
163
176
|
</UiField>
|
|
164
177
|
</UiAnchor>
|
|
178
|
+
<UiAnchor
|
|
179
|
+
anchor="bot-pinned"
|
|
180
|
+
label="Pinned"
|
|
181
|
+
:href="link('bot-pinned')"
|
|
182
|
+
class="settings-row"
|
|
183
|
+
>
|
|
184
|
+
<label class="notification-setting">
|
|
185
|
+
<span>
|
|
186
|
+
<strong>Pinned</strong>
|
|
187
|
+
<small>
|
|
188
|
+
Pin this Bot to the top of the sidebar. Unpin it to put it back in
|
|
189
|
+
the list.
|
|
190
|
+
</small>
|
|
191
|
+
</span>
|
|
192
|
+
<input v-model="pinned" type="checkbox" />
|
|
193
|
+
</label>
|
|
194
|
+
</UiAnchor>
|
|
165
195
|
<UiAnchor
|
|
166
196
|
anchor="bot-description"
|
|
167
197
|
label="Description"
|
|
@@ -335,12 +365,18 @@ async function save(): Promise<void> {
|
|
|
335
365
|
|
|
336
366
|
.bot-members {
|
|
337
367
|
display: flex;
|
|
368
|
+
flex-wrap: wrap;
|
|
338
369
|
align-items: center;
|
|
339
370
|
justify-content: space-between;
|
|
340
371
|
gap: 12px;
|
|
341
372
|
padding: 12px;
|
|
342
373
|
}
|
|
343
374
|
|
|
375
|
+
.bot-members > div {
|
|
376
|
+
min-width: 0;
|
|
377
|
+
flex: 1 1 14rem;
|
|
378
|
+
}
|
|
379
|
+
|
|
344
380
|
.bot-members strong,
|
|
345
381
|
.bot-members p {
|
|
346
382
|
display: block;
|
|
@@ -359,10 +395,19 @@ async function save(): Promise<void> {
|
|
|
359
395
|
font-size: var(--frock-text-sm);
|
|
360
396
|
}
|
|
361
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
|
+
*/
|
|
362
403
|
.bot-members > span {
|
|
363
|
-
max-width: 120px;
|
|
364
404
|
flex: 0 0 auto;
|
|
365
|
-
|
|
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;
|
|
366
411
|
}
|
|
367
412
|
|
|
368
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
|
|
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 {
|
|
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
|
-
|
|
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
|
|
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:
|
|
450
|
+
display: flex;
|
|
414
451
|
width: 100%;
|
|
415
452
|
min-width: 0;
|
|
416
|
-
|
|
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
|
-
|
|
65
|
-
|
|
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: "@
|
|
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: "@
|
|
844
|
+
providerModelId: "@frock/auto",
|
|
845
845
|
});
|
|
846
846
|
});
|
|
847
847
|
|