@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@frockbot/plugin-settings",
3
- "version": "0.3.9",
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.9",
25
- "@frockbot/client-core": "0.3.9",
26
- "@frockbot/client-ui": "0.3.9",
27
- "@frockbot/configuration-core": "0.3.9",
28
- "@frockbot/connection-core": "0.3.9",
29
- "@frockbot/kernel-composition": "0.3.9",
30
- "@frockbot/kernel-contracts": "0.3.9",
31
- "@frockbot/plugin-auth": "0.3.9",
32
- "@frockbot/plugin-shell": "0.3.9",
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 next = applyUserCommand(current, command, (packageId, version) =>
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,