@frockbot/plugin-settings 0.3.9 → 0.3.11
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 +10 -10
- package/src/client/BotSettingsSurface.vue +30 -0
- package/src/user.test.ts +45 -0
- package/src/user.ts +56 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@frockbot/plugin-settings",
|
|
3
|
-
"version": "0.3.
|
|
3
|
+
"version": "0.3.11",
|
|
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.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",
|
|
33
33
|
"cordis": "4.0.0-rc.8",
|
|
34
34
|
"vue": "3.5.41"
|
|
35
35
|
},
|
|
@@ -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"
|
package/src/user.test.ts
CHANGED
|
@@ -746,6 +746,51 @@ describe("User settings backend Contribution", () => {
|
|
|
746
746
|
},
|
|
747
747
|
}),
|
|
748
748
|
).resolves.toMatchObject({ status: "applied", revision: 4 });
|
|
749
|
+
|
|
750
|
+
// The mirror of the rule above, read the other way round. Disabling the
|
|
751
|
+
// dependency is allowed — refusing it would make a Package the User never
|
|
752
|
+
// chose able to pin one they did — and carries its dependents with it, so
|
|
753
|
+
// the account never lands in the state the enable path refuses:
|
|
754
|
+
// `custom-models=disabled provider-ollama-cloud=installed`.
|
|
755
|
+
await expect(
|
|
756
|
+
settings.executeConfiguration({
|
|
757
|
+
schemaVersion: 1,
|
|
758
|
+
userId: "user-1",
|
|
759
|
+
command: {
|
|
760
|
+
schemaVersion: 1,
|
|
761
|
+
type: "user/set-package-enabled",
|
|
762
|
+
commandId: "disable-custom-models",
|
|
763
|
+
expectedRevision: 4,
|
|
764
|
+
packageId: "custom-models",
|
|
765
|
+
enabled: false,
|
|
766
|
+
},
|
|
767
|
+
}),
|
|
768
|
+
).resolves.toMatchObject({ status: "applied", revision: 5 });
|
|
769
|
+
const cascaded = await settings.read("user-1");
|
|
770
|
+
expect(cascaded.packages[0]).toMatchObject({
|
|
771
|
+
packageId: "custom-models",
|
|
772
|
+
state: "disabled",
|
|
773
|
+
});
|
|
774
|
+
expect(cascaded.packages[1]).toMatchObject({
|
|
775
|
+
packageId: "provider-ollama-cloud",
|
|
776
|
+
state: "disabled",
|
|
777
|
+
});
|
|
778
|
+
|
|
779
|
+
// The cascade is not automatic re-enablement: turning the dependency back
|
|
780
|
+
// on leaves the dependent off until the User asks for it.
|
|
781
|
+
await settings.executeConfiguration({
|
|
782
|
+
schemaVersion: 1,
|
|
783
|
+
userId: "user-1",
|
|
784
|
+
command: {
|
|
785
|
+
schemaVersion: 1,
|
|
786
|
+
type: "user/set-package-enabled",
|
|
787
|
+
commandId: "re-enable-custom-models",
|
|
788
|
+
expectedRevision: 5,
|
|
789
|
+
packageId: "custom-models",
|
|
790
|
+
enabled: true,
|
|
791
|
+
},
|
|
792
|
+
});
|
|
793
|
+
expect((await settings.read("user-1")).packages[1]?.state).toBe("disabled");
|
|
749
794
|
});
|
|
750
795
|
|
|
751
796
|
test("applies the platform model through the backend Contribution", async () => {
|
package/src/user.ts
CHANGED
|
@@ -731,6 +731,54 @@ export class UserSettingsBackendContribution {
|
|
|
731
731
|
return undefined;
|
|
732
732
|
}
|
|
733
733
|
|
|
734
|
+
/**
|
|
735
|
+
* The mirror of `packageDependencyFailure`, read the other way round.
|
|
736
|
+
*
|
|
737
|
+
* Enabling B before A is refused, so leaving B enabled once A is switched
|
|
738
|
+
* off would put the account in exactly the configuration the enable path
|
|
739
|
+
* will not create. Refusing the disable is not the answer either: a Package
|
|
740
|
+
* the User never chose — `provider-ollama-cloud` depends on `web` — would
|
|
741
|
+
* make `web` undisableable. So the disable is allowed and carries its
|
|
742
|
+
* dependents with it, transitively. Model binding degrades to the platform
|
|
743
|
+
* default rather than failing, so a cascade that reaches the Package a Bot's
|
|
744
|
+
* model is bound to costs that Bot its choice, not its next reply.
|
|
745
|
+
*
|
|
746
|
+
* Platform-owned Packages are never cascaded: they cannot be disabled by any
|
|
747
|
+
* other path, and in this application they depend only on each other.
|
|
748
|
+
*/
|
|
749
|
+
private cascadeDisabledDependents(
|
|
750
|
+
settings: UserSettingsViewV1,
|
|
751
|
+
): UserSettingsViewV1 {
|
|
752
|
+
let packages = settings.packages;
|
|
753
|
+
for (;;) {
|
|
754
|
+
const enabled = new Set(
|
|
755
|
+
packages
|
|
756
|
+
.filter((pkg) => pkg.state === "installed")
|
|
757
|
+
.map((pkg) => pkg.packageId),
|
|
758
|
+
);
|
|
759
|
+
const cascaded = packages.map((pkg) => {
|
|
760
|
+
if (pkg.state !== "installed") return pkg;
|
|
761
|
+
if (this.platformOwnedPackageIds.has(pkg.packageId)) return pkg;
|
|
762
|
+
const dependencies = this.packageDependencies.get(
|
|
763
|
+
`${pkg.packageId}\u0000${pkg.version}`,
|
|
764
|
+
);
|
|
765
|
+
if (!dependencies) return pkg;
|
|
766
|
+
const missing = Object.keys(dependencies).some(
|
|
767
|
+
(dependencyId) => !enabled.has(dependencyId),
|
|
768
|
+
);
|
|
769
|
+
return missing
|
|
770
|
+
? { ...pkg, state: "disabled" as const, failure: undefined }
|
|
771
|
+
: pkg;
|
|
772
|
+
});
|
|
773
|
+
if (cascaded.every((pkg, index) => pkg === packages[index])) {
|
|
774
|
+
return packages === settings.packages
|
|
775
|
+
? settings
|
|
776
|
+
: { ...settings, packages };
|
|
777
|
+
}
|
|
778
|
+
packages = cascaded;
|
|
779
|
+
}
|
|
780
|
+
}
|
|
781
|
+
|
|
734
782
|
async readConfiguration(input: unknown): Promise<UserSettingsViewV1> {
|
|
735
783
|
const request = decodeUserConfigurationReadRpcV1(input);
|
|
736
784
|
// Settings owns the stored-record migration and platform-row repair. Run
|
|
@@ -1001,9 +1049,16 @@ export class UserSettingsBackendContribution {
|
|
|
1001
1049
|
await storage.put(receiptKey, { commandFingerprint, receipt });
|
|
1002
1050
|
return receipt;
|
|
1003
1051
|
}
|
|
1004
|
-
const
|
|
1052
|
+
const applied = applyUserCommand(current, command, (packageId, version) =>
|
|
1005
1053
|
this.settingDefinitions(packageId, version),
|
|
1006
1054
|
);
|
|
1055
|
+
// One command, one revision: the cascade is part of the disable the User
|
|
1056
|
+
// asked for, not a second write they have to reconcile against.
|
|
1057
|
+
const next =
|
|
1058
|
+
command.type === "user/uninstall-package" ||
|
|
1059
|
+
(command.type === "user/set-package-enabled" && !command.enabled)
|
|
1060
|
+
? this.cascadeDisabledDependents(applied)
|
|
1061
|
+
: applied;
|
|
1007
1062
|
const receipt: OperationReceiptV1 = {
|
|
1008
1063
|
schemaVersion: 1,
|
|
1009
1064
|
commandId: command.commandId,
|